Beruflich Dokumente
Kultur Dokumente
[10 pts] You should write your code "defensively" in the sense that you should make an
attempt to detect error conditions and react appropriately. For error conditions that could
result in a race condition or deadlock, your library routines should exit -- there is no way
to recover from these errors, so they should be fatal to the program. There is a convenient
macro ASSERT() that you can use to check for error conditions and abort if necessary
(grep through the Nachos source code files for examples of how to use it).
To help motivate you to get into the habit of testing for error conditions, write test
programs that test that your code correctly deals with the following situations: (1)
acquiring the same Lock twice, (2) releasing a Lock that isn't held, (3) deleting a Lock
that is held, (4) waiting on a condition variable without holding a Lock, (5) signaling a
condition variable wakes only one thread and broadcasting wakes up all threads, (6)
signaling and broadcasting to a condition variable with no waiters is a no-op, and future
threads that wait will block (i.e., the signal/broadcast is "lost"), (7) a thread calling Signal
holds the Lock passed in to Signal, and (8) deleting a lock or condition variable should
have no threads on the wait queue. These are the minimal set of error conditions for which
we'll test your Lock and Condition implementations. For an example of how to start
writing test programs, see the testing section of the hints.
Note: An excellent approach for having project members become familiar with an
implementation of a problem is to have them write many of the test cases. For example,
if you write the code implementing Locks and Conditions, have other members write
some of the more elaborate test cases (and vice versa). This approach will also give you
even more incentive to make sure it is correct first.
2. [15 pts] Implement synchronous send and receive of one word messages using locks and
condition variables. Create a "Mailbox" class with the 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, Receive waits until Send is called, at which point the copy is made and both
calls return. Your solution should work even if there are multiple senders and receivers
for the same mailbox. (Hint: this is equivalent to a zero-length bounded buffer.)
Note that you cannot use explicit wait queues, Sleep, or disable/enable interrupts to
implement Mailbox; the condition variables will do all of that for you. Also, it is not
necessary to "match" sending and receiving threads -- a receiver does not care which
sender it gets a message from, only that it does get a message if a sender is trying to send
one. If you do match them, though, that is fine.
You can implement the "Mailbox" class in synch.h and sync.cc, or in new files.
[5 pts] Write test cases that demonstrate that your implementation of the Mailbox class is
faithful to the semantics described above: a receiver will only return when a sender sends,
and blocks otherwise (and vice-versa); only one receiver and sender synchronize at a
time, even when there are multiple senders and receivers.
3. [15 pts] Implement Thread::Join(). Two threads are involved in Join; for the sake
of intuition, let's call them the parent and the child. At a high level, Join enables the parent
thread to wait for the child thread to finish. To do this, the parent thread is the one that
invokes Join, and it invokes it on the child:
4. (executing as the parent thread)
5.
6. Thread *child = new Thread("child", 1);
7. child->Fork(SomeProcedure, SomeArgument);
8. ...
9. child->Join(); // parent blocks until child terminates
To implement Join, start by adding a parameter to the thread constructor to indicate
whether or not Join will be called on this thread, and then implement the new Join method
using one of the high-level synchronization primitives (Locks/CVs or Semaphores); do
not create another "wait queue" and Sleep the waiting thread directly (i.e., do not do
anything that requires you to add code to disable/enable interrupts).
Use the following signatures for the updated constructor and Join method:
Thread(char* debugName, int join = 0);
void Join();
Your implementation should properly delete the thread control block (1) whether or not
Join is to be called, and (2) whether or not the thread being Joined finishes before the Join
is called. For (1), if Join will not be called on the thread, you can delete the TCB
immediately when the thread exits (as currently implemented). If Join will be called on
the thread, you must wait until after Join has been called and returns before you can delete
the TCB (you can assume that Join will eventually be called in this case). For (2), you do
not know whether the thread to be Joined will finish before another thread calls Join on
that thread -- i.e., the TCB for the child cannot be deleted even if the child terminates
before the parent calls Join on it. If the child finishes before the parent calls Join, you
must wait to delete the child's TCB until the parent calls Join.
The file join-example.cc is an example program where one thread calls Join() on another
[Sample Output]. It should help make the semantics and use of Join more concrete. Be
sure to note the use of the "-rs" switch to the nachos executable to randomizes context
switches.
[5 pts] Write test cases that test that (1) a thread that will be joined only is destroyed once
Join has been called on it, (2) if a parent calls Join on a child and the child is still
executing, the parent waits, (3) if a parent calls Join on a child and the child has finished
executing, the parent does not block, (4) a thread does not call Join on itself, (5) Join is
only invoked on threads created to be joined, (6) Join is only called on a thread that has
forked, and (7) Join is not called more than once on a thread (if it is, then this could easily
lead to a segmentation fault because the child is likely deleted).
10. [15 pts] Implement preemptive priority scheduling in Nachos. Priority scheduling is a key
building block for real-time systems. Add calls to the Thread class to set and get the
priority of the thread. When a thread is added to the ready list, the thread should insert
into the ready list in sorted order. When a thread is added to the ready list that is the same
priority as another thread in the list (including the currently running thread), the thread
should insert after the threads at that same priority (so that they will all get a chance to
run). On a context switch, if there is a higher priority thread at the head of the ready list,
the higher priority thread should run. If the thread at the head of the ready list has the
same priority, still perform the context switch so that threads of equal priority share the
CPU. If a thread is already on the ready (or a wait) list when setPriority changes the
priority, you do not have to re-sort it (although you can if you want). When threads are
waiting for a lock, semaphore, or condition variable, the highest priority waiting thread
should be woken up first.
Ejemplos de salida:
[cs120f] > ./nachos -rs 5
Forked off the joiner and joiner threads.
Smell the roses.
Smell the roses.
Smell the roses.
Waiting for the Joinee to finish executing.
Smell the roses.
Smell the roses.
Done smelling the roses!
Joinee has finished executing, we can continue.
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!
Ticks: total 414, idle 144, system 270, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0
Cleaning up...
Locks
We need to implement its two operations, Acquire and Release. We know that (1) we
need to make them atomic, and (2) they are going to block threads. So what we will do
is look at the implementation of the Semaphore class to see how it does these things.
Compiling
We are going to put the code for Lock::Acquire and Lock::Release in synch.cc. So we put our
code there where the skeleton functions are defined. Now we need to recompile Nachos. To
do this, all we need to do is rerun "make" in the threads directory.
Testing
Ok, now we have an implementations of the Lock class and we compiled it, removing
all compilation errors.
So how do we test it? This is where threadtest.cc comes in. If we look in threadtest.cc,
we see the function ThreadTest(). We know that ThreadTest() is called from main() in
main.cc. So what we will do is implement a new function in threadtest.cc, call it
LockTest(), and change the ThreadTest function in threadtest.cc to invoke that function.
Here is a modified version of threadtest.cc that does this. Note that, in this test, two
threads are created: the first thread runs, acquires the lock, yields to the second thread,
which runs, tries to acquire the lock but blocks, gives up the CPU to the first thread, and
the first thread continues, etc. This behavior is highlighted by the output below.
You can use the command line argument "-q" to invoke different test functions you
have implemented in threadtest.cc. To invoke LockTest, do the following:
% ./nachos -q 2
L1:0
L1:1
L2:0
L1:2
L1:3
L2:1
L2:2
L2:3
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!
Ticks: total 110, idle 0, system 110, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0
Cleaning up...
We used printf's to see how the threads were executing. We can also use Nachos' debug
facility to trace context switches and threads moving around on queues. Try running the
following:
% ./nachos -d t -q 2
Ok, we've written an implementation of the Lock class and written one test program for
it. Now you should implement other test programs for Lock, in particular test programs
that test various kinds of error conditions. Once you're confident that your Lock class
works, you can move on to implementing the Condition class. Go about implementing
Condition in the same way. Write down its pseudo code, look at the implementation of
Semaphore and Lock to see how each step should be implemented in Nachos, compile,
and then write test cases that validates its implementation. Implement and test one
behavior at a time so that you do not try to juggle too much.
example-writeup.html:
2. Mailbox
We used condition variables to implement the Mailbox class. The [name] variable was
used to synchronize the situation where a sender has to wait for a receiver. It is used by
the Send method when... and by the Recieve method when...
The [name] variable was used to synchronize the situation where a receiver has to wait
for a sender ...
To handle the situation where there are multiple senders and receivers, we ...
We tested our Mailbox implementation by creating ? test programs. The first program
tested the case that ... by ...
3. Join
...
4. Priorities
...
5. Whales
...
Summary
Everyone in our group contributed to the project. [person 1] and [person 2]
implemented Locks and Condition variables ...
Referencia:
http://cseweb.ucsd.edu/classes/fa14/cse120-b/projects/threads.html