[TestAndSet], [Spinlocks] [Definition and initial implementation of Semaphores], [Comments], [Nutt's Implementation], [An implementation for testing programs involving semaphores],
TestAndSet
We consider a machine instruction that supports concurrency.
Always be conscious of the hardware support for Operating Systems functions.
There are a number of reasons for this support. It may be the only way to
solve the problem and,
always, it allows solutions that are
more efficient than purely software solutions.
We can think of the TestAndSet instruction as a function implemented atomically in hardware. Here is this function:
SpinLocks
Spinlocks are abstract data types, or classes, that support
the Busy-Wait solution of the Mutual Exclusion Problem.
The operations on spinlocks are InitLock, Lock, and UnLock.
If mutex is a spinlock that has been initialized using InitLock,
then Critical Regions S1, S2, .., Sn can be protected from each other
by preceding each region by a call to Lock and following it by a call to
UnLock.
So the region Si becomes:
The pseudo code for implementing SpinLocks follows:
A big problem of the spinlock is that by repeatedly testandsetting a location in memory it can use a high percentage of the memory bandwidth, thus reducing the utilization of other procesors sharing the memory. This location that is repeatedly accessed is called a Hot Spot in the memory. A partial solution (in the case of cache-coherent shared memories) is to change the Lock operation as follows:
There is no guaranty of Fairness for spinlocks. That is, if two processes P and Q are trying to enter a critical region protected by a locked spinlock mutex, then there is no guaranty that they will be serviced in FIFO order. Their order of entry will be random. More serious is the problem that the process P may use the spinlock within a loop and use the lock repeatedly while Q is unable to acquire it. [How can this happen?] This behavior could be avoided by giving a "Nice" behavior to the UnLock operation: when the Unlock is executed it is checked if another process is waiting and if yes the current process defers (gives up control of CPU) to it. Of course, in most situation this is an unwarranted precaution.
Spinlocks also are dangerous to use in uniprocessor systems because of the Priority Inversion Problem. [A low priority process is in the critical region. A high priority process tries to enter the region. The low priority process cannot get the CPU which is held by the high priority process. The high priority process spins in the spinlock trying to enter the region being held by the low priority process. We have a deadlock. ]
Two possible solutions to the priority inversion problem:
A related problem is the formation of scheduling convoys. This occurs when a process ceases to run in the middle of a critical region [because of page fault, quantum expiration, external interrupt] and then all other processes sharing this region have to wait for its completion. A number of techniques are being introduced to deal with this problem, in particular Preemption-safe Locking and Non-blocking Algorithms. [Preemption-safe locking tries to avoid preemption during a critical region by having the kernel extend the quantum of a process in the critical region or by increasing its priority. Non-blocking algorithms, expecially for implementing shared counters and queues, make sure that if a process blocks within a critical region, it can be brought back to the beginning of the critical region and another process is allowed to enter the region.]
In general spinlocks with their busy waiting mechanism are desirable (as compared to non busy waiting mechanisms like semaphores) when the scheduling and context switching time are greater than the time required to execute the critical region, or when there are processors not required to do other tasks.
The efficient implementation of spinlocks is a great concern because spinlocks are the usual foundation for synchronization, expecially in multiprocessor systems. They are used everywhere and very frequently. As the use of multiprocessor systems increases so will the interest in efficient spinlocks and in alternative synchronization mechanisms
Definition and initial implementation of Semaphores
People use the term "Semaphore" to refer to a variety of synchronization
mechanisms.
Here by "Semaphore" we mean the "non-busy-waiting" analog of SpinLocks.
We show a possible implementation for semaphores. For now assume that
semaphore's operations are atomic.
type semaphore = record count : integer; 1. q : queue of processes; end record; procedure initsem (s : in out semaphore; n : integer) is begin s.count := n; initialize s.q to the empty queue; end; procedure P(s : in out semaphore) is begin 2. s.count := s.count - 1; 3. if s.count < 0 then 4. Remove this process from Running, add it to s.q, and reschedule; end; procedure V(s : in out semaphore) is begin 5. s.count := s.count + 1; 6. if s.count <= 0 and s.q is not empty then 7. Remove a process from s.q, add it to the Ready Queue, and reschedule; end;
Using semaphores we can achieve a number of different behaviors:
Semaphores do not suffer the same form of the Priority Inversion Problem as Spinlocks. You may see the kind of Priority Inversion Problem that can arise for Semaphores in the following note about the Mars Rover.
Nutt's Implementation
Here we show code similar to the code
used by G.J.Nutt in his Operating Systems book.
This code implements N semaphores for a multiprocessing system.
#define N .. /* The number of semaphores */ int value[N]; /* Keeps the count for the semaphores */ boolean sem[N]; /* Spinlocks to protect critical region */ /* of semaphore */ boolean hold[N]; /* Used to temporarily "enqueue" tasks */ /* waiting on sem */ void initsem(int s, m) { value[s] = m; sem[s] = FALSE; hold[s] = TRUE;} void P(int s) { while (TS(sem[s])); /* Lock */ value[s]--; if (value[s] < 0) { sem[s] = FALSE; /* Unlock */ while(TS(hold[s])) yield(Q); /* Here the process enters */ /* a new Critical Region where it gives */ /* control to Q, a scheduler. It is a loop */ /* since we may repeatedly be given control*/ /* back from yield and find a blocked semaphore.*/ }else sem[s] = FALSE;} /* Unlock */ void V(int s){ while (TS(sem[s])); /* Lock */ value[s]++; if (value[s] <= 0){ /* Somebody is waiting in hold to enter CR*/ while (!hold[s]); /* We check if there is a concurrent V. */ /* It is a loop */ /* because we might otherwise have two or more */ /* consecutive V operations all setting (see next */ /* statement) hold[s] to false. */ hold[s] = FALSE; /* Here we free a process waiting in P,*/ /* namely on hold[s]. */ } sem[s] = FALSE;} /* Unlock */
An implementation for testing programs that use semaphores
Here is code that implements semaphores as pipes.
Of course nobody would ever think to use such an implementation in practice.
This code is shown here for two reasons. Firstly, it can be used to test
concurrent programs with semaphores. Second, it reminds us that IPC
primitives can be implemented in terms of each other. That is, IPC primitives
are chosen not just on the basis of functionality, but also on the basis of
performance (things like data rate and latency).
/* sem.h - CreateSem, P, V, DelSem */ typedef int *semaphore; semaphore CreateSem (int count); /* Returns a new semaphore */ void P(semaphore s); /* P, or wait, or down on semaphore */ void V(semaphore s); /* V, or signal, or up on semaphore */ int DelSem(semaphore s); /* Deletes an existing semaphore */ /* sem.c - CreateSem, P, V, DelSem */ /* This implementation of semaphores is strictly for kicks since it assumes the availability of pipes! A semaphore is represented by a pipe. The P operation is simulated by reading a character from the pipe. The V operation is simulated by writing a character to the pipe. The kind of the semaphore is specified when the semaphore is created. If the argument is 0, we get a blocking semaphore, if 1, a boolean semaphore, and if n>1, a counting semaphore. */ #include <stdio.h> #include <sys/types.h> #include "sem.h" semaphore CreateSem (int count){ /* Create and return a new semaphore */ int *fd; int i; fd = (int *)malloc(8); /* Allocating space for two filedescriptors */ if (pipe(fd) < 0) { printf("Cannot create pipe.\n"); exit(0);} for(i=0;i < count;i++) write(fd[1], "x", 1); return(fd); } void P(semaphore s){ char c; read(s[0], &c, 1); } void V(semaphore s){ char c; write(s[1], &c, 1); } int DelSem(semaphore s){ /* Delete an existing semaphore */ close(s[0]); close(s[1]); free(s); }ingargio@joda.cis.temple.edu