Homework 3 is due Monday 3/5 at 12:01 AM ET.
The written problems and the programming problems are to be done individually. All homework submissions are to be made via Courseworks. Refer to the homework policy section on the class web site for further details.
Written Assignment (40 pts)
Exercise numbers refer to the course textbook, Modern Operating Systems(3rd edition). Each problem is worth 5 points. Make your answers concise. You'll lose points for verbosity.
- MOS 2.13 (assume uniprocessor and disk with unlimited bandwidth) In this problem your are to compare reading a file using a single-threaded file server and a multithreaded server. It takes 15 msec to get a request for work, dispatch it, and do the rest of the necessary processing, assuming that the data needed are in the block cache. If a disk operation is needed, as is the case one-third of the time, and additional 75msec is required, during which time the thread sleeps. How many requests/sec can the server handle if it is single threaded? If it is multithreaded?
- MOS 2.26 Show how counting semaphores(i.e., semaphores that can hold an arbitrary value) can be implemented using only binary semaphores and ordinary machine instructions.
- MOS 2.29 Synchronization within monitors uses condition variables and two special operations wait and signal. A more general form of synchronization would be to have a single primitive waituntil, that had an arbitrary Boolean predicate as parameter. Thus one could say, for example,waituntil x<0 or y+z<n The signal primitive would no longer be needed. This scheme is clearly more general then that of Hoare or Brinch Hansen, but it is not uses. Why not? Hint: Think about the implementation.
- MOS 2.46 (use figure 2-46 instead of 2-20) Consider the procedure put_forks in Fig. 2-46. Suppose that the variable state[i] was set to THINKING after the two calls to test, rather than before. How would this change affect the solution?
- MOS 2.50 Solve the dining philosophers problem using monitors instead of semaphores.
- MOS 2.51 Suppose that a university wants to show off how politically correct it is by applying the U.S. Supreme Court's "Separate but equal is inherently unequal" doctrine in gender as well as race, ending its long-standing practice of gender-segregated bathrooms on campus. However, as a concession to tradition, it decrees that when a woman is in a bathroom, other women may enter, but no men, and vice versa. A sign with a sliding marker on the door of each bathroom indicates which of three possible states it is currently in: Empty, Women present, Men present. In some programming language you like, write the following procedures woman_wants_to_enter, man_wants_to_enter, woman_leaves, man_leaves. You may use whatever counters and synchronization techniques you like.
-
Draw a thread schedule with two threads to show the error in the following software-based lock implementation.
int flag[2] = {0, 0}; // lock flags, one for each thread. // 1 indicates a thread wants to enter // the critical region; 0 indicates it doesn't lock() { flag[self] = 1; // self is the current thread's ID. while(flag[1-self]==1) // 1-self is the other thread's ID ; } unlock() { flag[self] = 0; }
- Will Eraser report a race on the following code? Why or why
not? Read about "bitfield" in 'K&R' if you don't know what it is.
struct { lock l1; lock l2; int f1:1; // bitfield, uses only 1 bit int f2:1; // bitfield, uses only 1 bit } s; // thread T1 // thread T2 lock(s.l1); lock(s.l2); s.f1 = 1; s.f2 = 1; unlock(s.l1); unlock(s.l2);
Programming Assignment (60 pts)
This programming assignment has two parts. In Part A, you'll extend xv6 to support threads; in Part B, you'll implement a readers-writer lock that favors writers.
The files needed for this assignment are distributed using the Git distributed version control software. To learn about Git, take a look at the Git user's manual, or, if you are already familiar with other version control systems, you may find this Git overview useful.
To get started, run the following commands
$ git clone http://debug.cs.columbia.edu/xv6.git xv6 $ cd xv6 $ git checkout -b hw3 origin/hw3
Submission instructions
When you are ready to hand in your solution, put your UNI in conf.mk, commit all your changes, and run make handin in the xv6 directory. This will generate hw3-programming-<your uni>.tar.gz for you, which you can then upload via courseworks. This tar ball includes a snapshot of all xv6 source code and a patch generated using git diff origin/hw3 hw3. Thus, be sure you commit all your changes (including the new files you add) before running make handin.
We will be grading your solutions with a grading program. You can run make grade to test your solutions with the grading program.
Part A: Implement Threads (40 pts)
Threads are essential in modern operating systems, especially because of the rise of the multicore. In this part of the assignment, you will modify the xv6 kernel to support threads.
We have provided you a basic thread design in xv6. We represent both processes and threads uniformly using struct proc. Each proc structure defined in proc.h has a unique ID, so that we can distinguish these structures. Since threads share address space, open file table, and other per-process data structures, we use struct threadcommon to keep track of these shared data structures. The first thread in a process is called the "main thread," and all other threads are called "non-main threads." When a process is created, its main thread is also created and the threadcommon field is initialized. Later, when non-main threads are created, their common pointers should point to the main thread's threadcommon field.
For Part A, you'll need to implement the following three system calls:
- int tfork(void (*entry)(void *), void *arg, void
*spbottom);
System call tfork() creates a new thread that runs function entry on argument arg, using stack spbottom. It can be called by both main or non-main threads. File hw3-test-multiple.c illustrates how to use tfork(); file hw3-common.h shows how to allocate stack space for threads before calling the system call. In our implementation, users must first allocate the stack. Note that on x86 stack grows downwards. In addition, to simplify your implementation, assume that function entry must call texit() to exit the current thread. tfork() should return -1 on error, and the ID of the newly created thread on success.Exercise 2. From homework 2 Written Assignment #1, you already read swtch.S and have a clear picture of how xv6 do context switching. Check them again and make sure you put correct information when you create a new thread to run a specific function before you implement tfork. - int twait(int tid);
System call twait(tid) is similar to the wait() system call, except it waits for a specific thread within the current process. This function should return -1 if tid is the main thread. That is, a non-main thread cannot wait for the main thread. - int texit(void);
System call texit() exits the current thread. It should return -1 if it is called by the main thread.
As discussed in class, adding thread support may require a re-design of process operations. We describe the changed semantics of the xv6 process operations below:
- getpid() called by any thread should return the pid of the main thread in the current process. In other ways, we use the main thread of a process to represent this process.
- kill(int pid) kills an entire process, including all threads. It should return -1 if pid is not the id of a main thread.
- wait() should return when one of the child processes exit; it should not return when one of the threads in the current process exits.
- exec() is not allowed if there are more than one thread in the current process.
- exit() should exit the current process, including all threads.
For Part A, you'll also need to modify these process operations so that they work correctly in the presence of threads. Note that the threadcommon field is shared by all threads in a process, and you must protect accesses to this field using a lock. We've defined this lock for you as the lock field in threadcommon, but you need to acquire and release this lock at the right places. We also define some macros in proc.h which may be helpful for your implementation.
Part B: Implement a Readers-Writer lock that favors writers (20 pts)
The page table of a process is also shared by all threads in the process and needs protection. For example, to get a system call argument, the kernel uses the current process's page table to translate a user address to a kernel address. Similarly, when a process calls sbrk(), the kernel modifies the page table of this process accordingly. If we are not careful, we may run into kernel panics. Consider the following scenario. Thread t1 issues a system call and traps into the kernel, and the kernel tries to access a system call argument defined on t1's user stack. Meanwhile, thread t2 calls sbrk() to unmap thread t1's user stack. Without proper synchronization, the kernel may panic.
Although we can protect the page table using the same lock that is used to protect the threadcommon structure, doing so would create unnecessary overhead, but most of the system calls only read the page table without modifying it. In fact, the only system call updates the page table is sbrk().
Thus, we would like to use a readers-writer lock to protect the page table. Only sbrk() grabs this lock in write mode, and all other system calls grab this lock in read mode. In addition, we would like to favor the writer because sbrk() is often called to expand the heap, i.e., the calling process is low on memory.
We have provided you a skeleton implementation of a readers-writer lock in rwlock.c and rwlock.h, and added a readers-writer lock pglock in the threadcommon field. In addition, we've added calls to grab this lock in read mode or write mode at various places.
For Part B, you'll need to implement the following functions defined in rwlock.c:
- void initrwlock(struct rwlock *m) initializes the readers-writer lock m.
- void destroyrwlock(struct rwlock *m) frees m.
- void readlock(struct rwlock *m) acquires m in read mode.
- void readunlock(struct rwlock *m) releases m which was acquired in read mode.
- void writelock(struct rwlock *m) acquires m in write mode.
- void writeunlock(struct rwlock *m) releases m which was acquired in write mode.
Note that your implementation must favor writers over readers: whenever there is a thread waiting to acquire the lock in write mode, you should let this thread acquire the lock as soon as possible, To illustrate this point, consider the following example. Suppose the lock is held in read mode and a thread A is waiting to acquire the lock in write mode. Another thread B comes, trying to acquire the lock in read mode. In an implementation that favors readers over writers, thread B can immediately acquire the lock in read mode and proceed. However, in your implementation, thread B must wait and thread A should grab the lock first.