CMSC 412
|
NOTE 9
|
March 16, 1999
|
Semaphores
1. Overview
Test-and-set and swap are low-level read-modify-write constructs, usually
provided as machine instructions. A semaphore is a higher-level construct,
usually provided as an OS system call. It has two properties:
-
It can be efficiently implemented on many architectures.
-
It is powerful enough to allow most ipc and synchronization problems to
be easily solved.
As with any programming construct, it is important to distinguish the definition
of a semaphore from its various implementations. The definition says what
properties you can assume about a semaphore when you use it to solve synchronization
problems. An implementation describes one way to achieve the properties
given an architecture.
2. What is a semaphore
Definition. A semaphore S has the following properties:
-
It always has a non-negative integer value (i.e., 0, 1, ...).
-
It can be initialized to a non-negative integer value.
-
A process can do a P operation (or "wait") on S, denoted P(S). P(S) atomically
decrements S by 1 only when S>0 holds; the process waits until S>0 holds.
-
A process can do a V operation (or "signal") on S, denoted V(S). V(S) atomically
increments S by 1.
-
Starvation-freedom: If a process is at P(S), then it eventually completes
execution of the P(S) provided
-
S>0 holds continuously after some point in time, or
-
S>0 holds repeatedly (because V(S) is repeatedly done).
End definition
When several processes are stuck at P(S) and a V(S) is executed, any
of the waiting processes may be allowed to get past P(S). We cannot assume
that processes are woken up in some order, e.g., the longest waiting process.
Sometimes we may explicitly state that a semaphore S is not starvation-free.
In this case, a waiting process can wait forever at P(S) even while the
sempahore is repeatedly signalled because other processes keep executing
P(S).
3. Using Semaphores
Solving Mutual Exclusion
Semaphores allow a simple solution to the N-process mutual exclusion problem:
// shared
Semaphore mutex := 1 ; // process in CS iff mutex=0
entry(i) {
P(mutex)
}
exit(i) {
V(mutex)
}
Note that this satisfies both safety and progress.
Imposing execution order across processes
Suppose we want process 1 to execute a statement S only after process 2
executes a statement T. This can be solved by
-
Introduce a semaphore, say synch, initialized to 0.
-
Insert P(synch) just before S.
-
Insert V(synch) just after T.
3. Semaphore implementations
Implementing a semaphore S means providing
-
data structures for S
-
code for P(S)
-
code for V(S)
Ensuring that P(S) and V(S) are atomic means ensuring that the code for
P(S) and the code for V(S) are executed mutually exclusively. For this
we need to make use of a solution to the CS problem. Any the previous solutions
will do, and we assume we have one called X, consisting of
Xvars // shared variables of X
Xentry(i) // entry code executed by process i
Xexit(i) // exit code executed by process i
3.1. Attempted semaphore implementation using busy-waiting and CS
solution X
Semaphore construct
|
Implementation
|
Semaphore S initially K |
record S {
integer val initially K,
Xvars appropriately initialized
} |
P(S) |
P(S) {
S.Xentry(i) // process
id i if needed in Xentry()
L: if S.val = 0
then { S.Xexit(i); go to L
}
else { S.val := S.val - 1;
S.Xexit(i) }
} |
V(S) |
V(S) {
S.Xentry(i)
S.val = S.val + 1;
S.Xexit(i)
} |
NOTE:
-
X may use busy-waiting (e.g. Bakery algorithm) or it may not (e.g. disabling
interrupts).
-
Even if X is starvation-free (i.e., satisfies the progress property of
mutual exclusion), the above semaphore implementation does not ensure that
S is starvation-free, because there is no ordering of the processes waiting
on S (just a "go to L").
3.1. Attempted semaphore implementation using blocked waiting, process
queues, and CS solution X
Semaphore construct
|
Implementation
|
Semaphore S initially K |
record S {
integer val initially K,
queue_of_pcb wait,
Xvars appropriately initialized
} |
P(S) |
P(S) {
S.Xentry(i) // process
id i if needed in Xentry()
if S.val = 0
then { S.Xexit(i);
WaitIn( S.wait ); // syscall defined below
}
else { S.val := S.val - 1;
S.Xexit(i) }
}
where
Syscall WaitIn( queue_of_pcb x ) {
UpdateRunQPCB();
move RunQ pcb to x;
Scheduler();
}
|
V(S) |
V(S) {
S.Xentry(i);
if S.wait is not empty
then { move S.wait.head
pcb to readyQ }
else { S.val = S.val
+ 1 };
S.Xexit(i)
} |
NOTE:
-
Above assumes that the system call WaitIn( x ) has its own mechanism to
ensure its atomicity. If this did protection did not extend to S.wait,
then the S.Xexit(i) preceding WaitIn(S.wait) should be moved to just before
calling Scheduler() in WaitIn(S.wait).
-
X may use busy-waiting (e.g. Bakery algorithm) or it may not (e.g. disabling
interrupts).
-
Implementation satisfies starvation-freedome assuming that the CS solution
X is starvation-free (i.e., satisfies the progress property of mutual exclusion)
and the queueing discipline for wait is fair (e.g., FIFO).
-
Even if CS solution X uses busy waiting, this can still be more efficient
than the busy-waiting implementation of S given previously, provided the
time to execute the P(S) body and V(S) body is large compared to Xentry
and Xexit (because busy waiting occurs only in Xentry and Xexit and not
in the body of P(S) or V(S)).
-
Irrespective of whether X or the semaphore implementation uses busy waiting,
we ensure that applications making use of semaphores do not incur busy
waiting at the application level.
-
In the text, S.val is an integer that can have negative values also; when
S.val is less than 0, its magnitude indicates the size of S.wait.