Skip to content

Split View: [운영체제] 08. 교착 상태: 예방, 회피, 탐지, 복구

|

[운영체제] 08. 교착 상태: 예방, 회피, 탐지, 복구

교착 상태란

교착 상태(Deadlock)는 두 개 이상의 프로세스가 서로 상대방이 보유한 자원을 기다리며 영원히 진행하지 못하는 상태다.

[교착 상태의 일상적 비유]

좁은 다리에서 양쪽에서 온 차가 마주침:
  <---AB --->
        [다리]
둘 다 상대방이 물러나기를 기다림 -> 교착 상태

시스템에서의 예:
스레드 1: lock(A) -> lock(B) 시도 (대기)
스레드 2: lock(B) -> lock(A) 시도 (대기)
// 교착 상태를 유발하는 코드 예시
#include <pthread.h>

pthread_mutex_t mutex_a = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_b = PTHREAD_MUTEX_INITIALIZER;

void *thread1(void *arg) {
    pthread_mutex_lock(&mutex_a);    // A 획득
    sleep(1);                         // 타이밍 문제 유발
    pthread_mutex_lock(&mutex_b);    // B 대기 (교착!)
    // ...
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
    return NULL;
}

void *thread2(void *arg) {
    pthread_mutex_lock(&mutex_b);    // B 획득
    sleep(1);                         // 타이밍 문제 유발
    pthread_mutex_lock(&mutex_a);    // A 대기 (교착!)
    // ...
    pthread_mutex_unlock(&mutex_a);
    pthread_mutex_unlock(&mutex_b);
    return NULL;
}

교착 상태의 4가지 필요 조건

교착 상태는 다음 네 가지 조건이 동시에 성립할 때만 발생한다.

  1. 상호 배제(Mutual Exclusion): 자원을 한 번에 하나의 프로세스만 사용 가능
  2. 점유 대기(Hold and Wait): 자원을 보유한 채 다른 자원을 추가로 요청하며 대기
  3. 비선점(No Preemption): 이미 할당된 자원을 강제로 빼앗을 수 없음
  4. 순환 대기(Circular Wait): P0 -> P1 -> P2 -> ... -> Pn -> P0 형태로 순환적으로 대기

네 조건 중 하나라도 제거하면 교착 상태를 방지할 수 있다.


자원 할당 그래프 (Resource-Allocation Graph)

교착 상태를 시각적으로 표현하는 방향 그래프다.

[자원 할당 그래프 요소]

프로세스:  (O)
자원 유형: 사각형 안의  (인스턴스)
요청 간선: Pi -> Rj  (프로세스가 자원을 요청)
할당 간선: Rj -> Pi  (자원이 프로세스에 할당됨)
[교착 상태가 있는 자원 할당 그래프]

    P1 --요청--> R1 (1) --할당--> P2
    ^                               |
    |                               |
  할당                            요청
    |                               |
    R2 (1) <--요청-- P3 <--할당-- R3 (1)

P1R1요청 (P2가 보유)
P2R3요청 (P3가 보유)
P3R2요청 (P1이 보유)
-> 순환 존재 -> 교착 상태!
[교착 상태가 없는 자원 할당 그래프 - 자원 인스턴스가 여러 개]

    P1 --요청--> R1 (2)  인스턴스1 --할당--> P2
                           인스턴스2 --할당--> P3

P2P3 중 하나가 R1을 반납하면 P1이 진행 가능
-> 순환이 있어도 교착 상태가 아닐 수 있음
   (자원 인스턴스가 2개 이상일 때)

규칙 요약은 다음과 같다.

  • 자원 유형당 인스턴스가 1개: 순환이 있으면 반드시 교착 상태
  • 자원 유형당 인스턴스가 여러 개: 순환이 있어도 교착 상태가 아닐 수 있음

교착 상태 처리 방법

