-
TCP/IP 소켓 프로그래밍 - 10장 : 멀티프로세스 기반의 서버 구현네트워크 2024. 6. 20. 02:23
10장10-1 프로세스의 이해와 활용
지금까지 공부해온 내용들로만 서버를 만들어 서비스를 제공하면,
연결요청의 순서에 따라 서비스를 제공받는 대기 시간이 많이 차이날 수 있다.
평균적인 만족도를 높이기 위해 다중접속 서버를 구현하는 방법에는 크게 3가지가 있다.
- 멀티 프로세스 기반 서버
- 멀티 플렉싱 기반 서버
- 멀티 쓰레딩 기반 서버
멀티 프로세스 기반 서버는 다수 프로세스를 생성하는 방식으로 서비스를 제공한다.
멀티 플렉싱 기반 서버는 입출력 대상을 묶어 관리하는 방식으로 서비스를 제공한다. ( 12장 )
멀티 쓰레딩 기반 서버는 클라이언트 수만큼 쓰레드를 생성해 서비스를 제공한다. ( 18장 )
멀티 프로세스 기반 서버는 windows에서 지원이 되지 않는 방식이다.
프로세스는 메모리 공간을 차지한 상태에서 실행중인 프로그램이다.
디스크에 저장되어있는 프로그램은 프로세스가 아니다.
프로세스는 운영체제의 관점에서 프로그램 흐름이 기본 단위이며,
여러 개의 프로세스가 생성되면 동시에 실행된다. (**단일 프로세서에서는 concurrent의 의미)
또한, 하나의 프로그램이 실행되는 과정에서 여러 프로세스가 생성되기도 한다.
“프로세스 ID”
모든 프로세스는 운영체제로부터 ID를 부여받는다.
숫자 1은 운영체제가 시작되자마자 실행되는 프로세스에게 할당되므로,
숫자 2이상의 정수를 부여해준다.
리눅스 ps 명령어에 a와 u 옵션을 지정해주면 아래와 같이 실행중인 프로세스를 확인할 수 있다.

프로세스 생성 방법 중 하나로, fork 함수가 있다.
fork함수는 아래와 같이 생겼다.
#include <unistd.h> pid_t fork(void); // 성공 시 프로세스 ID (PID), 실패 시 -1 리턴fork 함수는 호출한 프로세스의 복사본을 생성한다.
생성된 프로세스와 호출한 프로세스 모두 fork 함수 반환 이후 문장을 실행하게 된다.
이때, 메모리 영역까지 동일하게 복사하기 때문에,
이후 프로그램 흐름은 fork 함수의 반환 값을 기준으로 제어한다.
부모 프로세스는 fork함수의 반환 값 = 자식 프로세스의 PID이고,
자식 프로세스는 fork함수의 반환 값 = 0이다.
fork()이전의 변수들은 공유되지만
fork()호출 이후 각자의 프로세스에서 변경된 값은 다른 프로세스에게 영향을 미치지 않는다!
10-2 프로세스 & 좀비( Zombie ) 프로세스
프로세스가 할 일을 다하면 ( main 함수 실행을 완료하면 ) 사라져야 하지만,
좀비 프로세스가 되어 시스템의 중요한 리소스를 차지하고 있을 수 있다.
좀비프로세스를 제대로 처리하지 못하면 시스템에 부담을 주는 원인이 될 수 있다.
자식 프로세스는 exit함수를 호출하거나, main에서 return을 만나는 경우 종료되는데,
이때 인자를 전달하게 된다.
... exit(1); ... 또는 ... return 0; ...운영체제는 자식 프로세스가 만든 인자값 또는 반환값을 부모 프로세스에게 전달될 때까지
자식 프로세스를 소멸시키지 않으며, 이 상황에 있는 프로세스가 좀비 프로세스이다.
부모 프로세스가 자식 프로세스의 전달 값을 요청(함수 호출)해야 운영체제가 값을 전달해주며,
좀비 프로세스가 소멸되게 된다.
wait 함수 또는 waitpid 함수를 이용해 자식 프로세스의 전달 값을 요청할 수 있다.
wait 함수는 아래와 같은 구조로 이루어져 있다.
#include <sys/wait.h> pid_t wait(int * statloc); //성공 시 종료된 자식 프로세스의 ID, 실패 시 -1 리턴위 함수 호출시, 종료된 자식 프로세스가 있다면,
전달한 값을 매개변수로 전달된 주소의 변수에 저장한다.
추가적으로 다른 정보가 담겨 있어 별도의 매크로 함수로 값을 분리해주어야 한다.
매크로 함수로는
- WIFEXITED : 자식 프로세스가 정상 종료한 경우 ‘true’리턴
- WEXITSTATUS : 자식 프로세스의 전달 값 반환
wait 함수의 인자로 변수 nowstatus의 주소 값이 전달되면,
wait함수 호출 이후에는 다음과 같은 코드로 반환 값을 확인할 수 있다.
if(WIFEXITED(nowstatus)) { puts("정상 종료!"); printf("Child pass num : %d", WEXITSTATUS(nowstatus)); }wait 함수를 호출 했을 때, 종료된 자식 프로세스가 없으면
임의의 자식 프로세스가 종료될 때까지 blocking 상태에 놓일 수 있으니 주의해야 한다.
waitpid 함수를 사용하면 wait 함수의 블로킹 문제를 해결하면서, 좀비 프로세스를 소멸시킬 수 있다.
waitpid 함수는 아래와 같이 생겼다.
#include <sys/wait.h> pid_t waitpid(pid_t pid, int * statloc, int options); //성공 시 종료된 자식 프로세스의 ID(또는 0), 실패 시 -1 리턴 //pid : 종료 확인하고싶은 자식 프로세스 id , -1 전달 시 임의 자식 프로세스 종료 대기 //options : sys/wait.h 헤더파일에 선언된 상수 WNOHANG을 전달 시, 블로킹 상태에 있지 않게 된다. //WNOHANG 을 전달하면, 자식 프로세스가 없으면 0을 리턴한다.아래는 waitpid 함수가 블로킹 되지 않고 좀비 프로세스를 소멸시킨 예제 실행 결과이다.

