CSE 120:
Principles of Operating Systems
(Summer, 2005)
Home | Syllabus |
Schedule | Exams |
Homework | Projects
Projects Home | Subversion |
Subversion and Project 1 |
Javadoc | Autograder |
Autograder2
Projects
Project 1:
Stock Nachos has an incomplete thread system. In this assignment, 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 have
written for you. This thread system implements thread fork, thread completion,
and semaphores for synchronization. It also provides locks and condition variables
built on top of semaphores.
After installing the Nachos distribution, run the program nachos (in the proj1
subdirectory) for a simple test of our code. This causes the methods of nachos.threads.ThreadedKernel
to be called in the order listed in threads/ThreadedKernel.java:
The ThreadedKernel constructor is invoked to create the Nachos kernel.
This kernel is initialized with initialize().
This kernel is tested with selfTest().
This kernel is finally "run" with run(). For now, run() does nothing,
since our kernel is not yet able to run user programs.
Trace the execution path (by hand) for the simple test cases we provide. When
you trace the execution path, 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 TCB.contextSwitch(), that thread stops executing,
and another thread starts running. The first thing the new thread does is to
return from TCB.contextSwitch(). We realize this comment will seem cryptic to
you at this point, but you will understand threads once you understand why the
TCB.contextSwitch() that gets called is different from the TCB.contextSwitch()
that returns.
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 KThread.yield() (causing the scheduler to choose another thread to
run) anywhere in your code where interrupts are enabled, and your code should
still be correct. 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 KThread.yield() to
be called on your behalf in a repeatable (but sometimes 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
-s <some-long-value>", with a different number each time, calls to
KThread.yield() will be inserted at different places in the code.
You are encouraged to add new classes to your solution as you see fit; the
code we provide you is not a complete skeleton for the project. Also, there
should be no busy-waiting in any of your solutions to this assignment.
The Code Base
You should do "gmake" and run nachos in proj1.
To run nachos from your proj1 directory, execute:
../bin/nachos
Most
of the code you will work with resides in nachos/threads.
You'll want to write testcases in the selfTest method of some module. For instance, to test the join method
of the KThread class,
edit the selfTest method of KThread. Note that this method currently creates two threads and runs them.
You'll want to replace that code with code that, for example, creates a thread, and then joins that thread.
You'll want to make sure the join doesn't finish until the first thread finishes.
You'll probably find it useful to use the debugging log facilities of Nachos.
The -d flag of nachos uses the next argument to determine what subsytem of nachos to print debug
statements for. In particular, -d t displays threading log info.
So, in order to see (lots of) thread logging info, you'll want to use:
../bin/nachos -d t
Living with the Autograder
We are in the process of setting up an autograder. In order for you to verify
that your code is compatible with the autograder, there are some simple compatibility
tests you can run before you submit.
Since your submissions will (most likely) be processed by a program, there
are some very important things you must do, as well as things you must not do.
For all of the projects in this class...
- Do not modify Makefile, except to add source files. While we will ask you
to handin your entire nachos directory, the autograder will typically take
only certain files from your directory. Your Makefile will not be used.
- Only modify nachos.conf according to the project specifications. Like the
Makefile, your nachos.conf file will not be used. Do not rely on any additional
keys being in this file.
- Do not modify any classes in the nachos.machine package, the nachos.ag package,
or the nachos.security package.
- Do not add any new packages to your project. All the classes you submit
must reside in the packages we provide.
- Do not modify the API for methods that the autograder uses. This is enforced
every time you run Nachos by Machine.checkUserClasses(). If an assertion fails
there, you'll know you've modified an interface that needs to stay the way
it was given to you.
- Do not directly use Java threads (the java.lang.Thread class). The Nachos
security manager will not permit it. All the threads you use should be managed
by TCB objects (see the documentation for nachos.machine.TCB).
- Do not use the synchronized keyword in any of your code. We will grep for
it and reject any submission that contains it.
- Do not directly use Java File objects (in the java.io package). In later
projects, when we start dealing with files, you will use a Nachos file system
layer.
For this part, the only package that the autograder will use from your code
is nachos.threads, so don't add any source files to any other package. The autograder
will not call ThreadedKernel.selfTest() or ThreadedKernel.run(). If there is
any kernel initialization you need to do, you should finish it before ThreadedKernel.initialize()
returns. There are some mandatory autograder calls in the KThread code. Leave
them as they are.
- Nachos implements kernel threads via the class KThread in the file threads/KThread.java.
In this class, a method KThread.join() has been defined but not implemented;
your task is to implement this method. Suppose that thisThread is a thread object.
Then, thisThread->join() should cause the caller to wait until the thread "represented" by thisThread finishes.
If thisThread was already finished at the time of the call, then the call should return right away.
Note that another thread does not have to call join(), but if it is called, it must be called only once
(i.e. your solution does not need to conform to any requirements as to what happens if join()
is called a second time on the same thread). A thread must finish executing normally whether or not it is joined.
Your solution should not enable or disable interrupts directly.
- Implement condition variables directly, by 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 directly using semaphores
(you may of course still use locks, even though they indirectly use semaphores).
Once you are done, you will have two alternative implementations that provide the exact same functionality.
Your second implementation of condition variables must reside in class nachos.threads.Condition2
(that is, in the file threads/Condition2.java).
- A class called Alarm has been implemented in threads/Alarm.java.
This class uses the hardware timer interrupt to provide two services: (a)
preemption for preemptive CPU scheduling via the timerInterrupt() method , and (b)
to allow a thread to suspend its own execution until time has advanced to at least now + x
via the waitUntil(int x) method. This is useful for threads that operate in real-time,
for example, for blinking the cursor once per second. Currently, waitUntil(int x) is implemented
using busy waiting. Your task is to change the implementation of waitUntil(int x) so that the calling
thread blocks. You will need to modify timerInterrupt() as well to wake up sleeping threads when it is time.
There is no requirement that threads start running immediately after waking up;
just put them on the ready queue in the timer interrupt handler after they have waited for at least
the right amount of time. Do not fork any additional threads to implement waitUntil();
you need only modify waitUntil() and timerInterrupt(). waitUntil is not limited to one thread;
any number of threads may call it and be suspended at any one time.
- Implement synchronous send and receive of one word messages (also known as Ada-style rendezvous),
using condition variables (don't use semaphores!). Implement the Communicator class
(file threads/Communicator.java) with operations: void speak(int word) and int listen().
speak() atomically waits until listen() is called on the same Communicator object, and then
transfers the word over to listen(). Once the transfer is made, both can return.
Similarly, listen() waits until speak() is called, at which point the transfer is made, and both
can return (listen() returns the word).
Your solution should work even if there are multiple speakers and listeners for the same Communicator
(note: this is equivalent to a zero-length bounded buffer; since the buffer has no room,
the producer and consumer must interact directly, requiring that they wait for one another).
Each communicator should only use exactly one lock. If you're using more than one lock,
you're making things too complicated. Here are some tips for using Condition Variables for such problems.
- Your job is to help the formation of carbon dioxide (CO2).
You need to get one C atom and two O atoms all together at the same time.
The atoms are threads. Each C atom invokes a procedure cPresent() when it's ready to react,
and each O atom invokes a procedure oPresent() when it's ready. For this problem, you are to
write the code for cPresent() and cPresent(). The procedures must delay until there are at least
two O atoms and one C atom present, and then one of the procedures must call the procedure makeCarbonDioxide.
(which just prints out a debug message that a molecule was made). After the makeCarbonDioxide call,
two threads waiting in cPresent() and one thread waiting in cPresent() should return.
Write the code for cPresent() and cPresent() using either semaphores or
locks and condition variables for synchronization.
Your solution must avoid starvation and busy-waiting.
The class file that you should edit is in nachos/threads.
The Autograder will create one ReactCarbonDioxide object and a bunch of threads.
Each thread will invoke either the cPresent or oPresent routine.
Your task is only to write the ReactCarbonDioxide class correctly.