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 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,...); }
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).
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)
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.
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.
To develop good policy, we must consider the lifetime of a particular job. We view the lifetime on three time scales:
Jobs are performed in order of their arrival into the queue.
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ε |
Jobs are performed in the order of their run time length, with a bias towards jobs with shorter run times.
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ε |
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.
Process | Arrival Time | Run Time | Wait Times | Turn Around Time |
---|---|---|---|---|
A | 0 | 5 | ε | 15+15ε |
B | 1 | 2 | 2ε | 5+ε |
C | 2 | 4 | 3ε | 19+18ε |
D | 3 | 9 | 4ε | 11+14ε |
Sum | - | 20 | 10ε | 50+53ε |
Average Turn-Around Time: 12.5 + 3.5ε | ||||
Throughput: 20/(20+16ε) |
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.
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