Project 3: Preemptive Scheduling


Overview

You will extend the
provided kernel with preemptive multitasking and implement several synchronization primitives.

CJ Bell is the TA in charge.


Design Review

First, take a look at
what we expect from you in general.

Each of the following is worth 1 point, for a total of 5 points.

For the above three synchronization primitives, specify
  1. the fields of the structures that you will use, and
  2. the pseudo-code for each operation.

For example, this is an acceptable explanation for the previous system call mechanism:

_start() writes the address of kernel_entry() to address 0xf00. When a process makes a system call, it looks up the address of kernel_entry() at 0xf00 and calls it with the ID number of the system call. kernel_entry() saves the user context in the PCB, restores the kernel stack, then calls kernel_entry_helper(). kernel_entry_helper() multiplexes (switch) its argument and invokes the "do_" version of the system call. When kernel_entry_helper() returns, kernel_entry() saves the kernel stack, restores the user context, then returns to the process.

This is an example of an acceptable explanation of the previous implementation of lock:

The lock structure contains a bit indicating whether the lock is held or not and a queue of threads waiting for the lock. lock_init() sets the lock to not held and sets up an empty queue. lock_acquire() enters a critical section and tests if the lock is held. If it is, then it adds itself to the lock queue and blocks. Otherwise, it sets the held bit. Finally, it leaves the critical section. lock_release() also enters a critical section and tests if there are any threads waiting on the lock. If there are, it removes the next thread from the front of the queue and unblocks it; otherwise, it clears the held bit. Finally, it leaves the critical section.

Getting Started

Download the starter code.

Refer to previous projects for information on building and testing your project. Some versions of gcc may not support the -fno-stack-protector flag; it is safe to remove this flag from Makefile if you are using one of these versions.

We have provided the program, tasks which prepares tasks for your OS to run. Initially, you are given two sets of tasks to choose from: given and tsk_test. For example, to prepare a set of tasks for your OS, run ./tasks given; this will update the symbolic links in your main project directory so that, upon recompiling the project, your OS will run those tasks. If you wanted to test your OS with no processes running, you could type:

$ cp -r given noprocesses $ emacs noprocesses/tasks.c # edit tasks.c to not have any processes $ ./tasks noprocesses
Use this to test your kernel.

Preemptive Scheduling

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:irq0_entry; 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, CS, and return IP (in that order)
  3. Jump to the interrupt handler
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, then reenables interrupts.

As with entry.S:syscall_entry, it may be some time before entry.S:irq0_entry 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:SEND_EOI -- otherwise the hardware won't deliver any more timer interrupts. The other tasks entry.S:irq0_entry must perform are

  1. Increment the 64-bit counter, num_ticks
  2. Increment entry.S:disable_count to reflect the fact that interrupts are off
  3. If nested_count is zero, enter the kernel the way a system call would
  4. Yield. Take note of the hint about calling C functions below
  5. If nested_count was zero, leave the kernel the way a system call would
  6. Decrement entry.S:disable_count in anticipation of
  7. iret
nested_count 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:


Synchronization Primitives

The main file you will edit here is sync.[ch], but you will 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 enter_critical and leave_critical.

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

condition_init(cond) Initializes the specified memory to hold a condition variable.
condition_wait(lock, cond) Releases lock, blocks until signalled, then reacquires lock. The calling thread is responsible for acquiring lock before calling this function.
condition_signal(cond) Unblocks the first thread to have blocked on condition_wait(lock,cond). If no thread is blocked on cond, then do nothing.
condition_broadcast(cond) Unblocks all threads that have blocked on condition_wait(lock,cond). The order in which threads are unblocked is not important.

Semaphores

semaphore_init(sem, value) Initializes the specified memory to hold a semaphore whose starting value is value; a non-negative integer.
semaphore_down(sem) If the semaphore is not positive, then block. Decrement the semaphore once it is positive.
semaphore_up(sem) Increments the semaphore.

Fairness requirement: in a trace with exactly n ups, the downs that complete successfully are the first n invocations. Calling semaphore_init(sem, value) counts as value ups.

Barriers

barrier_init(bar, n) Initializes a barrier for use by n threads.
barrier_wait(bar) Blocks until a total of n threads have called barrier_wait(bar). The order in which threads are unblocked is unimportant. Immediate after the last call to barrier_wait(bar) returns, and possibly before the other calls have returned, the barrier is available for another round of waits.

Checklist

Note: this is not intended to be a thorough list; just a quick reference.

Grading

The following are each worth 2 points:

The writeup should include:

Your writeup does not need to cover how to build and run your code; we expect those details to remain the same.

We don't have strict rules concerning style; strive for clear code with meaningful comments.


Submission

Don't forget to complete the general requirements for each project!

To ensure compatability with our tests, do the following:

  1. ./tasks tsk_test
  2. make clean; make
  3. Fix any compilation errors without modifying files within tsk_test. Upon running, the screen should display which extra credits you have attempted. If the display is incorrect, then fix your #defines in scheduler.h.
  4. If you implemented the blocking-sleep extra credit, make sure that sleep spins before t=15 (before do_enable_blocking is called) and block afterwards.

When you are ready to submit, first run make distclean.

Submit via blackboard; only one person per group should submit.


Hints


Extra Credit

Blocking Sleeps

The functions sleep and do_sleep work by spinning on num_ticks. We did this partially to test your implementation of system call preemption and partially so you can earn 1 point of extra credit by implementing a blocking sleep function. Add the system call enable_blocking and function do_enable_blocking such that sleep spins for all tasks before enable_blocking is first called; after the call, sleep blocks.

If you implement blocking sleep, add #define EC_BLOCKING_SLEEP to scheduler.h.

Prioritized Task Scheduling

For an additional 1 point of extra credit, change the scheduler algorithm so that it allocates precessor time proportionally to each task's priority. If implemented, add #define EC_PRIORITIES to scheduler.h