데이터가 저장된 곳이 있을 거고, 프로그램은 그걸 가져와서 연산하고, 다시 특정 위치에 저장하는 방식이다. 딱 이것 때문에 synchronization문제가 발생한다. 그 특정 위치들에 동시에 접근하려고 하기 때문이다.
Storage는 데이터가 들어있는 저장 공간, execution은 연산 주체 정도로 생각하고 바라보자. synchronization 문제에 얽혀있는 당사자들을 위와 같이 바라볼 수 있다.
물론, 일반적인 경우에서는 발생하지는 않는다. 어차피 cpu가 하나이면 memory 건드는건 그 하나의 cpu이고, process도 자기 주소 공간만 건드리기 때문
하지만 multi processor인 경우, process간에 공유메모리가 있는 경우와 같이 데이터를 건드리는 주체(E-box)가 여러개인 상황에서 발생할 수 있다.
즉 정리하자면,
process synchronization은
이에 의해 발생하는 문제를 막기 위해 synchronization이 필요
race condition은 언제 발생할까?
간단한 예시부터 들어본다.
kernel 작업 수행 중 interrupt 발생시
Process가 system call하여 kernel mode로 들어갔는데 context switch가 일어난 경우
multiprocessor에서 shared memory 내에 kernel data가 있을 때
특히 kernel을 건드리는 경우 이런 문제가 발생할 확률이 높다. user level에서는 문제가 별로 안생길만한게 맞는데, kernel은 다르다. 여러 process들이 system call을 외쳐대기도 하고, 또 kernel이 요청받은 작업을 수행하다가도 다른 interrupt가 들어오면, 자기 자신이 자신을 멈추고 다른 작업을 수행해야하기 때문이다. 이 과정에서 kernel이 갖는 data 공간이 꼬일 가능성이 있다. 즉, system call에 의해 다양한 상황에서 kernel이 호출되고, 같은 data를 건드리게 되기 때문
조금 더 구체적인 예시를 봐보자
예시 1
두 프로세스가 같은 자원에 접근할 때이다.
이 경우, 특정 작업이 진행되는 동안만큼은 interrupt가 되지 않도록 하게 함으로써 문제 해결 가능
예시 2
두 프로세스가 kernel의 함수를 호출해 같은 자원을 건드리는 경우이다.
이 경우, kernel mode일 때 만큼은 할당 시간이 끝나도 cpu를 뺏기지 않도록, 즉 preemptive하지 않도록 정책을 설정하는 식으로 해결가능. user mode로 빠져나올 때 빼앗도록
예제 3
cpu가 여러개 있는 경우이다.
data에 접근시 해당 data에만 lock을 걸어버리는 방법으로 해결 가능하다. 접근하기 전에 lock을 걸고, 하고 싶은 연산을 쭉 수행하다가 모든 연산이 끝나면 다시 lock을 해제하는 것
어쨋든 이렇게 공유데이터에 접근하는 주체가 여러개 있다는 전재하에 발생하는 문제를 어떻게 풀 것인가에 대해 다룰것이다.
일관성 유지를 위해서는 협력하는 프로세스간의 실행 순서를 잘 정해줄 메커니즘이 필요한 것이다.
물론 공유 데이터의 동시 접근은 데이터 불일치 문제를 발생시킬 수 있지만, 공유데이터에 접근을 항상 하는 것은 아니다. 그러한 부분이 종종 있는 것이다. 그런 부분을critical section
이라고 한다. 아래서는 공유 데이터를 건드리는 프로세스들의 순서를 정해줄 메커니즘을 critical section이라는 부분을 기준으로 해결해나가는 다양한 방법들을 알아볼 것이다.
공유데이터에 접근하는 코드를 의미한다
do{
entry section
critical section
exit section
remainder section
} while(1);
Mutual Exclusion (상호 배제)
Progress
Bounded Waiting (유한 대기)
Synchronization variable 1개 선언
do{
while (turn != 0);
critical section
turn = 1;
remainder section
} while(1);
→ 문제가 있다. P1이 들어가야만 P0가 critical section에 들어갈 수 있다. 서로 critical section에 들어가는 빈도가 다를 수 있다. Progress 조건을 만족하지 않는다.
Synchronization variable process 개수만큼 선언
do{
flag[i] = true; // i wanna get in
while (flag[j]); // is another one also in? then wait
critical section
flag[i] = false; // i'm out now
exit section
remainder section
} while(1);
→ flag[i] = true;가 실행된 후 cpu를 뺏기면, 둘 다 flag가 true가 되어버리는 상태가 발생한다. Progress 조건을 만족하지 않는다.
1과 2를 합친 것
do{
flag[i] = true; // i wanna get in
turn = j; // set the turn to another one first
while (flag[j] && turn == j); // wait only if ...
critical section
flag[i] = false; // i'm out now
exit section
remainder section
} while(1);
→ 세 조건을 모두 잘 만족한다. Busy waiting(=spin lock)
이라는 문제가 발생하기는 한다. 결국 P0가 critical section에 들어와있는 상태로 cpu가 P1에게 넘어가고, P1이 critical section에 들어가고자 할 때, 아무리 while문을 돌아도 자신에게 차례가 넘어올리는 절대 없다. cpu 낭비 발생
Synchronization Hardware
하드웨어적으로도 해결 가능하다. Test & Modify를 atomic하게 수행할 수 있도록 지원하는 경우, 앞의 문제는 간단히 해결 가능하다.
잘 생각해보자. 따지고 보면 데이터를 읽고 쓰는 것을 하나의 instr으로 처리할 수 없었기에 공유 메모리에서 synchronization 문제가 발생했던 것이다. 그냥 이 과정 자체를 하나의 instr을 해결되도록 해버리면 해결되는 것
Test_and_Set이라는 instr이 주로 활용된다. 원래 값을 읽어내고, 그 자리를 1로 채운다.
boolean lock = false; // synchronization variable
do {
while (Test_and_Set(lock));
critical section
lock = false;
remainder section;
}
프로그래머가 매번 이런 작업을 하는 것은 꽤나 힘들 것이다. 이를 위해 추상 자료형인 semaphores라는 것을 정의하고 사용한다. 다음 시간에 알아보자