프로세스는 시스템 내에서 독립적으로 실행되기도 하고 데이터를 주고받으며 협업하기도 한다. 프로세스가 다른 프로세스와 데이터를 주고받는 프로세스 간 통신(IPC)에는 같은 컴퓨터 내에 있는 프로세스뿐만 아니라 네트워크로 연결된 다른 컴퓨터에 있는 프로세스와의 통신도 포함된다.
어떤 통신 방식을 이용할지 결정할 때는 오버헤드를 고려해야 한다. 예를 들어 같은 컴퓨터에 있는 프로세스끼리도 소켓을 이용하여 통신을 할 수 있다. IP주소로 127.0.0.1과 같인 루프백 주소를 사용하면 된다. 다만 소켓을 사용하려면 많은 전처리를 해야하기 때문에 다른 프로세스 간 통신 방법보다 느리다. 그래서 같은 프로세스 간 통신에는 소켓을 거의 사용하지 않는다.
프로세스 간 통신 방식은 단순하다. 즉 데이터를 주거나(send) 받는다(receive). 전역 변수를 이용하여 메시지를 주고받는 가장 단순한 통신 방식을 보여준다. 전역 변수는 우편함의 역할을 하며 보내는 사람이 우편함에 편지를 넣으면 받는 사람이 우편함을 열어 편지를 꺼내간다.
프로세스 간 통신은 동시에 실행되는 프로세스기리 데이터를 주고받는 작업을 의미한다.
통신은 데이터가 전송되는 방향에 따라 양방향 통신(duplex communication), 반양방향 통신(half-duplex communication), 단방향 통신(simplex communication)으로 나뉜다.
대부분의 통신은 양방향 통신이지만 전역 변수는 단방향 통신이다. 전역 변수를 1개만 이용하게 되면 두 쪽이서 동시에 보내면 한쪽의 값이 덮어쓰여진다. 즉, 전역 변수가 1개이면 한쪽 방향으로만 통신이 가능한 것이다. 만약 양방향 통신을 구현하려면 전역 변수를 2개 이용해야 한다.
전역 변수를 사용하는 통신 방식의 가장 큰 문제는 언제 데이터를 보낼지 데이터를 받는 쪽에서 모른다는 것이다. 그러므로 데이터를 받는 쪽에서는 반복적으로 전역 변수의 값을 점검하는 수 밖에 없다. 즉, 계속해서 전역 변수를 체크해야하믈 반복문을 사용하여 무한 실행하는 것이다. 이를 바쁜 대기(Busy Waiting)이라고 한다.
Busy waiting은 시스템 차원에서 큰 자원 낭비이므로 안좋은 프로그래밍의 전형적인 예이다.
while(true){
if(preValue != curValue){
// ... 전역 변수의 변화를 캐치
}
}
다음과 같은 예가 busy waiting이다. 이러한 busy waiting 문제를 해결하기 위해서는 데이터가 도착했음을 알려주는 동기화(synchronization)를 사용한다. 메신저에서 메시지가 도착했다고 알려주는 알람은 동기화의 대표적인 예이다. 동기화를 사용하면 Busy waiting을 굳이 하지않아도 운영체제가 알아서 알려준다.
프로세스 간 통신은 동기화 기능이 있느냐 없느냐에 따라 대기가 있는 통신(blocking communication)과 대기가 없는 통신(non-blocking communication)으로 구분된다. 대기가 있는 통신은 동기화 통신(synchronous communication), 대기가 없는 통신은 비동기화 통신(asynchronous communication)이라고도 한다.
전역 변수와 파일을 이용한 통신은 대기가 없는 통신의 대표적인 예이다. 전역 변수와 같이 공유 메모리를 이용하여 통신을 하든, 파일을 이용하여 통신을 하든, 보내는 쪽과 받는 쪽이 동기화되지 않는다. 대기가 없는 통신은 통신 오버헤드는 적지만 busy waiting 처럼 사용자가 직접 처리해야 하는 작업이 많다.
즉, 대기가 있는 통신은 운영체제가 자동으로 동기화를 제공해주는 경우가 많고, 대기가 없는 통신은 자동으로 동기화를 제공해주는 것이 아닌 사용자가 직접 동기화 작업을 해야한다.
프로세스 간 통신은 데이터를 주거나 받는 동작으로 이루어지며 이는 쓰기 연산과 읽기 연산을 간소화 할 수 있다.
send -> write(GV, message)
receive -> read(GV, message)
GV는 전역 변수로, message에 따라 GV에 값을 써주는 것이 send, message에 따라 GV를 읽는 것이 receive가 된다.
이러한 프로세스 간 통신 방식은 전역 변수 뿐만 아니라, 파이프, 소켓, 파일을 이용한 통신에도 동일하게 적용된다.
프로세스 간 통신에서 가장 중요한 것은 프로세스의 동기화이다. 동기화를 하는 것은 주방에 벨이 있는 것이다. 벨이 없다면 종업원은 요리가 나올 때까지 주방을 기웃거려야 한다. 하지만 벨이 있다면 음식이 나왔는 지를 알려주고 종업원은 다른 일을 하다가 손님에게 음식을 가져다주러 갈 수 있다.
전역 변수, 파일, 파이프, 소켓을 이용한 프로세스 간 통신과 각 통신에서 프로세스 동기화가 어떻게 이루어지는 지 알아보도록 하자
전역 변수를 이용한 통신은 공동으로 관리하는 메모리를 이용하여 데이터를 주고받는 것이다. 데이터를 보내는 쪽에서는 전역 변수나 파일에 값을 쓰고, 데이터를 받는 쪽에서 전역 변수의 값을 읽는다.
부모와 자식 프로세스 간의 통신에도 사용되고, extern 변수를 사용하여 include하여 사용할 수도 있다.
그러나 전역 변수를 이용한 양방향 통신은 busy waiting
을 해야하는 문제가 있다.
다음의 그림은 프로세스가 fork하여 만든 자식 프로세스와 전역 변수로 통신하는 모습을 보여준다. 전역 변수 R에 write를 하면 한 쪽은 read 할 수 밖에 없다. 때문에 전역변수 R, L 을 두어 양방향 통신을 하도록 만드는 것이다.
그런데, fork()로 만들어진 프로세스는 전역 변수 R이 바뀔 때까지 작동을 하지않고 check만 반복하게되는 busy waiting에 빠지는 단점이 있다.
파일은 열고(open) ,쓰기(write), 읽기(read), 닫기(close)로 총 4가지로 이루어져 있다.
int main(){
int fd;
char buf[5];
fd = open('com.txt', O_RDWR);
write(fd, "test", 5);
read(fd, buf, 5);
close(fd);
}
입출력은 다음과 같이 입출력 프로세스가 따로 존재하여 관리한다. 그렇다면 운영체제의 입장에서 보면 저장장치의 데이터를 읽고 쓰는 것도 일반 프로세스와 입출력 프로세스 간의 통신이다.
파일을 이용한 통신은 부모-자식 관계 프로세스 간 통신에 많이 사용되며 운영체제가 프로세스 동기화를 제공하진 않는다. 그래서 프로세스가 알아서 동기화를 해야하는데 주로 부모 프로세스가 wait() 함수를 이용하여 자식 프로세스의 작업이 끝날 때까지 기다렸다가 작업을 시작한다.
파이프는 운영체제가 제공하는 동기화 통신 방식으로 파일 입출력과 같이 Open() 함수로 descripter를 받고 작업을 한 후에 close로 마무리 한다. 파이프를 이용한 통신은 전역 통신과 마찬가지로 단방향 통신이다. 때문에 양방향 통신을 하기 위해서는 파이프를 두 개 사용해야 한다.
파이프는 queue와 같다. 때문에 한쪽에서는 write( push )하는 것이고, 한 쪽은 read( pop ) 밖에 할 수가 없다. 만약 프로세스 B가 파이프 1에 대해 읽기 연산을 수행했는데 프로세스 A가 파이프 1에 아직 쓰기 연산을 하지 않았다면 프로세스 B는 대기 상태가 된다. 이러한 대기 상태는 프로세스 A가 파이프 1에 데이터를 쓰는 순간 자동으로 풀려 동기화가 이루어진다. 프로세스 B는 Busy waiting를 하지 않아도 된다.
한 가지 헷갈리지 말아야 하는 것이 Wait은 프로세스를 block(wait) 상태로 만든다. 단 busy waiting은 계속해서 체크해야하기 때문에 계속해서 타임 슬라이스를 얻어 run 상태, ready 상태를 반복한다. 그렇기 때문에 wait보다 busy waiting이 훨씬 자원을 많이 소모하는 것이다.
파이프는 이름 없는 파이프와 이름있는 파이프로 나뉜다.
지금까지는 한 컴퓨터에 있는 프로세스 간 통신을 보았다. 여러 컴퓨터에 있는 프로세스끼리도 통신을 할 수 있다. 여러 컴퓨터에 있는 프로세스 간 통신은 네트워킹이라고 한다. 네트워킹 상황에서의 통신은 원격 프로시저 호출(RPC)이나 소켓을 사용한다. 프로시저 호출이 한 컴퓨터에 있는 함수를 호출하는 것이라면, 원격 프로시저 호출은 다른 컴퓨터에 있는 함수를 호출하는 것이다. 자바 같은 경우네는 다른 컴퓨터에 있는 객체의 메서드를 호출하는 것이 원격 프로시저 호출이다.
일반적으로 원격 프로시저 호출은 소켓을 이용하여 구현한다. 위의 그림은 소켓을 이용한 통신을 나타낸 것이다. 다른 컴퓨터에 있는 프로세스와 통신하려면 그 컴퓨터의 위치를 파악하고, 원격지의 시스템 내 여러 프로세스 중 어떤 프로세스와 통신을 할지도 결정해야 한다.
이때 통신하고자 하는 프로세스는 소켓에 쓰기 연산을 하면 데이터가 전송되고, 읽기 연산을 하면 데이터를 받게 된다.
소켓은 프로세스 동기화를 지원하므로, 데이터를 받는 쪽의 프로세스가 busy waiting
을 하지 않아도 된다. 양방향 통신을 하기위해 파이프는 2개를 사용했지만 소켓은 하나만 사용해도 양방향 통신이 가능하다.
참고로, 네트워크의 기본은 소켓이기 때문에 네트워크 프로그래밍을 소켓 프로그래밍이라고도 한다.
정리하면 다음과 같다.
종류 | 운영체제 동기화 지원 | Open/Close() 사용 |
---|---|---|
전역 변수 | 동기화 지원 x(busy waiting 사용) | x |
파일 | 동기화 지원 x(wait 함수 이용) | O |
파이프 | O | O |
소켓 | O | O |
주의할 것은 동기화라는 말은 프로세스 간의 통신할 때를 말한다. 어떤 공유 자원에 대한 동기화가 아닌 프로세스 call에 대한 통신이라고 생각하면 된다.