본문 바로가기

CS

쉽게 배우는 운영체제 | 프로세스 동기화

프로세스 간 통신

프로세스는 시스템 내에서 독립적으로 실행되기도 하고 데이터를 주고받으며 협업하기도 한다. 프로세스가 다른 프로세스와 데이터를 주고 받는 프로세스간 통신(IPC)에는 컴퓨터 내에 있는 프로세스뿐만 아니라 네트워크로 연결된 다른 컴퓨터에 있는 프로세스와의 통신도 포함된다.

 

프로세스 간 통신의 종류

 

- 프로세스 내부 데이터 통신: 하나의 프로세스 내에 2개 이상의 스레드가 존재하는 경우의 통신이다. 프로세스 내부의 스레드는 전역 변수나 파일을 이용하여 데이터를 주고받는다.

- 프로세스 간 데이터 통신: 같은 컴퓨터에 있는 여러 프로세스끼리 통신하는 경우로, 공용 파일또는 운영체제가 제공하는 파이프를 사용하여 통신한다.

- 네트워크를 이용한 데이터 통신: 여러 컴퓨터가 네트워크로 연결되어 있을 때도 통신이 가능한데, 이 경우 프로세스는 소켓을 이용하여 데이터를 주고받는다. 이처럼 소켓을 이용하는 프로세스간 통신을 네트워킹이라고 한다. 다른 컴퓨터에 있는 함수를 호출하여 통신하는 원격 프로시저 호출도 여기에 해당한다.

 

프로세스간 통신 분류

함수 호출도 데이터를 주고받는다는 의미에서 통신이라고 생각할 수도 있지만, 함수는 하나의 프로세스 내에서 순차적으로 실행되기 때문에 통신이라고 부르지 않는다. 프로세스간 통신은 동시에 실행되는 프로세스끼리 데이터를 주고 받는 작업을 의미한다.

 

- 양방향 통신(duplex communication): 데이터를 동시에 양쪽 방향으로 전송할 수 있는 구조로, 일반적인 통신은 모두 양방향 통신이다. 프로세스 간 통신에는 소켓 통신이 양방향 통신에 해당한다.

- 반양방향 통신(half duplex communication): 데이터를 양쪽 방향으로 전송할 수 있지만 동시 전송은 불가능하고 특정 시점에 한쪽 방향으로만 전송할 수 있는 구조이다. 반양방향 통신의 대표적인 예는 무전기이다.

- 단방향 통신(simplex communication): 모스 신호처럼 한쪽 방향으로만 데이터를 전송할 수 있는 구조이다. 프로세스간 통신에서는 전역 변수와 파이프가 단방향 통신에 해당.

 

대부분의 통신은 양방향 통신이지만 전역변수를 사용한 통신은 단방향 통신이다. 전역 변수를 사용한 통신의 가장 큰 문제점은 언제 데이터를 보낼지 데이터를 받는 쪽에서 모른다는 것이다. 그러므로 데이터를 받는 쪽에서는 반복적으로 전역 변수의 값을 점검하는 수밖에 없다. 우편함에 편지가 있는지 스스로 열어보는 것처럼 상태 변화를 살펴보기 위해 반복문을 무한 실행하며 기다리는 것을 바쁜 대기(busy wating)이라고 한다. 시스템 차원에서 큰 자원낭비인 바쁜 대기는 안 좋은 프로그램의 전형적인 예이다. 바쁜 대기 문제를 해결하기 위해서는 데이터가 도착했음을 알려주는 동기화(synchronization)를 사용한다. 메신저에서 메시지가 도착했다고 알려주는 알림은 동기화의 대표적인 예이다. 동기화를 사용하면 바쁜 대기를 사용하지 않아도 운영체제가 알아서 알려준다.

 

프로세스간 통신은 동기화 기능이 있느냐 없느냐에 따라서 대기가 있는 통신(blocking communication)과 대기가 없는 통신(non-blocking communication)으로 구분된다. 대기가 있는 통신은 동기화 통신(synchronous communication), 대기가 없는 통신은 비동기화 통신(asynchronous communication)이라고도 한다. 

 

