- 데이터 접근
Execution-Box: CPU, 컴퓨터내부, process
Storage-Box: Memory, Disk, process 주소 공간
공유 데이터(shared data)의 동시 접근(concurrent access)은 데이터의 불일치 문제(inconsistency)를 발생시킬 수 있다
일관성(consistency) 유지를 위해서는 협력 프로세스(cooperating process) 간의 실행 순서(orderly execution)를 정해주는 메커니즘 필요 => 조율
Storage-box를 공유하는 Execution-box가 여럿 있는 경우 발생 가능
여러 개의 process나 CPU가 동시에 공유 데이터를 접근하는 상황
데이터의 최종 연산 결과는 마지막에 그 데이터를 다룬 process에 따라 달라짐
Kernel 수행 중 interrupt
발생 시
1) kernel running
2) interrupt 발생
3) interrupt handler에서 load, sub, store => 1 감소
ex) 10 -> 9
4) kernel로 복귀
5) kernel에서의 load: 이때 interrupt 결과인 9가 아니라 기존에 저장해둔 10을 load
6) kernel에서의 add, store => 1 증가
ex) 10 -> 11
(10->9->10)이 아니라 (10->11)이 되어버림
* kernel 사이의 context switch. 양쪽 다 kernel code이므로 kernel address space 공유
=> Solution: kernel의 running 중에 interrupt가 들어와도 kernel의 process가 끝날 때까지 interrupt 처리 X
Process가 system call을 하여 kernel mode로 수행 중인데 context switch
가 일어나는 경우
1) Process A(in usermode)의 system call
2) kernel mode 전환
3) A의 time quantum expires & Process B needs CPU
=> Process A는 CPU를preempt
당함 (while in kernel)
4) Process B의 할당 시간 종료
5) Process A가 CPU 되돌려 받음
두 process의 address space 간에는 data sharing X
BUT! system call을 하는 동안, kernel address space의 data를 access하게 됨 (share)
이 작업 중간에 kernel에서 running 중인 process의 CPU를 Preempt? => race condition 발생
=> Solution: kernel mode 수행 중일 때는 CPU를 preempt X. kernel mode에서 user mode로 돌아갈 때 preempt
Multiprocessor
에서 shared memory 내의 kernel data
어떤 CPU가 마지막으로 count를 store했는가? => race condition
Multiprocessor의 경우 interrupt enable/disable로 해결되지 X
=> Solution
1) 한 번에 하나의 CPU만 kernel에 들어갈 수 있게 하는 방법 (kernel 전체를 lock / unlock)
2) kernel 내부에 있는 각 공유 데이터에 접근할 때마다 그 데이터에 lock / unlock
을 하는 방법
단순히 P1 수행 중 timer interrupt 가 발생해서 context switch가 일어나서 P2가 CPU를 잡는다고 발생하는 문제가 아님. shared data를 사용 중이거나, kernel mode일 때 발생.
race condition을 막기 위해서 concurrent process는
동기화(synchronize)
되어야 한다
n개의 process가 공유 데이터를 동시에 사용하기를 원하는 경우에 발생
Critical section: 각 code의 segment에서 공유 데이터를 접근하는 code
프로그램적 해결법의 충족 조건
- Mutual Exclusion
: 프로세스 Pi가 critical section 부분을 수행 중이면 다른 모든 프로세스들은 그들의 critical section에 들어가면 안 된다.
- Progress
: 아무도 critical section에 있지 않은 상태에서 critical section에 들어가고자 하는 프로세스가 있으면 critical section에 들어가게 해주어야 한다.
- Bounded Waiting
: 프로세스가 critical section에 들어가려고 요청한 후부터 그 요청이 허용될 때까지 다른 프로세스들이 critical section에 들어가는 횟수에 한계가 있어야 한다.
가정
- 모든 프로세스의 수행 속도 > 0
- 프로세스들 간의 상대적인 수행 속도는 가정 X
Initial Attemps to solve problem
- Synchronization variable: 프로세스들이 수행의 동기화(synchronization)를 위해 공유하는 변수
두 개의 process가 있다고 가정 P0, P1
do {
entry section
critical section
exit section
remainder section
} while(1);
int turn;
initially turn = 0; // Pi can enter its critical section if (turn == 1)
- Process P0
do {
while (turn != 0);
critical section // my turn
turn = 1; // P1의 critical section 수행
remainder section // your turn
} while(1);
- Process P1
do {
while (turn == 0);
critical section
turn = 0;
remainder section
} while(1);
Mutual exclusion (O) Progress (X)
과잉양보 : 반드시 한 번씩 교대로 들어가야만 함 (swap-turn)
P0이 turn을 P1의 값으로 바꿔줘야만 P1이 들어갈 수 있음
만약 특정 process가 더 빈번히 critical section을 들어가야 한다면?
boolean flag[2]; // 들어가고자 하는 intention
initially flag[모두] = false; // no one is in critical section
// Pi ready to endter its critical section if (flag[i] == true)
- Process P0
do {
// i = 0
flag[i] = true; // Pretend I am in
// 여기까지 수행 후 context switch가 일어나면 Pi가 critical section에 아직 들어가지 않았는데도
// Pj의 while문에 걸려서 더이상 진행하지 못한다.
while (flag[j]); // Is he also in? Then wait
critical section
flag[i] = false; // I am out now
remainder section
} while(1);
- Process P1
do {
// i = 1
flag[i] = true;
// 밑에 while문과 순서를 바꾸면 서로 양보하느라 critical section에 못 들어가는 문제는 해결됨
// 그러나 둘 다 들어가버리는 경우가 발생
while (flag[j]);
critical section
flag[i] = false;
remainder section
} while(1);
Mutual exclusion (O) Progress (X)
P0과 P1이 둘 다 flag[i] = true까지만 수행 후 critical section에 들어가지 못하고 끊임없이 양보 하는 상황 발생 가능
do {
flag[i] = true; // 내가 들어가고자 함
turn = j; // 상대방 turn
while (flag[j] && turn == j); // 상대방이 들어가고자 하고, 상대방 차례일 때 wait
critical section
flag[i] = false;
remainder section
} while(1);
Mutual exclusion (O) Progrss (O) Bounded Waiting (O)
Busy Waiting (=spin lock)
: 계속 CPU와 memory를 쓰면서 wait
Test_and_Set
: 하드웨어적으로 test & modify를 atomic(하나의 operation인것처럼)하게 수행할 수 있도록 지원하는 경우, 앞의 문제는 간단히 해결
instruction 하나로 데이터를 읽고 쓰는 작업이 모두 가능하도록 하드웨어적 지원이 있으면 lock 문제 해결 가능
Test와 Set 사이의 Context Switch
- Synchronization Variable
boolean lock = false;
- Process Pi
do {
while (Test_and_Set(lock)); // busy waiting
critical section // lock -> TRUE. false 전까지 계속 수행
lock = false; // SET
remainder section
}
각자 lock = TRUE에서 critical section 수행
수행 종료 후, lock = FALSE => 다른 process lock = TRUE. 서로 번갈아가며 atomic 수행
추상자료형
P(S):
while (s<=0) do no-op; // wait
S--; // S: resource 개수 count
// if S is positive, decrement & enter.
// otherwise, wait until positive (busy-wait)
V(S):
S++;
semaphore mutex; // initially 1: 1개가 critical section에 들어갈 수 있다
- Process Pi
do {
P(mutex); // if positive, dec-- & enter. otherwise, wait
critical section
V(mutex); // increment semaphore
remainder section
} while(1);
busy-wait(=spin lock): 효율적 X. => block & wakeup(=sleep lock) 방식의 구현
typedef struct {
int value; // semaphore ++ , --
struct process *L; // process wait queue
} semaphore;
- block: 커널은 block을 호출한 process를 suspend
시킴wait queue
에 넣음wakeup
시킴ready queue
로 옮김P(S): // resource 획득 연산
S.value--;
// 자원의 개수 X. '상황'을 나타냄. wakeup할 게 있는지 판단.
// 음수: 누군가가 resource 기다리고 있음, 양수: resource 여분이 있어서 기다리지 않고 사용 중
if (S.value < 0) {
add this process to S.L; // wait queue에 linked list 연결
block(); // suspend
}
V(S): // resource 반납 연산
S.value++;
if (S.value <= 0) {
remove a process P from S.L;
wakeup(P);
}
Busy-wait VS Block/Wakeup
: 일반적으로 Block/Wakeup 효율적. CPU 의미 있는 이용.
BUT! block/wakeup overhead가 있음
if critical section 길이 ⬆ : Block/Wakeup
⬇ : Block/Wakeup 오버헤드가 busy-wait 오버헤드보다 커질 수 있음
Deadlock: 둘 이상의 프로세스가 서로 상대방에 의해 충족될 수 있는 event를 무한히 기다리는 현상 (상호교차적) => 반드시 starvation 유발
context switch. 서로의 것을 위해 무한히 wait
ex) S, Q: 1로 초기화된 semaphore
P0 | P1 |
---|---|
P(S) | P(Q) |
P(Q) | P(S) |
... | ... |
V(S) | V(Q) |
V(Q) | V(S) |
P(S)와 P(Q) 서로 하나씩 차지.
P0의 P(S)는 P1의 P(S)를, P1의 P(Q)는 P0의 P(Q)를 서로 기다리며 계속 wait
V에 와야 각자 release
P(S) -> P(Q) 순으로 CPU 잡는다는 rule 만들면 deadlock 방지 가능
임시로 데이터를 저장하는 공간인 buffer의 크기가 유한함
생산자-소비자 문제 (producer-consumer)
binary semaphore
(for shared data's mutual exclusion)integer semaphore
(to show the # of full/empty buffer)semaphore full = 0, empty = n, mutex = 1; // n: buffer size
do { ...
produce an item in x
...
P(empty); // 1. empty buffer 확인
P(mutex); // ring buffer 접근. 공유데이터 lock
...
add x to buffer // 3. 데이터 입력
...
V(mutex); // 4. unlock
V(full); // 5. full buffer 하나 증가
} while (1);
do {
P(full); // 1. full buffer 확인
P(mutex); // 2. 공유데이터 lock
...
remove an item from buffer to y // 3. 데이터 제거
...
V(mutex); // 4. unlock
V(empty); // 5. empty buffer 증가
...
consume the item in y
...
} while (1);
Semaphore 업무
1. producer와 consumer가 동시에 공유 buffer 접근 막기 위해 공유 buffer 전체에 lock/unlock을 걸어 배타적 접근 => binary semaphore
2. buffer가 비었을 때 producer or consumer가 내용을 만들거나 꺼내갈 buffer가 없으면 가용자원의 개수 count => integer semaphore
한 process가 DB에 write 중일 때 다른 process 접근하면 X
write는 exclusion. BUT!! read는 동시에 여럿이 가능
int readcount = 0; // 현재 DB에 접근 중인 reader의 수 => 나중에 다 빠져나가는지 확인해야함
DB 자체;
semaphore mutex = 1, db = 1;
...
P(db);
...
writing DB is performed
...
V(db);
...
// starvation 발생 가능
P(mutex);
readcount++;
if (readcount == 1)
P(db); // block writer
V(mutex); // readers follow
...
reading DB is performed
...
P(mutex);
readcount--;
if(readcount == 0)
V(db); // enable writer
V(mutex);
semaphore chopsticks[5]; // initially all values are 1: 동시에 둘이 젓가락을 잡을 수 X. 무조건 한 명만.
do {
P(chopstick[i]);
P(chopstick[(i+1) % 5];
...
eat();
...
V(chopstick[i]);
V(chopstick[(i+1) % 5];
...
think();
...
} while(1);
Deadlock 의 가능성이 있음: 모든 철학자가 동시에 배가 고파져 왼쪽 젓가락을 집어버린 경우 => 오른쪽 젓가락은 아무도 집을 수 X
enum { thinking, hungry, eating } state[5]; // 상태. 공유 변수
semaphore self[5] = 0; // 다섯 명의 철학자가 젓가락을 두 개 다 잡을 수 있어서 잡을 권한을 줄것인가 여부. 0 => 없음 / 1 => 있음
semaphore mutex = 1; // lock
Philosopher i:
do {
pickup(i);
eat();
putdown(i);
think();
} while(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); // lock
state[i] = hungry;
test(i);
V(mutex);
P(self[i]);
}
// i가 젓가락 두 개를 다 잡을 수 있는 상태인지 test
void test(int i) {
// (i+4) % 5 : 왼쪽 철학자 eating X && (i+1) % 5 : 오른쪽 철학자 eating X && hungry : 내가 지금 배고픈 상태
if (state[(i+4) % 5] != eating && state[i] == hungry && state[(i+1) % 5] != eating) {
state[i] = eating;
V(self[i]); // 젓가락 잡는 권한 부여
}
}
Semaphore의 문제점
- 코딩하기 힘들다 (sync 맞추기 hard)
- 정확성 입증 어려움
- 자발적 협력이 필요함
- 한 번의 실수가 모든 시스템에 치명적 영향
ex)
V(mutex)
Critical Seciton
P(mutex)
=> Mutual Exclusion 깨짐
P(mutex)
Critical Section
P(mutex)
=> Deadlock
동시 수행 중인 process 사이에서 abstract data type의 안전한 공유를 보장하기 위한 high-level synchronization construct
=>lock
이 필요 없음
- 모니터 내에서는 한 번에 하나의 process만 활동 가능
- 프로그래머가 동기화 제약 조건(semaphore에서는 명시)을 명시적으로 코딩할 필요 X
- 프로세스가 모니터 안에서 기다릴 수 있도록 하기 위해 condition variable 사용
Condition variable
: 상태변수 (값을 가지는 변수가 아니라 어떤 조건을 만족하지 못하는 경우 process를 상태 변수 자신의 queue에 메달아두는 역할만 수행) condition x,y;
- x.wait()
: x.wait()을 invoke한 프로세스는 다른 프로세스가 x.signal()을 invoke하기 전까지 suspend된다
- x.signal()
: 정확하게 하나의 suspend된 프로세스 resume. suspned된 process가 없으면 nothing happens
monitor monitor-name {
shared variable declarations
// 한 번에 하나의 process만 수행
procedure body P1(...){
}
procedure body P2(...){
}
...
procedure body Pn(...){
}
{
initialization code
}
}
운영체제 - 반효경
http://www.kocw.net/home/cview.do?cid=3646706b4347ef09