Loading learning content...
We now understand what a safe state means conceptually—a state from which all processes can complete execution. But how do we determine programmatically whether a given state is safe?
The Safety Algorithm answers this question. It's a systematic procedure that attempts to construct a safe sequence by simulating resource allocation and release. If the algorithm succeeds in finding any valid ordering, the state is safe. If it fails to complete all processes through simulation, no safe sequence exists, and the state is unsafe.
This algorithm is the computational backbone of the Banker's Algorithm. Every time a process requests resources, the system hypothetically grants the request, runs the Safety Algorithm on the resulting state, and only commits the allocation if the state remains safe.
By the end of this page, you will understand the Safety Algorithm's data structures, its step-by-step execution procedure, complexity analysis, implementation considerations, and be able to trace through the algorithm manually on any system state.
The Safety Algorithm operates on well-defined data structures that capture the complete resource allocation state of the system.
Input Data Structures:
| Structure | Dimensions | Description |
|---|---|---|
Available | Vector[m] | Number of available instances of each resource type |
Max | Matrix[n][m] | Maximum claim of each process for each resource type |
Allocation | Matrix[n][m] | Current allocation to each process for each resource type |
Need | Matrix[n][m] | Remaining need: Need[i][j] = Max[i][j] - Allocation[i][j] |
Where:
n = number of processes in the systemm = number of resource typesWorking Data Structures (used during algorithm execution):
| Structure | Dimensions | Purpose |
|---|---|---|
Work | Vector[m] | Simulates available resources as processes complete; initialized to Available |
Finish | Vector[n] | Tracks which processes have been marked as completable; initialized to all false |
We copy Available into Work rather than modifying Available directly. This is because the Safety Algorithm is a simulation—we're asking 'what if processes completed in this order?' without actually changing system state. The actual allocation might not follow the sequence we discover.
The Safety Algorithm attempts to find a safe sequence by iteratively finding processes that could complete with current resources, simulating their completion (adding their allocated resources to the work pool), and repeating until all processes are accounted for—or no progress can be made.
Algorithm Steps:
123456789101112131415161718192021222324
SAFETY_ALGORITHM(Available, Max, Allocation, Need, n, m): // Step 1: Initialize working structures Work[m] ← Available[m] // Copy available resources Finish[n] ← [false, false, ...] // No process finished yet // Step 2: Find a process that can complete REPEAT: found ← false FOR i ← 0 TO n-1: IF Finish[i] = false AND Need[i] ≤ Work THEN: // Step 3: Simulate process completion Work ← Work + Allocation[i] // Resources released Finish[i] ← true found ← true // Note: Don't break - continue checking all processes // Continue until no unfinished process can complete UNTIL found = false // Step 4: Check final result IF all Finish[i] = true THEN: RETURN SAFE ELSE: RETURN UNSAFEDetailed Explanation:
Step 1 - Initialization:
Work starts as a copy of Available—the resources we can currently distributeFinish marks all processes as incompleteStep 2 - Search for Completable Process:
Need[i] ≤ Work means every resource type in the need vector is less than or equal to the corresponding type in the work vectorStep 3 - Simulate Completion:
Step 4 - Termination:
Here's a complete, production-quality implementation of the Safety Algorithm in C. This implementation includes detailed comments, proper memory handling, and clear structure for educational purposes.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
#include <stdio.h>#include <stdbool.h>#include <string.h> #define MAX_PROCESSES 10#define MAX_RESOURCES 10 /** * Checks if vector a is less than or equal to vector b * (component-wise comparison for all resource types) * * @param a First vector (typically Need) * @param b Second vector (typically Work) * @param m Number of resource types * @return true if a[i] <= b[i] for all i */bool vector_le(int a[], int b[], int m) { for (int j = 0; j < m; j++) { if (a[j] > b[j]) { return false; // Found a resource type where need exceeds work } } return true;} /** * Adds vector b to vector a (a = a + b) * Used to simulate releasing resources when process completes * * @param a Destination vector (modified in place) * @param b Source vector * @param m Number of resource types */void vector_add(int a[], int b[], int m) { for (int j = 0; j < m; j++) { a[j] += b[j]; }} /** * The Safety Algorithm * * Determines whether the current system state is safe by attempting * to construct a safe sequence. If successful, all processes can * complete; if not, deadlock is possible. * * @param n Number of processes * @param m Number of resource types * @param available Current available resources * @param allocation Current allocation matrix * @param need Current need matrix * @param safe_sequence Output: the safe sequence if one exists * @return true if state is safe, false otherwise */bool safety_algorithm( int n, int m, int available[], int allocation[MAX_PROCESSES][MAX_RESOURCES], int need[MAX_PROCESSES][MAX_RESOURCES], int safe_sequence[] // Output parameter) { // Step 1: Initialize working structures int work[MAX_RESOURCES]; bool finish[MAX_PROCESSES]; int sequence_index = 0; memcpy(work, available, sizeof(int) * m); memset(finish, false, sizeof(finish)); // Step 2 & 3: Find and simulate completing processes bool found; do { found = false; for (int i = 0; i < n; i++) { // Check if process i can complete if (!finish[i] && vector_le(need[i], work, m)) { // Process i can complete with current resources // Simulate completion: release its resources vector_add(work, allocation[i], m); // Mark as finished finish[i] = true; // Record in safe sequence safe_sequence[sequence_index++] = i; found = true; // Debug output (optional) printf("Process P%d can complete. Work: [", i); for (int j = 0; j < m; j++) { printf("%d%s", work[j], j < m-1 ? ", " : ""); } printf("]\n"); } } } while (found); // Step 4: Check if all processes finished for (int i = 0; i < n; i++) { if (!finish[i]) { printf("Process P%d cannot complete - STATE UNSAFE\n", i); return false; } } return true;} // Example usage and testint main() { int n = 5; // Number of processes int m = 3; // Number of resource types (A, B, C) int available[] = {3, 3, 2}; int allocation[MAX_PROCESSES][MAX_RESOURCES] = { {0, 1, 0}, // P0 {2, 0, 0}, // P1 {3, 0, 2}, // P2 {2, 1, 1}, // P3 {0, 0, 2} // P4 }; int need[MAX_PROCESSES][MAX_RESOURCES] = { {7, 4, 3}, // P0 {1, 2, 2}, // P1 {6, 0, 0}, // P2 {0, 1, 1}, // P3 {4, 3, 1} // P4 }; int safe_sequence[MAX_PROCESSES]; printf("Checking system safety...\n\n"); if (safety_algorithm(n, m, available, allocation, need, safe_sequence)) { printf("\nSYSTEM IS SAFE\n"); printf("Safe sequence: <"); for (int i = 0; i < n; i++) { printf("P%d%s", safe_sequence[i], i < n-1 ? ", " : ""); } printf(">\n"); } else { printf("\nSYSTEM IS UNSAFE - Deadlock possible\n"); } return 0;}Running this code produces:
Process P1 can complete. Work: [5, 3, 2]
Process P3 can complete. Work: [7, 4, 3]
Process P0 can complete. Work: [7, 5, 3]
Process P2 can complete. Work: [10, 5, 5]
Process P4 can complete. Work: [10, 5, 7]
SYSTEM IS SAFE
Safe sequence: <P1, P3, P0, P2, P4>
Understanding the computational cost of the Safety Algorithm is essential for evaluating its practicality in real systems.
Time Complexity:
| Operation | Frequency | Cost | Total |
|---|---|---|---|
| Outer loop (do-while) | At most n iterations | — | O(n) |
| Inner loop (for each process) | n processes per outer iteration | — | O(n) × O(n) |
| Vector comparison (Need ≤ Work) | Each inner iteration | O(m) | O(n²) × O(m) |
| Vector addition | At most n times total | O(m) | O(n) × O(m) |
Overall Time Complexity: O(n² × m)
Where:
n = number of processesm = number of resource typesReasoning:
n times (each iteration marks at least one process as finished, or we terminate)n processesO(m) timeSpace Complexity: O(n + m)
Work vector: O(m)Finish array: O(n)Safe_sequence array: O(n)Total additional space: O(n + m)
With n=100 processes and m=20 resource types, the algorithm performs roughly 200,000 operations per safety check. Since the algorithm runs for every resource request, a system handling 1000 requests/second would need 200 million operations/second just for safety checking. This overhead is significant and explains why pure deadlock avoidance is uncommon in general-purpose OS.
Optimization Opportunities:
Early Exit: The algorithm can terminate as soon as it finds that all processes can complete, but we're already doing this implicitly.
Smart Ordering: Start by checking processes that are closest to completion (smallest Need), as they're most likely to succeed.
Incremental Updates: Rather than running the full algorithm after each request, maintain auxiliary data structures that allow O(n × m) updates.
Process Subset: Only run safety checks on processes that could potentially be affected by the new allocation.
Caching: Cache recent safety results if system state is relatively stable.
For the Safety Algorithm to be trusted, we must prove it correctly identifies safe and unsafe states.
Theorem: The Safety Algorithm returns SAFE if and only if the state is safe.
Proof (Soundness - If algorithm returns SAFE, state is safe):
Assume the algorithm returns SAFE. Then:
Finish[i] are trueNeed[i] ≤ Work at some pointFor any process Pₖ marked finished at step k:
Proof (Completeness - If state is safe, algorithm returns SAFE):
Assume the state is safe, so a safe sequence <Pₐ, Pᵦ, ...> exists.
We show by induction that the algorithm will find some safe sequence (not necessarily the same one):
Base: Initially, Work = Available. For the first process Pₐ in the safe sequence, Need[a] ≤ Available = Work. So the algorithm will find at least one process to mark finished.
Inductive Step: Assume the algorithm has marked k processes finished. The Work vector equals Available plus all released allocations from these k processes. Since a safe sequence exists and the algorithm's marked processes could appear at the beginning of some safe sequence, the next process in that sequence can be satisfied by current Work. Therefore, at least one more process can be marked finished.
Conclusion: The algorithm will mark all n processes finished, returning SAFE. ∎
A crucial property: the algorithm finds some safe sequence if one exists, but doesn't need to find a specific one. The order in which the inner loop examines processes may affect which sequence is discovered, but not whether one is found.
Let's trace through the Safety Algorithm execution in complete detail using our standard example.
Initial State:
| Process | Allocation | Need |
|---|---|---|
| P₀ | (0, 1, 0) | (7, 4, 3) |
| P₁ | (2, 0, 0) | (1, 2, 2) |
| P₂ | (3, 0, 2) | (6, 0, 0) |
| P₃ | (2, 1, 1) | (0, 1, 1) |
| P₄ | (0, 0, 2) | (4, 3, 1) |
Initialization:
Work = [3, 3, 2] (copy of Available)
Finish = [false, false, false, false, false]
Sequence = []
Iteration 1:
| Process | Finish? | Need | Work | Need ≤ Work? | Action |
|---|---|---|---|---|---|
| P₀ | false | (7,4,3) | (3,3,2) | 7>3 ✗ | Skip |
| P₁ | false | (1,2,2) | (3,3,2) | 1≤3, 2≤3, 2≤2 ✓ | Can complete! |
After P₁ completes:
Work = [3, 3, 2] + [2, 0, 0] = [5, 3, 2]
Finish = [false, true, false, false, false]
Sequence = [1]
found = true → continue outer loop
Iteration 2 (continues scanning from P₂):
| Process | Finish? | Need | Work | Need ≤ Work? | Action |
|---|---|---|---|---|---|
| P₀ | false | (7,4,3) | (5,3,2) | 7>5 ✗ | Skip |
| P₁ | true | — | — | — | Already finished |
| P₂ | false | (6,0,0) | (5,3,2) | 6>5 ✗ | Skip |
| P₃ | false | (0,1,1) | (5,3,2) | 0≤5, 1≤3, 1≤2 ✓ | Can complete! |
After P₃ completes:
Work = [5, 3, 2] + [2, 1, 1] = [7, 4, 3]
Finish = [false, true, false, true, false]
Sequence = [1, 3]
Iteration 3:
| Process | Finish? | Need | Work | Need ≤ Work? | Action |
|---|---|---|---|---|---|
| P₀ | false | (7,4,3) | (7,4,3) | 7≤7, 4≤4, 3≤3 ✓ | Can complete! |
After P₀ completes:
Work = [7, 4, 3] + [0, 1, 0] = [7, 5, 3]
Finish = [true, true, false, true, false]
Sequence = [1, 3, 0]
Iteration 4:
Iteration 5:
Termination:
It's equally important to see how the algorithm detects unsafe states. Let's modify our example to create an unsafe condition.
Modified State:
| Process | Need | Work=(1,1,0) | Can Complete? |
|---|---|---|---|
| P₀ | (7, 4, 3) | 7>1, 4>1, 3>0 | NO |
| P₁ | (1, 2, 2) | 1≤1, 2>1 | NO |
| P₂ | (6, 0, 0) | 6>1 | NO |
| P₃ | (0, 1, 1) | 0≤1, 1≤1, 1>0 | NO |
| P₄ | (4, 3, 1) | 4>1, 3>1, 1>0 | NO |
Algorithm Execution:
Iteration 1:
Work = [1, 1, 0]
Check all processes: none can complete
found = false
Termination:
No safe sequence exists with these available resources. The system could deadlock if processes request their maximum needs.
In an unsafe state, the algorithm completes exactly one pass through the inner loop without finding any process to mark finished. This is the termination condition for detecting unsafety—when a complete scan yields no progress, no safe sequence is possible.
Why This State Is Unsafe:
With only (1, 1, 0) available:
The Banker's Algorithm would have prevented this state from ever being reached by refusing the allocation request that reduced Available from (3, 3, 2) to (1, 1, 0).
The Safety Algorithm is the computational core of deadlock avoidance. Let's consolidate the key insights:
What's Next:
The Safety Algorithm tells us whether a state is safe, but we haven't yet discussed how to handle incoming resource requests. The next page introduces the Resource Request Algorithm—the procedure that receives process requests, hypothetically grants them, checks safety, and decides whether to commit or defer the allocation.
You now understand the Safety Algorithm—the computational heart of deadlock avoidance. You can trace through the algorithm manually, analyze its complexity, understand its correctness, and recognize both safe and unsafe states.