Semaphore and Monitors
Semaphore and Monitors
By:
Mudasir Hamid
Student M.Sc.IT
Central University of Kashmir
[email protected]
Department of Information Technology
Semaphores
The various hardware-based solutions to the critical-section problem are complicated for
application programmers to use. To overcome this difficulty, we can use a synchronization tool
called a semaphore.
A semaphore S is an integer variable that, apart from initialization, is accessed only
through two standard atomic operations: wait () and signal (). The wait () operation was
originally termed P (from the Dutch probercn, "to test"); signal () was originally called V (from
verhogen, "to increment"). The definition of wait 0 is as follows:
Wait (s)
{
While (S<=0)
{
/*do nothing*/
}
S= S-1;
}
The definition of signal () is as follows:
Signal (S) {
S + +;
}
All the modifications to the integer value of the semaphore in the wait () and signal()
operations must be executed indivisibly. That is, when one process modifies the semaphore
value, no other process can simultaneously modify that same semaphore value. In addition, in the
case of wait(S), the testing of the integer value of S (S < 0), and its possible modification (S—),
must also be executed without interruption.
USAGE
Operating systems often distinguish between counting and binary semaphores. The value
of a counting semaphore can range over an unrestricted domain. The value of a binary
semaphore can range only between 0 and 1. On some systems, binary semaphores are known as
mutex locks, as they are locks that provide mutual exclusion.
We can use binary semaphores to deal with the critical-section problem for multiple
processes. The n processes share a semaphore, mutex, initialized to 1. Each process P, is
organized as shown in Figure 1.
Counting semaphores can be used to control access to a given resource consisting of a
finite number of instances. The semaphore is initialized to the number of resources available.
Each process that wishes to use a resource performs a waitQ operation on the semaphore
(thereby decrementing the count). When a process releases a resource, it performs a signal ()
operation (incrementing the count). When the count for the semaphore goes to 0, all resources
are being used. After that, processes that wish to use a resource will block until the count
becomes greater than 0.
We can also use semaphores to solve various synchronization problems. For example,
consider two concurrently running processes: P\ with a statement Si and Pi with a statement Si.
Suppose we require that So be executed only after Si has completed. We can implement this
scheme readily by letting Pi and Pi share a common semaphore synch, initialized to 0, and by
inserting the statements
Si;
Signal (synch);
in process P1, and the statements
wait(synch);
Si;
in process P2. Because synch is initialized to 0, P 2 will execute S2 only after P1 has invoked signal
(synch), which is after statement Si has been executed.
IMPLEMENTATION
The main disadvantage of the semaphore definition given here is that it requires busy
waiting. While a process is in its critical section, any other process that tries to enter its critical
section must loop continuously in the entry code. This continual looping is clearly a problem in a
real multiprogramming system, where a single CPU is shared among many processes.
do {
waiting(mutex);
// critical section
signal(mutex);
// remainder section
}while (TRUE);
Busy waiting wastes CPU cycles that some other process might be able to use productively. This
type of semaphore is also called a spinlock because the process "spins" while waiting for the
lock. (Spinlocks do have an advantage in that no context switch is required when a process must
wait on a lock, and a context switch may take considerable time. Thus, when locks are expected
to be held for short times, spinlocks are useful; they are often employed on multiprocessor
systems where one thread can "spin" on one processor while another thread performs its critical
section on another processor.)
To overcome the need for busy waiting, we can modify the definition of the wait () and
signal () semaphore operations. When a process executes the wait () operation and finds that the
semaphore value is not positive, it must wait. However, rather than engaging in busy waiting, the
process can block itself. The block operation places a process into a waiting queue associated
with the semaphore, and the state of the process is switched to the waiting state. Then control is
transferred to the CPU scheduler, which selects another process to execute.
A process that is blocked, waiting on a semaphore S, should be restarted when some
other process executes a signal() operation. The process is restarted by a wakeup () operation,
which changes the process from the waiting state to the ready state. The process is then placed in
the ready queue. (The CPU may or may not be switched from the running process to the newly
ready process, depending on the CPU-scheduling algorithm.)
Each semaphore has an integer value and a list of processes list. When a process must
wait on a semaphore, it is added to the list of processes. A signal () operation removes one
process from the list of waiting processes and awakens that process. The wait () semaphore
operation can now be defined as
wait(semaphore *S) {
S->value - -;
if (S->value < 0) {
add this process to S->list;
block();
}
}
signal(semaphore *S) {
S->value++;
if (S->value <= 0) {
remove a process P from S->list;
wakeup(P);
}
}
The block() operation suspends the process that invokes it. The wakeup(P) operation
resumes the execution of a blocked process P. These two operations are provided by the
operating system as basic system calls. Note that, although under the classical definition of
semaphores with busy waiting the semaphore value is never negative, this implementation may
have negative semaphore values. If the semaphore value is negative, its magnitude is the number
of processes waiting on that semaphore. This fact results from switching the order of the
decrement and the test in the implementation of the wait() operation.
The list of waiting processes can be easily implemented by a link field in each process
control block (PCB). Each semaphore contains an integer value and a pointer to a list of PCBs.
One way to add and remove processes from the list in a way that ensures bounded waiting is to
use a FIFO queue, where the semaphore contains both head and tail pointers to the queue. In
general, however, the list can use any queueing strategy. Correct usage of semaphores does not
depend on a particular queueing strategy for the semaphore lists.
The critical aspect of semaphores is that they be executed atomically- We must guarantee
that no two processes can execute wait() and signal() operations on the same semaphore at the
same time. This is a critical-section problem; and in a single-processor environment (that is,
where only one CPU exists), we can solve it by simply inhibiting interrupts during the time the
wait() and signal () operations are executing. This scheme works in a single processor
environment because, once interrupts are inhibited, instructions from different processes cannot
be interleaved. Only the currently running process executes until interrupts are re-enabled and
the scheduler can regain control.
In a multiprocessor environment, interrupts must be disabled on every processor;
otherwise, instructions from different processes (running on different processors) may be
interleaved in some arbitrary way. Disabling interrupts on every processor can be a difficult task
and furthermore can seriously diminish performance. Therefore, SMP systems must provide
alternative locking techniques—such as spinlocks—to ensure that waitO and signal0 are
performed atomically. It is important to admit that we have not completely eliminated busy
waiting with this definition of the wait() and signal() operations. Rather, we have removed busy
waiting from the entry section to the critical sections of application programs. Furthermore, we
have limited busy waiting to the critical sections of the wait () and signal () operations, and these
sections are short (if properly coded, they should be no more than about ten instructions). Thus,
the critical section is almost never occupied, and busy waiting occurs rarely, and then for only a
short time. An entirely different situation exists with application programs whose critical
sections may be long (minutes or even hours) or may almost always be occupied. In such cases,
busy waiting is extremely inefficient.
Suppose that P$ executes wait (S) and then Pi executes wait (Q). When Po executes
wait(Q), it must wait until Pi executes signal(Q). Similarly, when Pi executes wait(S), it must
wait until Po executes signal(S). Since these signal () operations cannot be executed, Po and Pi
are deadlocked. We say that a set of processes is in a deadlock state when every process in the
set is waiting for an event that can be caused only by another process in the set. The events with
which we are mainly concerned here are resource acquisition and release. However, other types
of events may result in deadlocks, problems related to deadlocks are indefinite blocking, or
starvation, a situation in which processes wait indefinitely within the semaphore. Indefinite
blocking may occur if we add and remove processes from the list associated with a semaphore in
LIFO (last-in, first-out) order.
Monitors
Although semaphores provide a convenient and effective mechanism for process
synchronization, using them incorrectly can result in timing errors that are difficult to detect,
since these errors happen only if some particular execution sequences take place and these
sequences do not always occur.
Monitors are written to make synchronization easier and correctly. Monitors are some procedures,
variables, and data structures grouped together in a package.
An early proposal for organising the operations required to establish mutual exclusion is the explicit
critical section statement. In such a statement, usually proposed in the form “critical x do y”, where “x” is
the name of a semaphore and “y” is a statement, the actual wait and signal operations used to ensure
mutual exclusion were implicit and automatically balanced. This allowed the compiler to trivially check
for the most obvious errors in concurrent programming, those where a wait or signal operation was
accidentally forgotten. The problem with this statement is that it is not adequate for many critical
sections.
A common observation about critical sections is that many of the procedures for manipulating shared
abstract data types such as files have critical sections making up their entire bodies. Such abstract data
types have come to be known as monitors, a term coined by C. A. R. Hoare. Hoare proposed a
programming notation where the critical sections and semaphores implicit in the use of a monitor were all
implicit. All that this notation requires is that the programmer encloses the declarations of the procedures
and the representation of the data type in a monitor block; the compiler supplies the semaphores and the
wait and signal operations that this implies. Using Hoare's suggested notation, shared counters might be
implemented as shown below:
var value: integer;
procedure increment;
begin
value := value + 1;
end { increment };
end { counter };
var i, j: counter;
Calls to procedures within the body of a monitor are done using record notation; thus, to increment one of
the counters declared in above example, one would call “i.increment”. This call would implicitly do a
wait operation on the semaphore implicitly associated with “i”, then execute the body of the “increment”
procedure before doing a signal operation on the semaphore. Note that the call to “i.increment” implicitly
passes a specific instance of the monitor as a parameter to the “increment” procedure, and that fields of
this instance become global variables to the body of the procedure, as if there was an implicit “with”
statement.
There are a number of problems with monitors which have been ignored in the above example. For
example, consider the problem of assigning a meaning to a call from within one monitor procedure to a
procedure within another monitor. This can easily lead to a deadlock. For example, when procedures
within two different monitors each calling the other. It has sometimes been proposed that such calls
should never be allowed, but they are sometimes useful! We will study more on deadlocks in the next
units of this course.
The most important problem with monitors is that of waiting for resources when they are not available.
For example, consider implementing a queue monitor with internal procedures for the enqueue and
dequeue operations. When the queue empties, a call to dequeue must wait, but this wait must not block
further entries to the monitor through the enqueue procedure. In effect, there must be a way for a process
to temporarily step outside of the monitor, releasing mutual exclusion while it waits for some other
process to enter the monitor and do some needed action.
Hoare’s suggested solution to this problem involves the introduction of condition variables which may be
local to a monitor, along with the operations wait and signal. Essentially, if s is the monitor semaphore,
and c is a semaphore representing a condition variable, “wait c” is equivalent to “signal(s); wait(c);
wait(s)” and “signal c” is equivalent to “signal(c)”. The details of Hoare’s wait and signal operations were
somewhat more complex than is shown here because the waiting process was given priority over other
processes trying to enter the monitor, and condition variables had no memory; repeated signalling of a
condition had no effect and signaling a condition on which no process was waiting had no effect.
Following is an example monitor:
monitor synch
integer i;
condition c;
procedure producer(x);
.
.
end;
procedure consumer(x);
.
.
end;
end monitor;
There is only one process that can enter a monitor, therefore every monitor has its own waiting list with
process waiting to enter the monitor.
Let us see the dining philosopher’s which was explained in the above section with semaphores, can be re-
written using the monitors as: