For this project, we assume that you will be working in the same groups as for JOS.
Tasks:
In the beginning (well, the relative beginning), there was UNIX. UNIX supported multiprogramming; that is, there could be multiple independent processes each running with a single thread of control.
As new applications were developed, programmers increasingly desired multiple threads of control within a single process so they could all access common data. For example, databases might be able to process several queries at the same time, all using the same data. Sometimes, they used multiple processes to get this effect, but it was difficult to share data between processes (which is typically viewed as advantage, since it provides isolation, but here it was a problem).
What they wanted was multithreading support: the ability to run several threads of control within a single address space, all able to access the same memory (because they all have the same mapping function). The programmers realized that they could implement this entirely in the user-level, without modifying the kernel at all, if they were clever.
As you'll discover in the last part of this assignment, there were some problems with this approach, which motivated kernel developers to include thread support in the kernel itself (and motivated researchers to do it better; see Scheduler Activations).
We recomend using gitlab as a version of source control for the project. The starter code can be downloaded for the project here.
simplethreads contains a lot of files, but you will only need to implement a few skeleton files and do not need to understand the details of the rest of the provided code. Pay attention to these directories and files:
Like many Unix programs, simplethreads uses a configure script to determine parameters of the machine needed for compilation. (In fact, you'll find many UNIX packages follow exactly the same build steps as simplethreads). In the simplethreads directory, run ./configure to generate an appropriately configured Makefile.
After running ./configure, you can immediately compile and link the distributed code. In the top directory, type make. (You can also build an individual part, such as the library, by running make in a subdirectory. Or, if you just want to compile one file, run make myfile.o from within the directory containing myfile.c.) Note that, as you make changes to the source, it is only necessary to repeat the last step (make).
Run make check. The test programs in test/ will be run, and messages will be printed to indicate whether or not each test had the correct result. Success at this point is simply having the test programs execute at all -- they will all fail, because you haven't yet completed the implementation of simplethreads.
A useful feature of simplethreads is that the underlying thread implementation can be configured to either use native, kernel-provided threads (pthreads) or the user-level threads that you'll implement (sthreads). Because both provide the same interface (once you've completed parts 1 and 2, anyway), applications that use simplethreads don't even know which version they're using. The default is sthreads, but to select pthreads instead, run configure --with-pthreads, then make clean.
In summary, the steps to get started are:
If you add a new source file, do the following:
For part 1, we give you:
It is your job to complete the thread system, implementing:
The routines in sthread_ctx do all of the stack manipulation, register storing, and nasty stuff like that. You should be able to use them based on the semantics explained in that file. Rather than focusing on the details of that low level manipulation, this assignment focuses on managing the thread contexts. Viewed as a layered system, you need to implement the green sthread_user box below:
At the top layer, applications use the sthread package (through the API defined in sthread.h). Immediately below that, sthread.c performs its function by using either the routines in sthread_pthread.c (a thin veneer over pthreads) or your implementation in sthread_user.c. (The choice depends on a switch you gave (or didn't give) when you ran configure before building the sthread library.) Your sthread_user.c, in turn, builds on the sthread_ctx functions (as described in sthread_ctx.h).
Applications (the top-layer) may not use any routines except those listed in sthread.h. They must not know how the threads are implemented; they simply request that threads be created (after initializing the library), and maybe request yields/exits. For example, they have no access to sthread_queue. Nor do they keep lists of running threads around; that is the job of the green box.
Similarly, your green box - sthread_user.c - should not need to know how sthread_ctx is implemented. Do not attempt to modify the sthread_ctx_t directly; just use the routines declared in sthread_ctx.h.
[New Thread
1024 (LWP 18771)]
. These messages refer to kernel threads.
For part 2, you'll use the thread system you wrote in part 1, extending it to provide support for mutexes and condition variables. Skeleton functions are again provided in lib/sthread_user.c. You need to:
So far, your threads are non-preemptive, which gives you atomic critical sections. For this part, get your synchronization primitives working with this non-preemptive assumption and start thinking about where the critical sections are (add comments if you find it useful). When you add preemption, you will have to go back and add appropriate protection to critical sections. The details about this are in part 4.
There are several famous synchronization problems in computer science. For part 3, your job is to implement a "food services" problem, an instance of the better-known multiple-producer, multiple-consumer problem. There are N cooks (each a separate thread) that constantly produce burgers. We'll assume for debugging purposes that each burger has a unique id assigned to it at the time it is created. The cooks place their burgers on a stack. Each time a cook produces a burger, she prints a message containing the id of the burger just cooked. There are M hungry students (each a separate thread) that constantly grab burgers from the stack and eat them. Each time a student eats a burger, she prints a message containing the id of the burger she just ate. Ensure, for health and sanity reasons, that a given burger is consumed at most once by at most one student.
Note that you are implementing an application now. That means the only interface to the thread system that you should use is that described by sthread.h (as distributed in the tar.gz). Do not use functions internal to sthreads directly from your solution to this problem.
Place your solution in a new file called test-burgers.c in the test directory (see the "To Add a Source File" section above for directions to build it). Make the program take 3 command-line parameters: N, M, and the total number of burgers to produce. For example, test-burgers 3 2 100 should simulate 3 cooks, 2 students, and 100 total burgers.
In this part, you will add preemption to your threads system. This part of the project is not a lot of work (it represents perhaps only 10% of the code you will write), but it's a little tricky. We've made it the last part of the project (aside from the "Report" part) so you won't get stuck on it and fail to get the multi-threaded web server running. (The multi-threaded web server does not require preemption.)
We provide you with:
See sthread_preempt.h for more details on the functions you are given.
It is your job to:
To initialize the preemption system, you must make a call to sthread_preemption_init, which takes two arguments: a function to run on every interrupt, and a period in microseconds, specifying how often to generate the timer interrupts. For example, sthread_preemption_init(func,50) will call func every 50 microseconds. You should add a call to sthread_preemption_init as the last line in your sthread_user_init() function. Make it so that the thread scheduler switches to a different thread on each interrupt.
The hard part will be figuring out where to add synchronization to your thread management routines. Think about what would happen if you were interrupted at various points in your code. For example, we don't want to be preempted when we're inside yield() and in the middle of switching to a different thread. The way to ensure that this never happens is by disabling interrupts. You are provided with a function splx(int splval), where splx(HIGH) disables interrupts and splx(LOW) enables them. Here is an example:
int oldvalue = splx(HIGH); // disable interrupts; put old // interrupt state (disabled / // enabled) into oldvalue // {critical_section}; splx(oldvalue); // restore interrupts to // original stateYou are also provided with two other synchronization primitives: atomic_test_and_set and atomic_clear. See sthread_preempt.h for a usage example. These will be useful for less important critical sections, such as ready queue manipulation. Note that it is also necessary to synchronize your mutex and condition variable code. For example, in sthread_user_mutex_lock, you will want to use atomic_test_and_set for grabbing a lock.
A note: timer interrupts are set up so that they only fire when you are executing your code (either the user application or the thread library). Interrupts occuring inside printf or other system functions will be dropped. Also, interrupts in the critical assembly code inside sthread_switch are also dropped. This simplifies your task greatly.
"Stress testing" is important. It's common to get this part of the project "90% correct." You may forget to disable interrupts in just one or two situations. This may not show up with cursory testing. That's what "race conditions" are all about -- they're devilishly difficult to track down because they're timing-dependent. But eventually they will show up!
IMPORTANT! The code that we have provided to support preemption works correctly only on Linux machines with an x86 architecture! Do not attempt this portion of the assignment on a Mac or a non-x86 machine! Also, at least one student in the past has reported weirdness in the preemption code when running on an AMD processor, so to be safe you may wish to stick to Intel processors (cat /proc/cpuinfo to check).
Include the following in a report to be turned in electronically on the due date. This should be at most 4 pages long.
Briefly describe the design of your user-level threads, synchronization primitives. Mention any design decisions you had to make, and why you made them.
Does your implementation work? Which parts work and which don't? For the ones that don't work, how do you think you would fix them?
Discuss conclusions you have reached from this project. What did you learn? What do you wish you had done differently?
First, make sure that you have followed the steps described above to add a new source file / test program / arbitrary file to your code. If you do not follow these instructions and run the autoreconf command, then these files may not be included in your submission.
Now, in your top-level simplethreads directory, run the following commands:
make distclean ./configure make dist
This will produce a file named simplethreads-2.01.tar.gz. Run tar tzvf simplethreads-2.01.tar.gz to check that all of the simplethreads files, and any new files you have added, are included in the archive. You can also extract the archived files via tar xzvf simplethreads-2.01.tar.gz. Upload this archive to the Catalyst DropBox for the class. Please ensure your report is contained in the archive that you submit to the DropBox.