운영체제 Synchronization
운영체제에서 Synchronization은 프로세스 간 자원 접근을 관리하고 일관성을 유지하기 위한 중요한 메커니즘이다. 여러 프로세스가 동시에 실행될 때 공유 자원에 대한 접근을 제어함으로써 데이터 무결성을 보장한다.
Background
프로세스는 동시에 실행될 수 있으며(concurrently), 언제든지 중단될 수 있어 부분적으로만 실행이 완료될 수 있다. 공유 데이터에 대한 동시 접근은 데이터 불일치를 초래할 수 있다. 데이터 일관성을 유지하기 위해서는 협력하는 프로세스들의 순차적 실행을 보장하는 메커니즘이 필요하다.
문제 예시로, 모든 버퍼를 채우는 생산자-소비자 문제의 해결책을 제공하고자 한다. 이를 위해 가득 찬 버퍼의 수를 추적하는 정수형 counter를 사용할 수 있다.
초기에 counter는 0으로 설정되며, 생산자가 새 버퍼를 생성한 후 증가시키고 소비자가 버퍼를 소비한 후 감소시킨다.
Producer
while (true) {
/* produce an item in next produced */
while (counter == BUFFER_SIZE) ;
/* do nothing */
buffer[in] = next_produced;
in = (in + 1) % BUFFER_SIZE;
counter++;
}
Consumer
while (true) {
while (counter == 0)
; /* do nothing */
next_consumed = buffer[out];
out = (out + 1) % BUFFER_SIZE;
counter--;
/* consume the item in next consumed */
}
Race Condition
Race Condition은 두 개 이상의 프로세스가 공통 자원을 병행적으로 읽거나 쓰는 동작을 할 때 발생하며, 실행 결과가 접근 순서에 따라 달라지게 된다. 예를 들어:
counter++ 는 다음과 같이 구현될 수 있다:
register1 = counter
register1 = register1 + 1
counter = register1
counter-- 는 다음과 같이 구현될 수 있다:
register2 = counter
register2 = register2 - 1
counter = register2
"count = 5"로 시작하는 다음 실행 순서를 고려해보자:
S0: producer execute register1 = counter {register1 = 5}
S1: producer execute register1 = register1 + 1 {register1 = 6}
S2: consumer execute register2 = counter {register2 = 5}
S3: consumer execute register2 = register2 – 1 {register2 = 4}
S4: producer execute counter = register1 {counter = 6}
S5: consumer execute counter = register2 {counter = 4}
이 경우, counter가 최종적으로 4가 되어 데이터 불일치가 발생한다.
Critical Section Problem
n개의 프로세스 {p0, p1, ... pn-1}로 구성된 시스템을 고려해보자. 각 프로세스는 Critical Section(임계 구역)이라는 코드 세그먼트를 가지고 있다. 이 구역에서 프로세스는 공통 변수를 변경하거나, 테이블을 업데이트하거나, 파일을 작성하는 등의 작업을 수행할 수 있다.
한 프로세스가 임계 구역에 있을 때, 다른 프로세스는 자신의 임계 구역에 들어갈 수 없다.
Critical Section 문제는 이를 해결하는 프로토콜을 설계하는 것이다.
각 프로세스는 임계 구역에 진입하기 위해 entry section에서 허가를 요청해야 하며, 임계 구역 이후에 exit section이 있을 수 있고, 그 후에 remainder section이 있다.
프로세스 Pi의 일반적인 구조는 다음과 같다:
do {
entry section
critical section
exit section
remainder section
} while (true);
프로세스 Pi를 위한 간단한 알고리즘:
do {
while (turn == j);
critical section
turn = j;
remainder section
} while (true);
Solution to Critical-Section Problem
Critical Section 문제의 해결책은 다음 조건을 만족해야 한다:
- Mutual Exclusion(상호 배제) - 프로세스 Pi가 임계 구역에서 실행 중이면, 다른 프로세스들은 자신의 임계 구역에서 실행할 수 없다.
- Progress(진행) - 임계 구역에서 실행 중인 프로세스가 없고, 일부 프로세스가 임계 구역에 진입하길 원한다면, 다음에 어떤 프로세스가 임계 구역에 진입할지 결정하는 것을 무기한 연기할 수 없다.
- Bounded Waiting(한정된 대기) - 프로세스가 임계 구역에 진입 요청을 한 후, 그 요청이 허용되기 전에 다른 프로세스들이 임계 구역에 진입할 수 있는 횟수에 제한이 있어야 한다.
이때 각 프로세스가 0이 아닌 속도로 실행된다고 가정하며, n개 프로세스의 상대적 속도에 대한 가정은 없다.
Critical-Section Handling in OS
OS에서 임계 구역 처리는 커널이 선점형(preemptive)인지 비선점형(non-preemptive)인지에 따라 두 가지 접근 방식이 있다:
- Preemptive - 커널 모드에서 실행 중일 때도 프로세스의 선점을 허용한다.
- Non-preemptive - 커널 모드를 종료하거나, 블록되거나, 자발적으로 CPU를 양보할 때까지 실행된다. 본질적으로 커널 모드에서는 Race Condition으로부터 자유롭다.
Peterson's Solution
이 솔루션은 문제를 해결하는 좋은 알고리즘적 설명을 제공한다. 두 프로세스 간의 솔루션이며, 기계어 명령어(load와 store)가 atomic하다고 가정한다.
두 프로세스는 다음 두 변수를 공유한다:
int turn;
Boolean flag[2]
turn 변수는 임계 구역에 진입할 차례를 나타내며, flag 배열은 프로세스가 임계 구역에 진입할 준비가 되었는지를 나타낸다. flag[i] = true는 프로세스 Pi가 준비되었음을 의미한다.
프로세스 Pi를 위한 알고리즘:
do {
flag[i] = true;
turn = j;
while (flag[j] && turn == j);
critical section
flag[i] = false;
remainder section
} while (true);
Peterson's Solution은 다음 세 가지 CS 요구사항을 충족한다:
- Mutual Exclusion이 보존된다.
- Progress 요구사항이 충족된다.
- Bounded-waiting 요구사항이 충족된다.
Synchronization Hardware
많은 시스템은 임계 구역 코드 구현을 위한 하드웨어 지원을 제공한다. 모든 해결책은 잠금(locking) 아이디어를 기반으로 한다.
- 단일 프로세서에서는 인터럽트를 비활성화할 수 있다.
- 다중 프로세서 시스템에서는 비효율적이다.
- 현대 기계는 특별한 atomic 하드웨어 명령어를 제공한다:
- 메모리 워드를 테스트하고 값을 설정하거나
- 두 메모리 워드의 내용을 교환
잠금을 사용한 임계 구역 문제 해결:
do {
acquire lock
critical section
release lock
remainder section
} while (TRUE);
test_and_set Instruction
정의:
boolean test_and_set (boolean *target) {
boolean rv = *target;
*target = TRUE;
return rv;
}
- 원자적으로 실행된다.
- 전달된 매개변수의 원래 값을 반환한다.
- 전달된 매개변수의 새 값을 "TRUE"로 설정한다.
test_and_set()을 사용한 해결책:
do {
while (test_and_set(&lock))
; /* do nothing */
/* critical section */
lock = false;
/* remainder section */
} while (true);
compare_and_swap Instruction
정의:
int compare_and_swap(int *value, int expected, int new_value) {
int temp = *value;
if (*value == expected)
*value = new_value;
return temp;
}
- 원자적으로 실행된다.
- 전달된 매개변수 "value"의 원래 값을 반환한다.
- "value" == "expected"인 경우에만 변수 "value"를 매개변수 "new_value"의 값으로 설정한다. 즉, 이 조건에서만 교환이 이루어진다.
compare_and_swap을 사용한 해결책:
do {
while (compare_and_swap(&lock, 0, 1) != 0)
; /* do nothing */
/* critical section */
lock = 0;
/* remainder section */
} while (true);
Bounded-waiting Mutual Exclusion with test_and_set
do {
waiting[i] = true;
key = true;
while (waiting[i] && key)
key = test_and_set(&lock);
waiting[i] = false;
/* critical section */
j = (i + 1) % n;
while ((j != i) && !waiting[j])
j = (j + 1) % n;
if (j == i)
lock = false;
else
waiting[j] = false;
/* remainder section */
} while (true);
Mutex Locks
이전 해결책들은 복잡하고 일반적으로 응용 프로그래머들이 접근하기 어렵다. OS 설계자들은 임계 구역 문제를 해결하기 위한 소프트웨어 도구를 구축한다.
가장 간단한 것은 Mutex Lock이다. 먼저 acquire()로 잠금을 획득한 다음 release()로 잠금을 해제하여 임계 구역을 보호한다. 잠금이 사용 가능한지 여부를 나타내는 Boolean 변수를 사용하며, acquire()와 release() 호출은 atomic해야 한다.
일반적으로 하드웨어 atomic 명령어를 통해 구현되지만, 이 솔루션은 busy waiting을 필요로 한다. 따라서 이 잠금은 spinlock이라고 불린다.
acquire()와 release():
acquire() {
while (!available)
; /* busy wait */
available = false;
}
release() {
available = true;
}
do {
acquire lock
critical section
release lock
remainder section
} while (true);
Semaphore
Semaphore는 프로세스가 활동을 동기화하는 더 정교한 방법을 제공하는 동기화 도구이다. Semaphore S는 정수 변수로, 두 가지 불가분(atomic) 연산을 통해서만 접근할 수 있다:
- wait()
- signal()
wait() 연산의 정의:
wait(S) {
while (S <= 0)
; // busy wait
S--;
}
signal() 연산의 정의:
signal(S) {
S++;
}
Semaphore Usage
- Counting semaphore - 정수 값이 제한되지 않은 도메인에서 범위할 수 있다.
- Binary semaphore - 정수 값이 0과 1 사이에서만 범위할 수 있다. Mutex lock과 동일하다.
다양한 동기화 문제를 해결할 수 있다. S1이 S2보다 먼저 발생해야 하는 P1과 P2를 고려해보자:
P1:
S1;
signal(synch); 0 -> 1
P2:
wait(synch); 1 -> 0
S2;
Binary semaphore로 Counting semaphore S를 구현할 수 있다.
Semaphore Implementation
동일한 semaphore에 대해 두 프로세스가 동시에 wait()와 signal()을 실행할 수 없도록 보장해야 한다.
따라서 구현은 wait와 signal 코드가 임계 구역에 배치되는 임계 구역 문제가 된다.
이제 임계 구역 구현에서 busy waiting이 있을 수 있지만, 구현 코드는 짧고 임계 구역이 거의 점유되지 않는 경우 busy waiting이 적다. 그러나 응용 프로그램이 임계 구역에서 많은 시간을 소비할 수 있으므로 이것은 좋은 해결책이 아닐 수 있다.
Semaphore Implementation with no Busy waiting
각 semaphore에는 연관된 대기 큐가 있다. 대기 큐의 각 항목에는 다음 두 가지 데이터 항목이 있다:
- value (정수 타입)
- 목록의 다음 레코드에 대한 포인터
두 가지 연산:
- block - 연산을 호출하는 프로세스를 적절한 대기 큐에 배치
- wakeup - 대기 큐에 있는 프로세스 중 하나를 제거하여 준비 큐에 배치
typedef struct{
int value;
struct process *list;
} semaphore;
Busy waiting이 없는 구현(계속):
wait(semaphore *S) {
S->value--;
if (S->value < 0) {
add this process to S->list;
block();
}
}
signal(semaphore *S) {
S->value++;
if (S->value <= 0) {
remove a process P from S->list;
wakeup(P);
}
}
Deadlock and Starvation
Deadlock(교착 상태)은 두 개 이상의 프로세스가 대기 중인 프로세스 중 하나에 의해서만 발생할 수 있는 이벤트를 무기한 기다리는 상황이다.
1로 초기화된 두 개의 semaphore S와 Q를 고려해보자:
P0 P1
wait(S); wait(Q);
wait(Q); wait(S);
... ...
signal(S); signal(Q);
signal(Q); signal(S);
Starvation(기아 상태)은 무한 차단을 의미한다. 프로세스가 중단된 semaphore 큐에서 제거되지 않을 수 있다.
Priority Inversion(우선순위 역전)은 낮은 우선순위 프로세스가 높은 우선순위 프로세스가 필요로 하는 잠금을 보유하고 있을 때 발생하는 스케줄링 문제이다. 이는 priority-inheritance protocol을 통해 해결된다.
Classical Problems of Synchronization
새로 제안된 동기화 체계를 테스트하는 데 사용되는 고전적인 문제들:
- Bounded-Buffer Problem(유한 버퍼 문제)
- Readers and Writers Problem(독자와 작가 문제)
- Dining-Philosophers Problem(식사하는 철학자 문제)
Bounded-Buffer Problem
- n개의 버퍼, 각각 하나의 항목을 보유할 수 있다.
- 값 1로 초기화된 Semaphore mutex
- 값 0으로 초기화된 Semaphore full
- 값 n으로 초기화된 Semaphore empty
생산자 프로세스의 구조:
do {
...
/* produce an item in next_produced */
...
wait(empty);
wait(mutex);
...
/* add next produced to the buffer */
...
signal(mutex);
signal(full);
} while (true);
소비자 프로세스의 구조:
Do {
wait(full);
wait(mutex);
...
/* remove an item from buffer to next_consumed */
...
signal(mutex);
signal(empty);
...
/* consume the item in next consumed */
...
} while (true);
Readers-Writers Problem
데이터 세트가 여러 동시 프로세스 간에 공유된다:
- Readers - 데이터 세트만 읽고 업데이트는 수행하지 않는다.
- Writers - 읽기와 쓰기 모두 할 수 있다.
문제 - 여러 readers가 동시에 읽을 수 있도록 허용해야 한다. 단 한 명의 writer만 공유 데이터에 동시에 접근할 수 있다.
공유 데이터:
- 데이터 세트
- 1로 초기화된 Semaphore rw_mutex
- 1로 초기화된 Semaphore mutex
- 0으로 초기화된 정수 read_count
writer 프로세스의 구조:
do {
wait(rw_mutex);
...
/* writing is performed */
...
signal(rw_mutex);
} while (true);
reader 프로세스의 구조:
do {
wait(mutex);
read_count++;
if (read_count == 1)
wait(rw_mutex);
signal(mutex);
...
/* reading is performed */
...
wait(mutex);
read_count--;
if (read_count == 0)
signal(rw_mutex);
signal(mutex);
} while (true);
Readers-Writers Problem Variations
- 첫 번째 변형 - writer가 공유 객체를 사용할 권한을 가지지 않는 한, reader는 대기하지 않는다. - reader 우선
- 두 번째 변형 - writer가 준비되면 가능한 한 빨리 쓰기를 수행한다. - writer 우선
- 두 가지 모두 starvation 상태를 초래할 수 있어 더 많은 변형이 생길 수 있다.
- 일부 시스템에서는 커널이 reader-writer Locks을 제공하여 문제를 해결한다. - 예) Posix
Dining-Philosophers Problem(식사하는 철학자 문제)
Dining-Philosophers Problem은 운영체제에서 프로세스 동기화와 교착상태 문제를 설명하는 고전적인 예제이다. 이 문제는 자원 할당과 프로세스 간 동기화의 복잡성을 잘 보여준다.
기본 개념
철학자들은 생애를 thinking(생각)과 eating(식사)을 번갈아 하면서 보낸다. 이들은 이웃과 상호작용하지 않으며, 가끔 그릇에서 식사하기 위해 2개의 chopsticks(젓가락)을 집으려고 시도한다(한 번에 하나씩). 식사를 하려면 두 개 모두 필요하며, 식사가 끝나면 두 개 모두를 놓는다.
5명의 철학자가 있는 경우, 공유 데이터는 다음과 같다:
- Rice bowl(밥그릇): 공유 데이터 집합
- Semaphore chopstick[5]: 1로 초기화된 젓가락 세마포어 배열
Dining-Philosophers Problem Algorithm
철학자 i의 구조는 다음과 같다:
do {
wait(chopstick[i]);
wait(chopStick[(i + 1) % 5]);
// eat
signal(chopstick[i]);
signal(chopstick[(i + 1) % 5]);
// think
} while (TRUE);
이 알고리즘의 문제점은 무엇일까? 이 접근 방식은 deadlock(교착상태)을 초래할 수 있다. 예를 들어, 모든 철학자가 동시에 배고파져서 자신의 왼쪽 젓가락(chopstick[i])을 집어 들었다고 가정해보자. 이 경우 모든 젓가락이 사용 중이므로, 어떤 철학자도 오른쪽 젓가락을 집어들 수 없게 되어 모두 영원히 기다리는 교착상태에 빠지게 된다.
Deadlock 처리 방법
다음과 같은 방법으로 교착상태를 해결할 수 있다:
- 동시에 최대 4명의 철학자만 테이블에 앉도록 허용한다. 이 방법은 circular wait(원형 대기) 조건을 방지하여 적어도 한 명의 철학자가 두 젓가락을 모두 얻을 수 있도록 보장한다.
- 두 젓가락이 모두 사용 가능한 경우에만 철학자가 젓가락을 집을 수 있도록 한다. 젓가락을 집는 행위는 critical section(임계 구역) 내에서 수행되어야 한다.
- 비대칭 해결책을 사용한다. 홀수 번호의 철학자는 먼저 왼쪽 젓가락을 집고 그 다음 오른쪽 젓가락을 집는다. 짝수 번호의 철학자는 먼저 오른쪽 젓가락을 집고 그 다음 왼쪽 젓가락을 집는다. 이 방법은 모든 철학자가 동시에 같은 방향의 젓가락을 집지 않도록 하여 교착상태를 방지한다.
Problems with Semaphores
세마포어의 잘못된 사용은 다양한 문제를 일으킬 수 있다:
- 잘못된 세마포어 연산 순서:
- signal(mutex) .... wait(mutex): 역순인 경우
- wait(mutex) ... wait(mutex)
- wait(mutex) 또는 signal(mutex)(또는 둘 다)의 누락
- 이러한 문제들은 deadlock과 starvation(기아 상태)을 초래할 수 있다.
Monitors
Monitor는 프로세스 동기화를 위한 편리하고 효과적인 메커니즘을 제공하는 고수준 추상화이다.
- Monitor는 abstract data type(추상 데이터 타입)으로, 내부 변수는 프로시저 내의 코드에서만 접근할 수 있다.
- 한 번에 하나의 프로세스만 monitor 내에서 활성화될 수 있다.
- 그러나 일부 동기화 방식을 모델링하기에는 충분히 강력하지 않다.
Monitor의 기본 구조:
monitor monitor-name
{
// shared variable declarations
procedure P1 (…) { …. }
procedure Pn (…) {……}
Initialization code (…) { … }
}
Condition Variables
Monitor 내에서 condition variables(조건 변수)는 다음과 같이 선언된다:
condition x, y;
조건 변수에 대한 두 가지 연산:
- x.wait() - 이 연산을 호출한 프로세스는 x.signal()이 호출될 때까지 일시 중단된다.
- x.signal() - x.wait()을 호출한 프로세스 중 하나(있는 경우)를 재개한다.
- 변수에 대한 x.wait()가 없으면 변수에 아무런 영향을 미치지 않는다.
조건 변수를 사용하면 프로세스가 특정 조건이 충족될 때까지 대기하고, 다른 프로세스에 의해 그 조건이 충족되면 신호를 받아 계속 실행할 수 있다. 이는 세마포어보다 더 구조화된 방식으로 동기화를 구현할 수 있게 해준다.
Monitor with Condition Variables
Monitor는 고수준 동기화 도구로, condition variables와 함께 사용되어 프로세스 간 효과적인 동기화 메커니즘을 제공한다.
Monitor는 공유 데이터에 대한 접근을 구조화하고 상호 배제를 보장한다.
Condition Variables Choices
특정 프로세스 P가 x.signal()을 호출하고, 프로세스 Q가 x.wait() 상태에 있을 때, 다음 단계로 무엇이 일어나야 할까? Q가 재개되면 P는 대기해야 한다. 여기에는 몇 가지 옵션이 있다:
- Signal and wait - P는 Q가 monitor를 떠나거나 다른 조건을 기다릴 때까지 대기한다.
- Signal and continue - Q는 P가 monitor를 떠나거나 다른 조건을 기다릴 때까지 대기한다.
두 방식 모두 장단점이 있으며, 언어 구현자가 결정할 수 있다. Concurrent Pascal에서 구현된 monitors는 절충안을 사용한다:
- signal을 실행하는 P는 즉시 monitor를 떠나고, Q가 재개된다.
- 이 방식은 Mesa, C#, Java를 포함한 다른 언어에서도 구현되어 있다.
Solution to Dining Philosophers
다음은 monitor를 사용한 Dining Philosophers 문제의 해결책이다:
monitor DiningPhilosophers
{
enum { THINKING, HUNGRY, EATING) state [5] ;
condition self [5];
void pickup (int i) {
state[i] = HUNGRY;
test(i);
if (state[i] != EATING) self [i].wait;
}
void putdown (int i) {
state[i] = THINKING;
// test left and right neighbors
test((i + 4) % 5);
test((i + 1) % 5);
}
void test (int i) {
if ( (state[(i + 4) % 5] != EATING) &&
(state[i] == HUNGRY) &&
(state[(i + 1) % 5] != EATING) ) {
state[i] = EATING ;
self[i].signal() ;
}
}
initialization_code() {
for (int i = 0; i < 5; i++)
state[i] = THINKING;
}
}
각 철학자 i는 다음 순서로 pickup()과 putdown() 연산을 호출한다:
DiningPhilosophers.pickup(i);
EAT
DiningPhilosophers.putdown(i);
이 해결책은 deadlock은 없지만, starvation은 여전히 가능하다.
Monitor Implementation Using Semaphores
Monitor는 semaphores를 사용하여 구현할 수 있다. 다음은 필요한 변수들이다:
semaphore mutex; // (initially = 1)
semaphore next; // (initially = 0)
int next_count = 0;
각 프로시저 F는 다음과 같이 대체된다:
wait(mutex);
...
body of F;
...
if (next_count > 0)
signal(next)
else
signal(mutex);
이렇게 함으로써 monitor 내에서 mutual exclusion이 보장된다.
Monitor Implementation - Condition Variables
각 condition variable x에 대해 다음과 같은 변수들이 필요하다:
semaphore x_sem; // (initially = 0)
int x_count = 0;
x.wait 연산은 다음과 같이 구현할 수 있다:
x_count++;
if (next_count > 0)
signal(next);
else
signal(mutex);
wait(x_sem);
x_count--;
x.signal 연산은 다음과 같이 구현할 수 있다:
if (x_count > 0) {
next_count++;
signal(x_sem);
wait(next);
next_count--;
}
Resuming Processes within a Monitor
조건 x에 여러 프로세스가 대기하고 있고, x.signal()이 실행되면, 어떤 프로세스가 재개되어야 할까? FCFS(First-Come-First-Served)는 자주 충분하지 않다.
conditional-wait 구문을 사용할 수 있다: x.wait(c)
- 여기서 c는 우선순위 번호이다.
- 가장 낮은 번호(가장 높은 우선순위)를 가진 프로세스가 다음에 스케줄링된다.
A Monitor to Allocate Single Resource
다음은 단일 자원을 할당하는 monitor의 예이다:
monitor ResourceAllocator
{
boolean busy;
condition x;
void acquire(int time) {
if (busy)
x.wait(time);
busy = TRUE;
}
void release() {
busy = FALSE;
x.signal();
}
initialization_code() {
busy = FALSE;
}
}
Monitor에서 condition variables를 활용하면, 공유 자원에 대한 접근을 효과적으로 관리하고 프로세스 간 동기화 문제를 해결할 수 있다. 특히 signal 연산의 의미(signal and wait vs signal and continue)는 구현에 따라 다를 수 있으며, 이는 동기화 문제 해결 방식에 영향을 미친다.
Monitor는 semaphores보다 더 고수준의 추상화를 제공하여 프로그래머가 동기화 문제를 더 쉽게 해결할 수 있게 해준다. 하지만 구현 방식에 따라 성능과 사용 편의성에 차이가 있을 수 있다.
'CS 지식 > 운영체제 (OS)' 카테고리의 다른 글
[운영체제] OS의 스케쥴링의 방식과 멀티레벨 스케쥴링, 스레드 스케쥴링 (PCS, SCS) - CPU Scheduling (0) | 2025.04.15 |
---|---|
[운영체제] OS의 스레드 종류(User, Kernel)와 관련 아키텍처 소개 (1) | 2025.04.13 |
[운영체제] OS의 프로세스(Process)의 개념과 구조, IPC 매커니즘 (0) | 2025.04.13 |
[운영체제] OS의 구조와 서비스 종류 소개, 시스템 프로그램과 시스템 콜 소개 (0) | 2025.04.13 |
[운영체제] OS의 듀얼모드와 작업(Operations)의 종류와 관리 기능 소개 (0) | 2025.04.13 |
댓글