Race Condition
Race Condition
Race Condition
- 여러 데이터들이 동시에 공유 데이터를 접근하는 상황
- Storage Box (memory, address space) 를 공유하는 Execution Box (CPU Process)가 여러 개 있는 경우 Race Condition이 발생할 수 있다.
OS에서 Race Condition이 발생하는 경우
1. kernel 모드 수행 중 인터럽트 발생 시
2. Process가 system call을 하여 kernel 모드로 수행중인데 context switch가 일어나는 경우
- 두 프로세스의 address space간에는 데이터 공유가 없으나 system call을 하는 동안에는 kernel address space의 데이터를 공유하기 때문에 race condition이 발생한다.
- 커널 모드에서 수행 중일 때는 CPU를 preempt하지 않도록 interrupt enable/disable을 사용하거나 커널 모드에서 사용자 모드로 돌아갈 때 preempt할 수 있도록 처리해서 해결
3. Multiprocessor에서 공유 메모리 내의 kernel data
- Multiprocessor의 경우에는 interrupt enable/disable로 해결되지 않음
- 한번에 하나의 CPU만이 커널에 들어갈 수 있게 하는 방법
- 커널 내부에 있는 각 공유 데이터에 접근할 때마다 그 데이터에 대한 lock / unlock을 하는 방법
Process Synchronization
Process Synchronization
- 공유 데이터의 동시 접근은 데이터 불일치 문제 (inconsistency)를 발생시킬 수 있다.
- 데이터의 일관성 유지를 위해서는 협력 프로세스 간의 실행 순서를 정해주는 매커니즘이 필요하다.
👉 race condition을 막기 위해서 concurrent process는 동기화되어야 한다.
Critical-Section Problem
Critical-Section Problem
- n개의 프로세스가 공유 데이터를 동시에 사용하기 원하는 경우, 각 프로세스의 code segment에는 공유 데이터를 접근하는 코드인 critical section이 존재
👉 하나의 프로세스가 critical section에 있을 때 다른 모든 프로세스는 critical section에 들어갈 수 없어야 한다.
프로그램적 해결법
프로그램적 해결법의 충족 조건
- Mutual Exclusion: 프로세스 P가 critical section을 수행 중이면 다른 모든 프로세스들은 그들의 critical section에 들어가면 안된다.
- Progress: 아무도 critical section에 있지 않은 상태에서 critical section에 들어가고자 하는 프로세스가 있으면 들어가게 해주어야 한다.
- Bounded Waiting: 프로세스가 기다리는 시간이 무한이 되지 않도록 해야 한다.
👉 프로세스들은 수행의 동기화를 위해 몇몇 변수를 공유할 수 있다. -> synchronization variable
Algorithm 1
- Synchronization variable: int turn Pi can enter its critical section if turn == i
Process P0
do {
while (turn != 0)
critical section
turn = 1
remainder section
} while (1)
- mutual exclusion O, progress X
- 과잉 양보: 반드시 한 번씩 교대로 들어가야 함 (swap-turn), 특정 프로세스가 더 빈번히 critical section에 들어가야 하는 경우 문제 발생
Algorithm 2
- Synchronization variable: boolean flag[2] initially all false, Pi ready to enter its critical section if flag[i] == true
Process Pi
do {
flag[i] = true; /* 이 행 이후에 다음행을 실행하기 전에 context switch가 발생한다면 */
while (flag[j]);
critical section
flag[i] = false
remainder section
} while (1);
- mutual exclusion O, progress X
- 동시에 flag[0] = flag[1] = true 되면 끊임 없이 양보하는 상황 발생 가능
Algorithm 3 (Peterson's Algorithm)
- Synchronization variable: combine algo1 + algo2
Process Pi
do {
flag[i] = true;
turn = j;
while (flag[j] && turn == j);
critical section
flag[i] = false;
remainder section
} while (1);
- meet all three requirements but Busy Waiting: 계속 CPU와 memory를 사용하며 기다려야 한다 (while)
Synchronization Hardware
- 하드웨어적으로 Test & modify를 atomic하게 수행할 수 있도록 지원하는 경우 앞의 문제는 간단히 해결
Semaphore
Semaphore S
- integer variable
- 아래의 두 가지 atomic 연산에 의해서만 접근 가능
P(S)
while (S <= 0) do no-op; // wait
S--;
V(S)
S++;
Busy - Wait
- Synchronization variable: semaphore mutex
Process Pi
do {
P(mutex);
critical section
V(mutex);
remainder section
} while (1);
- Busy-Wait는 효율적이지 못함
Block / Wakeup
Semaphore를 다음과 같이 정의한다.
typedef struct
{
int value; // semaphore
struct process *L; // process wait queue = 자원의 여분이 없어 기다리고 있는 프로세스 큐
}
- block: 커널은 block을 호출한 프로세스를 suspend시키고 이 프로세스의 PCB를 semaphore에 대한 wait queue에 넣는다.
- wakeup(P): block된 프로세스 P를 깨워서 이 프로세스의 PCB를 ready queue로 옮긴다.
P(S)
S.value--;
if (S.value < 0)
{
add this process to S.L;
block();
}
V(S)
S.value++;
if (S.value <= 0) { // S.L에 PCB가 대기하고 있는 경우
remove a process P from S.L;
wakeup(P);
}
- Critical section의 길이가 긴 경우 Block/Wakeup이 더 효율적
- Critical section의 길이가 매우 짧은 경우 Block/Wakeup 방식의 오버헤드가 Busy-Wait보다 커질 수 있다.
- 일반적으로는 Block / Wakeup 방식이 더 효율적
Two Types of Semaphores
- Counting semaphore: 도메인이 0 이상인 임의의 정수값, 주로 resource counting에 사용
- Binary semaphore (=mutex): 0 또는 1 값만 가질 수 있음, 주로 mutual exclusion (lock/unlock)에 사용
Deadlock and Starvation
Deadlock
- 둘 이상의 프로세스가 서로 상대방에 의해 충족될 수 있는 이벤트를 무한히 기다리는 현상
Starvation
- Infinite blocking: 프로세스가 semaphore queue에서 빠져나가지 못하는 현상
Classical Problems of Synchronization
Bounded-Buffer Problem (Producer-Consumer Problem)
- Producer: 데이터를 만들어 버퍼에 삽입하는 프로세스 <-> Consumer: 버퍼에서 데이터를 꺼내는 프로세스
- 공유 데이터에 접근할 때 interrupt가 발생하면 문제 발생
Solution
- semaphore full = 0, empty = n (size of buffer), mutex = 1;
Producer
do (
P(empty);
P(mutex);
add x to buffer
V(mutx);
V(full);
} while (1);
Consumer
do {
P(full)
P(mutex);
remove item from buffer to y
V(mutex)
V(empty)
} while (1);
Readers-Writers Problem
- 한 프로세스가 DB에 접근해서 write 중일 때 다른 프로세스가 접근하면 안된다.
- read 작업은 동시에 여러 프로세스가 해도 괜찮다.
Solution
- Writer가 DB에 접근 허가를 아직 얻지 못한 상태에서는 모든 대기중인 Reader들이 DB에 접근 가능
- Writer는 대기 중인 Reader가 하나도 없을 때 DB 접근 허용
- Writer가 DB에 접근 중이면 Reader는 접근 금지
- Shared data: int readcount = 0 (공유데이터를 읽고 있는 process 갯수)
- semaphore mutex = 1, db = 1;
Writer
P(db);
writing DB
V(db);
Reader
P(mutex);
readcount++;
if (readcount == 1) P(db) /* block writer */
V(mutex);
reading DB
P(mutex);
readcount--;
if (readcount == 0): V(db);
V(mutex);
👉 Writer에게 starvation 발생 가능
Dining-Philosophers Problem
- 여러 프로세스에게 제한된 리소스를 할당하는 상황에서 발생할 수 있는 문제
Solution 1
- semaphore chopstick[5]; initially all values = 1
Philosopher i
do {
P(chopstick[i]) /* 왼쪽 젓가락 */
P(chopstick[(i+1) % 5]); /* 오른쪽 젓가락 */
eat();
V(chopstick[i]);
V(chopstick[(i+1) % 5]);
think();
} while (1);
👉 모든 철학자가 동시에 왼쪽 젓가락을 집어버린 경우 -> Deadlock 발생
해결 방안
- 4명의 철학자만 테이블에 동시에 앉을 수 있도록 한다.
- 젓가락을 두 개 모두 집을 때에만 젓가락을 집을 수 있게 한다.
- 짝수(홀수) 철학자는 왼쪽(오른쪽) 젓가락부터 집도록 한다.
2번 해결방안
- enum {thinking, hungry, eating} state[5];
- semaphore self[5] = 0; semaphore mutex = 1;
void putdown(int i) {
P(mutex)
state[i] = thinking;
test((i+4) % 5);
test((i+1) % 5);
V(mutex)
}
void pickup(int i) {
P(mutex)
state[i] = hungry;
test(i);
V(mutex)
}
void test(int i) {
/* 젓가락 두 개를 모두 집을 수 있는 지 확인 */
if (state[(i+4)%5] != eating && state[i] == hungry && state[(i+1)%5] != eating) {
state[i] = eating;
V(self[i]);
}
}
Philosopher i
do {
picku(i);
eat();
putdown();
think();
} while (1);
Monitor
Monitor
Semaphore의 문제점
- 코딩이 어렵고 정확성의 입증이 어려움
- 자발적 협력(voluntary cooperation)이 필요하다 -> 프로세스 간 협조 필요
- 한 번의 실수가 모든 시스템에 치명적 영향
Monitor
- 동시 수행중인 프로세스 사이에서 abstract data type의 안전한 공유를 보장하기 위한 high-level synchronization construct
- OOP
monitor monitor-name
{
shared variable declarations
procedure body P1 (...) {}
procedure body P2 (...) {}
procedure body Pn (...) {}
{
initialization code
}
}
- 모니터 내에서는 한 번에 하나의 프로세스만이 활동 가능
- 프로그래머가 동기화 제약 조건 (semaphore)을 명시적으로 코딩할 필요 없이 객체 자체가 보장함
- 프로세스가 모니터 안에서 기다릴 수 있도록 하기 위해 condition variable 사용: condition x, y; (x, y: 객체)
- condition variable은 wait 와 signal 연산에 의해서만 접근 가능
- x.wait(): x.wait()를 invoke한 프로세스는 다른 프로세스가 x.signal()을 invoke하기 전까지 suspend된다. 연산 전에 자원이 가용상태인지 체크하고 가용상태가 아닌 경우에 wait연산을 호출한다.
- x.signal(): 정확하게 하나의 suspend된 프로세스를 resume한다. suspend된 프로세스가 없으면 아무 일도 일어나지 않는다.
Monitor를 활용한 솔루션
Bounded-Buffer Problem
monitor bounded_buffer {
int buffer[N];
condition full, empty; /* 데이터 위치를 가리키는 포인터 */
/* condition variable은 값을 가지지 않고 자신의 큐에 프로세스를 추가해서 sleep 시키거나 큐에서 프로세스를 꺠우는 역할만 담당 */
void produce (int x)
{
if there is no empty buffer: empty.wait();
add x to an empty buffer
full.signal()
}
}
Dining Philosophers Example
monitor dining_philosopher
{
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((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();
}
}
void init() {
for (int i=0;i<5;i++) {
state[i] = thinking;
}
}
}
Philosopher:
{
pickup(i);
eat();
putdown(i);
think();
}
'CS > 운영체제' 카테고리의 다른 글
07. Memory Management (0) | 2023.11.25 |
---|---|
06. Deadlocks (1) | 2023.11.21 |
[Week4] CPU Scheduling (0) | 2023.11.02 |
Week3: 프로세스(Process) 관리 (1) | 2023.10.28 |
Week 2: 시스템 구조 및 프로그램 실행 (1) | 2023.10.18 |