운영체제의 프로세스 관리 서비스 중에서는 스케줄링과 동기화가 가장 중요해요!
협력하여 실행되는 프로세스들은 실행 순서와 자원의 일관성을 보장해야 하기에 반드시 동기화되어야 합니다.
동시다발적으로 실행되는 프로세스들은 서로 데이터를 주고 받으면서 실행될 수 있다.
협력적으로 실행되는 프로세스들의 올바른 실행을 위해서는 동기화가 필수입니다.
프로세스 동기화 : 프로세스들 사이의 수행 시기를 맞추는 것
🧐 수행 시기를 맞추는 것?
- 실행 순서 제어
: 프로세스를올바른 순서로 실행한다.
- 상호 배제
: 동시에 접근하면 안되는 자원에하나의 프로세스만 접근하게한다.
Writer라는 프로세스와 Reader 프로세스가 동시에 실행 중이에요.
Writer 프로세스는 Book.txt 파일에 값을 저장하는 프로세스이고
Reader 프로세스는 Book.txt 파일에 저장된 값을 읽어들이는 프로세스에요
이 두 프로세스는 아무 순서로 실행되면 안됩니다 ❌
Reader 프로세스는 Writer 프로세스 실행이 끝나야 실행할 수 있어요. Reader 프로세스는 “Book.txt 안에 값이 존재한다”는 특정 조건이 만족되어야 실행을 할 수 있어요.
➡️ 동시에 실행되는 프로세스를 올바른 순서대로 실행하는 것
계좌에 10만원이 저축되어 있어요.
프로세스 A는 현재 저축된 금액에 2만원을 넣는 프로세스이고
프로세스 B는 현재 저축된 금액에 5만원을 넣는 프로세스에요
이제, 프로세스 A와 B가 동시에 실행되었어요. 당연히 실행 결과 17만원이 계좌에 남아야겠죠?
하지만! 동기화가 제대로 이루어지지 않으면 엉뚱한 결과가 나올 수 있어요 🥲
동기화가 이루어지지 않은 경우
프로세스 A와 B는 “잔액”이라는 데이터를 동시에 사용합니다.
A가 끝나기도 전에 B가 잔액을 읽어버렸기 때문에 엉뚱한 결과가 나왔습니다.
A와 B가 올바르게 실행되려면 한 프로세스가 잔액에 접근하면 다른 프로세스는 기다려야 해요.
동기화가 이루어진 경우
➡️ 상호 배제를 위한 동기화 : 동시에 접근해서는 안 되는 자원에 동시에 접근하지 못하게 하는 것
💡 상호 배제를 위한 동기화에 대해 더 알아보자!
생산자 : 물건을 계속해서 생산하는 프로세스
생산자() {
버퍼에 데이터 삽입
'총합' 변수 1 증가
}
소비자 : 물건을 계속해서 소비하는 프로세스
소비자() {
버퍼에서 데이터 빼내기
'총합' 변수 1 감소
}
생산자와 소비자는 “총합” 이라는 데이터를 공유합니다.
물건이 처음에 10개가 있었어요. 물건의 총합 변수는 10으로 초기화 되어 있습니다.
총합 = 10
생산자() {
버퍼에 데이터 삽입
'총합' 변수 1 증가
}
소비자() {
버퍼에서 데이터 빼내기
'총합' 변수 1 감소
}
이제, 생산자를 100,000번 소비자를 100,000번 동시에 실행합니다!
아마도 총합 변수가 계속 10으로 머물러 있겠죠?
하지만! 실제로 실행해 보면 총합이 10이 아닌 수가 되거나 실행 중 오류가 나기도 합니다.
➡️ 동시에 접근해서는 안 되는 자원에 동시에 접근했기에 발생하는 문제입니다.
💡 그렇다면 동시에 접근해서는 안되는 자원이 뭐지?
이전의 계좌 잔액 문제, 생산자 소비자 문제의 예시에서의 전역 변수 ‘잔액’, ‘총합’이라는 공동의 자원을 두고 작업을 했습니다.
이러한 자원을 공유 자원이라고 해요
동시에 실행하면 문제가 발생하는 자원에 접근하는 코드 영역을 임계 구역이라고 해요.
임계 구역에 먼저 진입한 프로세스가 있다면 작업이 마무리 될 때 까지 기다려야 합니다.
잘못된 실행으로 인해 여러 프로세스가 동시 다발적으로 임계 구역의 코드를 실행하여 문제가 발생하는 경우를 레이스 컨디션(race condition)이라고 합니다.
레이스 컨디션이 발생하면 데이터의 일관성이 깨지는 문제가 발생합니다.
상호 배제
: 한 프로세스가 임계 구역에 진입했다면 다른 프로세스는 임계 구역이 들어올 수 없다.
진행
: 임계 구역에 어떤 프로세스도 진입하지 않았다면 임계 구역에 진입하고자 하는 프로세스는 들어갈 수 있어야 한다.
유한 대기
: 한 프로세스가 임계 구역에 진입하고 싶다면 그 프로세스는 언젠가는 임계 구역이 들어올 수 있어야 한다.(무한정 대기해서는 안된다.)
😎 프로세스를 동기화하지 않으면 코드가 예기치 못하게 작동할 수 있다.
이번에는 동기화를 위한 대표적인 도구를 알아봅시다!
옷 가게에서 마음에 드는 옷이 있으면 손님은 탈의실에 들어가서 옷을 입어볼 수 있어요.
탈의실에는 한 명의 인원만 들어갈 수 있습니다. 손님은 탈의실이라는 자원을 이용합니다.
➡️ 손님은 “프로세스” 탈의실은 “임계 구역”이라고 할 수 있어요
그런데! 밖에서 탈의실이 사용중임을 어떻게 알 수 있죠?? 탈의실에 자물쇠가 걸려 있으면 사람이 있다고 판단합니다.
➡️ 이를 코드로 구현한 것이 뮤텍스 락이에요!
뮤텍스 락
: 동시에 접근해서는 안되는 자원에 동시에 접근하지 않도록 만드는 도구
상호 배제를 위한 동기화 도구입니다.
🤩 뮤텍스 락 구현 방법
- 자물쇠 역할
: 프로세스들이 공유하는 전역 변수 lock
- 임계 구역을 잠그는 역할
: acquire 함수
- 임계 구역의 잠금을 해제하는 역할
: release 함수
acquire 함수
: 프로세스가 임계 구역에 진입하기 전에 호출하는 함수
임계 구역이 이미 잠겨 있으면 열릴 때까지(lock이 false가 될 때까지) 반복적으로 확인합니다.
이런 대기 방식을 바쁜 대기(busy wait)라고 합니다.
release 함수
: 임계 구역에서 작업이 끝나고 호출하는 함수
현재 잠긴 임계 구역을 열어주는(lock을 false로 바꾸는) 함수입니다.
acquire와 release 함수를 임계 구역 전후로 호출해 하나의 프로세스만 임계 구역에 진입 할 수 있다.
acquire(); // 자물쇠 잠겨 있는지 확인, 잠겨 있지 않다면 잠그고 들어가기
// 임계 구역 // 임계 구역에서의 작업 진행
release(); // 자물쇠 반환
이를 통해 프로세스는….
임계 구역을 보호할 수 있어요.
😎 뮤텍스 락과 비슷하지만, 조금 더 일반화된 동기화 도구에요
뮤텍스 락은 하나의 공유 자원에 접근하는 프로세스를 상정한 방식이에요.
이번에는, 옷가게에 탈의실이 세 개 있다고 생각해 봅시다.
여전히 하나의 탈의실에는 한 사람만 들어갈 수 있지만, 세 명이 동시에 탈의실을 이용할 수 있어요.
➡️ 세마포는 공유 자원이 여러개 있는 상황에서도 적용 가능한 동기화 도구에요.
세마포는 철도 신호기에서 유래한 단어입니다.
기차는 신호기가 내려가 있으면 “멈춤”으로 간주하고 잠시 멈춥니다.
반대로 신호기가 올라가 있으면 “가도 좋다”로 간주하고 다시 움직이기 시작합니다.
➡️ 프로세스는 임계 구역 앞에서 멈춤 신호를 받으면 기다리고, 가도 좋다는 신호를 받으면 임계 구역에 들어가게 됩니다.
🤩세마포 구현 방법
- 전역 변수 S
: 임계 구역에 진입할 수 있는 프로세스의 개수(사용 가능한 공유 자원의 개수)
- wait 함수
: 임계 구역에 들어가도 좋은지, 기다려야 할지를 알려준다.
- signal 함수
: 임계 구역 앞에서 기다리는 프로세스에 “이제 가도 좋다”고 신호를 준다.
wait() {
while( S <= 0 ) // 임계 구역에 진입할 수 있는 프로세스 개수가 0 이하라면
; // 사용할 수 있는 자원이 있는지 반복적으로 확인하고,
S--; // 임계 구역에 진입할 수 있는 프로세스 개수가 하나 이상이면
// S를 1 감소 시키고 임계 구역으로 진입한다.
}
signal() {
S++ // 임계 구역에서 작업을 마친 뒤 S를 1 증가시킨다.
}
wait과 signal을 임계 구역 진입 전후에 호출합니다.
wait()
// 임계 구역
signal()
두개의 공유자원에 P1, P2, P3 순서로 접근한다고 생각해봅시다!
공유자원이 두개이니까 변수 S는 2가 되겠죠?
1. P1 wait() 호출. S는 현재 2이므로 S를 1 감소시키고 임계 구역 진입
2. P2 wait() 호출. S는 현재 1이므로 S를 1 감소시키고 임계 구역 진입
3. P3 wait() 호출. S는 현재 0이므로 무한히 반복하며 S 확인
4. P1 임계 구역 작업 종료. signal() 호출. S를 1 증가
5. P3이 S가 1이 됨을 확인. S를 1 감소시키고 임계 구역 진입
🧐 여기서 한 가지 문제 발생 !
사용할 수 있는 공유 자원이 없는 경우, 프로세스는 무작정 무한히 반복하면서 S를 확인해요
이렇게 되면CPU 주기를 낭비하게 됩니다.
그래서! 실제로 세마포는 다른 더 좋은 방법을 사용한다.
wait 함수는 사용할 수 있는 자원이 없으면 해당 프로세스를 대기 상태로 만들고, 세마포를 위한 대기 큐에 집어넣습니다.
다른 프로세스가 임계 구역에서 작업이 끝나고 signal 함수를 호출하면 signal 함수는 대기 중인 프로세스를 대기 큐에서 제거하고 프로세스를 준비 상태로 변경하여 준비 큐로 옮겨줍니다.
wait() {
S--;
if ( S < 0 ) {
add this process to Queue; // 해당 프로세스 PCB를 대기 큐에 삽입한다.
sleep(); // 대기 상태로 접어든다.
}
}
signal() {
S++;
if ( S <= 0 ) {
remove a process p from Queue; // 대기 큐에 있는 프로세스 p를 제거한다.
wakeup(p) // 프로세스 p를 대기 상태에서 준비 상태로 만든다.
}
}
두개의 공유자원에 P1, P2, P3 순서로 접근한다고 생각해봅시다!
공유자원이 두개이니까 변수 S는 2가 되겠죠?
1. P1 wait() 호출. S를 1 감소시키면 S는 1이므로 임계 구역 진입
2. P2 wait() 호출. S를 1 감소시키면 S는 0이므로 임계 구역 진입
3. P3 wait() 호출. S를 1 감소시키면 S는 -1이므로 대기 큐에 넣고 대기 상태로 전환
4. P1 임계 구역 작업 종료. signal() 호출. S를 1 증가시키면 0이므로 P3를 대기 큐에서 꺼내 준비 큐로 옮겨줌
5. 깨어난 프로세스 P3 임계 구역 진입
6. P2 임계 구역 작업 종료. signal() 호출. S가 1 증가하면 1
7. P3 임계 구역 작업 종료. signal() 호출. S가 1 증가하면 2
❗ 지금까지는 상호 배제를 위한 동기화 기법이었습니다.
이번에는 세마포를 이용해 프로세스의 순서를 제어하는 방법에 대해 알아봅시다.
세마포의 변수 S를 0으로 두고, 먼저 실행할 프로세스 뒤에 signal 함수, 다음에 실행할 프로세스 앞에 wait 함수를 붙이면 됩니다.
세마포에서 wait과 signal 함수를 잘못 사용해 예기치 못한 결과를 얻을 수 있다.
➡️ 새로운 동기화 도구인 모니터 등장!
모니터는 공유 자원과 공유 자원에 접근하기 위한 인터페이스(통로)를 묶어서 관리한다.
프로세스는 반드시 인터페이스를 통해서만 공유 자원에 접근하도록 한다.
공유 자원에 접근하고자 하는 프로세스를 큐에 삽입하고, 큐에 삽입된 순서대로 하나씩 공유 자원을 이용하도록 합니다.
모니터에 진입하기 위한 큐를 만들고, 모니터 안에는 항상 하나의 프로세스만 들어오도록 합니다.
모니터도 세마포와 마찬가지로 실행 순서 제어를 위한 동기화도 제공합니다. (조건 변수)
조건 변수에 대한 큐
: wait가 호출되어서 실행이 중단된 프로세스들이 삽입되는 큐
모니터에 이미 진입한 프로세스의 실행 조건이 만족될 때까지 기다리기 위해 만들어진 큐
상호 배제를 위한 큐
: 모니터에 진입하기 위해 삽입되는 큐
만약, 어떤 프로세스가 x.wait()을 호출하면, 조건 변수 x에 대한 큐에 삽입이 되고, 모니터가 비게 되어 새로운 프로세스가 모니터 안으로 들어올 수 있게 됩니다.
다시 signal 연산을 통해 실행이 재게될 수 있습니다. 그러면 다시 모니터 안으로 들어오게 됩니다.
근데! 모니터 안에는 하나의 프로세스만 있을 수 있다고 했죠?