There are three deadlock handling methods:
- Deadlock Prevention
- Deadlock Avoidance
- Deadlock Detection
Deadlock Prevention
Deadlock prevention works by ensuring that at least 1 of the necessary conditions for deadlock never happens.
Let’s examine each of the necessary conditions and decide whether we can prevent them from happening.
Mutual Exclusion Prevention
We typically cannot prevent deadlocks by denying the mutex condition, because some resources are intrinsically non-sharable.
For example, a mutex lock cannot be simultaneously shared by several processes.
Hold and Wait Prevention
To ensure that hold and wait never happens, we need to guarantee that whenever a process requests for a resource, it does NOT currently hold any other resources.
Possible through such protocols:
- Protocol 1: Must have all of its resources at once before beginning execution, otherwise, wait empty handed.
- Protocol 2: Only can request for new resources when it has none.
Example
Consider a process that needs to write
from a DVD drive to disk and then print
a document from the disk to the printer machine.
With protocol 1: The process must acquire all three resources: DVD drive, disk, and printer before beginning execution, even though the task with the disk and DVD drive has nothing to do with the printer. After it finishes both tasks, it releases both the disk and printer.
With protocol 2: The process requests for DVD drive and disk, and writes to the disk. Then it releases both resources. Afterwards, it submits a new request to gain access to the disk (again) and the printer, prints the document, and releases both resources.
Disadvantages
Starvation:
- Process with protocol 1 may never start if resources are scarce and there are too many other processes requesting for it.
Low resource utilization:
- In Protocol 1, resources are allocated at once but it is likely that they aren’t used simultaneously (e.g: the sample process requests a printer but it doesn’t utilize it when writing to disk first)
- In Protocol B, lots of time is wasted for requesting and releasing many resources in the middle of execution.
No Preemption Prevention (Allow Preemption)
To allow preemption, we need a certain protocols in place to ensure that progress is not lost.
A process is holding some resources and requests another resource that cannot be immediately allocated to it (that is, the process must wait). We can then decide to do either protocols below:
- Protocol 1: All resources the process is currently holding are preempted – implicitly released.
- Protocol 2: Check first if the resources requested are held by other waiting processes or not.
- If held by other waiting process: preempts the waiting process and give the resource to the requesting process
- If neither held by waiting process nor available: requesting process must wait.
This protocol is often applied to resources whose state can be easily saved and restored later, such as CPU registers and memory space. It cannot generally be applied to such resources as mutex locks and semaphores.
Circular Wait Prevention
To prevent circular wait, one must have a protocol that impose a total ordering of all resource types, and require that each process requests resources according to that order.
Example
We can assign some kind of id
to each resource and we need to design some acquiring protocols such as:
- The highest priority order of currently-held resources should be less than or equal to the current resource being requested, otherwise the process must release the resources that violate this condition.
- Each process can request resources only in an increasing order of enumeration.
This burdens the programmer to ensure the order by desig to ensure that it doesn’t sacrifice resource utilization unnecessarily.
Deadlock Avoidance
In deadlock avoidance solution, we need to spend some time to perform an algorithm to check BEFORE granting a resource request, even if the request is valid and the requested resources are now available.
This Deadlock Avoidance algorithm is called the Banker’s Algorithm. It’s job is to compute whether or not the current request will lead to a deadlock.
- If yes, the request for the resources will be rejected,
- If no, the request will be granted.
This algorithm is run each time a process request the shared resources.
The Banker’s Algorithm
The Banker’s Algorithm is comprised of two parts: The Safety Algorithm and the Resource Allocation Algorithm. The latter utilises the output of the former to determine whether the currently requested resource should be granted or not.
Required Attributes
Before we can run the algorithm, we need to know:
- Number of processes (consumers) in the system (denoted as
N
) - Number of resource types in the system (denoted as
M
), along with initial instances of each resource type at the start. - The maximum number of resources required by each process (consumers).
The System State
The algorithm maintains these four data structures representing the system STATE:
- available: 1 by M vector
available[i]
: the available instances of resourcei
- max: N by M matrix
max[i][j]
: maximum demand of processi
for resourcej
instances
- allocation: N by M matrix
allocation[i][j]
: current allocation of resourcej
instances for processi
- need: N by M matrix
need[i][j]
: how much more of resourcej
instances might be needed by processi
Part 1: Resource Allocation Algorithm
You shall read about this algorithm during Lab. As such, the explanation of the algorithm will not be repeated here.
As time progresses, more resource request or resource release can be invoked by any of the processes.
- Releasing Resources is trivial, we simply update the
allocation
,available
, andneed
data structures. - For each Resource Request received, the output of the algorithm can be either
Granted
orRejected
depending on whether the system will be in a safe state if the resource is granted
System State Update
Note that these requests are made sequentially in time, so don’t forget to update the system state as you grant each request. When considering subsequent new requests, we perform the resource allocation algorithm with the UPDATED states that’s modified if you have granted the previous request.
If the previous request is rejected however, no change in system state is made and you can leave it as is.
Part 2: The Safety Algorithm
This algorithm receives a copy of available
(named as work
), need
, and allocation
data structures to perform a hypothetical situation of whether the system will be in a safe state if the current request is granted.
If we find that all processes can still finish their task (the elements of the finish
vector is all True
), then the system is in a safe state and we can grant this current request.
- If you store each value of index
i
acquired, you can compute the sequence of possible process execution sequence
For example, given the following system state:
and current request R_1
made by P1
: [1,0,2]
, you may find that granting this request leads to a safe state and there exist two possible execution sequence (depending on how you iterate through the finish
vector (from index 0 onwards or from index N-1
backwards):
P1, P3, P4, P0, P2
, orP1, P3, P4, P2, P0
Complexity
Deadlock avoidance is time-consuming, since due to the expensive while
loop in the safety algorithm. The time complexity of the safety algorithm is \(O(MN^2)\) (which is also the complexity of the Banker’s Algorithm, since the complexity of the Resource Allocation Algorithm is way smaller).
Carefully think: why. Also, ask yourself: what is the space complexity of the Banker’s Algorithm?
Deadlock Detection
In deadlock detection, we allow deadlock to happen first (we don’t deny its possibility), and then detect it later.
It is different from deadlock avoidance. The latter is one step ahead since will not grant requests that may lead to deadlocks in the first place.
Deadlock detection mechanism works by providing two things:
- An algorithm that examines the state of the system to determine whether a deadlock has occurred
- An algorithm to recover from deadlock condition
Deadlock Detection Algorithm
This algorithm works similarly Part 2 of the Banker’s Algorithm (the safety algorithm), just that this algorithm is performed on the actual system state (not the hypothetical work
, need
and allocation
!)
Firstly, we need all the state information as in Banker’s algorithm: the available
, request
(renamed from need
), and allocation
matrices (no max/need). The only subtle difference is only that the need
matrix (in safety algorithm) is replaced by the request
matrix.
In a system that adapt deadlock detection as a solution to the deadlock problem, we don’t have to know the max
(resource needed by a process), because we will always grant a request whenever whatever’s requested is available, and invoke deadlock detection algorithm from time to time to ensure that the system is a good state and not deadlocked.
The request
matrix contains current requests of resources made by all processes at the instance we decide to detect whether or not there’s (already) a deadlock occuring in the system.
- Row
i
in therequest
matrix stands for: Processi
requiring some MORE resources that’s what’s been allocated for it.
The deadlock detection algorithm goes as follows:
-
Step 1: Initialize these two vectors:
work
(length is #resourcesM
) initialized to be ==available
finish
(length is #processesN
) initialized to be:False
ifrequest[i]
!= {0} (empty set)- True
otherwise
(means process i doesn’t request for anything else anymore and can be resolved or finished)
- No update of
allocation, need
needed. We are checking whether the CURRENT state is safe, not whether a HYPOTHETICAL state is safe.
-
Step 2: find an index
i
such that both conditions below are fulfilled,Finish[i] == False
request[i] <= work
(element-wise comparison for these vectors)
-
Step 3:
- If Step 2 produces such index
i
, updatework
andfinish[i]
,work += allocation[i]
(vector addition)finish[i] = True
- Go back to Step 2
- If Step 2 does not produce such index
i
, prepare for exit:- If
finish[i] == False
for somei
, then the system is already in a deadlock (caused by Processi
getting deadlocked by another Processj
wherefinish[j] == False
too) - Else if
finish[i] == True
for alli<N
, then the system is not in a deadlock.
- If
- If Step 2 produces such index
Complexity
This deadlock detection algorithm requires an order of \(O(MN^2)\) operations to perform.
Frequency of Detection
When should we invoke the detection algorithm? The answer depends on two factors:
- How often is a deadlock likely to occur?
- How many processes will be affected by deadlock when it happens?
This remains an open issue.
Deadlock Recovery
To recover from deadlock, we can either:
- Abort all deadlocked processes, the resources held are preempted
- Abort deadlocked processes one at a time, until deadlock no longer occurs
The second method results in overhead since we need to run the detection algorithm over and over again each time we abort a process. There are a few design choices:
- Which order shall we abort the processes in?
- Which “victim” shall we select? We need to determine cost factors such as which processes have executed the longest, etc
- If we select a “victim”, how much rollback should we do to the process?
- How do we ensure that starvation does not occur?
Deadlock Recovery remains an open ended issue. Dealing with deadlock is also a difficult problem. Most operating systems do not prevent or resolve deadlock completely and users will deal with it when the need arises.