Implementation
With a queueing semaphore we can create a simple solution to
critical section problems that does not require busy waiting.
The queueing semaphore is a special data structure. The data part consists
of an integer value and a list. It might be represented this way:
typedef struct
{
int value ;
struct process *L ;
} semaphore ;
The semaphore data stucture requires two operations wait and signal,
which must be implemented atomically. The following pseudo code
describes what the operations do, but does not give any clue about
how to implement the operations atomically:
void wait(semaphore S)
{
S.value--;
if (S.value<0)
{
add this process to S.L;
block() ;
}
}
void signal(semaphore S)
{
S.value++;
if (S.value<=0)
{
remove a process P from S.L;
wakeup(P);
}
}
One can implement block() and wakeup(P) as system calls. A call
to block() would put the calling process to sleep. The OS would
get control of the CPU and put the process that called block()
into a special sleep queue. The sleep queue is a data
structure that the OS maintains. A process is not runnable while
in the sleep queue. A call to wakeup(P) would cause the OS to
get control of the CPU and to remove P from the sleep queue and
return it to the ready queue.
On a uniprocessor, one can implement wait() and signal() (without
any busy waiting) as system calls. The OS can guarantee the
atomic execution of wait() and signal() if it does two things
while executing the code of wait() or signal():
- refuse to relinquish the CPU, and
- mask interrupts
Under those circumstances nothing can "sneak in" and run in the
CPU until after the wait() or signal() has completed.
Unfortunately the method described above is hard to generalize to
a multiprocessor platform. We would have to guarantee that no
code running on any of the other CPU's would do anything to
"conflict" with the critical section of code running the wait()
or the signal().
However on a multiprocessor, we could implement wait() and
signal() using one of the software solutions we examined earlier
in the chapter. For example, wait() could be implemented like
this:
typedef struct
{
boolean waiting[n] ;
boolean lock ;
int value ;
struct process *L ;
} semaphore ;
void wait(semaphore S, int me)
{
boolean willBlock=false ;
int wasLocked ;
/* Entry Code for making wait() atomic */
S.waiting[me]=true;
wasLocked=true;
while(S.waiting[me] && wasLocked) wasLocked=TestAndSet(S.lock);
S.waiting[me]=false;
S.value--;
if (S.value<0)
{
add this process to S.L;
willBlock=true ;
}
/* Exit Code for making wait() atomic */
int you=(me+1)%n ;
while ( (you != me) && !S.waiting[you] ) you=(you+1)%n ;
if (you==me)
{
if (willBlock) block(S.lock,false);
else S.lock=false ;
}
else
{
if (willBlock) block(S.waiting[you],false);
else S.waiting[you]=false ;
}
}
Note that the code above employs a modified version of the
block() system call. The meaning of block(x,v) is "block
the process making this call and then set the variable x
equal to the value v."
Why do we have to change the form of block()?
Basically it is due to a problem that comes up if a process P
executing a wait() needs to block. In that case P needs to block
and perform the exit code. Unfortunately no matter what
order P tries to perform these actions, it will do something
wrong.
If P blocks it can't do anything next, so it can't execute
the exit code.
Consider that if P does not set one of the flags to false --
S.lock or S.waiting[you] -- then none of the other processes
using the semaphore will be able to perform a signal() or a
wait(). All progress of the group of processes will stop. In
particular, no process will ever wake P up.
On the other hand it is not acceptable for P to set one of the
flags to false first and then block. The problem is that another
process Q might execute a signal() and a wakeup(P) before
P is able to block.
Therefore P could "miss" its wakeup. P might wake up later when
some other process executes a signal(), or it might never wake
up. Either way the error could have "disastrous" consequences.
The solution we employ here is to take the responsibility away
from the process P and place it with the OS. The OS sets the
flag to false after blocking P.
Note that the solution we posed for the multiprocessor does
require some busy waiting. However generally the amount of time
spent doing this busy waiting will be negligible. There are only
a few instructions involved in the wait and signal code, and
processes do their busy waiting only when waiting to perform
those short sequences of instructions.
Contrast that with the case of such code as that below. Here
some of the critical sections could be very long. There is the
potential, for example, that one process will executes a very
long time in its critical section and that several other
processes busy wait the whole time.
void SolveCS(int me)
{
local boolean wasLocked ;
local int you;
do
{
waiting[me]=true;
wasLocked=true;
while( waiting[me] && wasLocked )
wasLocked = TestAndSet(lock) ;
waiting[me]=false;
criticalSection(me) ; /* could be very long */
you=(me+1)%n ;
while ( (you!=me) && (!waiting[you]) )
you = (you+1)%n;
if (you==me) lock=false
else waiting[you]=false ;
remainderSection(me) ;
} while(1) ;
}
In the version of the code below, implementing the wait and
signal as described above for the multiprocessor case, the
processes are blocked most of the time while waiting to
enter their critical section. They only do busy waiting for a
brief time while executing wait() and signal().
As a result there is no significant busy waiting in this
solution.
---------------------
shared semaphore mutex ;
---------------------
void SolveCS(int me)
{
do
{
wait (mutex) ;
criticalSection(me) ; /* could be very long */
signal (mutex) ;
remainderSection(me) ;
} while(1) ;
}