Dining Philosophers
The problem:
![]()
Rules
- Philosophers alternate between thinking and eating
- 2 chopsticks are required to eat
- Philsophers are dignified and never grab with both hands -- they pick up the chopsticks one at a time
- A philospher is too polite to steal a chopstick from a colleague
- The philosophers cannot be allowed to starve
- More than one philosopher must be able to eat at a time.
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:
- Resources are an abstraction of any reason to wait.
- Resources come in different types and we can have different numbers of each type.
- A process/thread can acquire a resource, use it, and then fee it.
- In this context, resources are unshareable - they are serially reusable.
Now, let's formally define deadlock:
- The condition that arises when there exists a set of processes (or threads) such that each process holds a resource that another process in the set is waiting to acquire. The situation forces all processes in the set to wait forever.
Why Does Deadlock Occur?
Deadlock occurs if these conditions are satisfied:
- Mutual exclusion -- at least one resource must be held by a process.
- Hold and wait -- at least one process hold a resource while it is waiting for another resource.
- No preemption -- one process can't take another process's resources in order to make progress (nor can the OS)
- Circular wait -- there exists a circular chain of processes, each of which is waiting for a resource held by the next process in the chain.
Simple Defense: Serialization
This attacks the circular wait condition.
- One simple defense against deadlock is to serialize the request of resources.
- Enumerate all of the resources, giving each a number.
- Require that all processes request resources in the order of this enumeration. That is to say that they are designed so that they never request a resource with a lower number than the highest numbered resource that they hold.
- Circular wait is now impossible, because the chain of waiting cannot wrap around from the greatest back to the beginning. It will
Other Defenses
It is possible to attack the other three conditions as well -- but this is often trickier:
- Mutual exclusion -- if you can, make the resource sharable, ex. read-only files or pages of memory
- Hold and wait -- if you can't get what you want, release all your resources and try again (not bounded wait), or simply die!
- No preemption -- give the OS (or another process) a stick and let it take away resources. Perhaps it can kill the process, or perhaps it can put the process to sleep before the surgery.
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.
- Mutual exclusion - at least one resource must be held in a non-sharable way
- Hold and wait - at least one process is holding a resource and waiting for another resource that is currently in use
- No preemption (of holder) - while holding, no process can take the resource away from you
- 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:
- Mutual Exclusion - make resources sharable - example: read-only files
- Hold-and-wait - if requested resource is unavailable, release all currently allocated resources before waiting. But this allows livelock. Another approach is to request all resources at once in a single request. If processes request resources only once and are granted the requested resources at once, hold-and-wait is not satisfied and livelock is not possible.
- No preemption - make resources preemptable
- Circular wait - allow processes to wait for resources, but ensure that the waiting can't be circular. One approach might be to assign a precedence to each resource and force processes to allocated resources in order of increasing precedence. That is to say that if the highest precedence resource a process currently holds is X, the process cannot request any resource with precedence <= X. This forces resource allocation to follow a particular and non-circular ordering, so circular wait cannot occur. Waiting can happen, but can't be circular.
So, how do we do it? We can allow processes to hold only one resource. If a process requests another resource, it must free the one it is holding (also hold-and-wait)
Another, more practical approach is this. We can assign a numerical value (precedence) to each type of resource and require that processes obtain them in increasing order. For example, let's assume that a process is holding a collection of resources and the highest precendence of any of these held resources is P, this processes cannot request a resource with a precedence of less than P.
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:
- Processes - we'll draw these as circles and label them with the PID
- Resources - we'll draw these as rectangles and label them with the resource type. We'll put one dot inside of the box for each instance of the resource.
There are two types of edges:
- Requests - we'll label these with an arrow from the requesting process to the requested resource. Since the process doesn't care which instance fo the reosurce it gets, the arrow is drawn to the box, not to a particular dot therein.
- Resource use - we'll label these with an arrow from a particular instance of a resource to the process that is currently using it. Since a process is making exclusive use of a particular instance of a resource, we draw the arrow from a particular dot to the process that is using it.
![]()
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?
- Upon each request: early detection, but expensive
- When CPU is idle (nothing better to do)
- When many processes are blcoked (deadlock likely)
- Periodically
What To Do When Deadlock Is Detected?
Implementing Deadlock Detection
- Attack "No Preemption" Condition
- Pick a process, put it to sleep, and add its resources to the available pool.
- When other processes finish, wake it back up.
- 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.
- Picking the right victim(s) is very tricky.
- Time For A Sacrafice
- Kill victim(s), making their resources available Cleaning up can be messy -- half-written files, partially complete banking transaction, etc.
- Transaction-based systems with atomic transactions make this much easier. Clean-up is automatic upon the abort of a transaction.
We maintain two vectors. Each position in the vector represents a different resource type.
- E(xistence) - the number of each type of resurce in existence
- A(vailable) - the number of each type of resource unallocated
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(urrent) - The number of each type of resources held by each process.
- R(equest) - The number of each type of resource requested by each process.
cij is the number of Resourcej held by Processi
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
rij is the number of Resourcej requested 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 Algorithm:
Check for unmarked rows. If all rows are marked, unmark all rows and terminate. System is not deadlocked.
- 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.
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:
- The number of each type resource that it currently holds
- The maximum number of each type of resource that it might need
- The number of each type of resource that exist in system
If we know this, we can do the following:
- Make a copy of the current resource graph
- Add an assignment edge for each newly requested resource
- Add request edges for the maximum needs of each process
- Attempt to reduce the graph
- If we can completely reduce the graph, the system would remain in a safe state after the resource is allocated, so the resource can be allocated. Otherwise, the system would be unsafe and could potentially deadlock.
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.
- 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:
- undo changes to C, R, and A
- block the requesting process until A increases
- after A increases, check again.