ICS 2202 Chapter 2

27
2. PROCESS MANAGEMENT 2.1 Introduction to Processes All modern computers do multitasking. While executing a user program, a computer can also be reading from a disk and outputting text to a screen or a printer. The CPU switches from one program to another, running each for tens or hundreds of milliseconds. At any one instance, the CPU runs only one program, but in the course of one second it may work on several programs, thus giving the users the illusion of parallelism (pseudo-parallelism). 2.1.1 Process Concept A batch system executes jobs, whereas a time-shared system has user programs or tasks. The terms job and process are almost used interchangeably. All software that can be executed in a computer, including the operating system is organized into a number of sequential processes or just processes. A process is an executing program, including the current values of the program counter, registers and variables. The execution of a process must progress in a sequential fashion. That is, at one time, at most only one instruction is executed on behalf of the process. We emphasize that a program by itself is not a process; a program is a passive entity, whereas a process is an active entity. The difference between a process and a program is subtle, but crucial. A process is an activity of some kind. It has a program, input, output and a state. Although two processes may be associated with the same program, they are nevertheless considered as two separate execution sequences. For instance, several users may be running copies of the mail program, or the same user may invoke many copies of the editor program. Each of these is a separate process and their data sections will vary. Conceptually, each process has its own virtual CPU. The CPU in reality switches from one program to another. This is referred to as multiprogramming. In the figure below, we see the computer multiprogramming four programs in memory. Operating Systems 1 D C B A A B C D Four program counters

Transcript of ICS 2202 Chapter 2

Page 1: ICS 2202 Chapter 2

2. PROCESS MANAGEMENT

2.1 Introduction to Processes

All modern computers do multitasking. While executing a user program, a computer can also be reading from a disk and outputting text to a screen or a printer. The CPU switches from one program to another, running each for tens or hundreds of milliseconds. At any one instance, the CPU runs only one program, but in the course of one second it may work on several programs, thus giving the users the illusion of parallelism (pseudo-parallelism).

2.1.1 Process Concept

A batch system executes jobs, whereas a time-shared system has user programs or tasks. The terms job and process are almost used interchangeably. All software that can be executed in a computer, including the operating system is organized into a number of sequential processes or just processes.

A process is an executing program, including the current values of the program counter, registers and variables. The execution of a process must progress in a sequential fashion. That is, at one time, at most only one instruction is executed on behalf of the process. We emphasize that a program by itself is not a process; a program is a passive entity, whereas a process is an active entity. The difference between a process and a program is subtle, but crucial. A process is an activity of some kind. It has a program, input, output and a state.

Although two processes may be associated with the same program, they are nevertheless considered as two separate execution sequences. For instance, several users may be running copies of the mail program, or the same user may invoke many copies of the editor program. Each of these is a separate process and their data sections will vary.

Conceptually, each process has its own virtual CPU. The CPU in reality switches from one program to another. This is referred to as multiprogramming. In the figure below, we see the computer multiprogramming four programs in memory.

Figure 6

We see four processes, each with its own flow of control i.e. its program counter and each running independently of the other ones. In the second figure, over some period of time, all processes have made progress, but any instant only one process is actually running. A single processor may be shared among several processes, with some scheduling algorithm being used to determine when to stop work on one process and service a different one.

Operating Systems 1

D

C

B

A

A B C D

Four program counters

Page 2: ICS 2202 Chapter 2

Operating systems that support the process concept must provide some way to create all the processes needed. In some systems it is possible to have all the processes that would be needed present when the system comes up, while in others, some way is needed to create and destroy processes as the need arises. In UNIX processes are created by the FORK system call, which creates an identical copy of the calling process. The child process can also execute FORK, so it is possible to get a whole tree of processes. In essence, processes need a way of creating other processes. Consequently, each process has one parent, but zero, one, two or more children.

Although each process is an independent entity, processes often need to interact with other processes. For example, one process may generate some output that another process would use as input. For example, in the shell command:

cat chapter1 chapter2 chapter3 | grep tree

