COMP 3150Operating Systems

01 Lab overview

CourseCOMP 3150 / CMIS 3150 — Operating Systems
UnitUnit 7 — Deadlocks
Weight1.25% of the final grade (1/12 of the 15% laboratory component)
Estimated duration4–5 hours of hands-on work
SubmissionOne archive containing the C source files, the Makefile, the output logs, and a short lab report (see § 06)
EnvironmentLinux VM, gcc, make, valgrind, strace, optional graphviz for resource allocation graphs

This lab contributes to U7.1 through U7.5, and to course learning outcomes CLO3 and CLO8.

02 Learning objectives

By the end of this laboratory you will be able to

  • Explain the four necessary conditions for deadlock and demonstrate that each is independently necessary.
  • Construct and interpret a resource allocation graph, and identify cycles and knots.
  • Apply the four prevention techniques (mutual exclusion, hold-and-wait elimination, preemption, circular-wait breaking) and evaluate their cost.
  • Implement the Banker's algorithm for deadlock avoidance and demonstrate its safe-state test.
  • Implement a deadlock detector for a single-instance and a multiple-instance resource model.
  • Compare prevention, avoidance, detection, and recovery, and select the appropriate strategy for a given workload.

03 Background

3.1 The four necessary conditions

Deadlock can occur if and only if all four of the following conditions hold simultaneously:

  • Mutual exclusion. At least one resource is non-shareable — only one process may hold it at a time.
  • Hold and wait. A process holding at least one resource is waiting to acquire additional resources held by other processes.
  • No preemption. A resource can be released only voluntarily by the process holding it.
  • Circular wait. There is a closed chain of processes, each waiting for a resource held by the next.

The four conditions are independently necessary: if any one is removed, deadlock becomes impossible. This is why every prevention technique targets exactly one of them.

3.2 Resource allocation graphs

A resource allocation graph has two node sets (processes P and resources R) and two edge sets: a request edge from a process to a resource, and an assignment edge from a resource to a process. If every resource has a single instance, a cycle in the graph is a necessary and sufficient condition for deadlock. If resources have multiple instances, a cycle is necessary but not sufficient — the deadlock depends on which instance a process is waiting for.

3.3 Banker's algorithm

The Banker's algorithm is a deadlock avoidance technique. It maintains the matrices Allocation, Max, and the vector Available, and grants a request only if the resulting state is safe. A state is safe if there exists at least one sequence in which every process can finish using the currently available resources plus those held by processes that have already finished. If no such sequence exists, the request is denied (the requesting process is blocked and retries later).

# Safe-state test (n processes, m resource types)
Work = Available; Finish[i] = false for all i
while there is an i with Finish[i]==false and Need[i] <= Work:
    Work += Allocation[i]
    Finish[i] = true
return true if Finish[i] == true for all i

3.4 Detection and recovery

Detection runs a cycle-detection algorithm (for single-instance resources) or the Banker's-style reduction (for multiple instances) periodically or when the system suspects deadlock. Recovery may involve process termination (kill all deadlocked processes, or kill them one at a time until the cycle is broken) or resource preemption (take a resource from one holder and give it to a waiter, rolling the holder back to a safe point if the system supports checkpoints).

04 Guided activities

Activity 4.1 — Constructing a deadlock deliberately

  1. Create the lab directory and the file deadlock_basic.c:
    mkdir -p ~/comp3150/lab6 && cd ~/comp3150/lab6
  2. Write a program with two mutexes A and B and two threads. Thread 1 locks A, sleeps briefly, then locks B. Thread 2 locks B, sleeps briefly, then locks A. The sleep ensures the interleaving that produces deadlock.
  3. Run the program. Confirm that it hangs. Attach gdb to the running process (gdb -p <pid>), and use info threads and thread apply all bt to show that each thread is blocked on a pthread_mutex_lock call for the mutex held by the other.
  4. Take a screenshot or save the backtrace output as deadlock_basic.txt.
  5. Verify with valgrind --tool=helgrind that the tool reports a lock-order inversion. What does the report say, and how does it identify the two locks and the two threads?

Activity 4.2 — Breaking the circular wait

  1. Modify the program to impose a global lock ordering: both threads must acquire A before B. The deadlock disappears.
  2. Now modify the program to release the first lock before acquiring the second (breaks hold-and-wait). The deadlock also disappears, but the program's semantics change — the operation is no longer atomic across the two locks.
  3. Use pthread_mutex_trylock to implement the "no preemption" break: if the second lock cannot be acquired, release the first and retry. Add randomised back-off and verify the program terminates.
  4. For each of the three fixes, write two sentences stating which of the four necessary conditions it removes.

