With a queuing semaphore we can create a simple solution to a
critical section problem - one that does not require busy waiting.
The queuing 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 structure 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, depending on exactly how wakeup() works on the
system, 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, a lost wakeup can cause processes to malfunction.
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) ;
}