교착 상태를 다루는 네 가지 접근법이 있다.

  1. 예방(Prevention): 4가지 조건 중 하나를 원천적으로 제거
  2. 회피(Avoidance): 자원 요청 시 안전한 경우에만 허용
  3. 탐지 및 복구(Detection and Recovery): 교착 발생을 허용하고, 감지 후 복구
  4. 무시(Ignore): 교착 상태를 무시 (대부분의 OS가 채택, 이른바 오스트리치 알고리즘)

교착 상태 예방 (Prevention)

1. 상호 배제 부정

공유 가능한 자원(예: 읽기 전용 파일)은 상호 배제가 불필요하다. 하지만 프린터, 뮤텍스 같은 본질적으로 공유 불가능한 자원에는 적용 불가능하다.

2. 점유 대기 부정

프로세스가 자원을 요청할 때, 다른 자원을 보유하지 않도록 보장한다.

방법 A: 실행 전 필요한 모든 자원을 한꺼번에 요청
  장점: 단순
  단점: 자원 이용률 저하, 기아 가능

방법 B: 새 자원 요청 시 보유 자원을 모두 반납 후 재요청
  장점: 유연
  단점: 구현 복잡

3. 비선점 부정

자원을 보유한 프로세스가 다른 자원을 얻지 못하면, 보유 자원을 강제로 회수한다.

프로세스 P가 자원 A를 보유하고 B를 요청:
  B를 사용할 수 없으면:
    A를 강제로 해제 (선점)
    PAB 모두 기다리는 상태로 전환
    AB 모두 사용 가능해지면 재시작

CPU 레지스터나 메모리 같이 상태를 저장/복원할 수 있는 자원에만 적용 가능하다.

4. 순환 대기 부정

모든 자원 유형에 번호를 부여하고, 번호 순서대로만 요청하도록 강제한다.

// 순환 대기 예방: 자원 순서 규약
// 자원 번호: mutex_a = 1, mutex_b = 2, mutex_c = 3

// 올바른 사용: 항상 낮은 번호부터 획득
void safe_function() {
    pthread_mutex_lock(&mutex_a);    // 번호 1
    pthread_mutex_lock(&mutex_b);    // 번호 2
    pthread_mutex_lock(&mutex_c);    // 번호 3

    // 임계 영역

    pthread_mutex_unlock(&mutex_c);
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
}

// 잘못된 사용: 번호 역순으로 요청하면 교착 위험
void unsafe_function() {
    pthread_mutex_lock(&mutex_c);    // 번호 3
    pthread_mutex_lock(&mutex_a);    // 번호 1 (순서 위반!)
}

교착 상태 회피 (Avoidance)

교착 상태 회피는 시스템에 추가 정보를 제공하여, 자원 할당이 교착 상태를 유발할 수 있으면 요청을 거부한다.

안전 상태 (Safe State)

시스템이 안전 상태에 있다면, 어떤 순서로든 모든 프로세스를 완료할 수 있는 **안전 순서(safe sequence)**가 존재한다.

[안전 상태와 교착 상태의 관계]

+-------------------------------------------+
|              전체 상태                      |
|  +----------------------------------+     |
|  |          안전 상태                 |     |
|  |                                  |     |
|  |                                  |     |
|  +----------------------------------+     |
|            불안전 상태                     |
|       (교착 가능 영역 포함)                |
|                 +------+                  |
|                 |교착   |                  |
|                 |상태   |                  |
|                 +------+                  |
+-------------------------------------------+

안전 상태 -> 교착 상태 없음 (보장)
불안전 상태 -> 교착 상태 가능 (반드시는 아님)