The first process, running cat, concatenates and outputs three files. The second process, running grep, selects all line containing the word ‘tree.’ Depending on the relative speeds of the two processes, it may happen that grep is ready to run, but there is no input waiting for it. It must then block until some input is available.

When a process blocks, it does so because logically it cannot continue, typically because it is waiting for input that is not yet available. It is also possible for a process that is conceptually ready and able to run to be stopped because the operating has decided to allocate the CPU to another process for a while.

The figure below shows the states a process may be in:

1. Running state, when it is actually using the CPU2. Ready state, when it is temporarily stopped to let another process run3. Blocked state, when it is unable to run until some external event happens

Figure 7

Four transitions are possible, among these states. Transition 1 occurs when a process discovers that it cannot continue. In some systems the process executes, the BLOCK system call, to get into blocked state while in others, it happens automatically. Transitions 2 and 3 are caused by the process scheduler, which is a part of the operating system. Transition 2 occurs when the scheduler decides that the running process has run long enough, and it is time to let another process have some CPU time. Transition 3 occurs when all the other processes have had their share of the CPU and it is time for the first process to get the CPU to run again. Transition 4 occurs when the external event for which a process was waiting (for example, arrival of input) happens.

Operating Systems 2

Running

Blocked

Ready

12

3

4

1. Process blocks for input2. Scheduler picks another

process3. Scheduler picks this process4. Input becomes available

Page 3: ICS 2202 Chapter 2

2.1.2 Implementation of Processes

To implement, the process model, the operating system maintains a table called the process table or the Process Control Block (PCB) , with one entry per process. This entry contains information about the process’ state, its program counter, stack pointer, memory allocation, status of its open files, its accounting and scheduling information and everything else about the process that must be saved when the process is switched from running to ready state so that it can be restarted later as if it had never been stopped.

A typical PCB entry will contain the following fields: Registers, Program counter, Process status word, Stack pointer, Process state, time when the process is started, CPU time used, children’s CPU time, process id, list of open files etc.

Pointer Process state

Process number (id)

Program counter

Registers

Memory limits

List of open files

Figure 8 – Process Control Block

The PCB contains a lot of information including:

1. Process State – The state may be ready, blocked or running.2. Program counter – The counter indicates the address of the next instruction to be

executed for this process.3. CPU registers – The registers vary in number and type depending on the computer

architecture. They include accumulators, index registers, stack pointers and general-purpose registers.4. CPU scheduling information – This information includes a process priority, pointer to

scheduling queues and any other scheduling parameters.5. Memory management information – This information includes the value of the base

and limit registers, page tables or segment tables depending on the memory system used by the operating system.

6. Accounting information - This information includes the amount of CPU and real time used, account numbers, time limits, process numbers and so on.

7. I/O status information – This information includes the list of all I/O devices allocated to this process, list of open files etc

Operating Systems 3

Page 4: ICS 2202 Chapter 2

2.1.3 Threads

Traditionally, processes have a single thread of control and a single program counter in each process. In some modern operating systems, support is provided for multiple threads of control within a process. These threads of control are usually just called threads, or occasionally lightweight processes.

An example of where multiple threads might be used is in a file server process. It receives requests to read and write files and send back the requested data or accepts updated data. To improve performance, the server maintains a cache of recently used files in memory, reading from the cache and writing to the cache when possible. This situation lends itself to multi-threading. When a request comes in, it is handed to a thread for processing. If that thread blocks part way through waiting for a disk transfer, other threads are still able to run, so the server can keep processing new requests even while disk I/O is taking place. The model for single thread of control is not suitable, because it is essential that all file server threads access the same cache.

When multiple threads are present in the same address space, some of the fields in the process table (for example program counter, registers and state) are not per process, but per thread, so a separate thread table is needed, with one entry per thread. The program counter is needed because threads, like processes, can be suspended and resumed and when the suspension happens, the registers must be saved. Threads, like processes can also be in running, ready or blocked state.

