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.
    
      - 
        System Calls:
        Study the new system call mechanism and explain how it works. You should
        look at entry.S, 
        interrupt.c, and syslib.c.
      
 
      - 
        irq0_entry:
        Using what you know of system calls, explain in even greater detail what
        this does and how to use it.
      
 
      - 
        Condition Variables
      
 
      - 
        Semaphores
      
 
      - 
        Barriers
      
 
    
    For the above three synchronization primitives, specify
    
      - the fields of the structures that you will use, and
 
      - 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:
    
      - Disable interrupts
 
      - 
        Push the flags register, CS, and return IP (in that order)
      
 
      - 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
    
      - 
        Increment the 64-bit counter, 
        num_ticks
      
 
      - 
	Increment entry.S:disable_count to reflect the fact that interrupts
        are off
      
 
      - 
	If nested_count is zero, enter the kernel the
        way a system call would
      
 
      - Yield. Take note of the hint about calling C functions below
 
      - 
	If nested_count was zero, leave the kernel
        the way a system call would
      
 
      - 
	Decrement
        entry.S:disable_count
        in anticipation of 
      
 
      - 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: 
    
      - 
	Safety: To avoid race conditions, interrupts should be off
	whenever your code is accessing kernel data structures, primarily the
        PCBs and task queues. 
      
 
      - 
	Liveness: 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 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:
    
      - Preemptively scheduled threads and processes
 
      - Condition Variables
 
      - Semaphores
 
      - Barriers
 
      - Writeup, style, and submission mechanics
 
    
    
    The writeup should include:
    
 
      - 
        A statement regarding the correctness of your project. Are there any
        parts that you have not completed, that do not follow the specification,
        or otherwise don't work? In what way? If a shortcoming you discuss
        cascades into a disproportionate number of failures against test cases,
        we may restore some credit. State if you believe your implementation to
        be correct in all ways.
      
 
      - 
        An overview of your design, modulo aspects that we discessed in the
        design review.
    
 
    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:
    
      - ./tasks tsk_test
 
      - make clean; make
 
      - 
        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.
      
 
      - 
        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
    
      - We have provided a printf function.
 
      - 
        C functions are allowed to trash registers 
        %eax, %ecx, and 
        %edx. Keep this in mind when calling them from assembly.
      
 
      - 
        This example suffers from a race condition:
        
void condition_wait(lock_t *lock, condition_t *cond) {
    lock_release(lock);
    /* 1 */ 
    enter_critical();
    block(&cond->wait_queue);
    leave_critical();
    lock_acquire(lock);
}
       At location 1, we may be preempted, then another thread can acquire the
       lock and signal, resulting in a lost wakeup.
       
      - 
        As does this:
        
void semaphore_up(semaphore_t *sem) {
    enter_critical();
    ++sem->value;
    unblock_one(&sem->wait_queue);
    leave_critical();
}
void semaphore_down(semaphore_t *sem) {
    enter_critical();
    if(sem->value <= 0) {
        block(&sem->wait_queue);
    }
    --sem->value;
    leave_critical();
}
        Consider this trace: thread 1 calls down, but the value is 0, so it
        blocks; thread 2 calls up, unblocking thread 1; before thread 1 can run,
        however, thread 3 calls down; thread 1 now decrements the value to -1.
        Consider how to satisfy the fairness condition before applying the
        obvious fix.
       
      - 
      And this:
        
void barrier_wait(barrier_t *bar) {
    enter_critical();
    if(--bar->value > 0) {
        block(&bar->wait_queue);
        ++bar->value;
    } else {
        unblock_all(&bar->wait_queue);
        ++bar->value;
    }
    leave_critical();
}
        Suppose bar->value is initially 4, and thread 
        4 is the last of four threads to call 
        barrier_wait(bar). Thread 4 will unblock the other threads and
        increment bar->value to 1. Now, if thread 4
        immediately calls barrier_wait(bar) again, it
        will return immediately, contrary to the specification. 
       
  
 
  
  
  
    
    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