Return to the lecture notes index

Deadlock

Dining Philosophers

The problem:

Rules

Approach #1

  #define left(i) (i)
  #define right(i) ((i-1) % 5)
  MONITOR fork {
     int avail[5] = {2, 2, 2, 2, 2 }; /* forks available to each phil */
     condition hungry[5];

     entry pickup_fork (int phil) {
         if (avail[i] != 2) hungry[i].wait();
         avail[left(I)]--; avail[right(I)]--;
      }
    entry putdown_fork (int phil)  {
         avail[left(i)]++: avail[right(i)]++;
         if (avail[left(i)] == 2) hungry[left(i).signal();
         if (avail[right(i)] == 2) hungry[right(i)].signal();
    }
  }
  

Starvation: Why?

This approach fails due to a very common concern -- starvation. There is no guarantee that an individual philospher would ever be able to eat.

In general, starvation occurs because different parties are waiting for different conditions with no common way to select among them. The solution is very straight-forward. All parties should wait for the same condition. An easy fix might be a common queue for waiting processes.

Another Approach

This time let's look at a sempahore-based approach (I like to mix it up so that we don't play favorites).

  Semaphore chopstick[5] = { 1, 1, 1, 1, 1 };

  while (1)
  {
     P(chopstick[i]);
     P(chopstick[(i+1) % 5]);
     <<< eat >>>
     V(chopstick[i]);
     V(chopstick[(i +1) % 5]);
     <<< think >>>
  }
  

But this solution isn't right either! What can happen? It is possible that the philosophers won't cooperate resulting in no food for anyone -- consider what would happen if each philosopher grabbed his/her left chopstick. No one would be able to eat, because everyone would be waiting for someone else to put down his/her chopstick first. This is called deadlock.

What is Deadlock?

First, let's define a resource:

Now, let's formally define deadlock:

Why Does Deadlock Occur?

Deadlock occurs if these conditions are satisfied:

Simple Defense: Serialization

This attacks the circular wait condition.

Other Defenses

It is possible to attack the other three conditions as well -- but this is often trickier:

Dining Philosophers -- Serialized

If we enumerate the chopsticks and for each philosopher to request the lowest number chopstick first, deadlock cannot happen. This is the same as each philosopher picking up the chopstick on the left -- except for P4. She/he will pick up the right chopstick first, since it has a lower number -- this breaks circular wait. Notice that there isn't a cycle of dependency.

  Semaphore chopstick[5] = { 1, 1, 1, 1, 1 };

  while (1)
  {
     if (i < ((i+1) % 5)) {
          P(chopstick[i]);
          P(chopstick[(i+1) % 5]);
     } else {
         P(chopstick[(i+1) % 5]);
         P (chopstick[i]);
     }
     <<< eat >>>
     V(chopstick[i]);
     V(chopstick[(i +1) % 5]);
     <<< think >>>
  }

  

Conditions Necessary For Deadlock:

All of these conditions are necessary for deadlock to occur. If any condition is not satisified, the system is not in a deadlocked state. If it is impossible to satisfy all four conditions concurrently, deadlock is impossible.

  1. Mutual exclusion - at least one resource must be held in a non-sharable way
  2. Hold and wait - at least one process is holding a resource and waiting for another resource that is currently in use
  3. No preemption (of holder) - while holding, no process can take the resource away from you
  4. Circular wait - there must exist a circular chain of processes, each waiting for a resource held by next process in the chain

Deadlock Prevention

The goal of deadlock prevention is to prevent the necessary conditions from being satisified. Last time we discussed one simple approach, breaking the hold and wait condition by serializing the resquest of resources. Let's consider how we can attack each of the conditions:

Resource Allocation Graphs

Resource allocation graphs can be used describe a system's use of resources and demonstrate the existance of deadlocks.

Consider a resource allocation graph, G, a set of tuples, {V, E}, where V is a vertex and E is an edge.

There are two types of verticies:

Deadlock Detection

Single Instance of Each Resource Type

Given a resource allocation graph, we can perform deadlock detection, perhaps as part of the idle process. This works nicely, because if a deadlock exists, the deadlocked processes will be blocked, not running, so idle will have the opportunity to run.

If only a single instance of each resource type exists, then the system is deadlocked, if and only if there is a cycle in the graph. The most common solution is to kill -9 the processes in the set

Multiple Instances of Each Resource Type

If multiple instances of a single resource type exist, we must reduce the graph before checking for cycles. Unless we reduce the graph, a cycle does not indicate deadlock.

Reducing the graph involves asking the question, "What happens when this runnable process finishes?" Basically, we assume that any process that isn't waiting for a resource completes and releases the resources that it is hold. This in turn makes those resources available to other processes. The process of finding runnable processes and removing them from the graph is repearted, until there are no more runnable processes.

The system is in a deadlocked state, if and only if the graph is not reducible. That is to say that the graph shows the system to be deadlocked if and only if all runnable processes have been reduced (this implies a cycle).

When To Perform Deadlock Detection?

What To Do When Deadlock Is Detected?

  1. Attack "No Preemption" Condition
    1. Pick a process, put it to sleep, and add its resources to the available pool.
    2. When other processes finish, wake it back up.
    3. This assumes that the resource is still valid after process wakes up. This is true in some cases: RAM, disk, but not true in others: half-written magnetic tape.
    4. Picking the right victim(s) is very tricky.

  2. Time For A Sacrafice
    1. Kill victim(s), making their resources available Cleaning up can be messy -- half-written files, partially complete banking transaction, etc.
    2. Transaction-based systems with atomic transactions make this much easier. Clean-up is automatic upon the abort of a transaction.