Threads can be implemented by the operating system or user space (operating system not aware of the existence of threads). These two alternatives differ in performance. Switching threads is much faster when thread management is done in user space than when a kernel call is needed. This fact argues strongly for doing thread management in user space. On the other hand, when thread management is done entirely user space and one thread blocks, the kernel blocks the entire process, since it is not even aware that other threads exist. This fact argues strongly for doing thread management in the kernel.

Threads introduce a multitude of problems no matter how they are implemented. For instance, if a parent process has multiple threads, should the child also have them? If not, the process may not function properly, since all of them may be essential. However, if the child process gets as many threads as the parent, what happens if a thread was blocked for a READ call, say, from the keyboard. Are the two processes now blocked on the keyboard? When a line is typed, do both threads get a copy of it?

Another problem is related to the fact that threads may share data structures. What happens if one thread closes a file while another one is still reading from it? Suppose that one thread notices that there is too little memory and starts allocating more memory. Then in the process of execution, another thread also notices that there is little memory and also allocates memory. Does the allocation happen once or twice?

These problems show that introducing threads to an existing system without a fairly substantial system redesign is not going to work at all. The semantics of system calls have to be redefined and libraries have to be rewritten at the very least. All these have to be done in such a way as to remain backward compatible with existing programs with the limiting case of a process with only one thread.

Operating Systems 4

Page 5: ICS 2202 Chapter 2

2.2 Inter-Process Communication (IPC)

IPC provides a mechanism to allow processes to communicate and to synchronize their actions. There are three issues related to inter-process communication: how one process can pass information to another. Second, how to ensure that two or more processes do not get in each other’s way when engaging in critical activities. Third concerns proper sequencing of when dependencies are present.

2.2.1 Race Conditions

In some operating systems, processes working together may share some common storage that each can read and write. Shared storage may be in main memory or a shared file. When two or more processes are reading or writing some shared data and the final result depends on who runs precisely when are called race conditions.

2.2.2 Critical Sections

How do we avoid race conditions? The solution (involving shared memory and shared files) is to find some way of preventing more than one process from reading and writing the shared data at the same time. We need mutual exclusion – a way to ensure that if one process is using a shared resource (variable or file) the other processes will be excluded from doing the same thing. The choice of appropriate primitive operations for achieving mutual exclusion is a major design issue in any operating system.

The problem of avoiding race conditions can also be formulated in an abstract way. Part of the time, a process does computations that do not lead to race conditions. However, other times a process may be accessing shared memory or files or doing other critical things that could lead to race conditions. The part of the program where shared memory is accessed is called the critical region or critical section. If we could make arrangements such that no two processes were ever in their critical sections at the same time, we could avoid race conditions.

Although this requirement avoids race conditions, this is not sufficient for having parallel processes cooperate correctly and efficiently using shared data. We need four conditions to hold to have a good solution:

a) No two processes may be simultaneously inside their critical regionsb) No assumptions may be made about the speeds or number of CPUsc) No process running outside its critical region may block other processesd) No process should wait forever to enter its critical region

2.2.3 Achieving Mutual Exclusion by busy waiting

a) Disabling Interrupts

The simplest solution is to have each process disable all interrupts just after entering its critical region and re-enable them just before leaving it. CPU is normally switched from process to process as a result of clock and other interrupts and this means that CPU will not be switched to another process. This solution is not good because it is not good to give user processes the power to turn off interrupts. Furthermore, in multiprocessor systems, disabling interrupts affects only the CPU that executed the disable instruction. The others continue running and could access the shared data.

Operating Systems 5

Page 6: ICS 2202 Chapter 2

b) Lock variables

Consider having a single, (lock) variable, initially 0. When a process wants to enter its critical region, it first tests the lock. If the lock is zero, the process sets it to one and enters the critical region. Thus, a zero means that no process is in its critical section and a one means that some process is in its critical region. Unfortunately, this solution suffers the problem of race condition. Suppose one process reads the lock and finds it is zero. Before it can set it to one, another process that is scheduled, runs and sets the lock to one, two processes will be in their critical regions at the same time.

c) Strict Alternation

Another approach to the mutual exclusion problem is shown below:

