CSC443/343 Oct12

slide version

single file version

Contents

  1. Process States
  2. Process Scheduling
  3. CPU and I/O Bursts
  4. Metrics
  5. Policies and Mechanisms
  6. Scheduling Mechanisms: types
  7. Scheduling Mechanisms: algorithms
  8. Examples
  9. Relation between Arrival Time, Finish Time and Wait Time
  10. Gantt Charts
  11. Starvation
  12. Priority Algorithms and Starvation
  13. Scheduling Examples
  14. First Come First Serve (FCFS)
  15. Round Robin (RR)
  16. Shortest Remaining Time First (SRTF)
  17. Minix Scheduling
  18. Minix Scheduling Policy
  19. Minix pick_proc
  20. Minix Dispatcher
  21. Minix Dispatcher (cont.)
  22. Critical Sections
  23. Mutual Exclusion
  24. Progress
  25. Bounded Waiting
  26. Busy Waiting and Critical Sections
  27. Busy Waiting and Critical Sections
  28. Busy Waiting with Turn and Progress
  29. Trace Violating Progress
  30. Comment on the Trace
  31. Avoiding Busy Waiting
  32. Semaphores and the Critical Section Problem
  33. Synchronization in the Kernel
  34. Concurrency
  35. Causes of Concurrency in Kernel
  36. Interrupts
  37. Minix Programming
  38. Linux Programming with Select

Process States[1] [top]

A process has a state property whose value changes and at any time provides some information about the current activity of the process.

A simple set of possible values of process state is

        { READY, BLOCKED, RUNNING }
      

The process can itself cause changes of its state. E.g., [2] A RUNNING process makes a system call to request some operating system service causing its state to become BLOCKED until [3] the service is made available.

[1] The operating system can cause the state of a READY process to become RUNNING by scheduling that process to execute.

If the operating system can get control of the cpu, it can also [4] preempt the cpu from the previously running process, sending it back to the READY state and select a different process to run.

Process Scheduling[2] [top]

The kernel is responsible for allocating the processor (cpu) to processes: that is, for scheduling processes to run on the processor

The scheduler cannot influence how much total time a process needs the cpu or uses i/o devices, but it can affect how much time a process must wait in the READY state.

CPU and I/O Bursts[3] [top]

A process typically alternates between executing user code on the cpu (a cpu burst) and requesting and then waiting for I/O on some I/O device (an I/O burst). A process may also block for other system calls, but we will ignore those for now.

In what follows we will usually look at only a small fraction of a process's total CPU requirement - namely, a single CPU burst.

Metrics[4] [top]

Policies and Mechanisms[5] [top]

Example Policies

Scheduling Mechanisms: types[6] [top]

Scheduling Mechanisms: algorithms[7] [top]

Examples[8] [top]

We will consider the following processor scheduling algorithms applied to schedling processes for their next cpu burst (short-term scheduling):

(Remember, we are considering only a single cpu burst for a process.)

To compare different scheduling algorithms, we will consider and compute various measures:

Turnaround time - The total time required for a process to complete a cpu burst from its first arrival in READY for this cpu burst requirement.

Wait time - The total time spent in READY from its first arrival for this cpu burst until it completes this cpu burst.

Response time - The time spent in READY from its first arrival for this cpu burst until the first time it is scheduled to run for this cpu burst.

Average Wait time, average turnaround time, or average response time.

Relation between Arrival Time, Finish Time and Wait Time[9] [top]

Arrival time, here, means the first time the process arrives in READY for this cpu burst.

Finish Time means the time the process finishes executing this cpu burst.

With these definitions, there is an easy relation between arrival, finish, and wait times for a given cpu burst:

        Finish = Arrival + Wait + CPU_Burst
      
or rewriting this for Wait:
        Wait = Finish - Arrival - CPU_Burst
      

Note that this is independent of which scheduling algorithm is used. That is, it applies to any of the scheduling algorithms whether they are preemptive or not.

Turnaround time for a given cpu burst can also be calculated this way:

        Turnaround = Finish - Arrival
      

Gantt Charts[10] [top]

Gantt Charts are just a simple way to record the schedule produced by any of the scheduling algorithms.

The previous relation:

        Wait = Finish - Arrival - CPU_Burst
      

suggests that to calculate the Wait time for a process requires only determining the Finish time.

(The Arrival time and the CPU burst time will be given and are not under the control of the scheduling algorithm or the kernel scheduling routine.)

Starvation[11] [top]