Activity 4.3 — Resource allocation graph

  1. For the program in Activity 4.1, draw the resource allocation graph. Each thread is a process node; each mutex is a resource node with one instance.
  2. Render the graph with Graphviz (dot -Tpdf) and include it in your report.
  3. Extend the program to a third thread and a third mutex in a configuration that produces a longer cycle. Draw the new graph and confirm that the cycle is a single closed chain.
  4. Construct a program with no deadlock but a cycle in the graph. This is impossible for single-instance resources; explain why, and then construct the analogous case for multiple-instance resources: two processes each waiting for an instance of a resource, but the resource has two instances and the other process holds the one that satisfies its request.

Activity 4.4 — Banker's algorithm

  1. Implement the Banker's algorithm in banker.c:
    • The program reads an Allocation matrix, a Max matrix, and an Available vector from standard input (or a file).
    • It implements safe_state() that returns the safe sequence if one exists, or reports that the state is unsafe.
    • It implements request(pid, request_vector) that checks the request against Need, the request against Available, and then tests the resulting state for safety before granting the request.
  2. Test the implementation with the following state and verify that a safe sequence exists:
            Allocation   Max       Available
    P0      0 1 0        7 5 3     3 3 2
    P1      2 0 0        3 2 2
    P2      3 0 2        9 0 2
    P3      2 1 1        2 2 2
    P4      0 0 2        4 3 3
    One valid safe sequence is ⟨P1, P3, P4, P0, P2⟩. Write the state transitions step by step.
  3. Now issue the request P1: (1, 0, 2). The request exceeds the need for resource type 1; the algorithm must deny it. Then issue P4: (3, 3, 0) — this request cannot be granted because it exceeds Available. Finally issue P0: (0, 2, 0) and show that granting it leads to an unsafe state.
  4. Discuss the practical limitations of Banker's algorithm: the need to know Max in advance, the cost of the safety check on every request, and the fact that the algorithm cannot handle dynamic process arrival or resources that appear and disappear.

Activity 4.5 — Deadlock detection

  1. Write detect.c that implements the detection algorithm for multiple-instance resources:
    Work = Available
    Finish[i] = (Allocation[i] == 0) for all i
    while exists i with Finish[i] == false and Request[i] <= Work:
        Work += Allocation[i]
        Finish[i] = true
    Deadlocked = {i : Finish[i] == false}
  2. Feed the algorithm the states from Activity 4.4 and verify the expected results: after the initial state, no deadlock; after the erroneous grant of P0: (0, 2, 0) forced through, the deadlock set should be non-empty.
  3. Add periodic detection to a running program: spawn four threads that acquire and release mutexes, and a fifth thread that every second runs the detector on a shared snapshot of the lock state. Log every detection event.
  4. Simulate recovery by killing one of the detected deadlocked threads with pthread_cancel and verifying that the other threads proceed. Discuss why killing the wrong thread may leave shared data inconsistent, and how a real system decides which thread to victimise.

05 Exercises

  1. Dining philosophers, revisited. Implement the dining philosophers with each of the four prevention strategies — resource ordering, global semaphore, asymmetric acquisition, and trylock with back-off — and measure the total throughput (meals per second) for 100,000 meals per philosopher. Which strategy is fastest? Which is fairest?
  2. Ordered mutexes with a runtime checker. Modify the program from Activity 4.1 so that every mutex has a numeric rank, and each acquisition verifies that the rank is strictly greater than the rank of every mutex the thread already holds. On violation, print a diagnostic and abort. This is a runtime detector for lock-order inversion; explain why a static analyser (like Helgrind) is usually a better tool for the same purpose.
  3. Banker's algorithm, extended. Extend your implementation to support resource types that are added after some processes have started. The state must be re-tested for safety whenever a resource appears; describe when such a state transition is safe and when it is not.
  4. Deadlock in a database transaction. Simulate a small database with row-level locks. Two transactions T1 and T2 access rows R1 and R2 in opposite orders. Implement a wait-for graph and detect the cycle. Then implement deadlock recovery by aborting the transaction with the least work done (measured by number of rows modified). Compare this heuristic with aborting the newest transaction.
  5. Comparison of strategies. For a workload in which resources are requested in random order, measure the throughput of (a) no deadlock handling, (b) prevention by global ordering, (c) avoidance by Banker's, and (d) detection and recovery. Present the results and explain why real operating systems rarely use Banker's.
  6. Deadlock in a pipe-based shell pipeline. Construct a shell pipeline in which process A writes to pipe p1 and reads from pipe p2, while process B does the reverse. Use mkfifo and shell redirection to build the pipeline. The two processes deadlock on the first blocking write to an unread pipe. Explain why the standard shell's own pipeline construction (in which each process reads from the previous and writes to the next) does not deadlock.

