To avoid excessive busy-waiting is an important design goal.
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 *Q ;
} 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->Q;
sleep() ;
}
}
void signal(semaphore *S)
{
S->value++;
if (S->value <= 0)
{
remove a process P from S->Q;
wakeup(P);
}
}
One can implement sleep() and wakeup(P) as system
calls. A call to sleep() would suspend the calling process.
The OS would get control of the CPU and put the process that
called sleep() 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 *Q ;
} semaphore ;
void wait(semaphore *S, int me)
/* P.S. The second parameter would be hidden from the user */
{
boolean willSleep = false ;
/* Entry Code for making wait() atomic */
S->waiting[me] = true;
while( S->waiting[me] && TestAndSet(S->lock) ) no-op ;
S->waiting[me] = false;
/* Code that performs the wait operation */
S->value--;
if (S->value < 0)
{
add this process to S->Q;
willSleep = true ;
}
/* Exit Code for making wait() atomic */
int nextOne = (me+1)%n ;
while ( (nextOne != me) && !S->waiting[nextOne] ) nextOne = (nextOne + 1) % n ;
if (nextOne == me)
{
if (willSleep) sleep(S->lock,false); /* Ask to sleep and have OS clear the lock. */
else S->lock = false ; /* Clear the lock. */
}
else
{
if (willSleep) sleep(S->waiting[nextOne], false); /*Ask to sleep & OS to clear flag.*/
else S->waiting[nextOne] = false ; /* Clear the flag. */
}
}
The pseudo-code above is just a very rough idea of
what could be done.
Note that the pseudo-code employs a modified version of the
sleep() system call. The meaning of
sleep(x,v) is "make the process sleep,
and then set the variable x equal to the
value v."
Why use this form of sleep()?
Basically it is due to a technical problem that comes up
if a process P executing a wait() needs to sleep.
In that case P needs to sleep and perform the exit code.
Unfortunately no matter what order P tries to perform these
actions, something can go wrong.
If P sleeps 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[nextOne] -- 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 may not be acceptable for P to set one
of the flags to false first and then sleep.
The problem is that another process P' might execute a
signal() and a wakeup(P)
before P is able to sleep.
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 idea we use 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. However there is quite a range of
different ways that wait() or signal() might
be implemented, depending on what system calls the OS makes
available, and also depending on what machine language
instructions are available.
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, X, will execute a very long
time in its critical section. It would be possible that several
other processes could busy wait during all that time that X is in
its critical section.
void SolveCS(int me)
{
local int nextOne;
do
{
waiting[me] = true;
while( waiting[me] && TestAndSet(lock) ) no-op ;
waiting[me] = false;
criticalSection(me) ; /* could be very long */
nextOne = (me+1) % n ;
while ( (nextOne != me) && (!waiting[nextOne]) )
nextOne = (nextOne+1) % n;
if (nextOne == me) lock = false
else waiting[nextOne] = false ;
remainderSection(me) ;
} while(true) ;
}
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(true) ;
}