Starvation over a resource means that the resource manager's allocation policy does not guarantee that a request for the resource will be satisfied.

If starvation can occur for a resource, then there will be some set of conditions that can indefinitely prevent one request from being satisfied even while other requests for the resource are satisfied.

Priority Algorithms and Starvation[12] [top]

Scheduling priority can be either static or dynamic.

Static priority is assigned at process creation and does not change.

Dynamic priority may change during execution of a process.

A static priority scheduling algorithm may lead to starvation. In particular, continuing arrivals in READY of processes having higher priority than waiting processes in the READY could cause the lower priority waiting process to wait indefinitely; i.e., cause them to starve.

Scheduling Examples[13] [top]

Process Arrival Time CPU Burst
P1 0 4
P2 1 6
P3 2 4

First Come First Serve (FCFS)[14] [top]

FCFS - First Come First Serve. The Gantt chart can be used to find the finish times for this non-preemptive scheduling algorithm.

Gantt Chart:
        +------------------------------------+
        |  P1    |   P2      |   P3          |
        +------------------------------------+
        0        4          10              14

        Time     0  1  2  4 
        READY    P1 P2 P2 P2  ...
                       P3 P3
      

From the Gantt chart, finish times are:

Process Finish Wait
P1 4 0
P2 10 3
P3 14 8

So, for example, the wait time for P3 is:

        Wait = Finish - Arrival - CPU_Burst = 14 - 2 - 4 = 8
      

and the average wait time for these three:

        Avg. Wait = (0 + 3 + 8)/3 = 3.667
      

Round Robin (RR)[15] [top]

RR - Round Robin. Assume the quantum is 3. The Gantt chart can also be used to find the finish times for this preemptive scheduling algorithm.

Gantt Chart:
        +------------------------------------------+
        |  P1    |  P2  |  P3  | P1* |  P2*  | P3* |
        +------------------------------------------+
        0        3      6      9    10      13    14

        Time     0  1  2  3 
        READY    P1 P2 P2 P2  ...
        P3 P3
        P1
      

From the Gantt chart, finish times are:

Process Finish Wait
P1 10 6
P2 13 6
P3 14 8

So, for example, the wait time for P3 is:

        Wait = Finish - Arrival - CPU_Burst = 14 - 2 - 4 = 8
      

and the average wait time for these three:

        Avg. Wait = (6 + 6 + 8)/3 = 6.667
      

Shortest Remaining Time First (SRTF) [16] [top]

Gantt Chart:
        +------------------------------------------+
        |  P1      |  P3     |      P2             |
        +------------------------------------------+
        0          4         8                    14

        Time     0       1       2      
        READY    P1(4)   P1(3)   P1(2)
                         P2(6)   P2(6)
                                 P3(4)
      

From the Gantt chart, finish times are:

Process Finish Wait
P1 4 0
P2 14 7
P3 8 2

So, for example, the wait time for P3 is:

        Wait = Finish - Arrival - CPU_Burst = 8 - 2 - 4 = 2
      

and the average wait time for these three:

        Avg. Wait = (0 + 7 + 2)//3 = 3.000 
      

Minix Scheduling[17] [top]

Minix uses an array of 16 queues for scheduling.

Each queue is associated with a priority 0 to 15.

The higher the number the lower the scheduling priority.

Priority 15 contains a pointer to an "idle" process.

The idle process runs only if no other process is READY.

Minix Scheduling Policy[18] [top]

The Minix scheduling policy is determined by the sched function:

    1	
    2	PRIVATE void sched(rp, queue, front)
    3	     register struct proc *rp;/* process to be scheduled */
    4	     int *queue;/* return: queue to use */
    5	     int *front;/* return: front or back */
    6	{
    7	/* This function determines the scheduling policy.  It is called
    8	      whenever a
    9	 * process must be added to one of the scheduling queues to decide
   10	      where to
   11	 * insert it.  As a side-effect the process' priority may be updated.  
   12	 */
   13	 int time_left = (rp->p_ticks_left > 0);/* quantum fully consumed
   14	      */
   15	
   16	  /* Check whether the process has time left. Otherwise give a new
   17	    quantum 
   18	   * and lower the process' priority, unless the process already is in
   19	    the 
   20	   * lowest queue.  
   21	   */
   22	    if (! time_left) {/* quantum consumed ? */
   23	      rp->p_ticks_left = rp->p_quantum_size; /* give new quantum */
   24	    if (rp->p_priority < (IDLE_Q-1)) {   
   25	      rp->p_priority += 1;/* lower priority */
   26	    }
   27	  }
   28	
   29	  /* If there is time left, the process is added to the front of its
   30	    queue, 
   31	   * so that it can immediately run. The queue to use simply is always
   32	    the
   33	   * process' current priority. 
   34	   */
   35	  *queue = rp->p_priority;
   36	  *front = time_left;
   37	}

