CMSC 412 Project #3
Synchronization
Due Tuesday, March 23, at 6:00pm
New user programs for testing in src/user/
Overview
Synchronization Implementation. You will implement semaphores, a simple synchronization primitive. In addition, you will provide user programs with semaphore-manipulating system calls.
Semaphores
You will add system calls that provide user programs with semaphores, to enable thread synchronization among different threads. The system calls (on the user side) will be:
int
Open_Semaphore(const char *name, int ival)
int P(int sem)
int V(int sem)
int
Close_Semaphore(int sem)
Open_Semaphore
Open_Semaphore(name, ival) is a request by the current process to use a semaphore. The user gives a name for the semaphore, as well as the semaphore’s initial value, and will get back a semaphore ID, an integer between 0 and N - 1. The semaphore ID denotes a particular semaphore datastructure in the kernel, which you must implement. The semaphore ID is then passed by the user program to the operations P() and V(), described next, to wait or signal the associated semaphore.
Your operating system should be able to handle at least 20 (thus N = 20) semaphores whose names may be up to 25 characters long. If there are no semaphores left (i.e., there were N semaphores with unique names already given), ENOSPACE must be returned indicating an error.
The returned semaphore ID is chosen in one of two ways.
Think of a semaphore ID as like a file descriptor in UNIX: in that case, when you open a file, you get back a number (the file descriptor) that denotes that file. Subsequent read and write operations take that file descriptor as an argument, and the kernel figures out which file the number is associated with, and then performs the operations on that file. Just the same way, you will implement a semaphore datastructures within the kernel, and refer to them from user programs via their associated semaphore IDs.
P and V
The P(sem) system call is used to decrement the value of the semaphore associated with sempahore ID sem. This operation is referred to as wait() in the text. Similarly, the V(sem) system call is used to increment (signal() in the text) the value of the semaphore associated with sem.
As you know, when P() is invoked using a semaphore ID whose associated semaphore's count is less than or equal to 0, the invoking process should block. To block a thread, you can use the Wait function in the kernel. Each semaphore data structure will contain a thread queue for its blocked threads. The file thrqueue.h provides an implementation of a thread queue. You should look at kthread.h and kthread.c to see how it is declared and used. To wake up one thread/all threads waiting on a given semaphore, i.e. because of a V(), you can use Wake_Up_One()/Wake_Up() routines from kthread.h.
A process may only legally invoke P(sem) or V(sem) if sem was returned by a call to Open_Semaphore for that process (and the semaphore has not been subsequently destroyed). If this is not the case, these routines should return EINVALID.
Close_Semaphore
Close_Semaphore(sem) should be called when a process is done using a semaphore; subsequent calls to P(sem) and V(sem) (and additional calls to Close_Semaphore(sem) by this process) will return EINVALID.
Once all processes using the semaphore associated with a given semaphore ID have called Close_Semaphore, the kernel datastructure for that semaphore can be destroyed. A simple way to keep track of when this should happen is to use a reference count. In particular, each semaphore datastructure can contain a count field, and each time a new process calls Open_Semaphore, the count is incremented. When Close_Semaphore is called, the count is decremented. When the count reaches 0, the semaphore can be destroyed.
When a thread exits, the kernel should close any semaphores that the thread still has open. In your code, both the Sys_Close_Semaphore() function and at least some function involved in terminating user threads should be able to invoke the "real" semaphore-closing function.
Notes
In order not to clobber
syscall.c with too much functionality, you must put your semaphore
implementation in two new files sem.h and sem.c. Semaphore operations
need to be implemented within a critical section, so that operations execute
atomically.
Since you need to have multiple processes running concurrently to test the
functionality you will implement, your shell should be able to launch processes
in background. You can do as you did in either project 1 or 2.
In this, and other projects, you will rely heavily upon a list data structure. For this reason an implementation has been provided to you in list.h file. Please familiarize yourself with its syntax and functionality. It could be a little tricky to understand the syntax since functions are written using #define. Naturally you are always free to extend, modify, or write your own implementation that would better suit your needs.
Summary: New System Calls
Identifier |
Kernel Function |
User Function |
Effect |
SYS_OPEN_SEMAPHORE |
int Sys_Open_Semaphore(struct Interrupt_State* state) |
int Open_Semaphore(const char *name, int ival) |
if name is longer
than 25 characters, return ENAMETOOLONG. |
SYS_P |
int Sys_P(struct Interrupt_State* state) |
int P(int sem) |
might block |
SYS_V |
int Sys_V(struct Interrupt_State* state) |
int V(int sem) |
never blocks |
SYS_CLOSE_SEMAPHORE |
int Sys_Close_Semaphore(struct Interrupt_State* state) |
int Close_Semaphore(int sem) |
never blocks |
Testing your code
The files we provided can be used to test your semaphores:
% /c/ping.exe &
%
/c/pong.exe
Final Notes
We do not require that your
earlier projects worked; you should be able to implement this project directly
from the base kernel, without using the earlier kernel features. Perhaps
a small exception to this is the convenience of background processes for
testing purposes, but this is straightforward. However, the base kernel
that we have provided will have some important system calls and other routines
unimplemented. However, you can "implement" these routines by
simply removing the TODO("...") and having them return 0. This
goes for Sys_RegDeliver, Sys_Signal, and Check_Pending_Signal.
If you do choose to merge your kernel with the initial kernel for this
project (preferred), here is a trick that may help you; it takes two steps:
Let's assume in your home directory you have three directories: project2, project2-solution, project3.
So let's find the implementation differences first:
1.
2. $ cd ~/project2/src/geekos
3. $ diff -u -r . ~/project2-solution/src/geekos > ~/diff.patch
5.
6. $ cd ~project3/src/geekos
7. $ patch -p0 < ~/diff.patch
8. $ rm *orig
You will likely get a few errors when doing this, particularly in kthread.c. This is because some functions that you changed to get project 2 to work have been changed in the base kernel for this project; for example, this may be the case for functions Make_Runnable and Get_Next_Runnable For these functions, integrate the code by hand. You can look at the generated .rej files to see what parts of the merge didn't work out.