Version History

Version 1

Initial release.


Your task is to extend the support code we give you to a kernel with preemptively scheduled processes and kernel threads. This will consist mainly of writing the timer interrupt handler. You will also implement several synchronization primitives.

David Eisenstat is the TA in charge of this project.

Dates, Times, and Places

Precept I

October 16 and 17, 20:30–21:30, in COS 105. Slides (eventually):

Design Review

October 22, 18:00–22:00, in FC 010 (Fishbowl). Sign up at

Precept II

October 23 and 24, 20:30–21:30, in COS 105. Slides (eventually):

Project Due

November 5, 23:59

Design Review

To be determined precisely. Expect as usual that we will ask you to provide and discuss pseudo-code or code describing how the various parts of the system will work.



Acronym for Interrupt Descriptor Table. This table tells the hardware how to handle interrupts.


Acronym for Interrupt ReQuest.


IRQ 0 is the timer interrupt. Tasks that don’t yield will be interrupted, allowing the kernel to regain control.


When IRQ 0 is enabled, the hardware will occasionally generate spurious IRQ 7s that will crash the kernel if not handled.

Getting Started

The support code is in Type make to prepare an image file floppy.img for use with bochs. In the fishbowl, make boot loads this image onto a USB stick. Change /dev/sdf to the right device elsewhere.

Some versions of gcc may not support the flag -fno-stack-protector. If you are using one of these versions, it is safe to remove this flag.

Interrupt Handling

The x86 interrupt mechanism is quite complicated, so we will present only the details relevant to this project. Interrupts can be triggered in several different ways: hardware, software, exceptions. The timer interrupt, IRQ 0, is a hardware interrupt, as is IRQ 7. The system call mechanism, which uses the int instruction, is a software interrupt. Exceptions are generated for a variety of invalid actions: dividing by zero, for example, will cause an exception.

The support code installs handlers for IRQ 0, IRQ 7, the system call interrupt, and the various exceptions. Your job for this part is to complete the handler for IRQ 0, entry.S:irq0entry; the others are given to you as examples. The main file you will be working with is entry.S, but you may need to consult other files, especially interrupt.[ch] and scheduler.[ch]. In particular, the definition of the PCB structure has moved to scheduler.h.

The processor takes the following actions on an interrupt.

  1. Disable interrupts.

  2. Push the flags register and the instruction pointer onto the stack.

  3. Jump to the interrupt handler.

Note that if a process is interrupted, the memory addresses above (on the stack; below in physical memory) the top of its stack will be overwritten. This is unfortunate, but we can’t avoid it until processes run at a lower privilege level. For this project, and this project only, the kernel may use user stacks. Once the interrupt handler has run, it returns control to the interrupted task with the iret instruction, which pops the instruction pointer and the flags register, and enables interrupts.

As with entry.S:syscallentry, it may be some time before entry.S:irq0entry actually performs the iret, since other tasks may execute in the middle. IRQ 0 is a hardware interrupt, so you should acknowledge it quickly using the macro entry.S:SENDEOI—otherwise the hardware won’t deliver any more timer interrupts. The other tasks entry.S:irq0entry must perform are

  1. Increment the 64-bit counter numticks.

  2. Increment entry.S:disablecount to reflect the fact that interrupts are off.

  3. If nestedcount is zero, enter the kernel the way a system call would.

  4. Yield.

  5. If nestedcount was zero, leave the kernel the way a system call would.

  6. Decrement entry.S:disablecount in anticipation of

  7. iret.

nestedcount is a field in the PCB. For processes, it is 1 during a system call and 0 otherwise. For threads, it is always 1. You should study how interrupt.c changes this field during a system call.

This may not look very hard, but the tricky part is managing when interrupts are on, and when they are off. Your solution should satisfy the following two properties:


To avoid race conditions, interrupts should be off whenever your code is accessing kernel data structures, primarily the PCBs and task queues.


Interrupts should be on most of the time so that threads, processes, and system calls that take too long are preempted.

Synchronization Primitives

The main files you will edit here are sync.[ch], but you will probably need to consult queue.[ch] and scheduler.[ch]. You have to implement condition variables, semaphores, and barriers. We’ve given you an implementation of locks as a reference. These primitives must operate correctly in the face of preemption by turning interrupts on and off with the functions entercritical and leavecritical.

In the descriptions below, if an operation is performed on a uninitialized structure, the results are up to you. All unblocks should place the unblocked thread at the end of the ready queue.

Condition Variables

conditioninit(cond) initializes the specified memory to hold a condition variable. As prerequisites to calling conditionwait(lock, cond), the calling thread holds lock. The calling thread atomically releases the lock and blocks until unblocked by one of the following calls. It reacquires the lock before returning. conditionsignal(cond) unblocks the thread that blocked least recently during a conditionwait(cond), if there is one. conditionbroadcast unblocks all threads that blocked during a conditionwait(cond). The order in which the threads are unblocked is not important.


semaphoreinit(sem, value) initializes the specified memory to hold a semaphore whose starting value is value. value is non-negative. semaphoredown(sem) performs a blocking wait until the semaphore value is positive and then decrements it. semaphoreup(sem) increments the semaphore value, unblocking the thread that blocked least recently with a semaphoredown(sem), if that thread exists.


barrierinit(bar, n) initializes a barrier for use by n threads. When a thread calls barrierwait(bar), it blocks until n threads have called barrierwait(bar). The order in which the threads are unblocked is not important.

Extra Credit

The sleep()/dosleep() calls work by spinning on numticks. We did this partially to test your implementation of system call preemption and partially so that you could earn 1 point of extra credit by implementing a blocking sleep call. If you choose to do the extra credit, you should add a pair of calls enableblocking()/doenableblocking() that change the behavior of sleep from spinning to blocking for all tasks.



To be determined.

Grading Criteria

As usual, the maximum credit available for the project is 10 points. The high-level breakdown is to be determined.

The README should include the following:

The README should NOT contain instructions on how to build your project. We expect that make will prepare the image for bochs, and that make boot will transfer this image to a USB stick.

Strive for clear code with some comments. We don’t have strict rules concerning style.