협럭적 프로세스는 Race Condition의 위험이 있다.
: 여러개의 프로세스가 동일한 데이터에 접근하고, 이 프로세스들이 실행되는 순서에 따라 실행결과가 달라지는 상황
Race Condition이 발생되지 않게 하기위해서는 협력적 프로세스들의 질서있는 실행을 보장하여 데이터 무결성을 보장이 필요하다
데이터 무결성을 위해서는 프로세스끼리 데이터에 대한 동기화가 필요하다.
각 프로세스는 자신의 임계구역으로 진입하려면 진입 구역(entry section)에서 진입 허가를 요청해야 하고, 임계구역 뒤에는 퇴출 구역(exit section)이 따라올 수 있다. 코드의 나머지 부분들을 총칭하여 나머지 구역(reaminder section)이라고 한다.
Peterson 해결안은 Critical Section과 Remainder Section을 번갈아 가며 실행하는 두 개의 프로세스로 한정된다.
int turn;
boolean flag[2];
Critical Section으로 진입하기 위해서는 Pi는 먼저 flag[i]를 참으로 만들고, turn을 j로 지정한다. 이렇게 함으로써 Pi는 Pj가 Critical Section으로 진입하기를 원한다면 진입 가능하다는 것을 보장한다. 만일 두 프로세스가 동시에 진입하기를 원한다면 진입 turn은 거의 동시에 i와 j로 지정될 것이다. 이 때의 경우 turn의 궁극적인 값이 둘 중 누가 먼저 Critical Section으로 진입할 것인가를 결정한다.
flag[j]가 false(Pj가 remainder section 수행)가 되거나 turn이 i(Pj는 준비완료됬고, Pi가 Entry Section에서 대기하고 있음)일 경우 Pi는 Critical Section에 들어갈 수 있다.
Peterson의 해결안이 Ciritical Section 문제를 해결하기 위해선 위에서 명시한 것 처럼 3가지 요구조건을 만족해야 한다. 같이 살펴보자.
Critical Section 문제의 소프트웨어 기반 해결책은 최신 컴퓨터 아키텍처에서 작동하지 않을 수 있다.
읽고 쓰는 작업이 서로 다른 인스트럭션으로 이루어져 있기 때문이다. 인스트럭션이 수행되는 도중에는 인터럽트가 올 수 있기 때문에, 만약 읽기와 쓰기가 하나의 인스트럭션에서 이루어져 원자적인 명령어를 통해 Critical Section문제를 해결할 수 있다.
메모리 장벽 : 컴퓨터 아키텍처는 메모리의 모든 변경 사항을 다른 모든 프로세서로 전파하는 명령어를 제공하여 다른 프로세서에서 실행 중인 스레드에 메모리 변경 사항이 보이는 것을 보장한다. 이러한 명령어를 메모리 장벽(Memory Barriers) 또는 메모리 펜스(Memory Fences)라고 한다.
하드웨어 명령어 : 원자적인 연산
mutex lock은 mutual exclusion lock의 축약 형태로서 Critical Section을 해결하기위한 하드웨어 기반의 해결책보다 상위 수준의 해결책이다.
Mutex Lock의 기본 개념은 프로세스는 Critical Section에 들어가기 전에 반드시 lock을 획득해야 하고 Critical Section을 빠져나올 때 lock을 반환해야 한다.
Mutex Lock에 필요한 3가지
acquire() : Lock을 획득하는 함수
acquire() {
while(!available)
; /* busy wait */
available = false;
}
단점은 바쁜 대기(busy waiting)를 해야 한다는 것이다. 이러한 busy waiting은 대기하는 다른 프로세스가 계속해서 acquire함수를 호출해서 다른 프로세스가 생산적으로 사용할 수 있는 CPU 주기를 낭비한다.
프로세스가 락을 기다려야 하고 문맥 교환에 상당한 시간이 소요될 때 문맥 교환이 필요하지 않다는 장점이 있다.
최신 다중 코어 컴퓨팅 시스템에서 스핀락은 많은 운영체제에서 널리 사용된다. 일반적으로 락이 유지되는 기간이 문맥 교환을 두 번(1.스레드를 대기상태로 2.대기중인 스레드를 복원) 하는 시간보다 짧은 경우 Mutext Lock을 사용한다.
release() : Lock을 반환하는 함수
release() {
available = true;
}
available : Lock을 사용할 수 있는 지 여부를 표시하는 변수
세마포 S는 정수 변수로 사용되는데, 두개의 원자적 연산 wait() 와 signal()로 접근할 수 있다.
wait(S) {
while (S <= 0)
; // busy wait
S--;
}
signal(S) {
S++;
}
busy waiting을 피하기 위해 세마포 S를 대기하면서 일시 중지된 프로세스는 다른 프로세스가 signal() 연산을 실행하면 재시작되어야 한다. 프로세스는 sleep() 연산에 의해서 일시 중지되고 wakeup() 연산에 의하여 재시작된다.
세마포 구조
typedef struct {
int value;
struct process *list;
}semaphore;
wait()
wait(semaphore *S) {
S->value--;
if (S->value < 0) {
add this process to S -> list;
sleep();
}
}
signal()
signal(semaphore *S) {
S->value ++;
if (S->value <= 0) {
remove a process P from S -> list;
wakeup(P);
}
}