CS444 Class 23 Preemption

Reading: Chap 5 to pg. 332, 339-347, skip DMA, 348-360, Chap 10, Sec. 10.5: 771-773, 775-776,

--also 5.5.2 Clock Software (pp 390-392)

 

Preemption occurs when the scheduler switches processes when the current process is still runnable, i.e. not blocked or exiting. This is done when the process has used up its “quantum” of CPU time. It is basic to the handling of timesharing between runnable processes, in real OS’s. But our hw3 system did not do preemption, so we call its scheduler a non-preemptive scheduler. Real OS’s use preemptive schedulers.

 

The basic timesharing round-robin was first shown on pg. 85, case (c), as shown below, but the discussion there did not mention preemption because it hadn’t been defined yet in the book.  It is just called process switching, which of course is true, but not very specific.  But it is preemption.

02-01.jpg

 

Process Switch Cases

--at blocking, due to a blocking syscall or page fault

--at process exit

--at process creation

--preemption

 

On pg. 90, we see the classic process state diagram with the 4 transitions, as shown below. The transition (2) from running to ready is actually preemption, though again the discussion does not name it.

 

02-02.jpg

 

On pg. 148, non-preemptive and preemptive schedulers are discussed, and the importance of the tick interrupt handler in implementing preemption.  The tick handler is what counts down the quantum for a process, so should be the first to notice that a quantum has been exhausted.

 

In fact, other interrupt handlers can also cause rescheduling that results in preemption, but it would work to just do it in the tick handler.

 

In the last class we looked at the generic interrupt handler execution steps listed on pg. 349. This list assumes a preemptive scheduler (i.e., a “real OS”), because it reschedules processes from the interrupt handler.  It only does this, however, after finishing all the work for the interrupting device.  The scheduling work is done “on the way back” from the interrupt.

 

The fact that all the device-work is complete before rescheduling is done is important to maintain the principle that interrupt handlers should run fast, and never block on events.  With rescheduling at the end, the whole interrupt handler does not return until this process (the one interrupted) is scheduled again, which could be many ms later. 

 

However, the little runt stack that remains from the interrupt handler execution at the scheduling decision doesn’t matter to anything else on the system. It just stays there until the process is scheduled again, then gets popped off.

 

The reason that all serious OS’s have preemptive schedulers is that without preemption, user processes can run arbitrarily long using the CPU, as long as they don’t use any system calls. Thus the crucial capability is preemption of user execution, not kernel execution.  Thus in some OS’s, there is a test in the interrupt handler before trying preemption, checking if the interrupted execution was user level, and only trying preemption if so.  Linux and Windows have “fully preemptive schedulers”, meaning that much kernel code can be preempted as well as all user code.  Some kernel code has to be protected from preemption.

 

5.5.2 Clock Software (pp 390-392)

The periodic interrupts from the timer are called ticks, and the interrupt handler is called the tick handler.  The kernel uses it for: pg. 390

 

Now the second point is crucial to hw4.  More exactly, the quantum is the tick count left for this process to use before it gets preempted.  We’ll add a new member of PEntry, p_quantum for it.

 

Then we have (first draft of code) at the end of the tick handler:  Here QUANTUM should be 4, for 4 10-ms ticks.

 

       if (--curproc->p_quantum == 0) { 

/* preempting, later start over with full quantum */

             curproc->p_quantum = QUANTUM;    

              debuglog(“%”); /* mark it in debuglog */       

schedule();  /* find another process to run */

       }

 

Note that we don’t need to preempt process 0, because it runs as long as no user processes are runnable.  But schedule() will choose it again if necessary, so no special test for it is really needed.

 

Note that this code is part of an interrupt handler, so it runs with IF=0, since we never use sti in our interrupt handlers (Linux does use sti in interrupt handlers, to allow recursive interrupts.)  Thus IF = 0 at the call to schedule(), as it requires.

 

Note that with this simple re-initialization of p_quantum, when schedule() runs, all the preempted processes have the same p_quantum value, so it isn’t useful to use p_quantum in the decision-making (which process to schedule next.)  You can leave schedule() itself as before.

 

With this coding, kernel preemption is happening

Note that we can’t tell whether a tick happened from user code execution or kernel code execution, unless we add an “in-kernel” flag.  So we’re going to be preempting kernel execution (parts that are running with IF=1) as well as user execution.  All user code runs with IF=1. That’s OK, we just have to be careful to do cli() where needed.

 

Is it bad to preempt kernel execution?  No, it’s what Linux and (parts of) Windows does, in order to responsive.  Consider process A running a long system call and process B becoming ready because of a disk-done interrupt.  If kernel preemption is allowed, B can run before A finishes that system call, a good thing if B is more important than A.

 

Classic UNIX (pre-90s) disallowed preemption of kernel code, a practice known as “limited preemption.”  Linux is now using unlimited preemption, but evolved from limited preemption (up to and including v. 2.4) . Linux 2.6 onwards is “fully preemptive”, meaning that that (a subset of) kernel code can be preempted.

 

What’s the downside of allowing kernel preemption?  More forms of race conditions.  Consider the line of code in hw3.soln, incrementing a kernel global variable:

 

            number_of_zombie++;

 

Suppose this is implemented by two instructions, one to read the variable, and another to write the new value.  Then suppose process 1 executed the first instruction and then got preempted.  Process 2 executed both instructions, rereading the same value that process 1 saw (old) and writing old + 1.  But when process 1 resumes, it also writes old + 1.  One increment action got lost.  This is called the lost update problem

 

Solution?  Use kernel mutex.  For hw4, make IF=0 for this code. (Or verify that it is only one instruction, since interrupts occur strictly between instructions.)

 

But before you start worrying too much, note that this problem required a global variable, whereas most of the increments occur to local variables.  Local variables have homes on the process stack, and each process has its own private stack.  Thus local variables are process-private, and can’t suffer lost updates.  It would be good to do an audit of updates to global variables in your hw5 kernel code to check for such problems.

 

Note that using curproc in an interrupt handler is unusual, not done in i/o (not timer) interrupt handlers.  These execute as a “guest” in a “random” process image, and shouldn’t access anything about the current process.  But the timer interrupt is being used to time the current process, so this access is just right.

 

Hw4 Preemption (optional part for implementation, but be sure to understand it)

 

First, let’s look at the hw3 (no preemption case) output, and then see how it changes for hw4 with preemption.  Note that one char takes about 1 ms to be output at 9600 baud, the baudrate of systems 1-10.  Systems 11-14 have much slower processors, so not recommended for testing sched.

 

What we want to show here is that preemption improves performance of the system, by doing CPU-bound work in little spurts, allowing the other processes to queue up output when they get to run, and they get to run much more often.

 

Every time the outputting process runs, it fills the output queue, and these characters are typically output while a CPU-bound process is executing.  You see that over milliseconds, the system is doing two things at once with one CPU: running a loop and outputting chars at interrupt level.  That is happening even with a non-preemptive scheduler, but happens much more often with a preemptive scheduler.

 

For uprog[123].c, recall that the processes run like this, where denotes the computation loop: 

Main 1 runs like this: aaaaaaaaaazzzAAAAAAAAAA

main 2 runs like this: bbbbbbbbbbb

main 3 runs like this: ccccccccccc

 

For more details, see the handout on Preemption

 

Using hw4 provided files: no preemption

 

Actual output, except kprintf “SHUTTING DOWN”:

 

*aaaaaa**********cccccccc*ccaaaazz*z*********AAA*AAAAAAAbb*bb*bbbbbb*

Total runtime: 26 stars, so 260 ms

 

 

Hw4 Execution with preemption

 

Preemption of process 1 by process 2 is marked by %|(1-2) in the debug log. The tick occurs every 10ms. In the i/o bound case, the tick happens once every 10 chars, or possibly every 9 chars (allowing for some overhead.)  We’ve settled on QUANTUM = 4, so at the 4th tick in a CPU-bound process (for example, process 1 of uprog123), preemption occurs.  So a quantum lasts 40 ms. 

The following is a run for this case.  We can see an effect of preemption from watching the run: instead of the system stopping output, stuck in the computation loop in main1 or main3, it keeps doing output from the other processes while doing that computation.

 

Actual output, except kprintf “SHUTTING DOWN”:

 

*aaaaaa****aaaazz****z****bbbbbb**ccccc*cc***bbbbcc***AAAAA*AAAAAc*

 

In the preemptive case, output happens several times during the CPU bound execution.  Since the 6 chars take 6 ms to output, and the loop in main1 runs 40 ms before being preempted, there are (about) 34 ms of CPU expended at the spots on the CPU-bound bar, just before the 2 preemptions, and another spot where the loop is finishing up.

 

What we’re seeing here is that preemption makes the system as a whole more responsive and more efficient, allowing more overlap between computing with the CPU and doing i/o.