0% found this document useful (0 votes)
3 views10 pages

Chapter 5

The document discusses inter-process communication and synchronization challenges that can arise when multiple processes access shared resources. It describes the classic dining philosophers problem and introduces concepts like critical regions, race conditions, and mutual exclusion to prevent processes from interfering with each other when accessing shared data.

Uploaded by

Aadil
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views10 pages

Chapter 5

The document discusses inter-process communication and synchronization challenges that can arise when multiple processes access shared resources. It describes the classic dining philosophers problem and introduces concepts like critical regions, race conditions, and mutual exclusion to prevent processes from interfering with each other when accessing shared data.

Uploaded by

Aadil
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

Computer Engineering

Chapter 5

Inter-Process Communication

5.1 The Dining Philosphers Problem


As an indication of the sort of problems that can occur
when several independent processes are running on a
system, using shared resources, we consider the classical
Dining Philosophers Problem.
Figure 5.1 shows a table in the Philosophy Faculty dining
room. The philosophers arrive to find that lunch comprises
one bowl of Chinese noodles for each academic. The table
is circular, and between each bowl is one chopstick.
During lunchtime, it is usual for philosophers (unlike
engineers!) to alternate between eating and thinking.
Therefore, each philosopher will think for a while, and
then see whether both his chopsticks are free, in which
case he will eat for a while. After eating, he will replace
his chopsticks in order to think, and thereby allow his
neighbours the use of the implements.
Fig.5.1 Lunch-time in the Philosophy
If we imagine that the philosophers’ brains were Department
programmed in a particular way, we can suggest suitable
algorithms. (We shall describe these in C, although it is
unlikely that academics’ neural hardware uses this
language!) One course of action would be for a hungry philosopher to wait until his left chopstick is
free, grasp it, and wait for the right chopstick. When that is acquired, he eats until ready to think
again, at which time he replaces the chopsticks. Such an algorithm is used in the function below:-
#define TRUE 1
#define FALSE 0
#define N 5 /* Number of philosophers */
int chopstick_busy(int num) /* Returns 1 if chopstick is in use */
Process i (i = 0 .. N-1):-
philosopher(int i) /* i = philosopher’s number */
{
while (TRUE)
{
think();
while (chopstick_busy(i)) ; /* Wait for left stick */
take_chopstick(i);
while (chopstick_busy((i+1) % N) ; /* Wait for right stick */
take_chopstick((i+1) % N) ;
eat();
put_chopstick(i); /* Put down left stick */
put_chopstick((i+1) % N) ; /* Put down right stick*/
}
}

Unfortunately, the solution is faulty. Suppose at the outset all the philosophers were hungry. Each
would pick up his left chopstick and wait for the right one to become free. They would wait for ever!
The system would become deadlocked.
We soon realise that the only way to prevent trouble is for the philosophers to communicate with each
other:-

24
Computer Engineering

‘‘If it so please you, my esteemed colleague, I would be obliged if one of your chopsticks were made
available to me in the forthcoming period of ten minutes.’’
‘‘Hey, Algernon, you’ve eaten too much. Gimme a chopstick!’’
This illustrates the fact that there must be inter-process communication in a multiprogrammed system,
whenever there are shared resources.

5.2 Race Conditions


When two processes access a Shared Resource (common piece of memory or file), there may be
situations where the data for one process is lost. This is because the scheduler can suspend a process at
any arbritary time - even in the middle of a sequence of critical operations.
Consider a print spooler, which is a process rec
eiving the names of files to be printed, and sen
ding them in sequence to the printer driver. The
user processes put file names into a spooler dire
ctory. The spooler process takes names in tur
n from the directory and prints the files. The arra
ngement of the spooler directory is shown in Fig
ure 5.2
Two (global) variables contain number of nex
t free slot (‘‘in’ ’ ) and next file to print (‘‘o
ut’ ’ ).
Sup p o se p r o cesses A and B want files pri
nted . I t is p o ssib le that the fo llo wing se q
uence of events could happen:-
1. A reads value of ‘‘in’ ’ and stores 7 in local
variable. Fig.5.2 Access to a print spooler
2. A’ s quantum expires, so scheduler switches t
o B.
3. B reads ‘‘in’ ’ and stores 7 in local variable.
4. B puts file into slot 7, and increments ‘‘in’ ’ to 8.
5. Scheduler eventually switches back to A.
6. A looks at local variable, and finds ‘‘in’ ’ is 7.
7. A puts its file into slot 7, erasing previous contents.
8. A increments ‘‘in’ ’ to 8.
9. Printer process sees no problem and prints file in slot 7, but this is A’ s file. B’ s file is lost.
Such a condition is a Race Condition. Although the system (variables ‘‘in’ ’ , ‘‘out’ ’ and spooler
directory) does not appear to be corrupted, data has been lost.

5.3 The Producer-Consumer Problem


A particular problem, which is useful in assessing methods of preventing race conditions, is given
here. The problem is somewhat simpler than the Dining Philosophers problem, since it contains only
two processes.
Consider two processes passing items of data from one to the other (in one direction). Because of the
disparity in the rates of execution of the two processes, and in view of context switching by the
scheduler, a buffer is provided between them, as shown in Figure 5.3.

25
Computer Engineering

Fig.5.3 The Producer-Consumer situation

There is a global variable ‘‘count’ ’ , which indicates the number of items in the buffer. The Producer
process reads this value, and stops producing items when the buffer is full. Likewise the Consumer
process suspends taking items out of the buffer, when it is empty. The buffer is assumed to be a FIFO,
which does not leave spaces between items.
By comparing this scenario with the print spooler case, it can be seen that a race condition can occur
in just the same way. One process reads a value of ‘‘count’ ’ , and then is switched out by the
scheduler. When switched back to run again, it is unaware that the value of ‘‘count’ ’ has been changed
by the other process.
We will use this example in the discussions of some of the methods of preventing race conditions that
follow.

5.4 Critical Regions


To avoid race conditions, we must allow only one process to read/write to one shared resource (file,
memory) at a time.
Processes are normally engaged in one of two types of activity:-
1. Internal computations, using no shared resources.
2. Accessing shared resources - these parts of programs are known as Critical Regions.
To prevent the occurrence of race conditions we need to satisfy four criteria:-
1. No two processes may be simultaneously inside their Critical Regions.
2. No assumptions are made about relative processor speeds.
3. No process stopped outside its Critical Region should block other processes.
4. No process should have to wait abitrarily long to enter its Critical Region.
The action of preventing two processes operating in their Critical Regions simultaneously is known as
Mutual Exclusion. We will now look at some of the suggested ways of achieving this, and see if they
really meet the requirements.

5.5 Methods of Mutual Exclusion with Busy Waiting


5.5.1 Disabling Interrupts
Since the scheduler uses clock interrupts to determine the end of a quantum, at which point a process
switch is executed, we could arrange that any process entering its critical region could turn off all
interrupts. This would mean that the scheduler would not be able to switch processes until the critical
region had been left.
This would certainly provide a method of mutual exclusion. However, it is a dangerous strategy to
allow a user’ s program to turn off the system’ s interrupt mechanism. A program which hangs up, or
goes into an endless loop, could inhibit interrupts for evermore, thereby paralysing the whole system.

26
Computer Engineering

5.5.2 Lock Variables


A better method is to use a global variable, accessible to all processes, to indicate whether any process
has entered its critical region. The program fragment below shows a globally declared variable
(accessible to all processes), and a procedure which would form part of the code of each process. If
‘‘lock’ ’ is zero, then a process can enter its critical region after setting ‘‘lock’ ’ to be one. Any othere
process must then wait for the lock to be cleared before entering its critical region.
int lock; /* Globally declared variable */
Process i (i = 0 .. N-1):-
lock_variable(int i) /* Function run by all processes with access to “lock” */
{
while (1)
{
while (lock) ; /* Busy waiting - do nothing */
lock = 1; /* Show that I am entering the Critical Region */
critical_section();
lock = 0;
noncritical_section();
}
}

Although this looks attractive, it has a fatal flaw. The scheduler could switch processes after a process
has read the lock to be zero, but before it was set to one. This would produce a race condition in a
way similar to that discussed for the print spooler above.
5.5.3 Strict Alternation
A single integer globally-declared variable called ‘‘turn’ ’ is used to show which process is in its
critical region. After finishing its critical region, a process sets the variable so that other processes can
enter their critical regions.
To illustrate this, we consider a system with only two processes 0 and 1 executing. The argument can
easily be extended to a plurality of processes. The program fragment below contains a global
declaration of the variable ‘‘turn’ ’ , which indicates which process is in its critical region.
int turn; /* Globally declared variable */
Process i (i = 0 .. N-1):-
strict_alternation( int process_no) /* Run by all processes with access to “turn”
*/
{
while (1)
{
while (turn != process_no) ; /* Busy waiting - do nothing */
critical_section();
if (process_no == 0) turn = 1;
else turn = 0; /* Let other process have a turn */
noncritical_section();
}
}

The problem with Strict Alternation is that while a process is waiting to enter its critical region, it sits
in a loop, doing nothing. This is called busy waiting, and obviously wastes CPU time. Furthermore, it
is possible for the turn to be given to a process, which has no more critical region tasks to perform. In
that case the other process will wait a long time before entering its critical region, and possibly
indefinitely.

27
Computer Engineering

5.5.4 Peterson’s Solution


To overcome the excessive busy waiting of the previous method, we can use two global variables
‘‘turn’ and ‘‘interested’ ’ . This will mean that a process which receives its turn, but is not interested in
entering its critical region, will not hold up other processes.
Again, we consider two processes for simplicity in the program fragment below. If at any time ‘‘turn’ ’
equals the process number of either process, then that process may enter its critical region. Before
entering the critical region, a process offers the turn to the other process, in case it may be interested.
This provides alternation between the processes, but it is not ‘‘strict’ ’ , since only when processes are
interested do they utilise their turn.
#define TRUE 1
#define FALSE 0
int turn; /* Whose turn is it? */
int interested[2] = {0, 0}; /* All values initially FALSE */
Process i (i = 0 .. N-1):-
peterson( int i)
/* Run by all processes with access to “turn” & “interested” */
{
while (1)
{
interested[i] = TRUE; /* Show I am interested */
turn = 1 - i; /* Offer turn to other process*/
while (interested[1 - i] && turn == 1 - i) ;
/* Busy waiting while other process is interested and has turn */
critical_section();
interested[i] = FALSE; /*Show I am no longer interested*/
noncritical_section();
}
}

5.5.5 The TSL Instruction - Test and Set Lock


Another approach is to construct special hardware to achieve the mutual exclusion. It should be clear
by now, that the scheduler’ s ability to randomly switch processes can interrupt a sequence of
operations in a disastrous way. To prevent this, we can build into the hardware, the ability to execute
two or three instructions sequentially, with no chance of the scheduler interrupting. Such a sequence of
instructions is called an Atomic Action, and can be achieved by masking off the interrupts for a few
cycles.
We can arrange for a CPU to have a TSL as part of its instruction set, so that the following two
actions are performed atomically:-
1. Reading a value from a memory location (a lock variable) into a register.
2. Storing a particular value (perhaps a one) in the lock memory location.
The following piece of fictitious assembly language code shows how the mutual exclusion could be
obtained:-
entercr: tsl register, lock (* Copy lock to register and set lock *)
cmp register, #0 (* Was lock cleared ? *)
jnz entercr (* If lock set, then busy waiting *)
jsr crit_region (* Enter the subroutine containing CR *)
mov lock, #0 (* Clear the lock after leaving CR *)

28
Computer Engineering

5.6 Methods of Mutual Exclusion with Blocking


5.6.1 Sleep and Wakeup
All the above methods of mutual exclusion use busy waiting, which wastes CPU time, and reduces the
efficiency of the system. To avoid busy waiting, we can cause processes to block when they cannot
enter their critical sections. This means that they will effectively be asleep (and not using CPU time),
until an interrupt is received to wake them up. The system must provide two system calls:-
SLEEP() - to cause a process to block.
WAKEUP(process) - to unblock another process.
If we now apply this to the Producer-Consumer problem, a possible solution would be:-
Global variables:-
#define N 100 /* No of spaces in the buffer */
int count = 0; /* Count = no of items actually in the buffer */
Process 1:-
producer()
{
while (1)
{
if (count == N) sleep(); /* If buffer is full then go to sleep */
enter_item(); /* Put item in buffer */
count++; /* Increment count of buffer items */
if (count == 1) wakeup(consumer); /* Was buffer empty? */
}
}
Process 2:-
consumer()
{
while (1)
{
if (count == 0) sleep(); /* If buffer is empty go to sleep */
remove_item(); /* Take item from buffer */
count--; /* Decrement count of buffer items */
if (count == N-1) wakeup(producer); /* Was buffer full? */
}
}

However, although we have overcome busy waiting, we have not provided mutual exclusion. There is
a race condition, as shown in this sequence of events:-
1. Consumer reads count to be zero.
2. Scheduler swaps from Consumer to Producer
3. Producer reads count to be zero, enters item, sets count to be one.
4. Producer thinks Consumer is asleep. Sends a Wakeup signal.
5. But Consumer is not asleep. The Wakeup call is lost.
6. Scheduler swaps to Consumer, which thinks the count is zero, and goes to sleep.
7. Producer thinks Consumer is awake. It fills the buffer and goes to sleep.
8. Both sleep for ever! R.I.P.

29
Computer Engineering

The trouble was that the wakeup signal to a process which was not asleep was lost. We need a better
method to prevent this.
5.6.2 Semaphores
To prevent lost wakeups, we can create a special data type called SEMAPHORE, which stores integers
indicating the number and identities of processes waiting for wakeups calls. Semaphores must be
manipulated by system calls which perform ‘‘atomic operations’ ’ to check and increment/decrement
the semaphore value. (Usually this is achieved by a call which disables interrupts for a few cycles.)
We therefore define two system calls, "Down(sema)" and "Up(sema)", which can take any semaphore
variable as argument. The semaphore variable is actually a structure with an integer field and a queue
(linked list) for processses it has blocked. Its definition would be of the form:-
struct sema
{
int value; /* Zero means process in CR; non-zero means not in CR */
int *process_queue;
}
The operation of Down( ) is to check whether the semaphore is greater than zero. If it is, then the
semaphore is decremented. If the semaphore is already zero, then the calling process is put to sleep
(which means it is put on the list of blocked processes). It will only be awakened (unblocked) when
the semaphore is incremented by another process. The operation can be expressed in pseudo-C as:-
Down(sema.value)
{
if sema.value > 0
sema.value--;
else (block calling process; put process on end of queue);
}
The operation of Up( ) on a semaphore is to increment the semaphore, thereby waking up one process
which is waiting on that semaphore. Its operation is:-
Up(sema.value)
{
if (there are any processes in the queue)
(unblock first process in queue);
else sema.value++;
}
5.6.3 Using semaphores in the Producer-Consumer problem
A valid solution to the Producer-Consumer problem can be achieved by using three semaphores,
‘‘empty’ ’ , ‘‘full’ ’ , ‘‘mutex’ ’ . The first two are simply integers which are manipulated atomically by
Down() and Up(), without having any values in their queue fields. The "mutex" is a full semaphore
which has a queue (of maximum length one!) of blocked processes, as well as an integer which
indicates whether another process is in its critical region. The variable names refer to the "value" field
in each case to save the expressions becoming too cumbersome.
#define N 100 /* No of spaces in the buffer */
semaphore mutex = 1; /* Mutual exclusion for critical regions */
semaphore empty = N; /* Counts empty buffer slots */
semaphore full = 0; /* Counts full buffer slots */
Process 1:-
producer()
{
while (1)
{
down(empty); /* Decrement empty slot count */
down(mutex); /* Enter CR */
enter_item(); /* Put item in buffer */
up(mutex); /* Leave CR */
up(full); /* Increment full slot count */

30
Computer Engineering

}
}
Process 2:-
consumer()
{
while (1)
{
down(full); /* Decrement full count */
down(mutex); /* Enter CR */
remove_item(); /* Take item from buffer */
up(mutex); /* Leave CR */
up(empty); /* Increment empty slot count */
}
}

5.6.4 Using semaphores in the Dining Philosophers Problem


5.6.4.1 A workable but frustrating solution
Likewise a solution to the Dining Philosophers problem can be implemented using a semaphore called
‘‘mutex’ ’ , which ensures that only one philosopher is eating at a time:-
Global variables:-
#define N 5 /* No of philosophers */
semaphore mutex = 1; /* 0 if someone eating; 1 otherwise */
Process i (i = 1 .. N-1):-
philosopher(int i)
{
while (1)
{
think();
down(mutex);
take_chopstick(i);
take_chopstick((i+1)%N);
eat();
put_chopstick(i);
put_chopstick((i+1)%N);
up(mutex);
}
}
Although this will eliminate race conditions and busy-waiting, it is not an ideal solution. It implies
that only one person is eating at a time, while three chopsticks are lying unused on the table, even if
others are hungry. Such a situation may prove so frustrating that they start to use their chopsticks as
weapons, rather than for their proper purpose! Put into computer terms, this algorithm would provide
mutual exclusion, but would be an inefficient use of the shared resources.
5.6.4.2 A better solution
To enable two people to be eating at the same time, we need to introduce more complexity into the
solution. Whenever a philosopher finishes eating, it is necessary to test whether the people on either
side are waiting to eat, and if so to let one of them start. This means unblocking that particular
process. To achieve this we need an array of semaphores to show which persons are waiting. The
critical regions will only be the actions of taking and putting the chopsticks, not the time spent eating.
Global variables:-
#define N 5 /* No of philosophers */
#define LEFT (i-1)%N /* i’s left neighbour */
#define RIGHT (i+1)%N /* i’s right neighbour */

31
Computer Engineering

enum {THINKING, HUNGRY, EATING} state[N];


semaphore mutex = 1; /* Mutual exclusion for critical regions */
semaphore waiting[N]; /* One semaphore per philosopher */
Process i (i = 1 .. N-1):-
philosopher(int i)
{
while (1)
{
think();
down(mutex); /* Enter Critical Region for taking */
state[i] = HUNGRY;
if (state[i] == HUNGRY && state(LEFT) != EATING
&& state[RIGHT] != EATING
{
state[i] = EATING;
take_chopstick(i);
take_chopstick((i+1)%N);
up(waiting[i]);
}
up(mutex); /* Leave Critical Region for taking */
down(waiting[i]); /* Block if waiting */
eat();
down(mutex); /* Enter Critical Region for putting */
state[i] = THINKING;
if (state[LEFT] == HUNGRY && state[(LEFT-1)%N] != EATING
&& state[(LEFT+1)%N] != EATING
{
state[LEFT] = EATING;
up(waiting[LEFT]); /* Allow LEFT person to eat */
}
if (state[RIGHT] == HUNGRY && state[(RIGHT-1)%N] != EATING
&& state[(RIGHT+1)%N] != EATING
{
state[RIGHT] = EATING;
up(waiting[RIGHT]); /* Allow RIGHT person to eat */
}
up(mutex); /* Leave Critical Region for putting */
}
}
5.6.5 Event Counters
A method of providing mutual exclusion, similar to the previous one, is to create another special data
type, called the Event Counter. This is of type Integer, which has special function calls for
manipulating it by means of atomic actions. The purpose of an Event Counter is to keep track of the
total numbers of certain events in a process since the beginning of execution. By comparing values, it
is possible to cause processes to wait until others catch up, thus providing a form of mutual exclusion.
The three function calls possible for an event counter E are:-
Read(E) ---- return the current value of E
Advance(E) ---- atomically increment E
Await(E,v) ---- block the process until E >= v
The following solution to the Producer-Consumer problem uses two event counters:
‘‘in’ ’ = total number of items produced
‘‘out’ ’ = total number of items consumed since program began
Global variables:-

32
Computer Engineering

#define N 100 /* No of spaces in the buffer */


typedef int event_counter; /* Event counters are special kind of int */
event_counter in; /* Count of items inserted into buffer */
event_counter out; /* Count of items removed from buffer */
Process 1:-
producer()
{
int sequence = 0;
while (1)
{
sequence++;
await(out, sequence - N); /* Wait for room in buffer */
enter_item();
advance(in); /* Let consumer know about item */
}
}
Process 2:-
consumer();
{
int sequence = 0;
while (1)
{
sequence++;
await(in, sequence); /* Wait will item available */
remove_item();
advance(out); /* Let producer know item is gone */
}
}
5.6.6 Monitors
Programming with semaphores and event counters is tricky and time consuming. A monitor is a high
level synchronisation module, which does mutex by semaphores without the programmer knowing. It
is, however, a system program, and the compilers on the system need to know about the monitor and
make use of the semaphores at compile time.

5.7 Questions on the chapter


1. Consider a computer that does not have a TEST AND SET LOCK instruction, but does have an
instruction SWAP which swaps the contents of a register and a memory location in a single atomic
action. Can this the used to provide mutual exclusion? Write a small piece of C code to illustrate
your answer, utilising the "swap" instruction.
2. Write a C function to extend Peterson’s Solution to N processes. The value of N will be contained
in a global variable, and the function should take the process number as its argument.
3. Compare the two solutions to the Dining Philosophers Problem in sections 5.6.4.1 and 5.6.4.2. If in
the second case the array of semaphores called "waiting" is replaced by an array of integers, what
would happen to the operation of the program? Would it then function as the first solution?

33

You might also like