Sie sind auf Seite 1von 9

Exmen Parcial:

Tema: Problemas de Sincronizacin usando Nachos


Curso : Sistemas Operativos
UNI 2016-II
Fecha de Entrega: 23/05/2016
Grupos de 4 a 5 alumnos.
In this assignment, we will give you part of a working thread system. Your job is to complete it
and then use it to solve several synchronization problems.
The first step is to read and understand the partial thread system we've provided. This thread
system implements thread fork, thread completion, and semaphores for synchronization.
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 to Thread::Yield()
(which causes the scheduler to pick 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 later assignments, so understanding how to do this is crucial to being
able to do the project.
To aid you in this task, code linked in with Nachos will cause Thread::Yield() to be called
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 with nachos -rs # with a different number each time, calls to Thread::Yield()
will be inserted in different places in the code.
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 term, 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.
Don't worry if you don't have to write much code for each of these: the assignment is largely
conceptual and not a programming chore. For some hints on getting started, here are some
suggestions.
1. [15 pts] Implement condition variables using interrupt enable and disable to provide
atomicity. The file code/threads/synch.h defines the classes "Lock" and "Condition", and
it is your task to implement the functions defined by those classes in synch.cc. The file
badtest.cc is an example program that demonstrates how race conditions can happen
inside of Nachos. It is similar in spirit to the withdraw() example from lecture. [Sample
Output]

[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.

Use the following signatures for the methods:


void setPriority(int newPriority);
int getPriority();
Note: You need to use these names (including capitalization) and obey these signatures
so that our test programs will compile to your code.
The range of valid priorities is the entire range of an "int". Assume that all threads are
created with priority 0. Roughly speaking, threads set to have a negative priority have
"less priority", and threads set to have a positive priority have "more priority". Compare
thread priorities directly to determine higher priority (e.g., a priority of 1 is lower than a
priority of 2).
Extra credit: An issue with priority scheduling is "priority inversion". If a high priority
thread needs to wait for a low priority thread, such as for a lock held by a low priority
thread or for a Join to complete, and a middle priority thread is on the ready list, then the
high priority thread will never get the CPU because the low priority thread will not get
any CPU time. A partial fix for this problem is to have the waiting thread "donate" its
priority to the low priority thread while it is holding the lock. Implement this fix
separately for both situations: (1) the Lock class and (2) the Join method.
[10 pts] Write test programs that (1) demonstrate that threads with higher priority get
service first in the cases outlined above (both when added to the ready list, and when
woken up when waiting on a synchronization variable), and (2) (if doing the extra credit)
demonstrate that you solve the priority inversion problem for Locks and Join().
11. [10 pts] You have been hired by Greenpeace to help the environment. Because
unscrupulous commercial interests have dangerously lowered the whale population,
whales are having synchronization problems in finding a mate. The trick is that, to have
children, three whales are needed, one male, one female, and one to play matchmaker -literally, to push the other two whales together (we're not making this up!).
Your job is to save the whales using either semaphores or condition variables. Create a
"Whale" class with the operations Whale::Male(), Whale::Female(), and
Whale::Matchmaker(). We will represent each whale as a separate thread. A male
whale calls Male(), which waits until there is a waiting female and matchmaker;
similarly, a female whale calls Female() and must wait until a male whale and a
matchmaker are present, and a matchmaker calls Matchmaker() and must wait until a
male and female are present. Once all three are present, all three return out of the methods
(representing that a match has been made). Note that there can be many whales mating at
the same time, and so your solution must correctly handle the various cases that could
potentially arise (single male, female, matchmaker; multiple of each; only some of each;
etc.). If there are multiple whales of a given kind, when there is a mating it does not matter
which one mates, just that only one of them mates. Further, until whales of all three kinds
arrive, the whales of the other kinds must wait.
Structurally, this problem is similar to the "Mailbox" problem above. You can implement
the "Whale" class in synch.{h,cc} or in new whale.{h,cc} files. You will write test
programs in threadtest.cc (or another test file of your creation) that create a Whale
instance and then multiple threads to play the roles of male, female, and matchmater by
synchronizing through the Whale instance. Be sure to write test cases for the various
combinations of whale situations.

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...

Strategy for tackling Nachos projects


Here are some suggestions for getting started on the project. This reflects my style of
programming, but for each problem I would recommend implementing features one at a
time and testing each feature first before moving on to the next. One example of this is
part 1 of project 1 first, ignore condition variables for a moment and concentrate on
implementing the Lock class just by itself. Don't let how CVs work confuse you about
Locks. I would implement Acquire and Release, and then write a couple of test
programs to test basics of the Lock: have one thread acquire the lock, force a context
switch with Yield, have another thread try to acquire the lock to make sure it sleeps, etc.
One you finish the Lock and finish testing the Lock, then move on to CVs, etc. And
with Join, I would first implement the case where Join is called on a thread that still
exists (the easier case), and then write a test program to make sure that it works as
expected. And then I would implement the part of Join where Join is called on a thread
that has already finished. And so on. It may seem like it would take longer to make
progress, but in the long run it is faster because you don't have to debug lots of code at
one time.

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.

Implementing the methods


Let's start with Acquire. We need to make Acquire atomic. If we look at the
implementation of synch.cc:Semaphore::P(), we see that it disables interrupts for
atomicity. So we will do the same for Lock::Acquire. P() has to worry about keeping
track of a counter before it blocks, but for the Lock we don't. All we need to do is check
whether some other thread holds the Lock and block if so.
So pseudo code for Lock::Acquire is going to look something like:
Lock::Acquire()
{
disable interrupts;
while (the lock is held) {
put this thread (the current thread) on the wait Q for the lock;
block this thread (make the current thread go to sleep);
}
do some bookkeeping;
enable interrupts;
}

Each of these actions is implemented in Semaphore::P(), so you should be able to look


at the code for Semaphore::P() and do essentially the same thing.

We can do the same thing for Lock::Release:


Lock::Release()
{
disable interrupts;
remove a blocked thread from the Q, if there is one;
unblock the removed thread;
do some bookkeeping;
enable interrupts;
}

Again, each of these operations is used in the implementation of Semaphore::V(), so we


can essentially just copy them into the implementation of Lock::Release().
NOTE: I did not check for error conditions in this pseudo code, but you will want to in
your project solutions.

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:

Example Nachos Problem Writeup


The following is an example skeleton writeup for a solution to the Mailbox problem.
You don't have to describe your code line by line, just give us enough information to
understand your approach and how you tested it. And this is just an example skeleton -if there is a more convenient way for you to describe your solution, please use it.

1. Locks and Conditions


...

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

Das könnte Ihnen auch gefallen