- 대기가 없는 통신: 동기화를 지원하지 않는 통신 방식이다. 데이터를 받는 쪽에서 바쁜 대기를 사용하여 데이터가 도착했는지 여부를 직접확인한다.

- 대기가 있는 통신: 동기화를 지원하는 방식이다. 데이터를 받는 쪽은 데이터가 도착할 때까지 자동으로 대기 상태에 머물러 있다.

프로세스간 통신 종류

프로세스간 통신은 데이터를 주거나 받는 동작으로 이루어지며 이는 쓰기 연산과 읽기 연산으로 간소화할 수 있다. 다음은 전역변수 GV를 이용하여 send는 쓰기 연산으로, receive는 읽기 연산으로 변경한 것으로 연산만 바뀌었을 뿐 의미는 같다. 이러한 프로세스간 통신 방식은 전역 변수뿐만 아니라 파일, 파이프, 소켓을 이용한 통신에도 동일하게 적용된다.

 

send -> write(GV, message)

receive -> read(GV, message)

 

프로세스간 통신에서 가장 중요한 것은 프로세스 동기화이다. 동기화를 하는 것은 주방에 벨을 설치하는 것과같다. 벨이 없다면 요리가 나올 때까지 주방을 기웃거려야 하지만 벨이 있는 경우 종업원은 자기 일을 하다가 주방에서 벨 소리가 들릴 때 음식을 손님에게 가져다주면 된다. 

 

전역 변수, 파일, 파이프, 소켓을 이용한 프로세스 간 통신과 각 통신에서 프로세스 동기화가 어떻게 이루어지는지 알아보자.

 

전역 변수를 이용한 통신은 공동으로 메모리를 사용하여 데이터를 주고받는 것이다. 데이터를 보내는 쪽에서 전역변수나 파일에 값을 쓰고, 데이터를 받는쪽에서는 전역 변수의 값을 읽는다. 전역 변수를 이용한 통신 방식은 주로 직접적으로 관련이 있는 프로세스간에 사용한다. 예를 들어 부모 프로세스가 전역 변수를 전언한 후 자식 프로세스를 만들면 부모 프로세스와 자식 프로세스가 통신할 수 있다.

 

다음 코드를 보면 main() 이전에 정의된 전역 변수 GV는 부모 프로세스와 자식 프로세스가 공유하는 메모리 영역으로 GV에 데이터를 쓰거나 읽는 방법으로 두 프로세스가 통신한다.

 

int GV;

int main()
{
	int pid;
    pid = fork();
}

 

이제 파일을 이용한 통신에 대해 알아보자. 아래 코드는 저장장치에 파일을 읽고 쓰는 코드이다. 파일 입출력 코드는 크게 세부분으로 구성된다. 파일 열기(open), 쓰기(write), 또는 읽기(read) 그리고 파일을 닫는다.(close)

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
	int fd;
    char buf[5];
    
    fd = open("com.txt", O_RDWR);
    write(fd, "Test", 5);
    read(fd, buf, 5);
    
    close(fd);
    exit(0);
}

 

파일 입출력 코드를 프로세스 입장에서 살펴보자. 프로세스가 입출력 관리 프로세스에 쓰기를 요구하면 데이터가 저장되고 읽기를 요구하면 입출력 관리 프로세스로부터 데이터를 가져온다. 

 

프로세스가 입출력 관리 프로세스에 쓰기를 요구하면 데이터가 저장되고, 읽기를 요구하면 입출력 관리 프로세스로부터 데이터를 가져온다. 쓰기 연산은 하드디스크에 데이터를 전송하는 명령, 읽기 연산은 하드디스크로 데이터를 가져오는 명령이라고 할 수 있으므로 파일 입출력도 통신이다.  운영체제 입장에서 보면 저장장치의 데이터를 읽고 쓰는 것도 일반 프로세스와 입출력 프로세스간의 통신이다. 

 

 

파일을 이용한 통신은 부모-자식 프로세스 간 통신에 많이 사용되며 운영체제가 프로세스 동기화를 제공하지 않는다. 그래서 프로세스가 알아서 동기화를 해야 하는데 주로 부모 프로세스가 wait() 함수를 이용하여 자식 프로세스의 작업이 끝날 때까지 기다렸다가 작업을 시작한다. 

 

