- Authors

- Name
- Youngju Kim
- @fjvbn20031
교착 상태란
교착 상태(Deadlock)는 두 개 이상의 프로세스가 서로 상대방이 보유한 자원을 기다리며 영원히 진행하지 못하는 상태다.
[교착 상태의 일상적 비유]
좁은 다리에서 양쪽에서 온 차가 마주침:
<--- 차 A 차 B --->
[다리]
둘 다 상대방이 물러나기를 기다림 -> 교착 상태
시스템에서의 예:
스레드 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가지 필요 조건
교착 상태는 다음 네 가지 조건이 동시에 성립할 때만 발생한다.
- 상호 배제(Mutual Exclusion): 자원을 한 번에 하나의 프로세스만 사용 가능
- 점유 대기(Hold and Wait): 자원을 보유한 채 다른 자원을 추가로 요청하며 대기
- 비선점(No Preemption): 이미 할당된 자원을 강제로 빼앗을 수 없음
- 순환 대기(Circular Wait): P0 -> P1 -> P2 -> ... -> Pn -> P0 형태로 순환적으로 대기
네 조건 중 하나라도 제거하면 교착 상태를 방지할 수 있다.
자원 할당 그래프 (Resource-Allocation Graph)
교착 상태를 시각적으로 표현하는 방향 그래프다.
[자원 할당 그래프 요소]
프로세스: 원 (O)
자원 유형: 사각형 안의 점 (인스턴스)
요청 간선: Pi -> Rj (프로세스가 자원을 요청)
할당 간선: Rj -> Pi (자원이 프로세스에 할당됨)
[교착 상태가 있는 자원 할당 그래프]
P1 --요청--> R1 (1개) --할당--> P2
^ |
| |
할당 요청
| |
R2 (1개) <--요청-- P3 <--할당-- R3 (1개)
P1은 R1을 요청 (P2가 보유)
P2는 R3를 요청 (P3가 보유)
P3는 R2를 요청 (P1이 보유)
-> 순환 존재 -> 교착 상태!
[교착 상태가 없는 자원 할당 그래프 - 자원 인스턴스가 여러 개]
P1 --요청--> R1 (2개) 인스턴스1 --할당--> P2
인스턴스2 --할당--> P3
P2나 P3 중 하나가 R1을 반납하면 P1이 진행 가능
-> 순환이 있어도 교착 상태가 아닐 수 있음
(자원 인스턴스가 2개 이상일 때)
규칙 요약은 다음과 같다.
- 자원 유형당 인스턴스가 1개: 순환이 있으면 반드시 교착 상태
- 자원 유형당 인스턴스가 여러 개: 순환이 있어도 교착 상태가 아닐 수 있음
교착 상태 처리 방법
교착 상태를 다루는 네 가지 접근법이 있다.
- 예방(Prevention): 4가지 조건 중 하나를 원천적으로 제거
- 회피(Avoidance): 자원 요청 시 안전한 경우에만 허용
- 탐지 및 복구(Detection and Recovery): 교착 발생을 허용하고, 감지 후 복구
- 무시(Ignore): 교착 상태를 무시 (대부분의 OS가 채택, 이른바 오스트리치 알고리즘)
교착 상태 예방 (Prevention)
1. 상호 배제 부정
공유 가능한 자원(예: 읽기 전용 파일)은 상호 배제가 불필요하다. 하지만 프린터, 뮤텍스 같은 본질적으로 공유 불가능한 자원에는 적용 불가능하다.
2. 점유 대기 부정
프로세스가 자원을 요청할 때, 다른 자원을 보유하지 않도록 보장한다.
방법 A: 실행 전 필요한 모든 자원을 한꺼번에 요청
장점: 단순
단점: 자원 이용률 저하, 기아 가능
방법 B: 새 자원 요청 시 보유 자원을 모두 반납 후 재요청
장점: 유연
단점: 구현 복잡
3. 비선점 부정
자원을 보유한 프로세스가 다른 자원을 얻지 못하면, 보유 자원을 강제로 회수한다.
프로세스 P가 자원 A를 보유하고 B를 요청:
B를 사용할 수 없으면:
A를 강제로 해제 (선점)
P를 A와 B 모두 기다리는 상태로 전환
A와 B 모두 사용 가능해지면 재시작
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]
P1의 Need[1,2,2] <= [3,3,2]? 예!
Work = [3,3,2] + [2,0,0] = [5,3,2], Finish[P1] = true
2. Work = [5,3,2]
P3의 Need[0,1,1] <= [5,3,2]? 예!
Work = [5,3,2] + [2,1,1] = [7,4,3], Finish[P3] = true
3. Work = [7,4,3]
P4의 Need[4,3,1] <= [7,4,3]? 예!
Work = [7,4,3] + [0,0,2] = [7,4,5], Finish[P4] = true
4. Work = [7,4,5]
P2의 Need[6,0,0] <= [7,4,5]? 예!
Work = [7,4,5] + [3,0,2] = [10,4,7], Finish[P2] = true
5. Work = [10,4,7]
P0의 Need[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 같은 기법으로 대처하는 것이 일반적이다.