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.
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.
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.
- Throughput
- Turnaround time
- Waiting time
- Response time
Example Policies
- If there are N processes, each process's fraction of the cpu
cycles should be about 1/N.
- Average wait time for all processes as small as possible
- One class of processes (e.g. safety shutoff process)
should always run when ready before other classes.
-
Response time should be minimal and consistent.
- preemptive
- non-preemptive
- FCFS
- Round Robin
- Shortest Remaining Time First
- Priority
- Lottery
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.
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 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 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.
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.
Process |
Arrival Time |
CPU Burst |
P1 |
0 |
4 |
P2 |
1 |
6 |
P3 |
2 |
4 |
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
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
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 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.
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 }
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 */
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
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:
- Mutual Exclusion
- Bounded Waiting
- Progress
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.
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.
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.)
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.
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 0 | Thread 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.
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.
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.)
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.
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.
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.
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.
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.
Some causes (there are more):
- Interrupts
- preemption of the kernel
- multiple processors
It is a big problem if the kernel is
This must be prevented.
In Minix, see the lock and unlock macros.
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:
- reads and writes to the local "terminal" on the server
machine and reads and writes to the remote client.