프로세스 동기화 문제를 해결하는 방법으로 파이프가 있다. 파이프는 운영체제가 제공하는 동기화 통신 방식으로, 파일 입출력과 같이 open() 함수로 기술자를 얻고 작업을 한 후 close() 함수로 마무리한다. 파이프를 이용한 통신은 전역 변수를 이용한 통신 방식과 마찬가지로 단방향 통신이다. 파이프로 양방향 통신을 하려면 파이프를 2개 사용해야한다.

위 그림은 2개의 파이프를 이용한 통신을 나타낸 것이다. 파이프에 쓰기 연산을 하면 데이터가 전송되고 읽기 연산을 하면 데이터를 받는다. 만약 프로세스 B가 파이프 1에 대해 읽기 연산을 수행했는데 프로세스 A가 파이프 1에 아직 쓰기 연산을 하지 않았다면 프로세스 B는 대기 상태가 된다. 이러한 대기 상태는 프로세스 A가 파이프 1에 데이터를 쓰는 순간 자동으로 풀려 동기화가 이루어진다. 프로세스 B는 바쁜 대기를 하지 않아도 된다.

 

여러 컴퓨터에 있는 프로세스끼리 통신하려면 어떻게 해야할까? 여러 컴퓨터에 있는 프로세스간 통신을 네트워킹이라고 한다. 네트워킹 상황에서의 통신은 원격 프로시저 호출이나 소켓을 이용한다. 프로시저 호출이 한 컴퓨터에 있는 함수를 호출하는 것이라면, 원격 프로시저 호출은 다른 컴퓨터에 있는 함수를 호출하는 것이다. 자바 같은 객체지향 언어에서 다른 컴퓨터에 있는 객체의 메소드를 불러와 사용하는 것은 모두 원격 프로시저 호출의 예이다. 

 

일반적으로 원격 프로시저 호출은 소켓을 이용하여 구현한다. 아래 그림은 소켓을 이용한 통신을 나타낸 것이다. 다른 컴퓨터에 있는 프로세스와 통신하려면 그 컴퓨터의 위치를 파악하고, 원격지의 시스템 내 여러 프로세스 중 어떤 프로세스와 통신을 할지도 결정해야한다. 이때 통신하고자 하는 프로세스는 소켓에 쓰기 연산을 하면 데이터가 전송되고, 읽기 연산을 하면 데이터를 받게 된다.

 

 

 

소켓은 프로세스 동기화를 지원하므로 데이터를 받는 쪽의 프로세스가 바쁜 대기를 하지 않아도 된다. 양방향 통신을 하기 위해 파이프는 2개를 사용했지만 소켓은 하나만 사용해도 양방향 통신이 가능하다. 네트워크 프로그래밍을 흔히 소켓 프로그래밍이라고 하는 이유는 네트워킹의 기본이 소켓이기 때문이다.

 

정리

 

프로세스 간에 데이터를 주고받는 행위는 읽기와 쓰기 연산으로 단순화할 수 있다. 부모-자식 관계로 연결되어 있는 프로세스끼리는 전역 변수, 파일, 이름 없는 파이프 등을 이용하여 통신하고, 서로 독립적인 프로세스끼리는 이름 있는 파이프를 이용하여 통신한다. 또한 서로 다른 시스템에 있는 프로세스끼리는 소켓을 이용하여 통신한다.  이처럼 여러가지 통신 방식이 있지만 강조하고 싶은 점은 프로세스간 통신을 일기와 쓰기 연산으로 구현할 수 있다는 것이다.

 

동기화를 지원하는 프로세스간 통신에는 open()과 close() 함수가 사용된다. 전역 변수는 동기화 기능이 없기 떄문에 open()과 close()를 사용할 필요가 없으나 나머지 프로세스간 통신은 open()과 close() 함수를 사용한다. open()으로 키(descriptor)를 받아 작업을 시작하고, 자원을 다 사용하면 close()로 키를 반납한다. 이러한 open-read/wirte-close 구조는 파일, 파이프, 소켓에 동일하게 적용된다.