06 Deliverables

Submit a single archive named lab6-<lastname>-<studentnumber>.tar.gz containing:

  • deadlock_basic.c — Activity 4.1.
  • deadlock_fixed.c — Activity 4.2 with three prevention strategies selectable by a command-line flag.
  • banker.c — Activity 4.4.
  • detect.c — Activity 4.5.
  • philosophers.c — Exercise 1 (four prevention strategies selectable by a flag).
  • waitfor.c — Exercise 4 (wait-for graph with recovery).
  • The Makefile with one target per program and a clean target.
  • deadlock_basic.txt — the gdb backtrace and Helgrind report from Activity 4.1.
  • graphs/ — the Graphviz source and rendered PDFs for Activity 4.3.
  • outputs.txt — the output logs of all programs with the commands that produced them.
  • report.md (or report.pdf) — the lab report described below.

Lab report

The report should be 4–6 pages (excluding code) and contain:

  1. Deadlock characterisation. For each deadlock you produce, identify which of the four necessary conditions hold and, for the fixes, which condition is removed. Use the resource allocation graph to justify each claim.
  2. Banker's algorithm. The safe sequence for the test state, with the intermediate Work vectors, and the three denied requests with the reason for each denial.
  3. Detection results. The deadlock sets produced by the detector for each test state, and the recovery decisions with their justification.
  4. Throughput comparison. The measurements from Exercise 5, with a discussion of the trade-offs between the four strategies.
  5. Reflection. A discussion of why Linux, Windows, and most general-purpose operating systems do not implement Banker's algorithm, and what they do instead (lock ordering discipline, timeout and roll-back in databases, and detection with killing in a few kernel subsystems).

07 Grading rubric

Lab 6 grading rubric (100 points)
Criterion Points CLOs Exemplary (90–100%) Proficient (80–89%) Developing (70–79%)
Deadlock construction and characterisation 25 CLO3 Deadlocks are produced reliably, each necessary condition is identified, and the RAG correctly justifies the analysis. Deadlocks produced; characterisation mostly correct. Deadlocks produced but conditions or RAG analysis is incomplete.
Prevention strategies 20 CLO3, CLO8 All four prevention techniques implemented and correctly mapped to the conditions they remove. Most techniques implemented; mapping partially complete. Techniques implemented but with incorrect or missing mapping.
Banker's algorithm 25 CLO3, CLO8 Safety test and request handling are correct; all test cases produce the expected results with hand-verified transitions. Implementation correct on the main cases; edge cases incomplete. Implementation has bugs or fails on unsafe-state cases.
Detection and recovery 20 CLO3, CLO8 Detector identifies deadlock sets correctly; recovery is demonstrated and its trade-offs discussed. Detector works; recovery demonstrated but analysis is brief. Detector or recovery incomplete or incorrect.
Report quality 10 CLO3 Report is evidence-based, well-structured, and the reflection is insightful. Report is clear; some claims lack evidence. Report is disorganised or contains inaccuracies.

Late submission penalty: 10% per day, up to a maximum of three days, after which the lab is accepted only with documented extenuating circumstances.

08 Knowledge check

Check your understanding

Question 1

Why is a cycle in the resource allocation graph a sufficient condition for deadlock when resources have a single instance, but only a necessary condition when they have multiple instances?

