Semaphore
A semaphore S is an integer variable that, apart from initialization, is accessed only through two standard atomic operations: wait() and signal().
wait(S) { while S <= 0; S --; } signal(S) { S ++; }
There are two kinds of semaphores:
- counting semaphore
- binary semaphore, aka mutex locks
Using Semaphore
The use of semaphore is to sandwich a critical section using the pair of "wait" and "signal".
do { wait(mutex); critical section signal(mutex); remainder section } while(TRUE);
When a process executes wait() and finds that the semaphore value is not positive, the process blocks and is placed into a waiting queue associated with the semaphore, and the state of the process is switched to waiting state. The behavior of "wait" and "signal" are the following. Semaphore functions must execute atomically.
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 conceptual struct of semaphore is the following.
typedef struct ( int value; struct process * list; } semaphore;
Within linux kernel, the actual code defining semaphore type is:
struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };
POSIX semaphores have two kinds: named (sem_open) vs. unnamed (sem_init). Before Linux 2.6, linux only had unnamed, thread-shared semaphores. On 2.6+ and glibc with NPTL, Linux has complete implementation of POSIX semaphores. Need to link with cc -lrt. Named sempahores have system wide presence, while unnamed semaphores do not and have to be placed in a memory section shared by all threads and processes that need to be synchronized. Please consider:
man sem_open/close/init/wait/post/destroy (POSIX API) man semget/semop (an older set of semaphore API)
Anecdotally, inter-process shared memory can be set up using shmget (acquiring shared memory) and shmat (attach to shared memory).
Problems for Synchronization
Deadlock example.
P0 P1 wait(S); wait(Q); wait(Q); wait(S); signal(S); signal(Q); signal(Q); signal(S);
Priority inversion example - assuming there are three processes: high, middle and low; and high needs some results produced by low, low uses a resource that's also needed by middle, and middle preempts low before low can finish - this means high would have to wait after middle is done. Priority-inheritance protocol is a fix.
The overall goal of synchronization is to achieve: deadlock-free and starvation-free allocation of multiple shared resources among several processes. The most classic exercise is the "dining philosopher problem". The secret source of solving synchronization problem is to break the symmetry, or remove cycles in the dependency graph.
Bounded Buffer (consumer/producer problem)
semaphore mutex; // binary, initialized to 1 semaphore empty, full // counting //producer do { // produce an item in nextp wait(empty); wait(mutex); // add next to buffer signal(mutex); signal(full); } while (TRUE) //consumer do { wait(full); wait(mutex); // remove an item from buffer signal(mutex); signal(empty); // consume the item } while (TRUE)
Reader-writer Problem
In this case we assume the design goal being - no reader be kept waiting unless a writer has already obtained permission to use a shared object.
semaphore mutex; // binary, initialized to 1 semaphore wrt; // counting, initialized to 1 int readcount = 0; do { wait(wrt); // write signal(wrt); } while (TRUE); do { wait(mutex); readcount ++; if (readcount == 1) wait(wrt); signal(mutex); // reading wait(mutex); readcount --; if (readcount == 0) signal(wrt); signal(mutex); } while(TRUE)
Misc
Monitor is a high-level language construct (in languages like Java, C#). It is an abstract data type implemented using semaphore. It operates one procedure at a time (thus handling mutual exclusion). Cannot be used directly by multiple processes.