이 글은 혼자 공부하는 컴퓨터 구조+운영체제 교재와 강의를 참고하여 정리한 글입니다. 오타나, 잘못된 내용이 있으면 언제든지 알려주세요! 감사합니다.😊
프로세스 동기화
동기화
프로세스들은 동시다발적으로 실행된다.
프로세스들은 공동의 목적을 올바르게 수행하기 위해 서로 협력하며 영향을 주고받는다. 이 과정에서 자원의 일관성을 보장해야 하기에 반드시 동기화, synchronization되어야 한다. 즉, 프로세스들의 동기화를 고려해야 한다. 동기화란 뭘까?
동기화의 의미
공동의 목적을 위해 동시다발적으로 실행되는 프로세스는 서로 데이터를 주고받으며 협력하며 실행될 수 있다.
- 예시) 워드 프로세서 프로그램
- 맞춤법 검사 프로세스와 입력 내용을 화면에 출력하는 프로세스 등등
- 각기 다른 독립적인 프로세스이지만 공동의 목표를 위해 서로 협력하는 존재이다.
이렇게 협력적으로 실행되는 프로세스들을 아무렇게나 마구 실행해도 괜찮을까?
괜찮지 않다. 올바른 실행을 위해, 자원의 일관성을 보장해야 하기 때문에 프로세스들은 동기화되어야 한다.
프로세스 동기화란 그럼 뭘까?
- 프로세스 동기화란 프로세스들 사이의 수행 시기를 맞추는 것이다.
- 프로세스 동기화는 크게 두 가지를 의미한다.
- 실행 순서 제어: 프로세스를 올바른 순서대로 실행하기
- 상호 배제: 동시에 접근해서는 안 되는 자원에 하나의 프로세스만 접근하게 하기
참고: 프로세스뿐만 아니라 스레드도 동기화 대상이다. 실행의 흐름을 갖는 모든 것은 동기화의 대상이다.
실행 순서 제어를 위한 동기화 - reader writer problem
두 가지 프로세스가 존재한다고 가정하자.
- Writer: Book.txt 파일에 값을 저장하는 프로세스
- Reader: Book.txt 파일에 저장된 값을 읽어들이는 프로세스
Writer 프로세스와 Reader 프로세스가 동시에 실행 중이라고 가정해 보자.
두 프로세스는 무작정 아무 순서대로 실행되어서는 안 된다. 왜냐하면 실행의 순서가 있기 때문이다.
Reader 프로세스는 Writer 프로세스 실행이 끝나야 비로소 실행할 수 있기 때문이다. 다시 말해 Reader 프로세스는 'Book.txt 안에 값이 존재한다'는 특정 조건이 만족되어야만 실행이 가능하다는 것이다.
이렇게 동시에 실행되는 프로세스를 올바른 순서대로 실행하는 것이 실행 순서 제어를 위한 동기화이다.
상호 배제를 위한 동기화 - Bank account problem
- 상호 배제 (mutual exclusion)는 공유가 불가능한 자원의 동시 사용을 피하기 위한 동기화이다.
- 쉽게 말해 한 번에 하나의 프로세스만 접근해야 하는 자원에 동시 접근을 피하기 위한 동기화이다.
고전적이고 유명한 문제인 Bank account problem에 대해 알아보자.
현재 계좌에 잔액이 10만 원이 있다고 가정해 보자.
프로세스는 크게 두 가지가 있다.
- 프로세스 A: 현재 잔액에 2만 원을 추가하는 프로세스
- 계좌의 잔액을 읽어 들인다
- 읽어 들인 잔액에 2만원을 더한다.
- 더한 값을 저장한다.
- 프로세스 B: 현재 잔액에 5만 원을 추가하는 프로세스
- 계좌의 잔액을 읽어 들인다.
- 읽어 들인 잔액에 5만원을 더한다.
- 더한 값을 저장한다.
프로세스 A와 B는 동시에 실행 중이라고 가정해 보자.
프로세스 A와 B를 동기화를 고려하지 않고 무작정 동시에 실행하면 어떻게 될까? 당연히 17만 원이 계좌에 남을까?
그렇지 않다. 동기화가 제대로 이루어지지 않은 경우에는 전혀 엉뚱한 결과가 나올 수 있다.
- 프로세스 A와 B는 '잔액'이라는 데이터를 동시에 사용하는데 프로세스 A가 끝나기도 전에 프로세스 B가 잔액을 읽어 버렸기 때문에 엉뚱한 결과가 나온 것이다.
- 프로세스 A와 B를 올바르게 실행하기 위해서는 한 프로세스가 잔액에 접근했을 때 다른 프로세스는 기다려야 한다.
- 동시에 접근해서는 안 되는 자원에 동시에 접근하지 못하게 하는 것이 상호 배제를 위한 동기화이다.
생산자와 소비자 문제 - Producer & Consumer problem
상호 배제를 위한 동기화와 관련된 고전적이고 정말 유명한 문제인 생산자와 소비자 문제가 있다.
- 생산자(producer, 프로세스 혹은 스레드): 물건을 계속해서 생산한다.
- 생산자는 버퍼에 물건, 데이터를 넣는다.
- 그 후 '총합' 변수를 1 증가시킨다.
- 소비자(consumer, 프로세스 혹은 스레드): 물건을 계속해서 소비한다.
- 소비자는 버퍼에 물건, 데이터를 뺀다.
- 그 후 '총합' 변수를 1 감소시킨다.
- 생산자와 소비자는 '총합'이라는 데이터, 변수를 공유하고 있다.
참고: 생산자와 소비자는 동시에 실행되는 스레드가 될 수도 있다.
물건이 처음에 10개 있었다고 가정해 보자. 즉, 물건의 총합 변수를 10으로 초기화해 보자.
이 상태에서 생산자를 100,000번, 소비자를 100,000번 무작정 동시에 실행하면 '총합'은 몇이 될까? '총합'은 10이 되지 않을까?
그렇지 않다. 실행해 보면 '총합'이 0과 다른 값이 되거나, 오류를 발생하기도 한다.
왜 이러한 문제가 발생할까?
생산자 프로세스와 소비자 프로세스가 제대로 동기화되지 않았기 때문에 발생한 문제이다. 정확히 말하자면 동시에 접근해서는 안 되는 자원인 '총합'에 동시에 접근해서 발생한 문제이다.
공유 자원과 임계 구역
그렇다면 동시에 접근해서는 안 되는 자원이란 뭘까?
계좌 잔액 문제와 생산자 소비자 문제에서 동시에 실행되는 프로세스들은 전역 변수 '잔액', '총합'이라는 공동의 자원을 두고 작업했다. 이러한 자원을 공유 자원이라고 한다.
- 공유 자원(shared resource)은 여러 프로세스 혹은 스레드가 공유하는 자원이다.
- 공유 자원은 전역 변수, 파일, 입출력장치, 보조기억장치 등이 될 수 있다.
- 즉, 공동으로 이용하는 변수, 파일, 장치 등의 자원을 공유 자원이라 한다.
이 공유 자원 중에는 두 개 이상의 프로세스를 동시에 실행하면 문제가 발생하는 자원이 있다. 계좌 잔액 문제와 생산자 소비자 문제에서 '잔액', '총합' 변수가 이런 자원에 해당한다.
- 임계 구역(critical section)은 동시에 실행하면 문제가 발생하는 자원에 접근하는 코드 영역이다.
- 즉, 공유 자원에 접근하는 코드 중 동시에 실행하면 문제가 발생하는 코드 영역을 임계 구역이라 한다.
- 두 개 이상의 프로세스가 임계 구역에 진입하고자 하면 둘 중 하나는 대기해야 한다.
- 임계 구역에 먼저 진입한 프로세스의 작업이 마무리되면, 기다렸던 프로세스가 임계 구역에 진입한다.
임계 구역은 두 개 이상의 프로세스가 동시에 실행되면 안 되는 영역이다. 하지만 잘못된 실행으로 인해 여러 프로세스가 동시 다발적으로 임계 구역의 코드를 실행하여 문제가 발생하는 경우가 있다. 이를 레이스 컨디션이라고 한다.
- 레이스 컨디션(race condition)은 임계 구역에 동시에 접근하면 자원의 일관성이 깨질 수 있는 문제 상황을 의미한다.
- 계좌 잔액 문제와 생산자와 소비자 문제는 모두 레이스 컨디션의 사례로 볼 수 있다.
그렇다면 레이스 컨디션이 발생하는 근본적인 이유는 뭘까?
'고급 언어는 실행 과정에서 저급 언어로 변환되어 실행된다'라고 했다.
- 생산자와 소비자 문제를 다시 보면 '총합을 1 증가시킨다' 또는 '총합을 1 감소시킨다' 코드는 고급 언어 한 줄로 작성할 수 있다.
- 하지만 이는 컴퓨터 내부에서 여러 줄의 저급 언어로 변환되어 실행된다.
- 컴퓨터는 고급 언어가 아닌 저급 언어를 실행하기 때문에 여러 줄의 저급 언어로 변환된 고급 언어 한 줄을 실행하는 과정에서 문맥 교환이 일어날 수 있다.
상호 배제를 위한 동기화는 이와 같은 일이 발생하지 않도록 두 개 이상의 프로세스가 임계 구역에 동시에 접근하지 못하도록 관리하는 것을 의미한다.
운영체제는 임계 구역 문제를 세 가지 원칙 하에 해결한다. (상호 배제를 위한 동기화를 위한 세 가지 원칙)
- 상호 배제(mutual exclusion): 한 프로세스가 임계 구역에 진입했다면 다른 프로세스는 임계 구역에 들어올 수 없다.
- 진행(progress): 임계 구역에 어떤 프로세스도 진입하지 않았다면 임계 구역에 진입하고자 하는 프로세스는 들어갈 수 있어야 한다.
- 유한 대기(bounded waiting): 한 프로세스가 임계 구역에 진입하고 싶다면 그 프로세스는 언젠가는 임계 구역에 들어올 수 있어야 한다. 즉, 임계 구역에 들어오기 위해 무한정 대기해서는 안된다.
동기화 기법
프로세스의 동기화는 어떻게 이루어질까? 어떻게 해야 임계 구역에 오직 하나의 프로세스만 진입하게 하고, 올바른 실행 순서를 보장할 수 있을까? 대표적인 동기화 기법인 뮤텍스 락, 세마포, 모니터에 대해 알아보자.
뮤텍스 락 (Mutex lock: MUTual EXclusion lock)
- 뮤텍스 락은 동시에 접근해서는 안 되는 자원에 동시에 접근하지 않도록 만드는 도구, 즉 상호 배제를 위한 동기화 도구이다
탈의실에 있는 자물쇠와 같은 역할을 한다고 이해하자.
- 탈의실에는 한 명의 인원만 들어갈 수 있다.
- 손님들은 탈의실이라는 자원을 이용하고 탈의실 안에는 손님 한 명씩 들어올 수 있으니 손님은 '프로세스', 탈의실은 '임계 구역'이라고 볼 수 있다.
- 탈의실 밖에서 탈의실에 사람이 있는지 없는지 알 수 없는 상황이라면?
- 탈의실을 열어 보고 자물쇠가 걸려 있다면 탈의실 안에 사람이 있다고 판단하여 기다린다.
- 탈의실을 열어 보고 자물쇠가 걸려 있지 않다면 탈의실을 이용하면 된다.
이러한 자물쇠 기능을 코드로 구현한 것이 뮤텍스 락이다.
- 뮤텍스 락의 단순한 형태는 하나의 전역 변수와 두 개의 함수로 구현할 수 있다.
- 자물쇠 역할: 프로세스들의 공유하는 전역 변수 lock
- 임계 구역을 잠그는 역할: acquire 함수
- 임계 구역의 잠금을 해제하는 역할: release 함수
- acquire 함수
- acquire 함수는 프로세스가 임계 구역에 진입하기 전에 호출하는 함수이다.
- 만약 임계 구역이 잠겨 있다면 임계 구역이 열릴 때까지(lock이 false가 될 때까지 임계 구역을 반복적으로 확인한다.
- 만약 임계 구역이 열려 있다면 임계 구역을 잠근다. (lock을 true로 바꾼다)
- release 함수
- release 함수는 임계 구역에서의 작업이 끝나고 호출하는 함수이다.
- 현재 잠김 임계 구역을 열어준다. (lock을 false로 바꾼다)
- 임계 구역 전후로 호출함으로써 하나의 프로세스만 임계 구역에 진입할 수 있다.
- 이렇게 되면 프로세스는 임계 구역을 보호할 수 있다.
- lock을 획득할 수 없다면, 임계 구역에 진입할 수 없다면 무작정 기다린다.
- lock을 획득할 수 있다면, 임계 구역에 진입할 수 있다면 임계 구역을 잠근 뒤 임계 구역에서의 작업을 진행한다.
- 임계 구역에서 빠져나올 때엔 다시 임계 구역의 잠금을 해제한다.
- acquire 함수를 보면 임계 구역이 잠겨 있을 경우 프로세스는 무한히 반복적으로 lock을 확인한다. 이러한 대기 방식을 바쁜 대기(busy wait)라고 한다.
- 마치 탈의실 문이 잠겨 있는지 계속 반복하며 확인해 보는 것과 같다.
참고: C/C++, Python 등 일부 프로그래밍 언어에서는 뮤텍스 락 기능을 제공한다. 훨씬 정교하게 설계되어 있다.
세마포 (semaphore)
- 세마포는 뮤텍스락과 비슷하지만, 조금 더 일반화된 방식의 동기화 도구이다.
- 공유 자원이 여러 개 있는 경우에도 적용이 가능한 동기화 도구이다.
- 뮤텍스 락은 탈의실이 하나 있는 경우이고, 세마포는 탈의실이 여러 개 있는 경우이다.
참고: 세마포의 종류에는 이진 세마포(binary semaphore)와 카운팅 세마포(counting semaphore)가 있다. 이진 세마포는 뮤텍스 락과 비슷한 개념이고, 카운팅 세마포는 여러 공유 자원을 다룰 수 있다.
세마포는 철도 신호기에서 유래한 단어이다.
- 기차는 신호기가 내려가 있을 때는 '멈춤' 신호로 간주하고 잠시 멈춘다.
- 프로세스는 임계 구역 앞에서 멈춤 신호를 받으면 잠시 기다린다.
- 반대로 기차는 신호기가 올라와 있을 때는 '가도 좋다'는 신호로 간주하고 다시 움직인다.
- 프로세스는 임계 구역 앞에서 가도 좋다는 신호를 받으면 임계 구역에 진입한다.
세마포는 뮤텍스 락과 비슷하게 하나의 전역 변수와 두 개의 함수로 단순하게 구현할 수 있다.
- 전역 변수 S: 임계 구역에 진입할 수 있는 프로세스의 개수(사용 가능한 공유 자원의 개수)를 나타낸다.
- wait 함수: 임계구역에 들어가도 좋은지, 기다려야 할지를 알려준다.
- signal 함수: 임계구역 앞에서 기다리는 프로세스에 '이제 가도 좋다'라고 신호를 준다.
- 뮤텍스 락을 사용할 때 임계 구역 진입 전후로 acquire()와 release()를 호출했듯이 세마포도 임계 구역 진입 전후로 wait()와 signal()을 호출한다.
wait 함수는 다음과 같다.
- 만약 임계 구역에 진입할 수 있는 프로세스 개수가 0 이하라면
- 사용할 수 있는 자원이 있는지 무한히 반복적으로 확인하고,
- 임계 구역에 진입할 수 있는 프로세스 개수가 하나 이상이라면 S를 1 감소시키고 임계 구역에 진입한다. (내가 들어갈 거니까)
signal 함수는 다음과 같다.
- 임계 구역에서의 작업을 마친 뒤 S를 1 증가시킨다. (내가 빠져나갔으니까)
다음 예시를 보자.
세 개의 프로세스 P1, P2, P3가 두 개의 공유 자원에 P1, P2, P3 순서로 접근한다고 가정하자.
- 공유 자원은 두 개이기 때문에 변수 S는 2가 된다.
- 다음과 같은 순서로 실행된다.
- 프로세스 P1은 wait 함수를 호출하고, S는 현재 2이므로 S를 1 감소시키고 임계 구역에 진입한다.
- 프로세스 P2는 wait 함수를 호출하고, S는 현재 1이므로 S를 1 감소시키고 임계 구역에 진입한다.
- 프로세스 P3는 wait 함수를 호출하고, S는 현재 0이므로 무한히 반복하며 S를 확인한다.
- 프로세스 P1은 임계 구역 작업을 종료하고 signal 함수를 호출한다. 즉, S를 1 증가 시킨다.
- 프로세스 P3는 S가 1이 됨을 확인한다. S는 현재 1이므로 S를 1 감소시키고 임계 구역에 진입한다.
하지만 여기서는 한 가지 문제가 있다. 뮤텍스 락에도 해당되는 문제인데, 사용할 수 있는 공유 자원이 없는 경우 프로세스는 무한정 무한히 반복하며 S를 확인해야 한다.
- 마치 탈의실 문이 잠겨 있는지 아닌지 계속 반복해서 확인하는 것과 같다. Busy waiting
- 바쁜 대기를 반복하며 확인할 시간에 CPU는 더 생산성 있는 작업을 할 수 있다. 즉 CPU 사이클 낭비이다.
이를 해결하기 위해 어떻게 해야 할까? 세마포는 다른 더 좋은 방법을 사용한다.
- wait 함수는 사용할 수 있는 자원이 없을 경우 프로세스 상태를 대기 상태로 만든다.
- 해당 프로세스의 PCB를 세마포를 위한 대기 큐에 삽입한다.
- 사용할 수 있는 자원이 생겼을 경우 대기 큐의 프로세스를 준비 상태로 만든다.
- 즉, 다른 프로세스가 임계 구역에서의 작업이 끝나고 signal 함수를 호출하면 signal 함수는 대기 중인 프로세스를 대기 큐에서 제거하고, 프로세스 상태를 준비 상태로 변경한 뒤 준비 큐로 옮겨준다.
- 해당 프로세스의 PCB를 대기 큐에서 꺼내 준비 큐에 삽입한다.
코드로 나타내면 다음과 같다.
- 해당 프로세스 PCB를 대기 큐에 삽입한다.
- 대기 상태로 접어든다.
- 대기 큐에 있는 프로세스 p를 제거한다.
- 프로세스 p를 대기 상태에서 준비 상태로 만든다.
다음 예시를 보자.
공유 자원은 두 개, 접근하려는 프로세스는 P1, P2, P3 세 개이고 P1, P2, P3 순서로 임계 구역에 접근한다고 가정하자.
- 공유 자원이 두 개이므로 S는 2이다.
- 다음과 같은 순서로 실행된다.
- 프로세스 P1은 wait 함수를 호출한다. S를 1 감소시키면 S는 1이므로 임계 구역에 진입한다.
- 프로세스 P2는 wait 함수를 호출한다. S를 1 감소시키면 S는 0이므로 임계 구역에 진입한다.
- 프로세스 P3는 wait 함수를 호출한다. S를 1 감소시키면 S는 -1이므로 본인의 PCB를 대기 큐에 넣고 대기 상태로 전환한다.
- 프로세스 P1의 임계 구역 작업이 종료되면 signal 함수를 호출한다. S를 1 증가하면 0이므로 대기 상태였던 프로세스 P3를 대기 큐에서 꺼내 준비 큐로 옮겨준다.
- 깨어난 프로세스 P3는 임계 구역에 진입한다.
- 프로세스 P2의 임계 구역 작업이 종료되면 signal 함수를 호출하고 S를 1 증가하면 1이 된다.
- 프로세스 P3의 임계 구역 작업이 종료되면 signal 함수를 호출하고 S를 1 증가하면 2가 된다.
지금까지 세마포를 이용한 상호 배제를 위한 동기화 기법이었다. 세마포를 이용해 실행 순서 제어를 위한 동기화에 대해서 알아보자.
세마포를 이용하면 동시에 실행되는 프로세스의 실행 순서도 제어할 수 있다.
- 세마포의 변수 S를 0으로 둔다.
- 먼저 실행할 프로세스 뒤에 signal 함수, 다음에 실행할 프로세스 앞에 wait 함수를 붙이면 된다.
- 만약 P1이 먼저 실행되면 P1이 임계 구역에 먼저 진입한다.
- 만약 P2가 먼저 실행되면 P2는 wait 함수를 만나므로 P1이 임계 구역에 진입한다. 그리고 임계 구역의 실행을 끝내고 signal을 호출하면 P2가 임계 구역에 진입한다.
- 즉, 반드시 P1, P2 순서대로 실행된다.
참고: 세마포도 뮤텍스 락과 마찬가지로 많은 프로그래밍 언어에서 제공한다.
모니터 (monitor)
세마포는 사용하기가 조금 불편한 면이 있다. 왜냐하면 매번 임계 구역 앞뒤로 일일이 wait와 signal 함수를 명시하는 것은 번거로운 일이기 때문이다. 잘못된 코드로 인해 예기치 못한 결과를 얻을 수도 있다.
이에 최근에 등장한 동기화 두고가 바로 모니터이다. 모니터는 세마포에 비하면 사용자가 사용하기 훨씬 편리한 도구이다.
모니터를 활용하는 가장 대표적인 언어로는 Java가 있다.
모니터를 이용한 상호 배제를 위한 동기화
- 모니터는 공유 자원과 공유 자원에 접근하기 위한 인터페이스(통로)를 묶어 관리한다. 그리고 프로세스와 스레드는 반드시 인터페이스를 통해서만 공유 자원에 접근하도록 한다.
- 모니터를 통해 공유 자원에 접근하고자 하는 프로세스를 큐에 삽입하고, 큐에 삽입된 순서대로 하나씩 공유 자원을 이용하도록 한다.
- 즉, 모니터는 공유 자원을 다루는 인터페이스에 접근하기 위한 큐(모니터에 진입하기 위한 큐)를 만들고, 모니터 안에 항상 하나의 프로세스만 들어오도록 하여 상호 배제를 위한 동기화를 제공한다.
모니터를 이용한 실행 순서 제어를 위한 동기화
- 특정 조건을 바탕으로 프로세스를 실행하고 일시 중단하기 위해 모니터는 조건 변수(condition variable)를 사용한다.
- 조건 변수는 프로세스나 스레드의 실행 순서를 제어하기 위해 사용하는 특별한 변수이다.
- 조건 변수로는 wait와 signal 연산을 수행할 수 있다.
참고: 모니터가 조건 변수를 사용하긴 하지만 조건 변수와 모니터는 별개의 개념이다.
- 조건변수.wait(): 프로세스나 스레드의 상태를 대기 상태로 변경하고, 조건 변수에 대한 대기 큐에 삽입하는 연산이다.
- 조건변수.signal(): wait를 호출하여 큐에 삽입된 프로세스의 실행을 재개하는 연산이다. 즉, wait로 대기 상태로 접어든 조건 변수를 실행 상태로 변경한다.
모니터 안에는 하나의 프로세스만이 있을 수 있다.
- wait()를 호출했던 프로세스는 signal()을 호출한 프로세스가 모니터를 떠난 뒤에 수행을 재개한다.
- signal()을 호출한 프로세스의 실행을 일시중단하고 자신이 실행된 뒤 다시 signal()을 호출한 프로세스의 수행을 재개한다.
중요한 점은 모니터는 조건 변수를 이용하여 아래와 같은 프로세스 실행 순서 제어를 위한 동기화를 제공한다는 것이다.
- 특정 프로세스가 아직 실행될 조건이 되지 않았을 때에는 wait를 통해 실행을 중단한다.
- 특정 프로세스가 실행될 조건이 충족되었을 때에는 signal을 통해 실행을 재개한다.
'Computer Science > 운영체제' 카테고리의 다른 글
[운영체제] 가상 메모리 (0) | 2023.05.28 |
---|---|
[운영체제] 교착 상태 (0) | 2023.05.25 |
[운영체제] CPU 스케줄링 (2) | 2023.05.03 |
[운영체제] 파이썬으로 프로세스와 스레드 다루기 (0) | 2023.05.02 |
[운영체제] 프로세스와 스레드 (0) | 2023.05.02 |