CS111 Scribe Notes

Lecture 7: Scheduling Algorithms

February 01, 2012

By: Austin Chen, Alex Keopong, Pavan Challagulla, and Andrew Inscore

An Operating System is considered a master control program, which manages devices, RAM, network, CPU, etc. If one program exists, then it gets the CPU. However, in this day and age an operating system consists of many programs. The problem arises as to who gets the CPU. One solution could be to have a CPU for each program, however that is not very efficient. We approach our solution with even-driven programming.

Event-Driven Programming

Event-Driven Programming is a method used to allow programs to respond to different events. Applications are divided into small chunks with callbacks each triggered by an event. These callbacks must work quickly and return; therefore, we should not be waiting for an input, for output. Callbacks can schedule requests, and evoke events.

Here we view a simple event handler, and how callbacks can be used in pseudo code:

    OS = for(;;) {
        wait for next event e;
        invoke callback for e;
        c(e,...);
    }
    
Pros: Cons:

Threads

A thread is the smallest process an operating system can schedule. Most of the time, a thread is contained inside a process. So what exactly does a thread have? We can consider Thread = process - (file descriptor + memory + owner).

Thread memory model

Thread stack

Cooperative Threads

There are two main form of threads, one is known as cooperative threads. In cooperative thread, each thread yields. (i.e. voluntarily releases control of CPU). Once a thread is given control, it will continue to run until it yields or is blocked.

Sample pseudo code:

    for(;;) {
        i++;
        yield();
        j++;
        yield();
        k = k*i*j;
    }
    

The disadvantage of such a method is that it is slow and ugly. As you can see, cooperative threading means we yield on every syscall. This can lead to some serious problems in terms of efficiency, and is very vulnerable to thread starvation.

A typical implementation of yield()
syscall => INT => trap => trap handler in kernel
                                           save registers into thread register
                                           choose some other thread that's ready to run
                                           restore Thread 2's register
                                           RETI (return instruction, new instruction)


Uncooperative Threads

The other form of threading is known as uncooperative thread or preemptive threading. The scheduler in this method determines when a thread's use of CPU time is too long. A timer is used to interrupt.

In the implementation, the motherboard sends a signal to CPU every 10ns. The CPU corresponds and traps when it gets signal. The kernel then takes control.

A common example would be a low priority thread is running. If a higher priority thread becomes able to run, the thread handler will eventually pause the low priority thread to allow the higher priority thread to run. This becomes more efficient that cooperative threading.

Mechanism vs Policy

Cooperative and uncooperative threading are two mechanisms that we use for deciding when, and under what terms, a thread will yield. Policy refers to the way in which we choose the next thread to run, once the current thread has yielded.

Scheduling Scale

To develop good policy, we must consider the lifetime of a particular job. We view the lifetime on three time scales:

Scheduling Metrics

Scheduling metrics

Scheduling Statistics

Scheduling Policy Examples

First-Come First-Served (FCFS)

Jobs are performed in order of their arrival into the queue.

First come first served
Process Arrival Time Run Time Wait Times
A 0 5 ε
B 1 2 4+2ε
C 2 4 5+3ε
D 3 9 13+4ε
Sum - 20 22+10ε
Average Turn-Around Time: 10.5 + 2.5ε
Throughput: 20/(20+4ε)
Average Wait Time: 5.5 + 2.5ε
Pros: Cons:

Shortest Job First (SJF)

Jobs are performed in the order of their run time length, with a bias towards jobs with shorter run times.

shortest job first
Process Arrival Time Run Time Wait Times
A 0 5 ε
B 1 2 4+2ε
C 2 4 9+4ε
D 3 9 4+3ε
Sum - 20 17+10ε
Average Turn-Around Time: 9.25 + 2.5ε
Throughput: 20/(20+4ε)
Average Wait Time: 4.25 + 2.5ε
Pros: Cons:

Round Robin Scheduling

This scheduling policy is similar to First-Come First-Serve (FCFS) except that we don't let a job finish, but let it have turns of quantum time. In a practical system this quantum time is typically 10ms, because that is when the timer interrupt fires.

In deciding which process to run next, we let the process that has waited as long as possible to run again. Round Robin is sort of quantized FCFS, except that if the process has just run then we put it at the end of the queue.

shortest job first
Process Arrival Time Run Time Wait Times Turn Around Time
A 0 5 ε 15+15ε
B 1 2 5+ε
C 2 4 19+18ε
D 3 9 11+14ε
Sum - 20 10ε 50+53ε
Average Turn-Around Time: 12.5 + 3.5ε
Throughput: 20/(20+16ε)
Pros: Cons:

If an implementation of round robin places new process at the beginning of the queue, then starvation is a possibility if too many new processes arrive. To avoid starvation, new processes should be put at the end of the queue.

Priority Scheduling

In this scheduling policy, neither do we care about the process that came first nor do we care about the process with the shortest job. Instead, we assign priorities to jobs and then run the highest priority job first.

Process priorities are segregated into two categories:

The system can use whatever algorithm it likes to assign priorities. It can decide that the priority is the job life, which then leads to Shortest Job First (SJF) algorithm. Or it can decide that the priority is arrival time, which results in First-Come First-Serve (FCFS) scheduling policy. It can have hybrid priority models too, using both arrival time and job life.

By convention, high priority jobs have low numbers. And low priority jobs have high priority numbers. In Linux or Unix, priorities are called niceness to workaround this ambiguity. niceness = - priority