Figure 9 – Proposed solution to critical section

The integer variable turn, initially zero, keeps track of whose turn it is to enter the critical region and examine and update the shared memory. Initially process zero inspects turn and finds it to be zero and enters its critical region. Process one also finds it to be zero and therefore sits in a tight loop continually testing turn to see when it becomes one. Continuously testing a variable until some value appears is called busy waiting. This should be avoided since it wastes CPU time.

When process zero leaves the critical region, it sets turn to one to allow process one to enter its critical region. Process one enters its critical region, at the point of leaving the critical region it sets turn to zero. Now suppose that process zero does not need to enter its critical soon, but its process one that is ready to enter the critical section again. Process one cannot enter its critical region because it is not it’s turn. Consequently, it has to wait.

This situation violates condition 3 above. Process one is being blocked by a process that is not in its critical section.

d) The TSL Instruction

Many computers have the instruction TEST AND SET LOCK. This instruction works by reading the contents of the memory word into a register and then stores a non-zero value at that memory address. The operations of reading a word and storing into it are guaranteed to be indivisible – no other process can access the memory word until the instruction is finished. The CPU executing the TSL instruction locks the memory bus to prohibit other CPUs from accessing memory until its done.

To use the TSL instruction, we will use a shared variable, lock to coordinate access to shared memory. When lock is zero, any process may set it to one using the TSL instruction. When it is done, the process sets lock back to zero using an ordinary MOVE instruction. This is a good solution but also require busy waiting.

Operating Systems 6

