The first step is to read and understand the partial thread system
we have written for you. This thread system implements thread fork,
thread completion, along with semaphores, locks, and conditional
variables for synchronization. When you
run the Nachos binary, it executes a function called
ThreadTest()
(located in
threads/threadtest.cc
). This function creates two threads
that take turns using the machine by repeatedly calling
currentThread->Yield()
.
You should trace this execution path by hand to better understand how
Nachos threads are implemented. When doing this, it is helpful to keep
track of the state of each thread and which procedures are on each
thread's execution stack. You will notice that when one thread calls
SWITCH()
, another thread starts running; the first thing
the new thread does is to return from SWITCH()
. This
comment may seem cryptic to you at this point, but you will understand
threads much better once you understand why the SWITCH()
that gets called is different from the SWITCH()
that
returns. (Note: because gdb
does not understand threads,
you will get bizarre results if you try to trace across a call to
SWITCH()
in gdb
.)
The files for this assignment are:
Properly synchronized code should work no matter what order the scheduler chooses to run the threads on the ready list. In other words, we should be able to put a call toThread::Yield
(causing the scheduler
to choose another thread to run) anywhere in your code where interrupts
are enabled without changing the correctness of your code.
You will be asked to write properly synchronized code as part of the
later assignments, so understanding how to do this is crucial to
being able to do the project.
To aid you in this, code linked in with Nachos will cause
Thread::Yield
to be called on your behalf in a repeatable but unpredictable way.
Nachos code is repeatable in that if you call it repeatedly with the
same arguments, it will do exactly the same thing each time.
However, if you invoke ``nachos -rs N
'', with a different
number N
each time, calls to Thread::Yield
will be inserted at different places in the code. This will
change the sequence in which the threads execute.
You can also use
the -d
option to print out the tick-by-tick thread
information.
Make sure to run various test cases against your solutions to these problems; for instance, for Problem 2, create multiple producers and consumers and demonstrate that the output can vary, within certain boundaries.
Warning: in our implementation of threads, each thread is assigned a
small, fixed-size execution stack. This may cause bizarre problems
(such as segmentation faults at strange lines of code) if you declare
large data structures to be automatic variables (e.g.,
"int buf[1000];
").
You will probably not notice this during the semester, but if you do,
you may change the size of the stack by modifying the StackSize define in
switch.h.
Although the solutions can be written as normal C routines, you will find organizing your code to be easier if you structure your code as C++ classes. Also, there should be no busy-waiting in any of your solutions to this assignment.
Condition
class) directly using
interrupt enable and disable to provide atomicity. We have provided a
sample implementation that uses semaphores; your job is to provide an
equivalent implementation without using semaphores; once you are done,
we will have two alternative implementations that provide the exact
same functionality.
You do not need to reimplement the Lock
class; you can use the
provided version.
Mailbox
class with operations
Mailbox::Send(int message)
and
Mailbox::Receive(int *message)
. Send
atomically waits until Receive is
called on the same Mailbox and then copies the message into the
receive buffer. Once the copy is made, both can return. Similarly, the
Receive waits until Send is called, at which point the copy is made,
and both can return. Your solution should work even if there are
multiple senders and receivers for the same Mailbox. You solution should make use of only condition variables and locks for synchronization. Do not use semaphores. If there are multiple senders and receivers on the same Mailbox, you may choose any ordering you wish, as long as your choice is clearly documented in your README file and no messages are lost.
void Thread::Join(void)
in Nachos.
This routine causes the
currently running thread to block until the joined thread has
completed. Add an argument to the thread constructor that says whether
or not a Join may be called on this thread (i.e.,
"Thread::Thread(char* threadName, bool joinMayBeCalled)
").
Note that you do not need to
implement Join
so that it returns the value returned by the forked
thread.
It is legal for the parent thread to never call
Thread::Join
. Your solution should properly delete the thread
control block whether or not Join is called, and whether or not
the forked thread finishes before the Join is called. That is,
the Thread object should be freed exactly once in all cases.
The parent thread may call Join()
on a child thread arbitrarily
many times. Each one after the first is a "subsequent" join. (Having
this functionality is convenient if Join()
might be called along
many different paths of execution.) The following code fragment is
legal and must be handled correctly:
Thread *child = new Thread(...); child->Fork(...); child->Join(); ... child->Join(); // doesn't do anything.Here, "subsequent" only refers to function calls on the same Thread object, not joins to a different child thread.
Similarly, calling Join()
on an "unjoinable" thread is a
no-op.
Alarm:GoToSleepFor(int howLong)
" to
go to sleep for a period of time. The alarm clock can be implemented
using the hardware Timer device (cf. timer.h
). When the
timer interrupt goes off, the Timer interrupt handler checks to see
if any thread that had been asleep needs to wake up now. There is no
requirement that threads start running immediately after waking up;
just put them on the ready queue after they have waited for the
approximately the right amount of time.
ArrivingGoingFromTo(int atFloor, int toFloor)
''. This
should wake up the elevator, tell it which floor the person is on, and
wait until the elevator arrives before telling it which floor to go to.
The elevator is amazingly fast, but it is not instantaneous -- it takes
only 100 ticks to go from one floor to the next. You may find it useful
to use your solution for Problem 4 here. For simplicity, you can assume
there's only one elevator, and that it holds an arbitrary number of people.
You are allowed to give higher priority to those threads going to or
departing from the Zoo!