CS474 - Mutual Exclusion
Hardware Solutions
There are a number of mutual exclusion solutions that depend on the
computer's hardware. If useable, these are really the best way to go,
since they will be the most efficient you'll find. But they aren't
always useable....
- Disable interrupts: if you simply disable the processor's
interrupts, you can't have a context switch occur while the
interrupts are disabled. Note that this only works if you have
a single processor. A frequent mistake in operating system
design is to assume in your initial design that the OS is
intended for a single CPU and using techniques like this to
control critical sections. When the OS is extended to multiple
CPUs, it will take years to fix all the race conditions that
arise...
-
Use an atomic instruction to record the old state of a bit, and
change its state. The canonical example is tsb, or
``test and set bit.'' This instruction will examine a specifed
bit in memory, set the condition codes depending on what it
contains, and then set the bit to a value of 1. In effect, it
attempts to grab a lock, and remembers whether it succeeded or
not. Note that this actually requires two memory transactions:
one to read the old value of the word, and one to write the new
value. In a multi-processor system, this requires the ability
to ``atomic-ize'' several memory transactions (for example,
using the Intel ``lock'' prefix to lock the bus for the duration
of an instruction). Not all multi-processor systems use busses,
or support locking.
Software Solutions
Let's go through a series of algorithms that attempt to solve the
synchronization problem. Any correct solution must meet the following
criteria:
- Mutual Exclusion
- Only one process at a time may be in the critical section
- Speeds
- No assumptions can be made about the relative speed or the number
of CPUs.
- Progress
- If no processes are in the critical section, and some processes
want to enter the critical section, only those processes that want to
enter can participate in the decision and the decision can't be
postponed indefinitely
- Bounded Waiting
- There must be a bound on the number of times that other processes
are allowed to enter the critical section while a particular process
is waiting to enter
We'll simplify the problem a bit by assuming we have two processes,
process 0 and process 1. After we've got a two-process solution,
we'll give a multiple-process solution.
A Process Using Mutual Exclusion
We model a process as a loop, as follows:
while (1) {
/* non critical section code */
enter(); /* also called entry code */
/* critical section code */
leave(); /* also called exit code */
}
First try: see if the other guy has the lock
Assume lock is a shared variable
int lock;
void enter()
{
while (lock == 1);
lock = 1;
}
void leave()
{
lock = 0;
}
Problem: Entry code is itself a critical section, and can fail. Doesn't
provide mutual exclusion.
Second try: take turns
Assume turn is a shared variable, and ME is
a local symbol that is defined to contain the number of the current
process.
int turn;
void enter(int me)
{
while (turn != me);
}
void leave(int me)
{
turn = 1 - me;
}
Problem: Doesn't satisfy the progress requirement.
Third try: Keep track of whether the other guy wants in
Assume me as in the second try, and also assume a
shared boolean array flag[2] which keeps track of whether
flag[i] wants to enter its critical section
int flag[2];
void enter(int me)
{
flag[me] = 1;
while (flag[1-me]);
}
void leave(int me)
flag[me] = 0;
}
Problem: Doesn't satisfy bounded waiting. As a matter of fact, both
processes can get stuck in the entry code and wait there forever (sort
of like Disney's ``Chip & Dale'').
Fourth and last try (Peterson's Algorithm): combine second and
third
Make all the assumptions from both the second and third tries
int flag[2];
int turn;
void enter(int me)
{
flag[me] = 1;
turn = 1-me;
while (flag[1-me] && (turn == (1-me)));
}
void leave(int me)
{
flag[me] = 0;
}
This solution meets all of the requirements.
Multiple Processes
This algorithm is called the ``bakery algorithm,'' because it's based
on the ``take a number'' box that bakeries use (actually, I've never
seen one in a bakery. But the Motor Vehicle Division uses one).
Assume two shared arrays, each with one element per process that might
be trying to enter the critical section:
choosing[NUMPROCS] and
number[NUMPROCS].
number[j]
- This array is used to keep track of all the processes currently
trying to enter the critical section. The first process to try
to enter gets assigned the number 1, the second gets 2, the
third gets 3, and so forth. When all the processes that tried
to enter the critical section have left it again, we start
assigning numbers again with 1.
choosing
- While we're comparing the number we chose to the numbers
everybody else chose so we can see if we can get in, we may
encounter somebody who's still choosing their number. If we do,
we wait until they're done. We use this array to announce
whether we're currently choosing or not.
What strikes me as crucial to this code is that there are no
write-write races. A process never modifies a variable owned by
somebody else, though it looks at variables owned by others.
- Entry Code
-
int choosing[NUMPROCS];
int number[NUMPROCS];
void enter(int me)
{
choosing[me] = true;
for (j = 0; j < NUMPROCS; j++)
if (number[j] > number[me])
number[me] = number[j];
number[me]++;
choosing[me] = false;
for (j = 0; j < NUMPROCS; j++) {
while (choosing[j]);
while (number[j] &&
((number[j] < number[me]) ||
((number[j] == number[me]) && j < me)));
}
- Exit code
-
void leave(int me)
{
number[me] = 0;
}
Last modified: Fri Sep 9 11:12:34 MDT 2005