Show answer
With single-instance resources, each request edge from a process points at a unique resource that is held by a specific other process. The cycle therefore describes an actual chain of waiting: each process waits for a resource that the next process in the cycle holds, and none can proceed. With multiple-instance resources, a request edge from a process points at a type of resource, not a specific instance. The process may be waiting for any instance of that type, and an instance held by one of the processes in the cycle may be the one that would satisfy it. A cycle therefore indicates the possibility of deadlock, not its certainty — the deadlock depends on which instance each process is waiting for. The detection algorithm for multiple instances (the Banker's-style reduction) resolves this by simulating the grant of resources and checking whether every process can finish.

Question 2

A system has four processes P0–P3 and three resource types A, B, C with 10, 5, and 7 total instances. Current state:

        Allocation   Max
P0      0 1 0        7 5 3
P1      2 0 0        3 2 2
P2      3 0 2        9 0 2
P3      2 1 1        2 2 2
Is the state safe? If so, give a safe sequence.

Show answer
Available = (10, 5, 7) − (0+2+3+2, 1+0+0+1, 0+0+2+1) = (10 − 7, 5 − 2, 7 − 3) = (3, 3, 4). Need = Max − Allocation: P0 = (7, 4, 3), P1 = (1, 2, 2), P2 = (6, 0, 0), P3 = (0, 1, 1). Safe sequence: P1, P3, P0, P2. P1 (1, 2, 2) ≤ (3, 3, 4) → Work becomes (3+2, 3+0, 4+0) = (5, 3, 4). P3 (0, 1, 1) ≤ (5, 3, 4) → Work becomes (5+2, 3+1, 4+1) = (7, 4, 5). P0 (7, 4, 3) ≤ (7, 4, 5) → Work becomes (7, 5, 5). P2 (6, 0, 0) ≤ (7, 5, 5) → Work becomes (10, 5, 7). All processes finish; the state is safe.

Question 3

Explain why preventing deadlock by breaking hold-and-wait usually causes lower resource utilisation than breaking circular wait.

Show answer
Breaking hold-and-wait requires a process to request all its resources before it starts, or to release all held resources before requesting a new one. The first approach forces the process to hold resources it will not need for some time, blocking other processes that could have used them in the interim. The second approach can livelock: a process that repeatedly releases and re-requests may never acquire all the resources it needs, and it wastes work acquiring resources it must later release. Breaking circular wait by imposing a global ordering, by contrast, allows processes to acquire resources incrementally, so each resource is held only as long as it is needed. The trade-off is that the global ordering imposes a discipline on programmers, and a resource that cannot be ordered (for example, an anonymous lock allocated at run time) cannot participate.

Question 4

Describe the two main recovery strategies from deadlock, and give one advantage and one disadvantage of each.

Show answer
Process termination. Kill one or more deadlocked processes. Advantage: simple and immediate — the cycle is broken as soon as a victim is killed. Disadvantage: if the victim held partially completed work, that work may need to be redone; if the victim held a resource whose state is inconsistent, the system may need to be repaired. Recovery by killing all deadlocked processes is wasteful (some were not the cause of the deadlock), and killing them one at a time requires re-running detection after each kill. Resource preemption. Take a resource from one holder and give it to a waiter. Advantage: less destructive than termination when the preempted process can be rolled back to a checkpoint. Disadvantage: requires the system to support checkpointing and rollback — without them, preempting a resource from a process that has already modified it may leave the process in an inconsistent state. It also requires choosing a victim (often the process with the least work done), and repeated preemption of the same process can cause starvation.

Question 5

In practice, why do most operating systems not implement the Banker's algorithm, and what do they do instead?

Show answer
Banker's algorithm requires every process to declare its maximum resource requirements in advance — an assumption that is unrealistic for general-purpose workloads, where a process's needs depend on its input and on decisions made at run time. It also requires the safety check to run on every resource request, which is expensive when the number of processes and resource types is large. It cannot handle dynamic process arrival, and it interacts poorly with paging, where the "resources" (pages) are managed by the virtual-memory subsystem on a much finer granularity. What general-purpose systems do instead is prevent deadlock by convention: kernel code follows strict lock-ordering rules, and tools like lockdep verify the ordering dynamically; databases use detection with timeouts and roll-back; and some subsystems use lock-free algorithms that avoid deadlock by design. Deadlock is therefore treated as a programming error to be caught during development, not as a runtime condition to be avoided by the scheduler.

09 References

  • Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 8.
  • Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. Operating Systems: Three Easy Pieces, Chapter 32.
  • Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 6.
  • Coffman, E. G., Elphick, M. J., & Shoshani, A. System Deadlocks. ACM Computing Surveys, 1971.
  • Havender, J. W. Avoiding Deadlock in Multitasking Systems. IBM Systems Journal, 1968.
  • Dijkstra, E. W. Cooperating Sequential Processes. Academic Press, 1968.
  • Linux man-pages: man 3 pthread_mutex_lock, man 3 pthread_mutex_trylock, man 1 valgrind (Helgrind section).
👤