운영체제에서는 프로세스의 상호배제 문제를 해결하기 위해 세마포어(semaphore)라는 도구를 사용한다. 상호배제란 여러 프로세스가 동시에 같은 자원(임계영역)을 건드리지 않도록 막는 것이다.
세마포어란?
쉽게 비유하면 자원의 좌물쇠 같은 역할이다. 세마포어는 정수형 변수(s)로 만들어졌는데, 이 값은 사용 가능한 자원의 개수나 잠김/열림 상태를 나타낸다.
- 예를 들어 s = 3 이면 사용 가능한 자원이 3개 있다는 뜻이니, 프로세스가 3개까지 들어갈 수 있다는 신호다.
- s = 0 이면 빈 자원이 없으니 대기하라는 뜻이다.
처음에 세마포어 s를 생성할 때, 상황에 맞춰 값을 설정한다. 이 값은 0 이상의 정수여야 한다. 그리고 이 s는 P와 V라는 두 가지 연산으로만 변할 수 있다. 즉, P 연산과 V 연산이 세마포어의 열쇠 같은 것이다.
쉬운 이해를 위해 프로세스-> 비행기 승객, 자원-> 비행기 화장실로 비유해보겠다.
P 연산: 문 잠그기
P(s)는 기내 화장실에 빈 칸이 있는지 확인하고 승객A를 들여보낸 후 문을 잠그는 역할이다.
이를 C언어 코드로 다음과 같이 쓸 수 있다.
void P(semaphore s) {
if (s > 0) // 빈 칸이 있으면
s--; // 승객A를 들여보내서 한 칸을 차지하게 함
else // 빈 칸이 없으면
승객A를 대기시킴; // 승객A는 대기 줄로 ㄱㄱ
}
- s가 0보다 크면 자원이 있다는 뜻이다. 프로세스A가 들어오면 s를 1만큼 줄인다. (문을 잠근다) 예를 들어 처음에 s = 2 였다면, 프로세스에 자원이 할당되면서 s = 1이 된다.
- s가 0이면 자원이 없다는 뜻이다. 프로세스는 대기 상태로 바뀌고, s의 대기 큐에 추가된다. 이 때 s의 값은 변하지 않고 그대로 0이다.
V연산: 문 열기
V(s)는 승객A가 화장실을 다 쓰고 나와서 문을 여는 행위다. ("나 다 썼음! 다음 사람 ㄱㄱ")
코드로 보면 다음과 같다.
void V(semaphore s) {
if (대기 중인 승객 없음) // 줄 서있던 승객이 없으면
s++; // 화장실 한 칸을 빈칸으로 표시
else // 줄 서있던 승객B가 있으면
대기 중인 승객B를 들여보냄; // 승객B를 서있던 줄에서 나오게 해서 화장실로 들여보내기
}
대기 큐에 프로세스가 없으면: s를 1만큼 늘려서 사용 가능한 자원이 생겼다는 것을 알린다. (문을 연다) 예를 들어, s = 0이었다면 s = 1이 된다.
대기 큐에 프로세스가 있으면: 대기 큐의 맨 앞에 있는 프로세스를 준비 상태로 바꿔준다. 그러면 그 프로세스가 곧 실행이 되면서 자원을 쓸 수 있게 된다. 곧바로 실행이 되므로 s 값은 변하지 않는다.
P와 V는 기본연산(primitive operation)
P와 V는 중간에 끊기지 않고 한 번에 쭉 실행되는 연산이라서 '기본연산'이라고 한다.
예를 들어 프로세스 A가 P(s)를 수행하면서 s > 0을 확인하고 s--를 하기 전에, 다른 프로세스 B가 갑자기 끼어들어서 s 값을 바꿀 수 없다는 의미다.
즉, 누가 화장실을 쓰고 있는 동안에는 다른 사람이 화장실에 들어갈 수 없는 것처럼, 한 프로세스가 P 연산이나 V 연산을 하는 동안 다른 프로세스가 끼어들 수 없다. 이것이 상호배제를 보장하는 핵심이다.
임계영역에 대한 상호배제 (Mutual Exclusion) 문제 해결
임계영역(Critical Section)이란?
임계영역은 여러 프로세스나 스레드가 동시에 접근하면 안 되는 공유 자원을 다루는 코드 부분이다. 비유하자면 "한 번에 한 명만 들어가서 써야 하는 화장실" 같은 거다. 예를 들어, 비행기 화장실이 임계영역이라고 생각하면, 승객 A와 B가 동시에 들어가면 곤란하다. (⊙_⊙;) 반드시 한 명씩만 들어가야 한다.
프로그래밍에서는 공유 변수나 파일, 데이터베이스 같은 자원을 다루는 코드가 임계영역이 될 수 있다.
임계영역을 제대로 관리하지 않으면 데이터가 엉망이 되거나 프로그램이 오작동할 수 있다. 그래서 '상호배제'라는 규칙을 만들어 한 번에 한 프로세스만 임계영역에 들어가도록 해야 한다. 그걸 도와주는 것이 세마포어다.