while (TRUE) { while (turn! = 0) /*wait*/ critical region (); turn = 1; non-critical-region();

while (TRUE) { while (turn! = 1) /*wait*/ critical region (); turn = 1; non-critical-region();

Page 7: ICS 2202 Chapter 2

2.2.4 Mutual Exclusion by Sleep and Wakeup

Solutions to mutual exclusion looked at so far require busy waiting; when a process wants to enter its critical region, it checks to see if entry is allowed. If not, the process just sits in a tight loop waiting until entry is allowed. This approach wastes CPU time. Now we look at some IPC primitives that block instead of wasting the CPU time when they are not allowed to enter their critical regions. One of the simplest is the pair SLEEP and WAKEUP. SLEEP is a system call that causes the caller to block. The WAKEUP call awakens a sleeping process.

a) Producer-Consumer Problem

Two processes share a common fixed-size buffer. One of them the producer, puts its information into the buffer, and another one, the consumer, takes it out. There is trouble when the producer wants to put a new item into the buffer, but its already full. The solution is for the producer to go to sleep, be awakened when the consumer has removed one or more items from the buffer. Similarly, if the consumer wants to remove an item from the buffer and sees that the buffer is empty, it goes to sleep until the producer puts something in the buffer and wakes it up.

This approach though sounds simple; it also leads to race conditions. The variable count will be used to keep track of the number of items in the buffer and N is the maximum items the buffer can hold. The producer’s code will first test the value of count to see whether it is N. If it is, the producer will go to sleep; if it is not, the producer will add an item and increment count.

The consumer code is similar: first test count to see whether it is zero. If it is, go to sleep; if it is nonzero, remove an item and decrement the counter. Each of the processes also tests to see, if the other should be sleeping, and if not, wakes it up. The Code for the consumer and producer is shown below:

Figure 10 –Solving Producer-Consumer using SLEEP and WAKEUP

Race condition can occur because access to count is unconstrained. The buffer can be empty and the consumer has just read count to see it is zero. At that instant, the scheduler decides to stop running the consumer temporarily and starts running the producer. The producer enters an item in the buffer, increments count, and notices that it is now one. Reasoning that the consumer must be sleeping, the producer calls WAKEUP to wake the consumer.

Unfortunately, the consumer is not asleep, so the signal is lost. When the consumer runs next, it will test the value of count, it previously read and finds it to be zero and goes to sleep. Sooner or later the producer will fill the buffer and also go to sleep. Both will sleep forever.

Operating Systems 7

#define N 100int count = 0;

void producer(void){while (TRUE) { produce_item(); if (count ==N) sleep(); enter_item(); count = count + 1; if (count == 1) wakeup(consumer); }}

void consumer(void){while (TRUE) { if (count ==0) sleep(); remove_item(); count = count - 1; if (count == N - 1) wakeup(consumer); consume_item(); }

}

Page 8: ICS 2202 Chapter 2

2.2.5 Semaphores

A semaphore can have the value zero, indicating that no wakeups were saved or some positive value if one or more wakeups were pending. We have two operations DOWN and UP (generalizations of SLEEP and WAKEUP) respectively. The DOWN operation checks if the value is greater than zero. If so, it decrements it and continues. If the value is zero, the process is put to sleep without completing the down for the moment. Checking the value, changing it and going to sleep is all done in a single, indivisible, atomic action. It is guaranteed that once a semaphore operation has started, no other process can access the semaphore until the operation is completed.

The UP operation increments the value of the semaphore addressed. If one ore more processes were sleeping on that semaphore, unable to complete an earlier down operation, one of them is chosen by the system and is allowed to complete its DOWN. The operation of incrementing the semaphore and and waking up a process is also indivisible.

Solving the Producer-Consumer Problem using semaphores

It is important that semaphores be implemented in an indivisible way. The normal way is to implement UP and DOWN as system calls, with the operating system briefly disabling all interrupts while it is testing the semaphore, updating it and putting the process to sleep.

Figure 11 –Solving Producer-Consumer using Semaphores

This solution uses three semaphores: one called full for counting the number of slots that are full, one called empty for counting the number of slots that are empty, and one called mutex to make sure that the producer and consumer do not access the buffer at the same time.

Full is initially 0, empty is initially equal to the number of slots in the buffer and mutex is initially 1. Semaphores that are initialized to 1 and used by two or more processes to ensure that only one of them can enter its critical section at the same time are called binary semaphores. If each process does a DOWN just before enter its critical region and UP just after leaving, mutual exclusion is guaranteed.

Operating Systems 8

#define N 100typedef int semaphore;semaphore mutex = 1;semaphore empty = N;semaphore full = 0;

void producer(void){ int item;

while (TRUE) { produce_item(&item); down(&empty); down(&mutex); enter_item(item); up(&mutex); up(&full); }}

void consumer(void){ int item;

while (TRUE) { down(&full) down(&mutex) remove_item(&item); up(&mutex) up(&empty) consume_item(item); }

}

Page 9: ICS 2202 Chapter 2

We have used semaphores in two different ways. The mutex semaphore is used for mutual exclusion. It is designed to guarantee that only one process at a time will be reading or writing the buffer and associated variables. The second use is for synchronization. The full and empty semaphores are needed to guarantee that certain event sequences do or do not occur. In this case, they ensure that the producer stops running when the buffer is full, and the consumer stops running when it is empty.

2.2.6 Monitors

Semaphores also have their problem. Suppose that the two DOWNS in the producer code were reversed in order, so mutex was decremented before empty instead of after it. If the buffer was completely full, the producer would block, with mutex set to 0. Consequently, the next time the consumer tried to access the buffer, it would do a DOWN on mutex, now 0, and block too. Both processes would stay blocked forever and no more work would be done leading to a deadlock.

To make it easier to write correct programs, a higher level of synchronization primitive called a monitor. A monitor is a collection of procedures, variables and data structures that are grouped together in a special kind of a module or package. Processes may call the procedures in a monitor whenever they want to, but they cannot directly access the monitor’s internal data structures from procedures declared outside the monitor.

Example:

monitor Counter {private:int count = 0;public:int value( ) { return count; }void incr( ) { count = count + 1; }void decr( ) { count = count - 1; }

}

Monitors have an important property that makes them useful for achieving mutual exclusion: only one process can be active in a monitor at any instant. Monitors are a programming language construct, so the compiler knows they are special and can handle calls to monitor procedures differently from other procedure calls. When a process calls a monitor procedure, the first instruction of the procedure will check if any other process is active within the monitor. If so, the calling process will be suspended until the other process has left the monitor. If no other process is in the monitor, the calling process may enter.

Here we also need a way for processes to block when they cannot proceed. In the producer-consumer problem, it is easy enough to put all the tests for buffer-full and buffer-empty in monitor procedures, but how should the producer block when it finds the buffer full?

The solution lies in the introduction of condition variables, along with two operations on them, WAIT and SIGNAL. When a monitor procedure discovers that it cannot continue, it does some wait on a condition variable say, full. This causes the calling process to block and allows another previously prohibited process to enter now. For example, the consumer can wake up the producer by doing a SIGNAL on the condition variable.

To avoid two processes in the monitor at the same time there are several proposals that have been put. For example, letting the new awakened process to run, suspending the other. Another way is by requiring that a process doing a SIGNAL must exit the monitor immediately. We will use, this second approach. If the

Operating Systems 9

Page 10: ICS 2202 Chapter 2

SIGNAL is done on a condition variable on which several processes are waiting, only one of them as determined by the scheduler is revived.A skeleton of the producer-consumer problem with monitors is shown below:

Figure 12 –Solving Producer-Consumer using Monitors

The operations WAIT and SIGNAL are not the same as SLEEP and WAKEUP looked at earlier. They are similar, but with one crucial difference: SLEEP and WAKEUP failed because while one process was trying to go to sleep, the other was trying to wake it up. This cannot happen with monitors. The automatic mutual exclusion on monitors procedures guarantees that if say the producer is inside the a monitor procedure, discovers that the buffer is full, it will be able to complete the WAIT operation without having to worry about the possibility of the scheduler switching to the consumer just before WAIT completes. The consumer will not even be allowed into the monitor until the WAIT has finished.

2.2.7 Message Passing

This method of inter-process communication uses two primitives SEND and RECEIVE, which like semaphores and unlike monitors, are system calls rather than language constructs. These two primitives can be used as follows:

send (destination, &message)

receive (source, &message)

The former call sends a message to a given destination and the latter one receives a message from a given source. If no message is available, the receiver may block until one arrives.

These primitives have challenging design issues. For example, the network can lose messages. To guard against lost messages, the sender and receiver can agree that as soon as a message has been received, the

Operating Systems 10

procedure producer;begin while true do begin produce_item ProducerConsumer.enter; end;end;

procedure consumer;begin while true do begin ProducerConsumer.remove; consume_item

end;end;

monitor ProducerConsumer condition full, empty; integer count;

procedure enter begin if count = N then wait(full); enter_item; count := count + 1; if count = 1 then signal (empty); end;

procedure remove begin if count = 0 then wait(empty); remove_item; count := count - 1; if count = N - 1 then signal (full); end; count := 0;end monitor;

Page 11: ICS 2202 Chapter 2

recipient will send an acknowledgment message. If the sender has not received the acknowledgement within a specified time interval, it retransmits the message.

What if the message is received correctly, but the acknowledgement is lost. The sender will retransmit so the receiver will get it twice. It is essential that receiver can distinguish between a new message and a retransmission of an old one.

Message systems also have to deal with the question of how processes are named, so that the process specified in a SEND or RECEIVE call is unambiguous. Authentication is also an issue, how do you know when communicating with the real file server and not with an imposter?

The Producer-Consumer problem with message passing

We assume that all messages are of the same size and that all messages sent, but not received are buffered automatically by the operating system. A total of N messages is used, analogous to the N slots in shared buffer. The consumer starts by sending N empty messages to the producer. Whenever the producer has an item for the consumer, it takes an empty message and sends and sends back a full one. The total amount of messages remains constant at a given time.

If the producer works faster than the consumer, all messages will end up full, waiting for the consumer; the producer will be blocked, waiting for an empty to come back. If the consumer works faster, then the reverse happens all messages will be empty, waiting for the producer to fill them up and the consumer will be blocked waiting for a full message.

If the producer works faster than the consumer, all messages will end up full, waiting for the consumer; the producer will be blocked, waiting for an empty to come back. If the consumer works faster, then the reverse happens all messages will b empty, waiting for the producer to fill them up and the consumer blocked waiting for full message.

# define N 100

void producer (void) { int item; message m;

while (TRUE) { produce_item (&item); receive (consume, &m); build_message (&m, item); send(consumer, &m); }}

Figure 13 –Solving Producer-Consumer using Message passing

How are processes addressed? One way is to assign each process a unique address and have messages addressed to processes. Another way is by use of a mailbox. A mailbox buffers a certain number of messages. When mailboxes are used, the address parameters in the SEND and RECEIVE are mailboxes and not processes. When a process tries to send to a mailbox that is full, it is suspended until a message is removed.

Operating Systems 11

void consumer (void) { int item, I; message m; for (i = 0; i < N; i++) send(producer, &m);

while (TRUE) { receive(producer, &m); extract_item(&m, &item); send (producer,&m); consume_item(item) }}

Page 12: ICS 2202 Chapter 2

For the producer-consumer problem, the producer would send messages to the consumer’s mailbox and the consumer would send empty messages to the producer’s mailbox. It is clear that destination mailbox holds messages that have been sent to the destination process, but have not been accepted.The other option apart from using mailboxes is to eliminate buffering. If a SEND is done before a RECEIVE, the sending process is blocked until the RECEIVE happens at which point, the message is copied directly from sender to receiver without buffering. Similarly, if a RECEIVE is done before a SEND, the receiver process is blocked until the SEND happens. This is easier to implement than buffering, but the sender and receiver are forced to run in lockstep.

2.3 Classical IPC problems

The Dining Philosophers Problem

This problem can be stated as follows. Five philosophers are seated around a circular table. Each philosopher has a plate of spaghetti. The spaghetti is so slippery that a philosopher needs two forks to eat it. Between each pair of plates is one fork.

The life of a philosopher consists of alternate periods of eating and thinking. When a philosopher gets hungry, she tries to acquire her left and right fork, one at a time in either order. If she successfully acquires two forks, she eats for a while and then puts down the forks and continues to think.

The challenge is writing a program for each philosopher that does what is supposed to be done without getting stuck? Below is a possible solution:

void philosopher (int i) { while (TRUE) { think(); take_folk(i); take_folk((i +1) % N) eat(); put_folk(i); put_folk((i +1) % N); }}

The procedure take_fork waits until a specified fork is available and seizes it. Unfortunately, this solution is not good. Suppose, all philosophers took their left forks simultaneously, then, none will be able to take their right forks and there will be a deadlock. We could modify the program so that after taking the left fork, the program checks to see if the right fork is available. If it is not, the philosopher puts down the left one and waits for some time and then repeats the process. This proposal also leads to starvation, as with a little bit of bad luck all the philosophers could start the algorithm simultaneously picking up their left forks and seeing that their right forks were not available putting down their left forks again simultaneously and so on forever.

One improvement that has no deadlock and no starvation is to protect the five statements, following the call to think, by a binary semaphore. Before starting to acquire forks, a philosopher would do a down on mutex, after replacing the forks, she would do an UP on mutex. Theoretically, the solution is adequate, but practically it has a performance bug, Only one philosopher can be eating at any instant. With five forks available we should be able to allow two philosophers to eat simultaneously.

The solution presented below is correct and allows the maximum parallelism for an arbitrary number of philosophers. It uses an array, state, to keep track of whether a philosopher is eating, thinking or hungry (trying tot acquire forks. A philosopher may move only into eating state if neither neighbor is eating. The program uses an array of semaphores, one per philosopher, so hungry philosophers should block if the

Operating Systems 12

Page 13: ICS 2202 Chapter 2

needed folks are busy. Note that each process run the procedure philosopher at its main code, but other procedures take_forks, put_forks and test are ordinary procedures.

Operating Systems 13

Page 14: ICS 2202 Chapter 2

Solving the dining philosophers problem

Operating Systems 14

Page 15: ICS 2202 Chapter 2

2.4 Process Scheduling

It is often that at any given time there are two processes that can be run. When more than one process can be run, the operating system must decide which one to run first. The part of the operating system that makes this decision is known as the scheduler and the algorithm it uses is called the scheduling algorithm.

The scheduler is usually concerned with deciding the policy and not the mechanism. There are various consideration when deciding a good scheduling algorithm. Some of them include:

1. Fairness – making sure that each process gets its fair share of the CPU2. Efficiency – keep the CPU busy 100% of the time3. Response time – minimize response time for interactive users4. Turnaround time – minimize the time batch users must wait for output5. Throughput – maximize the number of jobs processed per unit time.

Some of these goals are contradictory. Any scheduling algorithm that favors some class of jobs hurts another class of jobs. The CPU time is finite and to give one user more you have to give another user less. It should be noted that it is also difficult to predict the behavior of all jobs. Some may be CPU-intensive while others are I/O-intensive and you cannot for sure determine when one process will block. All computers have an electronic time that causes interrupts periodically. This could be used to ensure that no process runs for too long. At each clock interrupt, the operating system decides whether the currently running process should be allowed to continue or whether it has had enough CPU time for the moment.

The strategy of allowing processes that are logically runnable to be temporarily suspended is called pre-emptive scheduling as opposed to non-preemptive scheduling where a process is allowed to run to completion. As seen earlier, a process can be suspended at any time and without warning so that another process can run. This leads to race conditions that necessitate the use of semaphores, monitors and messages.

The non-preemptive scheduling though easy to implement, it is not suitable for general-purpose systems with multiple competing users because letting one process to run for as long as it wanted would mean denying service to other processes indefinitely.

2.4.1 Round Robin Scheduling

This is one of the oldest, simplest, fairest and most widely used algorithms. Each process is assigned a time interval, called its quantum. If the process is still running at the expiry of its quantum, the CPU is preempted and given to another process. If the process is blocked or finished before the expiry of its quantum the CPU is given to another process. To implement this algorithm, the scheduler maintains a list of runnable processes. When a process uses up its quantum it is put on the end of the list.

The major consideration in this algorithm is the length of the quantum. Since context switching also takes time, if the length of the quantum is short, the CPU will spend more time doing context switching rather than doing actual processing work. To improve the CPU efficiency, we should set a longer quantum time, but not too long since it may cause poor response time.

Figure 14 – Round robin scheduling

Operating Systems 15

Page 16: ICS 2202 Chapter 2

2.4.2 Priority Scheduling

Round robin makes an assumption that all processes are equally important. The need to take external factors into consideration leads to priority scheduling. Each process is assigned a priority and the runnable process with the highest priority is allowed to run.

In a computer system there are multiple processes, some more important than others. For example a daemon process sending electronic mail should be assigned a lower priority than a process displaying video on the screen in real time. To prevent high priority processes from running indefinitely, the scheduler may decrease the priority of currently running process at each clock interrupt. If this action causes the its priority to drop below that of the next highest priority process, a process switch occurs. Alternatively, each process may be assigned a maximum quantum that it is allowed to hold the CPU continuously.

Priorities can be assigned to process statically or dynamically by the system to achieve certain goals. It is often convenient to group processes into priority classes and use priority scheduling among the classes but round robin scheduling within each class.

Figure 15 – Priority Scheduling

2.4.3 Shortest Job First

The algorithms looked at so far are designed for interactive systems. Let us look at an algorithm that is appropriate for batch jobs for which the run times are known in advance. When several equally important jobs are sitting in the input queue waiting to be started, the scheduler should use the shortest job first. In the figure below, we find four jobs A, B, C and D with run times of 8,4,4 and 4 minutes respectively. By running them in that order, the turnaround time for A is 8 minutes, B is 12 minutes, for C is 16 minutes and for D is 20 minutes for an average of 14 minutes.

Figure 16 – shortest job first Scheduling

Now let us consider running these jobs using the shortest job first as shown in figure 16(b). The turn around times is now 4,8,12 and 20 minutes for an average of 11 minutes. Shortest job first is optimal.

Operating Systems 16