10-3 시그널 핸들링
waitpid 함수만 사용하여 자식 프로세스의 인자를 받으려고 할 경우,
부모 프로세스는 주기적으로 자식 프로세스의 종료 상태를 확인해야 한다.
이 문제를 시그널을 이용해 해결할 수 있다.
시그널은 특정 상황이 발생했음을 운영체제가 프로세스에게 전달하는 메시지를 말한다.
시그널 핸들링은 메시지에 반응해, 메시지에 대응되는 작업을 진행하는 것을 말한다.
프로세스는 자식 프로세스가 종료될 시,
운영체제에게 원하는 함수를 호출하도록 미리 요구할 수 있다.
이를 시그널 등록이라고 한다.
시그널 등록 함수는 아래와 같다.
#include <signal.h> void (*signal(int signo, void (*func)(int)))(int); //시그널 발생시 호출할, 이전에 등록한 함수의 포인터 리턴반환형이 void인 함수 포인터임을 알아두자.
첫번째 인자는 특정 상황에 대한 정보(시그널)를 전달한다.
두번째 인자는 실행할 함수의 주소 값을 전달한다.
signal 함수를 통해 등록 가능한 “특정 상황”과 “대응되는 상수”에는 아래와 같은 경우들이 있다.
- SIGALARM - alarm 함수 호출 통해 등록된 시간이 된 상황
- SIGINT - “ctrl + c”가 입력된 상황
- SIGCHILD - 자식 프로세스가 종료된 상황
내가 임의로 만든 함수 “myfunc”를 자식 프로세스가 종료될 때 호출되도록 시그널 등록하려면,
signal(SIGCHILD, myfunc);라고 쓰면 된다.
이때, myfunc 함수는 2가지 조건을 지켜야 한다.
- 매개변수형이 int이고
- 반환형이 void 이어야 한다.
⇒ void(*myfunc)(int)
SIGALRM 시그널과 관련해 alarm 함수를 알아두면 좋다.
alarm 함수는 아래와 같이 생겼다.
#include <unistd.h> unsigned int alarm(unsigned int seconds); // SIGALRM이 발생할 때까지 남은 시간을 초단위로 리턴위 함수는 전달된 시간(초 단위)이 지나면 SIGALRM 시그널이 발생한다.
인자로 0을 전달하면, 이전에 설정된 SIGALRM 시그널 발생 예약이 취소된다.
위 함수를 이용해 시그널 발생만 예약해놓고, 호출할 함수를 지정하지 않으면,
프로세스가 그냥 종료된다.
Sleep 함수의 호출로 프로세스가 블로킹되어 있더라도, 시그널이 발생하면 프로세스는 깨어난다.
이후 다시 잠들지 않는다!
sigaction 함수는 signal 함수보다 훨씬 안정적이며, signal 함수를 대체할 수 있다.
signal 함수는 유닉스 계열 운영체제에 따라 동작방식에 차이가 있을 수 있지만,
sigaction 함수는 차이가 없다.
sigaction 함수의 원형은 아래와 같다.
#include <signal.h> int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact); //성공 시 0, 실패 시 -1 리턴 //act : 시그널 발생시 호출될 함수 정보 //oldact : 이전에 등록된 시그널핸들러의 함수포인터를 얻는데 사용된 인자, 필요없으면 0위 함수를 이용하기 위해서는 sigaction 구조체 변수를 선언 및 초기화해야 한다.
sigaction 구조체는 아래와 같다.
struct sigaction { void (*sa_handler)(int); sigset_t sa_mask; int sa_flags; }sa_handler 멤버 변수에 시그널 핸들러의 함수 포인터 값을 저장하면 된다. (말이 어려워서 그렇지 함수명 넣어주면 된다. )
( sa_mask, sa_flags관련은 다른 책을 추천해줄테니 그것으로 공부하라고 함. )sigaction으로 remove_zombie.c 예제를 따라하면 주의할 점은,
signal 핸들러에서 처리 중에 signal이 발생하면 무시될 수 있다는 점이다.
이 부분에 대해서는 좀 더 찾아볼 필요가 있겠다.따라해보려면 자식 프로세스들의 sleep 시간을 서로 다르게 해주자.
10-4 멀티태스킹 기반의 다중접속 서버

