Part B: Here is code to replace the semaphores in our thread package with locks.
/**** * * thread manager extension for locks * * ****/ /* lock representation known only to lock methods */ struct thread_lock { int lstate; /* either LOCKED or UNLOCKED */ struct thread_queue queue; }; #define LOCKED 0 #define UNLOCKED 1 void thread_lock_init( struct thread_lock * lk ) /* call this to initialize lock lk to UNLOCKED */ { lk->lstate = UNLOCKED; thread_queue_init( &lk->queue ); } void thread_claim( struct thread_lock * lk ) /* call this within a thread to claim a lock */ { if (lk->lstate == UNLOCKED) { lk->lstate = LOCKED; /* deadlock */ lk->thread = current; } else { if (_setjmp( current->state ) == 0) { /* deadlock */ t->lock = lk; /* deadlock detection algorithm */ thread_enqueue( current, &lk->queue ); current = thread_dequeue( &readylist ); if (current == thread_null) { /* crisis */ thread_error = "possible deadlock"; _longjmp( thread_death, 1 ); } _longjmp( current->state, 1 ); } } } void thread_release( struct thread_lock * lk ) { struct thread * t = thread_dequeue( &lk->queue ); if (t == thread_null) { /* deadlock */ lk->thread = thread_null; lk->lstate = UNLOCKED; } else { /* deadlock */ t->lock = thread_semaphore_null; /* deadlock */ lk->thread = t; thread_enqueue( t, &readylist ); } }
Part C: To allow automatic deadlock detection, we have to add some new pointers to the representations of locks and threads. Specifically, in each thread, we put a pointer called current_lock, and in each lock, we put a pointer called current_thread. These pointers are manipulated as shown by the lines commented /*deadlock*/ in the above code.
If we ever find a cycle in these pointers, we have a deadlock. The point where such a cycle can be created is commented /*deadlock detection algorithm*/ in the above code.
The problem remains of deciding what objects will serve as roots of the marking process. One solution is to treat each stack frame (activation record) as a special kind of object, so each distinct activation record format would have, associated with it, a marking method that calls the mark method of each object referenced from that activation record. To initiate marking, then, we simply call the mark method of the topmost activation record on the stack. This, in turn, calls the mark methods of everything pointed to from the current activation record, including the activation record below it on the stack, all the way back to the global level.
An alternative to introducing new methods would be to augment each item on the heap or on the run-time stack with a data structure indicating what words in that item are pointers. Then, a single marking function could recursively traverse the stack and heap, marking each object reachable by any pointer. This universal marking function would be likely to be slower than having a specialized mark method for each class, but it would require less code.
Part B: If you had to implement UNIX using a microkernel-based approach, such items as the file system, timers, and probably all I/O would be outside the kernel, leaving behind only process and memory management, along with some kind of interprocess communication. In fact, Tannenbaum's Minix system (used in his lower-level operating systems text) is exactly such a re-implementation of UNIX based on such a microkernel.