CMSC 421 Operating Systems Lecture Notes (c) 1994, 1997 Howard E. Motteler Semaphores =========== Example: Bounded Buffer ------------------------ Suppose we have a producer, a consumer, and a shared, bounded buffer, Suppose we have - a sleep() call, that puts the caller to sleep, i.e., moves it to the ``waiting'' state - a wake(P) call, that `wakes' process P, i.e., moves it back to the ``ready'' state We can use sleep() and wake() instead of busy waiting N is buffer size; count the number of things in the buffer; initially count, in, and out are all 0 Producer: while (alive) { generate(&V); // generate some data if (count == N) sleep(); // buffer is full buffer[in] = V; // we're awake now, so assume in = (in+1) % N; // there's space in the buffer count++; if (count == 1) wake(consumer); // if buffer *was* empty, } // then wake the consumer Consumer: while (alive) { if (count == 0) sleep(); // nothing to consume V = buffer[out]; // we're awake now, so assume out = (out+1) % N; // there's something to consume count--; if (count == N-1) wake(producer) // if buffer *was* full, // then wake the producer use(V); // use the data } Consider the following possible sequence of events (such sequences are sometimes called ``traces'') - Suppose buffer is empty and count is 0 - consumer sees count == 0 - there is a context switch - producer adds V to buffer, increments count to 1, and tries to wake the consumer - consumer does sleep() - but the consumer has missed the wakeup message! - when the buffer fills, the producer will also sleep, leaving both asleep forever Problem: a wakeup call was sent to a process that was not yet sleeping! (Question: can we treat the test of count and the sleep(), together, as a critical section?) One solution is to keep a queue of wake-up calls, or simply a count of pending wake-ups (i.e., to use semaphores) Semaphores ----------- DOWN() - generalization of sleep() UP() - generalization of wake() typedef struct { int val; process_list L; } sem; DOWN(sem * S) { S->val = S->val - 1; if ( S->val < 0 ) { add this process id to S->L; move this process to "waiting" state; } } UP(sem * S) { S->val = S->val + 1; if (S->val <= 0) { remove a process p from S->L; move p to "ready" state; } } If the list L is a FIFO queue, we have bounded waiting We say a process P ``waits on semaphore S'' if P is on S->L Semaphores must execute atomically, except when ``asleep'' - on a single processor system we can disable interrupts - on a multi-processor system we may need software mutex (Given mutual exclusion we can implement semaphores, and given semaphores we can implement mutual exclusion) If S->val is negative, then abs(S->val) is the number of waiting processes A separate counter could be used for the number of waiting processes, or we could just count the length of L Example: Semaphores for mutual exclusion ----------------------------------------- Initially, lock->val = 1 Note: we will often temporarily forget about the process list L, and say lock=1 rather than lock->val = 1 P0, P1: : DOWN(lock); CS(); UP(lock); one process: initially lock = 1 P0: DOWN(lock) lock = 0 P0: CS() lock = 0 P0: UP(lock) lock = 1 two processes: initially lock = 1 P0: DOWN(lock) lock = 0 P0: CS() lock = 0 P1: DOWN(lock) lock = -1 P1 waits P0: UP(lock) lock = 0 P1 is awakened P1: CS() lock = 0 P1: UP(lock) lock = 1 initial lock value two processes with reentry of P0: initially lock = 1 P0: DOWN(lock) lock = 0 P0: CS() lock = 0 P1: DOWN(lock) lock = -1 P1 waits P0: UP(lock) lock = 0 P1 is awakened P0: DOWN(lock) lock = -1 P0 waits P1: CS() lock = -1 P1: UP(lock) lock = 0 P0 is awakened P0: CS() lock = 0 P0: UP(lock) lock = 1 initial lock value three processes: initially lock = 1 P0: DOWN(lock) lock = 0 P0: CS() lock = 0 P1: DOWN(lock) lock = -1 P1 waits P2: DOWN(lock) lock = -2 P2 waits P0: UP(lock) lock = -1 P1 is awakened P1: CS() lock = -1 P1: UP(lock) lock = 0 P2 is awakened P2: CS() lock = 0 P2: UP(lock) lock = 1 initial lock value We see that abs(lock) counts waiting processes In many implementations the semaphore value never goes negative, and there is a separate counter for the number of waiting processes Semaphores With Busy Waiting ----------------------------- If mutual exclusion is available, we can implement a form of "counting semaphores" without kernel operations In this case, the semaphore is just an integer variable We would like such a semaphore to work as follows DOWN'(s) { UP'(s) { while (s <= 0); s++; s--; } } For this to work, both UP'() and DOWN'() must be atomic, - except that in DOWN'(), mutual exclusion must be released between successive ``while'' tests, - so that another process has a chance to modify the semaphore variable s - If the ``while'' test is false, i.e., if s > 0, then s-- should guaranteed to follow without interruption Question: Suppose the TSL instruction is available. How can itu be used to correctly implement this simple counting semaphore For the busy-waiting semaphore, the semaphore value is never negative, and there is no count of waiting processes There is no fixed bound on waiting, if two or more processes are waiting on such a semaphore - the process that is awakened depends on how the scheduler runs the ``while'' tests - if the scheduler is ``fair'' (e.g., round robin), then eventually every waiting process will catch a signal, but we can't put a bound on how long this wait may be A comparison of "regular" and "bsw" (for busy-waiting) semaphores: regular bsw two processes: sem.val sem initially 1 1 P0: DOWN(lock) 0 0 P0: CS 0 0 P1: DOWN(lock) -1 0 P1 wait starts P0: UP(lock) 0 1,0 P1 wait completes P1: CS 0 0 P1: UP(lock) 1 1 Semaphore Applications ======================= Synchronization ---------------- Suppose S1 in P1 must always happen before S2 in P2 (e.g., S1 is a write and S2 is a read) initially synch is 0 P1: P2: : : S1; DOWN(synch) UP(synch); S2; : : Deadlock --------- Deadlock: two or more processes waiting on each other Example: suppose s = q = 0; P0: P1: : : DOWN(s) DOWN(q) : : UP(q) UP(s) : : P0 waits for P1, and P1 waits for P0 This can easily happen if signals and waits get out of order (more examples in section on deadlocks) Bounded Buffer #1 ------------------ Use three semaphores: full=N, empty=0, mutex=1 Producer: while (alive) { produce V; DOWN (full); full = full-1; if full < 0, wait DOWN (mutex); add V to buffer; UP (mutex); UP (empty); empty = empty+1; if empty <=0, then } wake process waiting on empty Consumer: while (alive) { DOWN (empty); empty = empty-1; if empty < 0, wait DOWN (mutex); remove V from buffer; UP (mutex); UP (full); full = full+1; if full <=0, then use V; wake process waiting on full } When the buffer is empty, - full = N - empty = 0 - consumer waits on empty when the buffer is full, - full = 0 - empty = N - producer waits on full full empty initially: N 0 producer: DOWN(full) N-1 0 producer: UP(empty) N-1 1 consumer: DOWN(empty) N-1 0 consumer: UP(full) N 0 full empty initially: N 0 consumer: DOWN(empty) N -1 consumer waits producer: DOWN(full) N-1 -1 producer: UP(empty) N-1 0 wakes consumer consumer: UP(full) N 0 Bounded Buffer #2 ------------------ In practice, we might do something simpler, with a single semaphore mutex, initially 1, for mutual exclusion Producer: while (alive) { produce V; DOWN(mutex); get mutual exclusion while (full(buf)) { wait while the buffer is full: UP(mutex); release mutual exclusion sleep(1); sleep DOWN(mutex); get mutual exclusion again } add V to buf; UP(mutex); release mutual exclusion } Consumer: while (alive) { DOWN(mutex); get mutual exclusion while (empty(buf)) { wait while the buffer is empty: UP(mutex); release mutual exclusion sleep(1); sleep DOWN(mutex); get mutual exclusion again } remove V from buf; UP(mutex); use V; }