운영체제는 프로세스를 실행 단위로 사용한다. 프로세스는 독립적인 메모리 공간을 갖는데, 왜 웹서버를 멀티 프로세스로 구현하는 경우가 있을까? 그것은 소켓을 통해 데이터를 수/송신할 때 사용하는 read/write 함수가 블로킹 함수이기 때문이다. 모든 경우에서 블로킹이 일어나는 것은 아니고 특정 종류의 파일 디스크립터를 통해 read/write할 때 버퍼가 비어있거나 가득차면 발생한다. 이번 글을 통해 read/write 블로킹과 프로세스를 복사하여 멀티 프로세스로 동작하는 프로그램의 구현 방법에 대해 알아보자.
파일 디스크립터는 크게 "빠른 파일 디스크립터"와 "느린 파일 디스크립터"로 나눌 수 있다. 빠른 파일 디스크립터와 느린 파일 디스크립터의 결정적인 차이는 블로킹 여부이다. 빠른 파일 디스크립터를 read하거나 디스크립터에 write할 때 블로킹이 일어나지 않고 느린 파일 디스크립터를 read하거나 디스크립터에 write할 때 블로킹이 일어날 수 있다는 차이가 있다. 일반적인 파일을 나타내는 디스크립터는 빠른 디스크립터이다. 즉 우리가 touch 명령을 통해 생성한 파일을 read/write할 때 블로킹이 일어날 수 없다. 하지만 파이프나 소켓과 같은 파일 디스크립터는 느린 디스크립터이고 블로킹이 일어날 수 있다.
상대에게 수신한 데이터가 없는데 read를 하거나 출력 버퍼가 가득찼는데 write를 하면 블로킹이 발생한다. 수신한 데이터가 없을 때 read를 하면 상대로부터 데이터를 수신할 때까지 블로킹되고 출력 버퍼가 가득차있는데 write한 경우 출력 버퍼에 빈 공간이 생길 때 까지 블로킹된다.
블로킹이 일어나기 때문에 서버를 단순히 구현하면 병렬적으로 병렬적으로 여러 클라이언트와 통신하지 못한다. 여러 방법을 사용하여 구현할 수 있는데 크게 세 가지가 있다.
이 글에서는 여러 프로세스를 사용하는 프로그램을 구현하는 방법을 알아볼 것이다.
프로세스는 fork를 통해 복사할 수 있고 부모/자식 여부는 fork 함수의 반환 값을 통해 판단할 수 있다. 이를 통해 부모와 자식이 다른 로직을 수행하도록 구현할 수 있다.
자식 프로세스는 실행이 완료되면 혹은 exit 함수가 호출되면 운영체제에 값을 반환한다. 이 때, 부모 프로세스가 자식 프로세스의 반환 값을 읽어들일 때까지 프로세스 테이블에서 자식 프로세스는 지워지지 않는다. 어떻게 부모 프로세스가 자식 프로세스의 반환 값을 읽을 수 있을까? 두 개의 함수를 통해 이를 할 수 있다.
wait 함수는 자식 프로세스 중에 값을 반환한 프로세스가 있을 때 까지 대기하는 함수이다. 이 함수는 자식 프로세스의 상태에 대한 정보를 인자 포인터를 통해 부모 프로세스에게 전달한다.
waitpid는 특정 자식 프로세스가 값을 반환할때까지 부모 프로세스의 흐름을 블로킹한다. wait과는 다르게 WNOHANG 옵션을 줘서 블로킹하지 않도록 만들 수 있다. 종료된 자식 프로세스가 존재하지 않을 때, waitpid가 0을 반환한다.
wait을 무분별하게 사용하면 블로킹이 일어나서 부모 프로세스가 정상적으로 기능할 수 없고 waitpid의 경우도 WNOHANG 옵션을 주더라도 반복적으로 waitpid를 호출해야한다는 문제를 갖고있다. 이러한 문제는 시그널 핸들링을 통해 해결할 수 있는데 자식 프로세스의 종료를 나타내는 시그널이 존재하기 때문이다.