Minix pick_proc[19] [top]

The pick_proc function simply selects the highest priority process to run:

    1	
    2	PRIVATE void pick_proc()
    3	{
    4	/* Decide who to run now.  A new process is selected by setting
    5	      'next_ptr'.
    6	 * When a billable process is selected, record it in 'bill_ptr', so
    7	      that the 
    8	 * clock task can tell who to bill for system time.
    9	 */
   10	 register struct proc *rp;/* process to run */
   11	 int q;/* iterate over queues */
   12	
   13	  /* Check each of the scheduling queues for ready processes. The
   14	    number of
   15	   * queues is defined in proc.h, and priorities are set in the task
   16	    table.
   17	   * The lowest queue contains IDLE, which is always ready.
   18	   */
   19	   for (q=0; q < NR_SCHED_QUEUES; q++) {
   20	      if ( (rp = rdy_head[q]) != NIL_PROC) {
   21	         next_ptr = rp;/* run process 'rp' next */
   22	         if (priv(rp)->s_flags & BILLABLE) 
   23	             bill_ptr = rp;/* bill for system time */
   24	         return; 
   25	      }
   26	  }
   27	}

Minix Dispatcher[20] [top]

There are several global Minix kernel variables related to scheduling processes:

/* Process scheduling information and the kernel reentry count. */
EXTERN struct proc *prev_ptr;   /* previously running process */
EXTERN struct proc *proc_ptr;   /* pointer to currently running process */
EXTERN struct proc *next_ptr;   /* next process to run after restart() */
EXTERN struct proc *bill_ptr;   /* process to bill for clock ticks */

Minix Dispatcher (cont.)[21] [top]

The previous Minix scheduling routines in the kernel determine which process should run next.

But the restart() function in the Minix kernel is responsible for causing the next process to actually run.

Here is about a third of the code for restart:


_restart:

! Restart the current process or the next process if it is set. 

        cmp     (_next_ptr), 0          ! see if another process is scheduled
        jz      0f
        mov     eax, (_next_ptr)
        mov     (_proc_ptr), eax        ! schedule new process 
        mov     (_next_ptr), 0
0:      mov     esp, (_proc_ptr)        ! 
        ....                            
        ....
        iretd                           ! continue process

Critical Sections[22] [top]

A critical resource is a resource that is accessible by two or more threads, but which can only be used by one thread at a time.

A critical section is the portion of a thread's code which accesses a critical resource.

The critical section problem is the problem of providing a way for threads to execute their critical section code so that the following 3 conditions are satisfied:

  1. Mutual Exclusion
  2. Bounded Waiting
  3. Progress

Mutual Exclusion[23] [top]

Only one process should be accessing the critical resource at a time.

Example of violation of the mutual exclusion requirement

The original "producer" thread and "consumer" thread sharing the variable x with no synchronization is an example. The result was that the output did not reflect each change in x.

Progress[24] [top]

Only processes trying to gain access to the critical section should affect which process will succeed and one process should be allowed to gain access.

That is, the decision about which process gains access should not be delayed indefinitely. (No "deadlock".)

Example of violation of the Progress requirement

The "consumer" thread that tries to print x more times than the producer changes x is an example.

The result is that the "consumer" hangs waiting for the producer to make another change. No progess is made since the producer thread is no longer trying to access and change x, the critical resource.

Bounded Waiting[25] [top]

This is a fairness requirement.

If other processes are trying to access the critical resource when a new process also tries, there should be a guaranteed bound on the number of times these other processes are allowed to access the resource before the new process gains access.

If there is no guarantee, then it is possible that some process may starve waiting for the critical resource.

Note that if no bound is guaranteed, starvation may or may not occur. But if a bound is guaranteed, then starvation will not occur.

(No good concrete example at this point.)

Busy Waiting and Critical Sections[26] [top]

The code using value_avail doesn't solve the critical section problem in general. For one thing, the code doesn't work for more than two threads. (What if there were two printing threads?)

The code doesn't even solve the critical section problem for two threads.

Busy Waiting and Critical Sections[27] [top]

First, make a minor change. Rename the shared variable as turn.

The idea would be (if it worked) that when turn is 0, thread 1 would wait.