따라서 두 가지 요구 조건을 만족해야 한다.
1. 한 프로세스가 임계영역에 있으면 다른 프로세스는 들어올 수 없음.
2. 그 프로세스가 끝나면 다른 프로세스가 들어갈 수 있어야 함.
진입영역은 프로세스가 임계영역에 들어가기 전에 "나 들어가도 되니?"를 확인하는 문이라면, 해제영역은 종료된 프로세스가 나오면서 "다음 프로세스 들어와도 돼!"라고 알려주는 문이라고 할 수 있다.
세마포어로 해결하는 방법
앞에서 살펴본 세마포어로 상호배제 문제를 해결할 수 있다. 다시 비행기 화장실을 예로 들면, 세마포어 변수(mutex)는 화장실 문의 상태를 나타내는 숫자라고 생각하면 된다.
***mutex는 Mutual Exclusion의 줄임말. 세마포어에서 mutex라는 이름으로 변수를 쓰는 이유는 이 변수가 임계영역에 한 번에 한 프로세스만 들어가게 해서 상호배제를 보장하기 때문이다.
- mutex = 1: 화장실이 비었음(들어가도 됨).
- mutex = 0: 화장실이 사용 중임(기다려야 함).
세마포어는 두 가지 연산으로 작동한다.
- P(mutex): 진입영역에서 문을 잠그라고 요청.
- V(mutex): 해제영역에서 문을 열라고 알림.