Implementing Deadlock Detection

We maintain two vectors. Each position in the vector represents a different resource type.

E =
e0 e1 e2 e3 ... en-1

A =
a0 a1 a2 a3 ... an-1

We maintain two matricies. Each column represents a different resource; each row represents a different process.

C =
c00 c01 c02 c03 ... c0n-1
c10 c11 c12 c13 c.. c1n-1
... ... ... cij ... ...
cm-10 cm-11 cm-12 cm-13 ... cm-1n-1
cij is the number of Resourcej held by Processi

R =
r00 r01 r02 r03 ... r0n-1
r10 r11 r12 r13 ... r1n-1
... ... ... rij ... ...
rm-10 rm-11 rm-12 rm-13 ... rm-1n-1
rij is the number of Resourcej requested by Processi

Algorithm:

  1. Check for unmarked rows. If all rows are marked, unmark all rows and terminate. System is not deadlocked.

  2. Find an unmarked row, Ri, such that Ri < A (Each Rij is less than corresponding Aj)

    If no such row exists, unmark all processes and terminate The system is deadlocked.

    Otherwise, the Pi's request can be satisfies and will terminate.

  3. Adjust the A vector, such that A = A + Ci (Each Aj = Aj + Cij)

    Mark Processi

    Goto Step 1

Deadlock Avoidance

Deadlock prevention disallows many different ways of using resources, because they could potentially lead to deadlock. But if the resources suffer reasonable low contention, deadlock could be unlikely. The result is that resources are left idle to prevent the infrequent or unlikely occurance of deadlock. This leads to low resource utilization. Deadlock avoidance is an alternative to deadlock prevention that can yield higher resource utilization. But nothing comes for free. The cost of deadlock avoidance is that we must know much more about how the various processes will request the resources.

The goal of deadlock avoidance is to ensure that the system is always in a safe state. When in a safe state, deadlock is not possible. But if we are in an unsafe state, deadlock could occur. The behavior of the collection of processes determines if deadlock results in an unsafe state, whereas no action of a process can lead to deadlock if the system is in a safe state.

To ensure that the system remains in a safe state, we must know the following for each process:

If we know this, we can do the following:

Example:

2 processes
1 type of resource
5 instances of this resource (5 of them)

Max resource needs:

Process 1: 4
Process 2: 4

Process 1 Process 2 Safe/Unsafe
Initial state 0 allocated 0 allocated safe
Later 1 allocated 2 allocated safe
Even later 2 allocated 2 allocated unsafe

Implementing Deadlock Avoidance w/Banker's Algorithm

The Banker's Algorithm can be used to avoid deadlock in systems with multiple resources of each type. On each request, the OS checks if granting that request might leas to deadlock.

The OS uses a worst-case analysis: "Can all process complete even if each presents its maximum demands, after I satisfy this request?"

Initial Configuration

The initial state of the system is captured in one vector, (E)ntirety, and one matrix, (L)ine of credit. The rows (left to right) represent resources. The columns (top to bottom) in the matricies represent the processes.

The Entirety vector represents the total number of each type of resource available. Each entry is the count of a different resource. (Note: The Entirety vector is the same as the Existence vector we used in the detection algorithm. The labeling changed between the lecture and the slides.)

(E)ntirety holds the number of available resources of each type. Each entry represents the total count of a each resource.

E =
e0 e1 e2 e3 ... en-1

(L)ine of credit represents the total number of each type of resource that a process is permitted to allocate. This is analagous to the (R)equest matrix we saw before, except that this time it represents the maximum possible total request, not necesarily the current request.

L =
l00 l01 l02 l03 ... l0n-1
l10 l11 l12 l13 ... l1n-1
... ... ... lij ... ...
lm-10 lm-11 lm-12 lm-13 ... lm-1n-1

Current State

At any point during the course of execution, the state of the system is represented using additional data structures: a vector, (A)vailable, and a matrix, (C)urrent allocations.

(A)vailable holds the number of currently available resources of each type (total resources less current allocations):

A =
a0 a1 a2 a3 ... an-1

C =
c00 c01 c02 c03 ... c0n-1
c10 c11 c12 c13 c.. c1n-1
... ... ... cij ... ...
cm-10 cm-11 cm-12 cm-13 ... cm-1n-1

We can also computer the (R)esidual credit matrix. The Residual credit is the maximum number of a resource that a process is allowed to allocate less the number of that resource that it has already allocated.

R = L - C

R =
r00 r01 r02 r03 ... r0n-1
r10 r11 r12 r13 ... r1n-1
... ... ... rij ... ...
rm-10 rm-11 rm-12 rm-13 ... rm-1n-1

We can represent a new request by a process, Pi, as a vector N(ew):

N =
n0 n1 n2 n3 ... nn-1

Is The Requested Allocation Safe?

To check to see if the request is safe, we pretend that it is granted.

  1. Update state variables
    • Ci = Ci + N
    • Ri = Ri - N
    • A = A - N
  • Run deadlock detection as we did before with C, R, and A

    If Deadlock Is Detected

    If the potential for deadlock is detected:

    1. undo changes to C, R, and A
    2. block the requesting process until A increases
    3. after A increases, check again.