When turn is 1, thread 0 would wait.

The code for the two threads would be modified to test turn like this:

Thread 0Thread 1
0)   for(i = 0; i < N; i++) {
1)     while ( turn != 0 )      
        {                       
        }                       
2)     critical section        
3)     turn = 1;               

4)     remainder section       
     }
5)                              
0)     for(j = 0; j < M; j++) {
1)       while ( turn != 1 )    
          {                     
          }                     
2)       critical section      
3)       turn = 0;             

4)       remainder section     
       }
5)                            

Although this would solve the synchronization problem where the two threads must alternate, it would not solve the critical section problem as it would violate the progress requirement.

Busy Waiting with Turn and Progress[28] [top]

To show that the progress requirement is not met, we need only show a trace of one possible schedule for which progress is not met.

In the trace that follows, a schedule of switches between Thread 0 and Thread 1 is constructed which illustrates that progress is not met.

The line numbers indicate that the thread is about to execute that line in its code; that is, that line will be the next instruction executed.

Context switches between threads could occur after any instruction. So we are free to switch at any point in our attempt to find a switching schedule for which progress is not met.

Trace Violating Progress[29] [top]

Threaed 0	 Thread 1	   turn
		 	  	     0
0)
1)
2)-------------->0)
		 1)
		 1)
		 1)
2)<--------------1)
3)
4)				     1	
0)		 	  	     
5)-(finished)--->1)
                 1)
		 2)
		 3)
		 4)                  0
	         0)
                 1)
		 1)
		 1)
		 1) (stuck forever; no thread in its c.s.)

Comment on the Trace[30] [top]

The problem occurs if the values of N and M are different. For example if N is 1 and M is > 1, then the trace shows how thread 0 could go through its critical section 1 time and finish. Then thread 1 is able to go through its critical section 1 time, but when it tries to go through a second time, it must wait indefinitely, even though no thread is in its critical section.

So progress is not met for this particular schedule.

But the progress requirement must be met for all possible schedules in order to solve the critical section problem. So this is not a solution to the critical section problem.

Avoiding Busy Waiting[31] [top]

Windows and also Solaris Unix have system calls for busy waiting (so you don't have to write the code yourself). These are also called "spin locks".

Generally, these should be used only in special situations and typically when the machine in use has multiple processors.

In other cases, a process should wait by blocking so that processor cycles are not wasted.

To get processes to actually block requires using either directly or indirectly a system call, since only the kernel can directly manage a process. That is, a process may block itself by making an approriate system call.

Semaphores and the Critical Section Problem[32] [top]

In constrast to the difficulty of solving the critical section problem using busy waiting, a simple solution can be provided using semaphores.

This use of semaphores will easily meet the mutual exclusion and the progress requirements. Depending on the implementation of the semaphores, the same code using semaphores may also meet the bounded waiting requirement.

A single semaphore with initial value of 1 is created. Each thread using the critical resource uses this semphore as below to enter and leave its critical section:

sem_t mutex; // The semaphore. Its initial value should be 1

// Thread code for entering, leaving 
// its critical section:

 sem_wait(&mutex);
 critical section
 sem_post(&mutex);

When a thread using this code is in its critical section, the value of mutex semaphore is 0. Consequently all other threads trying to execute sem_wait(&mutex) to enter their critical sections will block.

When no thread is in its critical section, the value of mutex is 1.

This works for any number of threads, not just 2.

Synchronization in the Kernel[33] [top]

User level programs such as a multi-threaded web server may need to employ semaphores or other techniques to ensure mutual exclusion or to provide synchronization.

One reason is that user level processes are preemptible and so can't make any assumptions about how they will be scheduled.

What creates the need for synchronization is concurrency.

Concurrency[34] [top]

On a multiple processor machine, more than 1 process can be executing simultaneously - true concurrency.

On a uniprocessor, execution of multiple processes can be interleaved as a result of scheduling.

This is sometimes called pseudo concurrency as opposed to true concurrency, but the synchronization issues are essentially the same.

Both kinds will be referred to as concurrency and disgtinguished only in cases where it makes a difference.

Causes of Concurrency in Kernel[35] [top]

Some causes (there are more):

Interrupts[36] [top]

It is a big problem if the kernel is

This must be prevented.

In Minix, see the lock and unlock macros.

Minix Programming[37] [top]

Linux Programming with Select[38] [top]

Write a program, rserver and a client program similar to the functionality of a remote login.

The rserver should use the select system call to handle: