해결 : concurrent process 를 동기화(synchronize - 순서대로 접근하게) 해야함.
우리가 배우는 Single processor 환경에서는 사용자 프로세스의 공유 메모리가 아니면 race condition 이 잘 일어날 일이 없는데,
OS 커널 코드는 여러 프로세스가 실행할 수 있기 때문에 문제가 생긴다.
해결 : 중요한 변수에 작업 중일때는 인터럽트 처리를 못하도록 한다.
-> 근데 그걸 어떻게 효율적으로 할지...
사용자 프로세스가 시스템콜을 통해 커널 모드로 프로세스 실행중에 timer interrupt 가 걸리면?
해결 : 커널 모드에서 수행 중일 때는 CPU를 preempt 하지 못하게 한다.
CPU 가 여러개 - 앞의 해결책으로는 해결할 수가 없음.
해결책
1. 한번에 하나의 CPU 만 커널에 들어갈 수 있게 한다. -> 비효율적...
2. 공유 데이터에 접근할 때 그 데이터에 대한 lock / unlock 을 걸어서 프로세서의 접근을 제어한다. -> 효율적.
공유 데이터에 접근하는 '코드'
do {
entry section // lock
critical section
exit section // unlock
remainder section
} while(1);
int turn = 0;
// turn 이 i 이면 Pi 가 critical section 에 들어갈 수 있음.
do {
while (turn != 0); // turn 이 0 이 아니면 무한루프 대기.
critical section
turn = 1; // 나올 때 상대방의 차례로 만들어 줌.
remainder section
} while(1);
boolean flag[2] = { false }; // flag [i] == true 이면 Pi 가 critical section 에 접근할 준비가 되었다.
do {
flag[i] = true; // 나 들어가고 싶어.
while (flag[j]); // 상대방의 flag 확인.
critical section
flag[i] = false; // 나올 때 unlock
remainder section
} while(1);
do {
flag[i] = true; // 나 들어가고 싶어.
turn = j; // turn 역시 들어가기 전에 바꿔 놓음.
while (flag[j] && turn == j); // 상대방의 flag 를 들고 있고 상대방의 차례이면 기다림.
critical section
flag[i] = false; // 나올 때 unlock
remainder section
} while(1);
flag[j] && turn == j
boolean lock = false;
do {
while (Test_and-Set(lock)); // lock 이 걸려있는지 체크하고, 안걸려있으면 lock = true 를 atomic 하게 넣는다.
critical section
lock = false; // 나올 때 unlock
remainder section
} while(1);
Semaphore S
P(S): while (S <= 0) do no-op
S--;
V(S): S++;
Semaphore mutex;
do {
P(mutex);
critical section
V(mutex);
remainder section
} while(1);
-> while 문으로 계속 기다리는 busy-waiting 은 효율적이지 못하므로 block & wakeup 방식으로 구현한다.
P(S)
S.value--;
if (S.value < 0) { // 공유 자원 접근 불가
add this process to S.L;
block(); // 자도록.
}
V(S)
S.value++;
if (S.value <= 0) { // 자원을 내놨는데 기다리는 프로세스가 있음.
remove a process P from S.L;
wakeup(P); // 일어나도록.
}
생산자 프로세스는 데이터를 생산해서 버퍼에 넣고, 소비자 프로세스는 데이터를 버퍼에서 꺼내가서 소비함.
크기가 유한한 버퍼.
buffer x;
semaphore full = 0, empty = n, mutex = 1; // 들어있는 버퍼 개수, 비어있는 버퍼 개수, 버퍼에 하나의 프로세스만 접근하게 하기 위한 lock
do {
produce an item in x
P(empty); // 비어있으면 버퍼 얻어오면서 빈개수--
P(mutex); // 버퍼에 접근하기 위한 락
add x to buffer
V(mutex);
V(full); // 가득찬개수++
} while(1)
do {
P(full);
P(mutex);
remove an item from buffer to y
V(mutex);
V(empty);
comsume the item in y
} while(1);
시나리오 : 공유 자원에 데이터를 읽기만 하는 Reader 와 데이터를 변경할 수 있는 Writer 존재.
Reader 는 한번에 여러 프로세스가 접근해도 되지만,
Writer 가 접근 중일 때는 다른 프로세스가 접근할 수 없음.
db;
int readcount; // 현재 DB에 접근 중인 Reader 의 수.
semaphore mutex = 1; // readcount 를 위한 lock
semaphore db = 1; // db 에 대한 lock
P(db); // lock
writing DB is performed;
V(db); // unlock
P (mutex); // readcount 에 대한 lock
readcount++;
if (readcount == 1) P(db); // 처음으로 접근하는 reader 라면 db에 lock
V(mutex);
reading DB is performed
P(mutex);
readcount--;
if (readcount == 0) V(db); // Reader 가 있다면, Writer 는 이걸 기다렸다가 접근 가능
V(mutex);
만약 Reader 가 읽는 중에 계속해서 reader 가 접근하게 된다면 계속해서 reader 의 접근을 허용하므로 문제가 있다. reader 를 일정 수까지만 허용하는 등의 조치가 필요.
5명의 철학자가 원탁에 둘러앉아서 밥을 먹는데 젓가락이 철학자 양 옆으로 1개씩 총 5개 놓여있다.
철학자는 생각을 하거나 식사를 할 수 있는데, 젓가락 양쪽을 모두 집어야 식사를 할 수 있다.
양 옆의 철학자가 공유하는 자원 젓가락
Semaphore chopstick[5] = { 1 };
각 프로세스인 철학자
do {
P(chopstick[i]);
P(chopstick[(i + 1) % 5];
eat();
V(chopstick[i]);
V(chopstick[(i + 1) % 5];
think();
} while(1);
enum { thinking, hungry, eating } state[5]; // 철학자의 상태
semaphore self[5] = 0; // 양쪽 젓가락을 잡을 권한. 처음엔 전부 권한이 없음 (0) -> 보통 semaphore 는 양수 값에 시작하는데 0으로 시작하는게 특이한 포인트
semaphore mutex = 1; // lock : 옆 철학자의 상태가 바뀌면 나의 젓가락을 집을 권한에도 영향이 오기 때문에 상태를 바꾸기 전에 lock 을 걸어야 함.
do {
pickup(i);
eat();
putdown(i);
think();
} while(1);
void pickup(int i) {
P(mutex); // lock for state
state[i] = hungry;
test(i); // 젓가락 2개를다 집을 수 있는지.
V(mutex); // unlock
P(self[i]); // self[i] 1 -> 0 : 만약 젓가락을 잡을 수 있다면 젓가락 사용 / 없다면 대기.
}
void putdown(int i) {
P(mutex); // lock for state
state[i] = thinking;
// 양 옆 철학자가 내가 젓가락을 쥐고있어서 밥을 못먹었다면 먹을수 있게 해준다.
test((i + 1) % 5); // 오른쪽
test((i - 1) % 5); // 왼쪽
V(mutex); // unlock
}
void test(int i) {
// 조건 : 양 쪽이 식사중이 아니면서 나는 배고픔.
if (state[(i - 1) % 5] != eating && state[i] == hungry && state[(i + 1) % 5] != eating) {
state[i] = eating;
V(self[i]); // self[i] 0 -> 1 / 젓가락을 잡을 권한을 줌.
}
}
근데 이렇게 코딩하니까...
P(mutex)
critical section
V(mutex) // 잘못해서 P(mutex) 라고 써버리면... 무한대기 상태에 빠짐.
-> Monitor 로 해결해보자.
프로세스 A가 모니터 안에 와서 operation 을 하고 있다가 CPU 를 빼앗겨서 프로세스 B가 모니터에 들어오려고 하면, 이미 모니터에 active 되어있는 프로세스 A가 있으므로, 프로세스 B는 entry queue 에 줄서서 기다리게 됨.
프로세스가 모니터를 빠져나가거나 잠들어서 active 프로세스가 0개가 되면 엔트리 큐에 있는 프로세스가 모니터로 들어옴.
monitor 내부에 있는 procedure (함수로 구현됨) 를 통해서만 각 공유 자원에 접근할 수 있게 하며, 여러 procedure 에 동시에 접근할 수 없다. -> lock 걸 필요가 없다.
monitor monitor-name {
shared variable declarations
procedure body P1 (...) {
...
}
procedure body P2 (...) {
...
}
...
procedure body Pn (...) {
...
}
{
init
}
}
monitor bounded_buffer {
int buffer[N];
condition full, empty; // 값을 가지지 않고, 자신의 큐에 프로세스를 매달아서 sleep 시키거나 큐에서 프로세스를 깨워주는 역할만 한다.
// full : 내용이 있는 버퍼를 기다리는 프로세스를 줄세운다.
// empty : 없는
void produce(int x) {
if there is no empty buffer
empty.wait(); // 생산자 기준 자원 없음 -> 재움
add x to an empty buffer
full.signal(); // 소비자 깨움
}
void consume(int x) {
if there is no full buffer
full.wait(); // 소비자 기준 자원 없음 -> 재움
add x to an full buffer
empty.signal(); // 생산자 깨움
}
}
monitor dining_philosopher {
enum { thinking, hungry, eating } state[5];
condition self[5];
void pickup(int i) {
state[i] = hungry;
test(i); // 젓가락 2개를다 집을 수 있는지.
if (state[i] != eating)
self[i].wait(); // semaphore 일 땐 P 연산으로 들어가던 것.
}
void putdown(int i) {
state[i] = thinking;
test((i + 1) % 5);
test((i - 1) % 5);
}
void test(int i) {
if (state[(i - 1) % 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 {
do {
pickup(i); // 모니터 안에 있는 연산을 이용.
eat();
putdown(i); // 모니터 안에 있는 연산을 이용.
think();
} while(1);
}
출처 / 참고
반효경 교수님의 2014 운영체제 6. Process Synchronization, 2, 3, 4 강의를 듣고 포스팅하고,
공룡책을 읽고 추가 정리합니다.
사진 출처는 강의 자료.