동작 과정
비행기 화장실에 앞에 승객 A와 B가 있다고 해보자.
초기값 mutex = 1 : 화장실이 비어있다.
승객A가 진입영역에서 P(mutex)를 실행:
- mutex = 1 "안에 사람 없음" → mutex를 0으로 바꾸고 화장실에 들어감.
- 이제 화장실 문이 잠긴다. (mutex = 0).
승객B도 화장실에 가려고 한다.
- B가 진입영역에서 P(mutex)를 실행:
- mutex = 0 "안에 사람 있음" → B는 대기 상태로 바뀌고 mutex 대기 큐(줄)에 추가됨.
- mutex는 여전히 0.
A가 화장실에서 볼일을 끝내고 해제영역에서 V(mutex)를 실행:
- 대기 큐에 B가 있음 → B를 준비 상태로 보냄.
- mutex는 계속 0 (왜냐면 B가 곧 들어갈 거니까).
운영체제가 승객B를 실행 상태로 선택하면:
- 승객B의 P(mutex)가 완료되고 화장실에 들어감.
- mutex는 여전히 0 (사용 중).
승객B가 화장실에서 볼일을 끝내고 V(mutex)를 실행:
- 대기 큐가 비었음 → mutex를 1로 바꿈.
이제 화장실이 비어서 다음 사람이 들어올 수 있음.
상호 배제 정리
- P(mutex) :
프로세스가 임계영역에 들어가기 위해 세마포어 mutex를 확인하고 제어하는 연산이다.- 만약 mutex가 1이면 (임계영역이 비어있는 상태), 그 프로세스는 mutex를 0으로 바꾸고 임계영역에서 코드를 실행 시작한다.
- 만약 mutex가 0이면 (다른 프로세스가 이미 임계영역을 실행 중), 현재 프로세스는 대기 상태로 전이되고 mutex의 대기 큐에 추가된다.
- V(mutex) :
프로세스가 임계영역 실행을 끝내고 나올 때 세마포어 mutex를 갱신하는 연산이다.- 대기 큐가 비어있다면 (기다리는 프로세스가 없음), mutex를 1로 바꿔서 임계영역이 다시 사용 가능함을 표시한다.
- 대기 큐에 프로세스가 있다면, 가장 먼저 기다리던 프로세스를 꺼내 준비 상태로 전이시킨다. 이 경우 mutex는 0으로 유지된다 (새 프로세스가 곧 임계영역에 들어가기 때문).
- 상호배제 보장:
mutex 값이 0 또는 1로 관리되며, 한 번에 하나의 프로세스만 임계영역에서 코드를 실행할 수 있다. 다른 프로세스는 mutex가 0인 동안 대기해야 하므로 동시에 접근하는 일이 없다.
세마포어는 또한 프로세스의 동기화 문제를 해결할 수 있다.
동기화 해결
동기화(synchronization)란?
프로세스 동기화는 여러 프로세스가 작업을 하는 다중 프로세스 환경에서 특정 작업의 실행 순서를 보장하는 메커니즘이다. 세마포어는 이러한 순차적 실행을 보장하기 위한 효과적인 동기화 도구로 사용된다.
예를 들어, 프로세스 A가 코드 S1 실행을 완료한 이후에만 프로세스 B가 코드 S2를 실행하도록 순서를 제어하는 것이다.
세마포어 설정
세마포어 변수 sync는 동기화 상태를 관리하며, 초기값은 0으로 설정된다.
- sync = 0 : 코드 S1이 아직 완료되지 않았으므로 코드 S2의 실행이 불가능한 상태.
- sync = 1 : 코드 S1이 완료되어 코드 S2의 실행이 허용된다.
sync의 초기값 0은 프로세스 B가 코드 S2를 실행하기 전에 프로세스 A의 코드 S1 실행을 기다리도록 강제한다.
세마포어 연산의 배치
동기화를 구현하기 위해,
코드 S2 앞에 P(sync)를 배치 : 프로세스 B가 코드 S2를 실행하기 전에 sync의 상태를 확인한다.
코드 S1 뒤에 V(sync)를 배치 : 프로세스 A가 코드 S1을 완료한 후 sync를 갱신하여 B의 실행을 허용한다.
동작 과정
- sync = 0: 코드 S1이 수행되기 전이므로 코드 S2는 수행될 수 없다.
- 프로세스 B가 P(sync)를 호출한다.
- sync = -1이 되어 조건 sync > 0이 거짓이 된다. 따라서 프로세스 B는 대기 상태로 전이된다.
- B는 sync의 대기 큐에 추가되며, sync 값은 0으로 유지된다.
- 프로세스 A가 코드 S1을 실행 완료하고 V(sync)를 호출한다.
- sync의 대기 큐에 프로세스 B가 존재하므로, sync 값을 1로 증가시키지 않고 대기 큐에서 B를 제거하여 준비 상태로 전이시킨다.
- sync는 여전히 0으로 유지된다. B가 곧 코드 S2를 실행할 것이기 때문에 동기화 상태가 변하지 않는 것.
- 운영체제의 스케줄러가 프로세스 B를 실행 상태로 디스패치하면,
- B의 P(sync) 연산이 완료되고, 코드 S2의 실행이 시작된다.
- sync = 0 은 계속 유지됨.
5. 프로세스 B2가 코드 S2를 수행한 이후
동기화가 단일 이벤트로 끝난다면 추가 V(sync) 연산 없이 종료될 것이고,
후속 작업이 있다면 V(sync)를 호출해서 sync = 1로 전환할 수 있다.
상호배제와 동기화의 차이점
여기까지 공부해 보면, 앞에서 살펴본 '상호배제 문제'와 '동기화 문제'에 대한 세마포어 연산이 살짝 헷갈릴 법 하다.
왜 mutex의 초기값은 1로 시작하는데 sync의 초기값은 0으로 시작할까?
상호배제는 자원의 사용 가능성에 초점을 맞추기 때문에, 처음부터 자원을 사용할 수 있게 하려고 mutex = 1로 시작한다.
반면 동기화는 순서 제어에 초점을 맞춘다. 프로세스 B가 프로세스 A를 기다리려면 초기 상태에서 프로세스 B를 막아야 하므로 sync = 0으로 시작한다.
특성
|
상호배제 (mutex) | 동기화 (sync) |
목적 |
임계영역에 한 번에 한 프로세스만 접근
|
특정 순서대로 프로세스 실행 제어
|
초기값 |
mutex = 1 (자원 사용 가능)
|
sync = 0 (조건 미충족)
|
P연산 |
자원을 잠가서 독점
|
조건 확인 후 대기
|
V연산 |
자원을 풀어 다음 프로세스 허용
|
조건 충족 신호로 대기 프로세스 깨움
|
상태 전이 |
A가 들어가면 mutex = 0, B 대기
|
B가 먼저 오면 대기, A가 끝내면 진행 |
'KNOU CS > 운영체제' 카테고리의 다른 글
가상 메모리와 주소 변환에 대해... 내가 이해하려고 쓰는 글✏ (0) | 2025.04.21 |
---|---|
[운영체제] 병행 프로세스 - 세마포어 완벽 정리 Part 3 - 판독기-기록기 문제 (0) | 2025.03.19 |
[운영체제] 병행 프로세스 - 세마포어 완벽 정리 Part 2 - 생산자-소비자 문제 (0) | 2025.03.18 |