이전 글에서 임계구역 문제를 해결하기 위한 동기화 하드웨어 방법은 복잡할 뿐 아니라 응용 프로그래머가 사용할 수 없는 방법이라고 하였다. 이를 위해 운영체제 설계자들은 소프트웨어 도구들을 개발하였는데 그중 가장 간단한 도구가 바로 mutex 락이다.
mutex 락 역시 lock을 사용하는 방법으로 프로세스는 임계구역에 들어가기 전에 반드시 lock을 획득해야 하고 임계구역을 빠져나올 때 lock을 반환해야 한다. mutex 락은 available이라는 변수로 락의 가용 여부를 표시한다.
mutex 락을 사용하는 프로세스의 구조는 다음과 같다. acquire() 메서드를 통해 lock을 획득하고 release() 메서드를 통해 lock을 반환한다.
do {
acquire() //lock을 획득
--critical section--
release() //lock을 반환
--remainder section--
} while (true);
acquire() 메서드와 release() 메서드는 다음과 같다.
acquire() {
while (!available)
; /* busy wait */
available = false;
}
release() {
available = true;
}
지금까지 설명한 동기화 하드웨어의 방법들의 단점은 바쁜 대기(Busy Waiting)을 해야 한다는 것이다. 프로세스가 임계구역에 있는 동안 임계구역에 들어가기 원하는 다른 프로세스들은 acquire() 메서드를 호출하는 반복문을 계속 실행해야 하기 때문이다. lock이 가용해지기를 기다리면서 프로세스가 계속 회전을 하고 있기 때문에 spinlock이라고 부른다. 바쁜 대기는 다른 프로세스가 더 생산적인 작업에 사용할 수 있었던 CPU 사이클을 낭비하게 된다.
하지만 lock을 기다리는 동안 상당한 시간을 소모하는 문맥 교환을 전혀 필요로 하지 않는 장점도 있다. 따라서 프로세스들이 짧은 시간 동안만 lock을 소유할 것이라고 예상되면 spinlock이 유용하다.
다음으로 mutex 락과 유사하게 동작하지만 프로세스들이 자신들의 행동을 더 정교하게 동기화할 수 있는 방법인 세마포어(Semaphore)에 대해서 알아보자.
세마포어는 이진(Binary) 세마포어와 카운팅(Counting) 세마포어가 존재한다. 이진 세마포어는 mutex 락과 유사하며 0과 1사이의 값만 가능하다. 반면 카운팅 세마포어는 제한 없는 영역(Domain)을 가진다. 또한 세마포어는 가용한 자원의 개수로 초기화된다.
각 자원을 사용하려는 프로세스는 세마포어에 wait() 연산을 수행하며, 세마포어 값을 -1만큼 감소시킨다. 반대로 자원을 반환할 때는 signal() 연산을 수행하며, 세마포어 값을 +1만큼 증가시킨다. 이를 통해 세마포어 값이 0이 되면 모든 자원이 사용 중임을 알 수 있다.
각 메서드들의 구조는 다음과 같다.
wait(S) {
while (S <= 0)
; //바쁜 대기
S--;
}
signal(S) {
S++;
}
하지만 이 방법도 마찬가지로 바쁜 대기의 문제가 발생한다. 이를 해결하기 위해 세마포어 연산의 정의를 변경하였다.
프로세스가 wait() 연산을 실행하고 세마포어 값이 양수가 아닌 것을 발견한다면, 프로세스는 반드시 대기해야 한다. 이때 바쁜 대기 대신 자신을 봉쇄할 수 있도록 한다. 봉쇄 연산은 프로세스를 세마포어에 연관된 대기 큐에 넣고, 프로세스의 상태를 대기 상태로 전환한다. 그 후에 제어가 CPU 스케줄러로 넘어가고, 스케줄러는 다른 프로세스를 실행하기 위하여 선택한다.
세마포어 S를 대기하면서 봉쇄된 프로세스는 다른 프로세스가 signal() 연산을 실행하면 wakeup() 연산을 통해 재시작되도록 한다.
위의 방법들로 개선한 세마포어의 구조는 다음과 같다. 먼저 세마포어를 구조체를 이용하여 정의한다.
typedef struct {
int value;
struct process *list;
} semaphore;
각 세마포어는 한 개의 정수 value와 프로세스 리스트 list를 가진다. 프로세스가 세마포어를 기다려야 한다면, 이 프로세스를 세마포어의 프로세스 리스트에 넣어준다.
void wait(semaphore *S) {
S->value--;
if (S->value < 0) {
이 프로세스를 S->list에 넣는다;
block(); //봉쇄
}
}
이후 signal() 메서드를 통해 프로세스 리스트에서 한 프로세스를 꺼내서 그 프로세스를 깨워준다.
void signal(semaphore *S) {
S->value++;
if (S->value <= 0) {
S->list로부터 하나의 프로세스 P를 꺼낸다;
wakeup(P) //깨움
}
}
이 방법 또한 바쁜 대기를 완전히 해결한 것은 아니지만 바쁜 대기를 진입 코드에서 응용 프로그램의 임계구역으로 이동하였다.
또한 바쁜 대기를 wait()와 signal() 메서드들의 임계 구역에만 국한시켰으며, 이 구역은 매우 짧다. 그러므로 임계구역은 거의 항상 비어 있으며, 바쁜 대기는 드물게 발생하며, 발생하더라도 그 시간이 아주 짧을 것이다.
다만, 임계구역이 매우 길거나 거의 항상 점유되어 있는 응용 프로그램들을 갖는 상황에는 바쁜 대기는 극도로 비효율적이다.
마지막으로 모니터(Monitors)이다. 세마포어를 활용하여 임계구역 문제를 해결할 수 있지만 프로그래머가 세마포어를 잘못 사용하면 다양한 유형의 오류가 너무나도 쉽게 발생할 수 있다. 이러한 동기화 문제를 해결하기 위해 나온 것이 바로 모니터이다.
세마포어가 low level language 방식이었다면 모니터는 high level language 방식이라고 볼 수 있다.
모니터의 구조는 다음과 같다.
모니터의 동작원리는 다음과 같다.
어떤 공유 데이터에 대해 모니터를 지정해놓으면, 프로세스는 그 데이터를 접근하기 위해 모니터에 들어가야만 한다. 즉, 모니터 내부에 들어간 프로세스에게만 공유 데이터를 접근할 수 있는 기능을 제공하는 것이다. 또한 프로세스가 모니터에 들어가고자 할 때 다른 프로세스가 모니터 내부에 있다면 입장 큐에서 기다려야 한다.
세마포어의 동기화 오류를 모니터는 프로시저를 호출하여 간단히 해결할 수 있다
'Computer Science > 운영체제' 카테고리의 다른 글
교착상태(Deadlock) (0) | 2021.07.06 |
---|---|
Blocking / Non-Blocking & Synchronous / Asynchronous (0) | 2021.07.06 |
프로세스 동기화(Process Synchronization)(1) (0) | 2021.07.05 |
CPU 스케줄링 (0) | 2021.07.05 |
프로세스 스케줄링(Process Scheduling) (0) | 2021.07.05 |
댓글