Part 02 프로세스 관리
Chapter 05 프로세스 동기화
1. 프로세스 간 통신
① 프로세스 간 통신의 개념
먼저 개념부터 이해해보도록 하자. 하나의 프로그램에는 프로세스가 여러 개 존재한다. 이 프로세스 간의 협업이 이루어지려면 데이터를 주고받아야 한다. 이것을 바로, 프로세스 간 통신이라고 한다. 어떻게 데이터를 주고받을 것인지에 대한 이야기를 하는 것을 말한다. 서로 다른 컴퓨터 간에 통신도 이루어지며 이것을 ‘네트워크’라고 한다.
통신 방식은 다음과 같은 3가지가 있다.
① 공유 메모리를 통해 파일을 공유하기도 한다.
② 파이프
같은 컴퓨터 안 서로 다른 프로세스 간의 통신을 의미한다.
③ 소켓
네트워크로 연결된 서로 다른 컴퓨터 간의 통신을 말한다.
② 통신 방향에 따른 분류
통신의 방향면에서 분류하면 크게 3가지로 나뉜다.
① 양방향(Duplex) 통신: 양방향으로 데이터를 주고받을 수 있고, 소켓을 이용해서 통신한다. 예시로는 전화기가 있다.
② 단방향(Simplex) 통신: 반대쪽으로는 데이터를 보낼 수 없다. 대표적인 예가 모스부호 통신이다.
③ 반양방향(Half Duplex) 통신: 무전기
③ 통신 구현 방식에 따른 분류
통신을 구현 방식에 따라서 분류할 수 있다.
① 동기화 통신: 상대방의 데이터를 받아주기 위해 대기하는 통신
② 비동기화 통신: 서로 주고받기로 약속한 적이 없지만 무작정 데이터가 날라오는 경우를 말한다.
④ 파일 입출력
찜질방과 찜질방을 이용할 수 있는 권리에 해당하는 Locker Key가 있다. 샤워를 하고 잠을 잤다. 작업이 다 끝나면 Locker Key를 반환함으로써 나는 거기서 빠져나오게 된다. 파일의 입출력도 똑같은 원리로 동작한다. p.251 코드를 보도록 하자. fd = open()은 해당 파일을 열겠다는 뜻이다. 열면, 거기에 대한 권리를 그 앞에 있는 fd에게 준다. 이것이 바로 찜질방 들어갔을 때의 Locker Key가 된다. open이라는 것은 정당한 파일의 주인인지, 해당 파일이 있는지, 해당 파일에 접근할 정당한 권리가 있는지 확인하는 과정이다. 이 과정을 통과하면 write(fd);에 저장된다. 즉, 파일에 대한 권리가 생긴 것을 의미한다. 이후, read(fd);를 통해 사용하고, 마지막으로는 close(fd); 나오게 된다. 다시 말해, 파일 입출력은 open, read, write, close. 끝이다. 오늘 첫 번째 말하고자 하는 사항은 모든 통신이 이러한 구조를 가진다는 것이다.
⑤ 프로세스 간 통신 정리 (p.252)
나는(교수님은) 파일 입출력도 통신이라고 생각한다. 프로세스와 입출력 관리자와의 통신에 해당하는 것이다. 파일에서 데이터 읽어주세요, read 함수를 사용해서 입출력을 일으키는 통신이라고 생각한다. p.253 두 개의 프로세스 간에는 pipe가 일어나는데, pipe는 단방향 시스템이다. pipe1, pipe2 두 개를 열어서 양쪽으로 통신을 해야 한다. 그리고 이 pipe의 특징은 read가 먼저 일어날 경우, write가 올 때까지 자동으로 먼저 기다려준다. 그래서 이를 ‘동기화 통신’이라 부른다. socket은 양방향 통신에 해당하며 서로 다른 컴퓨터에 있는 프로세스 간의 통신을 수행한다. 나머지 내용은 네트워크에 관한 내용들로, 나중에 천천히 읽어보면 된다.
2. 공유 자원과 임계 구역
가장 복잡한 내용에 해당한다.
현재까지의 다음과 같은 내용을 배웠다.
process -> fork -> 0 0
0 0
thread 통신
① 임계구역
10만원이 예금된 상황에서 process p1은 10만원 추가, process p2는 5만원을 추가한다(p.255). 정상적인 경우, 예금이 총 25만원이 되는 것이 정상이다. 그런데, 두 개가 동시에 실행이 돼서 process p1이 현재 예금 10만원을 읽었다. 이후, process p2가 동시에 읽어버리고 예금 15만원이라고 먼저 저장시킨 뒤에, p1이 예금 10만원을 넣어서 20만원이라고 저장을 시켜버린다. 그러면 실제 저장되는 20만원이 된다. 즉, 동시에 작업이 일어난다면 이런 문제가 발생할 수 있다. 4구 가스레인지에서는 4개의 작업을 동시에 수행할 수 있다(p.258). 즉, 공유가 가능한 자원이다. 그런데, 어떤 자원들은 공유가 불가능하다. 대표적으로 프린트가 그렇다. 여자 전신 사진과 물고기 사진을 반반 공유해서 프린트하면 인어공주가 출력된다. 따라서 프린터는 공유할 수 없는 자원이다. 이와 같이 공유할 수 있는 것과 공유할 수 없는 것을 어떻게 처리할 것인가를 고려해야 하는 상황에 이르게 되고, 공유할 수 없는 것을 ‘임계구역(Critical Section)’이라고 명명하기로 약속했다.
- 임계구역 Critical Section : 공유 자원 접근 순서에 따라 실행 결과가 달라지는 프로그램의 영역
② 임계구역을 해결하는 조건 (p.260)
임계구역을 해결하는 조건 3가지를 무조건 만족해야 한다는 것이 매우 중요하다. 밑줄 및 별표 하도록 하자.
① 상호 배제 Mutual Exclusion : Critical Section에 한 번에 하나만 들어가야 한다. 둘이 동시에 들어갈 수 없다.
화장실에 둘이 손잡고 들어가지 말라는 것과 같다.
② 한정 대기 Bounded Waiting : 무한정 대기가 아닌, 최소한 특정 시간 내에는 들어갈 수 있어야 한다는 것을 의미한다.
③ 진행의 융통성 Progress Flexibility : 내 작업이 다른 사람의 작업을 방해해서는 안 된다.
3. 임계구역 문제 해결 방법
임계구역 문제해결방법에 대해 p.261 기본코드를 통해 확인해보자. 첫 번째 기본 코드는 lock = false로 잠겨져 있다는 의미다.
① 상호 배제 문제
<Process P1 Code>
while (lock == true);
lock이 false 될 때까지 계속해서 빙글빙글 돈다.
lock = true;
cs 일을 본 뒤,
lock = false; 처리를 해준다.
------- 공유변수: boolean lock = false; -------
<Process P2 Code>
while (pck == true);
lock = true;
c.s
lock = false;
겉으로는 문제가 없어 보이지만 timeout, lock이 false인 상태에서 2번으로 넘어간다. 한 화장실에 2명이 들어가는 문제가 생긴다. mutual exclusion 상호 배제를 위배하게 된다. 그래서 p.264 코드를 시도한다.
② 한정 대기 문제
<Process P1 Code>
lock1= false
lock2 = false
두 개의 잠금 장치를 사용하면 문제가 없지 않을까?
lock1 = true;
즉, 경쟁자가 생기지 않도록 먼저 lock을 거는 것이다.
그 다음에, while(lock2 == true); 점검을 하여 혹시 누가 먼저 들어왔는지 확인한다.
lock1 = false;
------- 공유변수: boolean lock1 = false; -------
------- 공유변수: boolean lock2 = false; -------
<Process P2 Code>
lock2 = true;
while(lock1 == true);
lock2 = false;
그런데, P1의 lock1=true; 다음 while갈 때 timeout이 걸려 버리고 P2에서는 while에서 무한루프에 빠진다. 이후, P1도 무한루프에 빠져 결과적으로 bounded waiting의 문제가 발생한다. 즉, 한정 대기 조건을 충족하지 않아 무한 대기 상황이 펼쳐지는 것이다. 그래서 p.266 코드를 시도한다.
③ 진행의 융통성 문제
<Process P1 Code>
lock = 1
while(lock == 2)
cs
lock = 2; // lock을 빠져나올 때 2를 만들어준다.
------- 공유변수: int lock = 1; -------
<Process P2 Code>
while(lock == 1)
cs
lock = 1;
2가 들어가기 위한 첫 번째 조건은 1이 들어가서 lock을 2로 바꿔줘야 한다는 것이다. 1번 다음 2번이라고 순서를 지정하였으므로 progress flexibility를 위배한 것이다. 그래서 피터슨이라는 알고리즘을 만든다. 그런데 임계구역이 1개고 사용자가 2명일 때만 해결된다. 프로세스와 임계구역이 상관없이 만든 것이 데커 알고리즘이다. 그래서 데커 알고리즘을 곰곰이 생각해보면 화장실이 아니라 금고에 들어가는 것처럼 복잡한 알고리즘이다.
④ Semaphore
init(n) // 초기화 à RS = n;
n = critical section의 개수
p() // 들어갈 때 à if RS > 0 then RS
else block();
v() // 나올 때 à ++RS
wake_up(+);
물론 이걸 하기 위해선, p.267. 아까 1번 코드, mutual exclusion이 위배되는 코드에 두 코드가 찢어진 상태에서 타임아웃이 걸리면 에러가 났었다. 이것을 찢어지지 못하게 합치는 방법도 있었다. 그것이 test and set이라 하여 두 코드를 하나로 합치는 방법이다. test and set으로 Error날 수 있는 곳을 다 막아버리면 이 Semaphore Code는 무적이다.
⑤ Semaphore Code는 정말 무적일까? (p.271)
앞선 예금 10만원 예제로 돌아가보자. Semaphore는 1이다. 같이 쓰는 저장공간이 1개라는 뜻이다. RS == 0으로 만들어 놓고 critical section으로 들어가 버린다. RS가 0이 돼 버렸기 때문에 blocking 상태로 빠진다. RS를 1로 만들어주고 wake up을 시켜줄 때까지 기다렸다가 예금 10만원을 읽어서 25만원을 만들어주면 끝난다. 사용하는 프로세스 3개, 즉 화장실 사용하려는 사람은 3명인데 칸은 2칸인 경우 어떻게 작동하는지 p.273을 살펴보자. Semaphore 이니셜 값은 2가 된다. 첫 번째 p1이 들어오면 RS를 1로 만들어 놓고 critical section으로 들어간다. 다음 p2는 RS를 0으로 만들어 놓는다. p3이 들어오면 block이 되어 대기상태로 빠져 있는다. RS가 1로 바뀌고 wake up이 일어나면서 p3가 깨어난다. 그 사이 p2가 끝나서 RS가 다시 1로 가고 p3가 끝나면서 결국 모든 값이 available. Semaphore는 코드가 극히 짧다. 그러므로 피터슨 알고리즘과 데커 알고리즘을 분석할 필요 없다. 이렇게 좋은 알고리즘이 있는데 특별히 그런 알고리즘들을 분석할 필요가 없다.
⑥ 모니터
p하고 v를 안 치고 들어가는 새치기, p가 아닌 v를 쓰는 경우 등, 어떻게 사용하는지에 따라서 또 문제가 생긴다. 그것을 해결하기 위해 만든 것이 ‘모니터’다. p.275 increase 코드에 java코드가 밑에 있다. 밑의 코드 못 읽든 읽든 상관없다. 중요한 것은 increase를 사용하는 애들이 들어올 때 순차적으로 사용하도록 하는 것이다. Semaphore 가 불안정해서가 아니라, Semaphore를 사용하려는 애들이 잘못 사용하려는 경우 문제를 일으키므로 정리를 하겠다는 의미다. 사실, OS의 옛날 처음 구현될 당시에는, 다 Semaphore 로 구현했다. 그런데 현대에 와서 OS가 커지면서 모니터라는 개념이 등장한 것이다. 과거의 OS 시스템은 다 Semaphore를 통해서 critical section을 맡고 있다.
4. [심화학습] 파일, 파이프, 소켓 프로그래밍
① 파일
심화학습을 먼저 설명하면 (p.279), 2개의 프로세스 간의 통신 코드가 나온다. Child는 파일에 write를 한다. Parent는 read를 수행한다. fork()를 하기 전에 파일을 먼저 open한다. 기억해야 할 것은, fork()를 하면 코드만 복사가 되는 것이 아니라 그 권리까지도 Child에게 넘어간다는 점이다. 자원까지 한꺼번에 복사되어 넘어간다는 것을 의미한다. 그리고 Child Part와 Parent Part는 서로 독립적으로 돌아가게 된다. 해당코드는 Child가 쓰는 값을 Parent가 받아서 작업을 수행하는 내용을 담은 코드다.
write(fd) -> send
read(fd) <- receive
위와 같이 보아도 무방하다. 그런데 이보다 더 좋은 방식은 파이프다.
② Pipe
Child가 Write를 하고 Parent의 Read가 먼저 일어난다면 어떻게 될까? 그러면 둘이 독립적이므로 못읽는다. 통신이 안 되기 때문이다. 먼저 보내준 것이 있어야 읽을 수 있는 것이다. 이것이 바로 비동기라는 단점이다. 즉, 데이터가 없으면 어떤 값도 읽을 수 없으므로 비동기 방식이라고 부른다. 파이프도 서로 다른 프로세스 간의 통신에 해당한다. p.281를 보면 open 대신 pipe로 바뀐 것을 확인할 수 있다. 그런데 파이프는 단방향 통신이므로 두 개를 열어서 한 개를 막아야 한다는 것을 내용을 함의하고 있다. pipe를 열어서 양쪽에서 하나씩 close를 먼저 해버리는 과정인 것이다. pipe의 장점이 뭐냐 하면, Write 전에 먼저 Read를 하면 Write가 올 때까지 Read는 항상 기다려주는 것이다. 즉, Write가 올 때까지 Blocking 된다. Blocking된다는 것은 자동으로 멈춘다는 것을 의미한다. 오늘 얘기하고자 하는 핵심 내용은 사실, 네트워킹이다. 한 장을 더 넘겨보자.
③ 네트워킹과 Socket
유닉스 안에서 카톡처럼 채팅하는 코드를 만든다고 가정하자. p.285에서 데이터를 주고받는 클라이언트 코드, p.286 서버 코드다. 이 코드들을 다듬으면 ‘카톡’이 된다. 막연하게 어렵다 생각할 필요는 없다. 직접 해보면 사람이 만든 것인데 왜 어렵겠는가. 이 코드를 읽을 수 있는 방법을 설명하겠다. fd = open() 이 자리를 socket으로 바꾸면 끝이다. socket -> write(save) -> read(receive) -> close 이 과정을 벗어나지 않는다.
p.275 자바로 작성한 모니터 내부 코드(일부)
monitor shared_balance {
private:
int balance = 10; /* shared data */
boolean busy = false;
condition mon; /* condition variable */
public:
increase(int amount) { /* public interface */
if(busy==true) mon.wait(); /* waiting in queue */
busy = true;
balance = balance + amount;
mon.signal(); /* wake up next waiting process */
}
…
클라이언트 코드 sp = socket, 서버 코드 sp ~~가 있다. pipe 코드와 socket 코드까지 한꺼번에 보여드리는 이유는 구조가 똑같기 때문이다. 물론, 생각을 해보자. 이 컴퓨터에서 저 컴퓨터에 있는 프로세스와 통신을 하기 위해 중간에 해야 할 일이 또 있다. 그러나, 중요한 것은 구조는 똑같다는 것이다. 어떤 방식으로 통신을 하든 간에 내용은 동일하다. 막상 뚜껑을 열어보니 네트워크 프로그래밍도 껌이겠구나 생각해주면 좋겠다. 그런데 여기서 끝나면 서운하다. p.283을 살펴보자.
④ 통신에 필요한 추가 작업
p.283의 그림은 추가적으로 필요한 것이 무엇인지 설명해주는 그림이다. 우선, socket을 열고 read, write를 한 뒤, close 하는 것은 똑같다. 그런데 거기에 중간 과정이 또 하나 있다. 먼저 connect 즉, 접속 시도하는 작업이 하나 있다. 이후, 서버 쪽에는 accept라는 함수가 있다. 이 두 개의 함수가 매칭이 되어야지 실제 연결이 이루어진다. 그전에 또 무엇이 있어야 하냐면, 네트워크를 컴퓨터에 연결하는 Bind라는 과정이 필요하다. 실제 네트워크 연결을 해주는 것이 bind다. 그다음에 bind만 되었다고 작업을 시작하는 것이 아니라, Listen이라고 하여 시작점이 서버에 들리도록 하는 작업이 부가적으로 필요하다. 그런 것들 외에는 나머지의 내용들은 굉장히 부수적이다.
'Operating System > OS' 카테고리의 다른 글
Ⅶ 물리 메모리 관리 (0) | 2023.11.28 |
---|---|
Ⅵ 교착 상태 (0) | 2023.11.28 |
Ⅳ CPU 스케줄링 (0) | 2023.11.28 |
Ⅱ 컴퓨터의 구조와 성능 향상 (0) | 2023.11.28 |
Ⅰ 운영체제의 개요 (0) | 2023.11.28 |