멀티프로세스 기반 다중접속 에코 서버의 모델은 위와 같다.
에코 서버는 클라이언트의 연결요청이 있을 때마다,
자식 프로세스를 생성해 서비스를 제공한다.
에코 서버가 거치는 과정은 다음과 같다.
- 부모 프로세스는 accept 함수 호출을 통해서 연결요청을 수락한다.
- 이때 리턴 받은 소켓의 파일 디스크립터를 자식 프로세스를 생성해 넘겨준다.
- 자식 프로세스는 전달받은 파일 디스크립터를 바탕으로 서비스를 제공한다.
자식 프로세스는 부모 프로세스가 갖는 것들을 전부 복사하기 때문에,
파일 디스크립터를 자식 프로세스에게 넘겨주기 위한 별도의 과정이 필요없다.
멀티 프로세스 기반 에코 서버 예제의 실행 결과는 아래와 같다.

fork 함수를 호출하게 되면,
부모 프로세스가 지니고 있던 서버 소켓과 클라이언트와 연결된 소켓 (따라서 총 2개 )의
파일 디스크립터가 자식 프로세스에게 전달된다.
“소켓은 프로세스의 소유가 아니라 운영체제의 소유이며,
파일 디스크립터는 프로세스의 소유이다.”

fork를 하게 되면 다음과 같이 두 프로세스에서 사용하는 파일 디스크립터가 동일한 소켓을 가리키게 된다.
서로에게 상관 없는 파일 디스크립터를 닫아주지 않으면, 소켓이 소멸되지 않을 수 있다.

위와 같이 만들어 주기 위해서, 예제 코드에선 아래와 같이 썼다.
... if(pid==0) close(serv_sock); //자식 프로세스는 서버 소켓과 연결된 파일 디스크립터를 소멸. ... else close(clnt_sock); //부모 프로세스는 클라이언트와 소통하는 소켓과 연결된 파일 디스크립터를 소멸. ...
10-5 TCP의 입출력 루틴 ( Routine ) 분할
클라이언트 입장에서 프로세스를 추가로 생성하면,
데이터의 송신과 수신을 분리할 수 있고 이를 통해,
서버로부터 데이터가 수신되지 않아도 추가적으로 데이터를 전송할 수 있다!

클라이언트의 부모 프로세스는 데이터 수신을 담당하고
자식 프로세스는 데이터의 송신을 담당한다.
부모 프로세스는 수신 관련 코드만 작성하면 되고,
자식 프로세스는 송신 관련 코드만 작성하면 되므로,
프로그램의 구현이 보다 편해진다.

입출력 루틴 분할의 또다른 장점은
동일한 시간 내에 데이터 송수신 분량이 더 많게 할 수 있다는 것이다.
클라이언트는 데이터의 수신 여부(echo)에 상관없이 데이터 전송이 가능하기 때문이다.
특히, 데이터 전송속도가 느린 환경에서 성능 차이가 많이 드러난다.
echo_mpclient.c 예제 코드에서 주의할 점이 있는데,
void write_routine(...) ... shutdown(sock, SHUT_WR); return; ...이 부분에서 shutdown을 써주어 EOF 전달을 명시적으로 해주어야 한다.
파일 디스크립터가 복제된 상황에서 한번의 close호출로는 EOF 전달을 기대할 수 없기 때문이다.
'네트워크' 카테고리의 다른 글
TCP/IP 소켓 프로그래밍 - 12장 : IO 멀티플렉싱 ( Multiplexing ) (0) 2024.06.23 TCP/IP 소켓 프로그래밍 - 11장 : 프로세스간 통신 (0) 2024.06.21 TCP/IP 소켓 프로그래밍 - 9장 : 소켓의 다양한 옵션 (0) 2024.06.19 TCP/IP 소켓 프로그래밍 - 8장 : 도메인 이름과 인터넷 주소 (0) 2024.06.18 TCP/IP 소켓 프로그래밍 - 7장 : 소켓의 우아한 연결종료 (0) 2024.06.18