은행원 알고리즘 (Banker's Algorithm)

Dijkstra가 제안한 교착 상태 회피 알고리즘이다. 은행이 대출할 때 파산하지 않도록 관리하는 것에 비유된다.

필요한 자료 구조는 다음과 같다 (n = 프로세스 수, m = 자원 유형 수).

Available[m]:  각 자원 유형의 가용 인스턴스 수
Max[n][m]:     각 프로세스의 최대 요구량
Allocation[n][m]: 현재 각 프로세스에 할당된 양
Need[n][m]:    각 프로세스의 잔여 필요량 (Max - Allocation)

안전성 알고리즘

[안전성 검사 알고리즘]

1. Work = Available 복사
   Finish[i] = false (모든 i에 대해)

2. 다음 조건을 만족하는 i를 찾음:
   Finish[i] == false AND Need[i] <= Work

3. 찾으면:
   Work = Work + Allocation[i]
   Finish[i] = true
   2단계로 돌아감

4. 못 찾으면:
   모든 Finish[i]true이면 -> 안전 상태
   아니면 -> 불안전 상태

예제

[은행원 알고리즘 예제]

자원: A(10), B(5), C(7)

        Allocation    Max        Need       Available
        A  B  C      A  B  C    A  B  C    A  B  C
P0      0  1  0      7  5  3    7  4  3    3  3  2
P1      2  0  0      3  2  2    1  2  2
P2      3  0  2      9  0  2    6  0  0
P3      2  1  1      2  2  2    0  1  1
P4      0  0  2      4  3  3    4  3  1

안전 순서 찾기:
1. Work = [3,3,2]
   P1Need[1,2,2] <= [3,3,2]?!
   Work = [3,3,2] + [2,0,0] = [5,3,2], Finish[P1] = true

2. Work = [5,3,2]
   P3Need[0,1,1] <= [5,3,2]?!
   Work = [5,3,2] + [2,1,1] = [7,4,3], Finish[P3] = true

3. Work = [7,4,3]
   P4Need[4,3,1] <= [7,4,3]?!
   Work = [7,4,3] + [0,0,2] = [7,4,5], Finish[P4] = true

4. Work = [7,4,5]
   P2Need[6,0,0] <= [7,4,5]?!
   Work = [7,4,5] + [3,0,2] = [10,4,7], Finish[P2] = true

5. Work = [10,4,7]
   P0Need[7,4,3] <= [10,4,7]?!
   Finish[P0] = true

안전 순서: P1 -> P3 -> P4 -> P2 -> P0
현재 시스템은 안전 상태!

자원 요청 알고리즘

[P1자원 (1,0,2)를 추가 요청한 경우]

1. Request[1] = [1,0,2] <= Need[1] = [1,2,2]?  (유효한 요청)
2. Request[1] = [1,0,2] <= Available = [3,3,2]?  (자원 있음)
3. 가정적으로 할당:
   Available = [3,3,2] - [1,0,2] = [2,3,0]
   Allocation[1] = [2,0,0] + [1,0,2] = [3,0,2]
   Need[1] = [1,2,2] - [1,0,2] = [0,2,0]

4. 안전성 검사 실행:
   새로운 Available = [2,3,0]으로 안전 순서 존재 확인
   -> 안전하면 요청 허용, 아니면 거부
// 은행원 알고리즘 Java 구현
public class BankersAlgorithm {
    private int[][] max;
    private int[][] allocation;
    private int[][] need;
    private int[] available;
    private int numProcesses;
    private int numResources;

    public BankersAlgorithm(int[][] max, int[][] alloc,
                             int[] avail) {
        this.max = max;
        this.allocation = alloc;
        this.available = avail;
        this.numProcesses = max.length;
        this.numResources = avail.length;
        this.need = new int[numProcesses][numResources];

        for (int i = 0; i < numProcesses; i++)
            for (int j = 0; j < numResources; j++)
                need[i][j] = max[i][j] - allocation[i][j];
    }

    public boolean isSafe() {
        int[] work = available.clone();
        boolean[] finish = new boolean[numProcesses];
        int count = 0;

        while (count < numProcesses) {
            boolean found = false;
            for (int i = 0; i < numProcesses; i++) {
                if (!finish[i] && canAllocate(need[i], work)) {
                    // 자원 회수
                    for (int j = 0; j < numResources; j++)
                        work[j] += allocation[i][j];
                    finish[i] = true;
                    count++;
                    found = true;
                    System.out.println("P" + i + " 실행 가능");
                }
            }
            if (!found) return false;
        }
        return true;
    }

    private boolean canAllocate(int[] need, int[] work) {
        for (int j = 0; j < numResources; j++)
            if (need[j] > work[j]) return false;
        return true;
    }
}

교착 상태 탐지 (Detection)

교착 상태 발생을 허용하고, 주기적으로 탐지 알고리즘을 실행한다.

자원 유형당 인스턴스가 1개인 경우

대기 그래프(Wait-for Graph)에서 순환을 탐지한다.

[대기 그래프]

자원 할당 그래프에서 자원 노드를 제거하고
프로세스 간 직접 의존 관계만 표시

P1 --> P2 --> P3
 ^            |
 |            v
 +---- P4 <--+

순환 존재: P1->P2->P3->P4->P1 -> 교착 상태!

자원 유형당 인스턴스가 여러 개인 경우

은행원 알고리즘과 유사한 탐지 알고리즘을 사용한다.

[교착 상태 탐지 알고리즘]

1. Work = Available
   Allocation[i] != 0이면 Finish[i] = false
   Allocation[i] == 0이면 Finish[i] = true

2. Finish[i] == false AND Request[i] <= Work인 i를 찾음

3. 찾으면: Work = Work + Allocation[i]
           Finish[i] = true
           2단계로 돌아감

4. Finish[i] == false인 프로세스가 있으면
   -> 해당 프로세스들이 교착 상태

탐지 알고리즘은 얼마나 자주 실행해야 할까?

  • 교착 상태가 자주 발생하면: 자주 실행 (오버헤드 증가)
  • 드물게 발생하면: 간격을 두고 실행 (탐지 지연)
  • CPU 이용률이 40% 이하로 떨어지면 실행하는 방법도 있음

교착 상태 복구 (Recovery)

프로세스 종료

  • 모든 교착 프로세스 종료: 확실하지만 비용이 큼
  • 하나씩 종료: 교착이 해소될 때까지 하나씩 종료. 매번 탐지 알고리즘 재실행

종료 순서 결정 기준은 다음과 같다.

  • 프로세스 우선순위
  • 지금까지 사용한 자원과 시간
  • 완료까지 남은 자원
  • 종료해야 할 프로세스 수
  • 프로세스 유형 (대화형 vs 배치)

자원 선점

교착 프로세스로부터 자원을 강제로 회수한다.

[자원 선점 시 고려 사항]

1. 희생자 선택: 어떤 프로세스에서 자원을 빼앗을 것인가?
   비용 최소화 기준으로 선택

2. 롤백: 자원을 빼앗긴 프로세스를 안전한 상태로 되돌림
   - 완전 롤백: 처음부터 다시 시작
   - 부분 롤백: 교착을 벗어나기에 충분한 시점까지만 롤백

3. 기아 방지: 같은 프로세스가 항상 희생자로 선택되지 않도록
   롤백 횟수를 비용 요소에 포함

실제 시스템에서의 교착 상태 처리

대부분의 운영체제(Linux, Windows)는 교착 상태를 무시하는 전략을 사용한다. 교착 상태 예방이나 회피의 오버헤드가 크고, 교착 상태가 드물게 발생하기 때문이다.

응용 수준에서의 대처법은 다음과 같다.

// trylock을 사용한 교착 방지
int try_acquire_both(pthread_mutex_t *a, pthread_mutex_t *b) {
    while (1) {
        pthread_mutex_lock(a);

        if (pthread_mutex_trylock(b) == 0) {
            return 0;  // 성공: 두 잠금 모두 획득
        }

        // b를 얻지 못하면 a도 해제하고 재시도
        pthread_mutex_unlock(a);
        usleep(rand() % 1000);  // 잠시 대기 후 재시도
    }
}
// Java의 tryLock을 사용한 교착 방지
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class DeadlockFree {
    private final ReentrantLock lockA = new ReentrantLock();
    private final ReentrantLock lockB = new ReentrantLock();

    public void safeMethod() throws InterruptedException {
        while (true) {
            boolean gotA = lockA.tryLock(100, TimeUnit.MILLISECONDS);
            boolean gotB = lockB.tryLock(100, TimeUnit.MILLISECONDS);

            if (gotA && gotB) {
                try {
                    // 두 잠금 모두 획득 - 안전하게 작업
                    System.out.println("두 자원 모두 획득!");
                    break;
                } finally {
                    lockA.unlock();
                    lockB.unlock();
                }
            }

            // 하나라도 실패하면 보유한 잠금 해제
            if (gotA) lockA.unlock();
            if (gotB) lockB.unlock();

            // 랜덤 대기 후 재시도 (라이브락 방지)
            Thread.sleep((long)(Math.random() * 100));
        }
    }
}

정리

교착 상태는 상호 배제, 점유 대기, 비선점, 순환 대기의 네 가지 조건이 동시에 성립할 때 발생한다. 예방은 조건 하나를 제거하고, 회피는 은행원 알고리즘으로 안전 상태를 유지하며, 탐지는 교착 발생 후 감지하여 복구한다. 실제 시스템에서는 교착 상태를 무시하거나, 응용 수준에서 trylock 같은 기법으로 대처하는 것이 일반적이다.

[OS Concepts] 08. Deadlocks: Prevention, Avoidance, Detection, Recovery

What is Deadlock

Deadlock is a state where two or more processes are each waiting for a resource held by the other, unable to make progress forever.

[Everyday Analogy of Deadlock]

Two cars meeting on a narrow bridge from opposite sides:
  <--- Car A      Car B --->
        [Bridge]
Both wait for the other to back off -> Deadlock

In a system:
Thread 1: lock(A) -> attempts lock(B) (waits)
Thread 2: lock(B) -> attempts lock(A) (waits)
// Code example that causes deadlock
#include <pthread.h>

pthread_mutex_t mutex_a = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_b = PTHREAD_MUTEX_INITIALIZER;

void *thread1(void *arg) {
    pthread_mutex_lock(&mutex_a);    // Acquire A
    sleep(1);                         // Timing issue
    pthread_mutex_lock(&mutex_b);    // Wait for B (deadlock!)
    // ...
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
    return NULL;
}

void *thread2(void *arg) {
    pthread_mutex_lock(&mutex_b);    // Acquire B
    sleep(1);                         // Timing issue
    pthread_mutex_lock(&mutex_a);    // Wait for A (deadlock!)
    // ...
    pthread_mutex_unlock(&mutex_a);
    pthread_mutex_unlock(&mutex_b);
    return NULL;
}

Four Necessary Conditions for Deadlock

Deadlock occurs only when all four conditions hold simultaneously.

  1. Mutual Exclusion: A resource can be used by only one process at a time
  2. Hold and Wait: A process holds resources while waiting to acquire additional ones
  3. No Preemption: Resources already allocated cannot be forcibly taken away
  4. Circular Wait: P0 -> P1 -> P2 -> ... -> Pn -> P0 circular waiting pattern

Removing any one of the four conditions prevents deadlock.


Resource-Allocation Graph

A directed graph that visually represents deadlock.

[Resource-Allocation Graph Elements]

Process: Circle (O)
Resource type: Dots inside a rectangle (instances)
Request edge: Pi -> Rj  (process requests resource)
Assignment edge: Rj -> Pi  (resource assigned to process)
[Resource-Allocation Graph with Deadlock]

    P1 --request--> R1 (1) --assigned--> P2
    ^                                     |
    |                                     |
  assigned                             request
    |                                     |
    R2 (1) <--request-- P3 <--assigned-- R3 (1)

P1 requests R1 (held by P2)
P2 requests R3 (held by P3)
P3 requests R2 (held by P1)
-> Cycle exists -> Deadlock!
[Resource-Allocation Graph without Deadlock - Multiple Instances]

    P1 --request--> R1 (2)  Instance1 --assigned--> P2
                             Instance2 --assigned--> P3

When P2 or P3 releases R1, P1 can proceed
-> Cycle may exist but no deadlock
   (when resource has 2+ instances)

Summary of rules.

  • One instance per resource type: A cycle means definite deadlock
  • Multiple instances per resource type: A cycle may or may not mean deadlock

Deadlock Handling Methods

There are four approaches to dealing with deadlock.

  1. Prevention: Fundamentally remove one of the 4 conditions
  2. Avoidance: Only grant resource requests when safe
  3. Detection and Recovery: Allow deadlock to occur, detect and recover
  4. Ignore: Ignore deadlock (adopted by most OSes, the so-called ostrich algorithm)

Deadlock Prevention

1. Negate Mutual Exclusion

Sharable resources (e.g., read-only files) do not need mutual exclusion. However, this cannot apply to inherently non-sharable resources like printers and mutexes.

2. Negate Hold and Wait

Ensure a process holds no other resources when requesting a new one.

Method A: Request all needed resources at once before execution
  Pros: Simple
  Cons: Low resource utilization, possible starvation

Method B: Release all held resources before requesting new ones
  Pros: Flexible
  Cons: Complex implementation

3. Negate No Preemption

If a process holding resources cannot obtain additional resources, its held resources are forcibly reclaimed.

Process P holds resource A and requests B:
  If B is unavailable:
    Forcibly release A (preempt)
    Put P in waiting state for both A and B
    Restart when both A and B are available

This is only applicable to resources whose state can be saved/restored, such as CPU registers and memory.

4. Negate Circular Wait

Assign numbers to all resource types and enforce requests in order of increasing numbers.

// Circular wait prevention: Resource ordering protocol
// Resource numbers: mutex_a = 1, mutex_b = 2, mutex_c = 3

// Correct usage: Always acquire in ascending order
void safe_function() {
    pthread_mutex_lock(&mutex_a);    // Number 1
    pthread_mutex_lock(&mutex_b);    // Number 2
    pthread_mutex_lock(&mutex_c);    // Number 3

    // Critical section

    pthread_mutex_unlock(&mutex_c);
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
}

// Incorrect usage: Requesting in reverse order risks deadlock
void unsafe_function() {
    pthread_mutex_lock(&mutex_c);    // Number 3
    pthread_mutex_lock(&mutex_a);    // Number 1 (order violation!)
}

Deadlock Avoidance

Deadlock avoidance provides additional information to the system, refusing requests that could lead to deadlock.

Safe State

If the system is in a safe state, there exists a safe sequence that can complete all processes in some order.

[Relationship between Safe State and Deadlock]

+-------------------------------------------+
|              All States                    |
|  +----------------------------------+     |
|  |          Safe States              |     |
|  |                                  |     |
|  |                                  |     |
|  +----------------------------------+     |
|            Unsafe States                  |
|       (includes deadlock region)          |
|                 +------+                  |
|                 |Dead- |                  |
|                 |lock  |                  |
|                 +------+                  |
+-------------------------------------------+

Safe state -> No deadlock (guaranteed)
Unsafe state -> Deadlock possible (not certain)

Banker's Algorithm

A deadlock avoidance algorithm proposed by Dijkstra, analogous to a bank managing loans to avoid bankruptcy.

Required data structures (n = number of processes, m = number of resource types).

Available[m]:     Number of available instances per resource type
Max[n][m]:        Maximum demand per process
Allocation[n][m]: Current allocation per process
Need[n][m]:       Remaining need per process (Max - Allocation)

Safety Algorithm

[Safety Check Algorithm]

1. Work = copy of Available
   Finish[i] = false (for all i)

2. Find i such that:
   Finish[i] == false AND Need[i] <= Work

3. If found:
   Work = Work + Allocation[i]
   Finish[i] = true
   Go back to step 2

4. If not found:
   If all Finish[i] are true -> Safe state
   Otherwise -> Unsafe state

Example

[Banker's Algorithm Example]

Resources: A(10), B(5), C(7)

        Allocation    Max        Need       Available
        A  B  C      A  B  C    A  B  C    A  B  C
P0      0  1  0      7  5  3    7  4  3    3  3  2
P1      2  0  0      3  2  2    1  2  2
P2      3  0  2      9  0  2    6  0  0
P3      2  1  1      2  2  2    0  1  1
P4      0  0  2      4  3  3    4  3  1

Finding safe sequence:
1. Work = [3,3,2]
   P1's Need[1,2,2] <= [3,3,2]? Yes!
   Work = [3,3,2] + [2,0,0] = [5,3,2], Finish[P1] = true

2. Work = [5,3,2]
   P3's Need[0,1,1] <= [5,3,2]? Yes!
   Work = [5,3,2] + [2,1,1] = [7,4,3], Finish[P3] = true

3. Work = [7,4,3]
   P4's Need[4,3,1] <= [7,4,3]? Yes!
   Work = [7,4,3] + [0,0,2] = [7,4,5], Finish[P4] = true

4. Work = [7,4,5]
   P2's Need[6,0,0] <= [7,4,5]? Yes!
   Work = [7,4,5] + [3,0,2] = [10,4,7], Finish[P2] = true

5. Work = [10,4,7]
   P0's Need[7,4,3] <= [10,4,7]? Yes!
   Finish[P0] = true

Safe sequence: P1 -> P3 -> P4 -> P2 -> P0
The system is currently in a safe state!

Resource Request Algorithm

[When P1 additionally requests resources (1,0,2)]

1. Request[1] = [1,0,2] <= Need[1] = [1,2,2]? Yes (valid request)
2. Request[1] = [1,0,2] <= Available = [3,3,2]? Yes (resources exist)
3. Hypothetical allocation:
   Available = [3,3,2] - [1,0,2] = [2,3,0]
   Allocation[1] = [2,0,0] + [1,0,2] = [3,0,2]
   Need[1] = [1,2,2] - [1,0,2] = [0,2,0]

4. Run safety check:
   Check if safe sequence exists with new Available = [2,3,0]
   -> Allow request if safe, deny if unsafe
// Banker's Algorithm Java Implementation
public class BankersAlgorithm {
    private int[][] max;
    private int[][] allocation;
    private int[][] need;
    private int[] available;
    private int numProcesses;
    private int numResources;

    public BankersAlgorithm(int[][] max, int[][] alloc,
                             int[] avail) {
        this.max = max;
        this.allocation = alloc;
        this.available = avail;
        this.numProcesses = max.length;
        this.numResources = avail.length;
        this.need = new int[numProcesses][numResources];

        for (int i = 0; i < numProcesses; i++)
            for (int j = 0; j < numResources; j++)
                need[i][j] = max[i][j] - allocation[i][j];
    }

    public boolean isSafe() {
        int[] work = available.clone();
        boolean[] finish = new boolean[numProcesses];
        int count = 0;

        while (count < numProcesses) {
            boolean found = false;
            for (int i = 0; i < numProcesses; i++) {
                if (!finish[i] && canAllocate(need[i], work)) {
                    // Reclaim resources
                    for (int j = 0; j < numResources; j++)
                        work[j] += allocation[i][j];
                    finish[i] = true;
                    count++;
                    found = true;
                    System.out.println("P" + i + " can execute");
                }
            }
            if (!found) return false;
        }
        return true;
    }

    private boolean canAllocate(int[] need, int[] work) {
        for (int j = 0; j < numResources; j++)
            if (need[j] > work[j]) return false;
        return true;
    }
}

Deadlock Detection

Allow deadlock to occur and periodically run a detection algorithm.

Single Instance per Resource Type

Detect cycles in a wait-for graph.

[Wait-For Graph]

Remove resource nodes from the resource-allocation graph
and show only direct dependencies between processes

P1 --> P2 --> P3
 ^            |
 |            v
 +---- P4 <--+

Cycle exists: P1->P2->P3->P4->P1 -> Deadlock!

Multiple Instances per Resource Type

Use a detection algorithm similar to the banker's algorithm.

[Deadlock Detection Algorithm]

1. Work = Available
   If Allocation[i] != 0 then Finish[i] = false
   If Allocation[i] == 0 then Finish[i] = true

2. Find i where Finish[i] == false AND Request[i] <= Work

3. If found: Work = Work + Allocation[i]
             Finish[i] = true
             Go to step 2

4. If Finish[i] == false for any process
   -> Those processes are in deadlock

How often should the detection algorithm run?

  • If deadlock occurs frequently: Run often (increased overhead)
  • If rare: Run at intervals (delayed detection)
  • Run when CPU utilization drops below 40%

Deadlock Recovery

Process Termination

  • Terminate all deadlocked processes: Certain but costly
  • Terminate one at a time: Terminate one by one until deadlock resolves. Rerun detection algorithm each time

Criteria for determining termination order.

  • Process priority
  • Resources and time used so far
  • Resources remaining until completion
  • Number of processes that need to be terminated
  • Process type (interactive vs batch)

Resource Preemption

Forcibly reclaim resources from deadlocked processes.

[Resource Preemption Considerations]

1. Victim selection: Which process to take resources from?
   Select based on minimizing cost

2. Rollback: Roll back the victimized process to a safe state
   - Total rollback: Restart from the beginning
   - Partial rollback: Roll back just enough to break the deadlock

3. Starvation prevention: Ensure the same process is not always the victim
   Include rollback count in cost factors

Deadlock Handling in Real Systems

Most operating systems (Linux, Windows) use the strategy of ignoring deadlock. The overhead of deadlock prevention or avoidance is high, and deadlock occurs rarely.

Application-level countermeasures are as follows.

// Deadlock prevention using trylock
int try_acquire_both(pthread_mutex_t *a, pthread_mutex_t *b) {
    while (1) {
        pthread_mutex_lock(a);

        if (pthread_mutex_trylock(b) == 0) {
            return 0;  // Success: both locks acquired
        }

        // If b cannot be obtained, release a and retry
        pthread_mutex_unlock(a);
        usleep(rand() % 1000);  // Wait briefly then retry
    }
}
// Deadlock prevention using Java's tryLock
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class DeadlockFree {
    private final ReentrantLock lockA = new ReentrantLock();
    private final ReentrantLock lockB = new ReentrantLock();

    public void safeMethod() throws InterruptedException {
        while (true) {
            boolean gotA = lockA.tryLock(100, TimeUnit.MILLISECONDS);
            boolean gotB = lockB.tryLock(100, TimeUnit.MILLISECONDS);

            if (gotA && gotB) {
                try {
                    // Both locks acquired - work safely
                    System.out.println("Both resources acquired!");
                    break;
                } finally {
                    lockA.unlock();
                    lockB.unlock();
                }
            }

            // If either failed, release held lock
            if (gotA) lockA.unlock();
            if (gotB) lockB.unlock();

            // Random wait then retry (prevent livelock)
            Thread.sleep((long)(Math.random() * 100));
        }
    }
}

Summary

Deadlock occurs when all four conditions - mutual exclusion, hold and wait, no preemption, and circular wait - hold simultaneously. Prevention removes one condition, avoidance maintains safe state via the banker's algorithm, and detection detects and recovers after deadlock occurs. In real systems, it is common to either ignore deadlock or handle it at the application level with techniques like trylock.