[TCP/IP 소켓 프로그래밍] (7-1) 다중 접속 서버 - 프로세스
출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저
둘 이상의 클라이언트에게 서비스를 제공하기 위한 다중 접속 서버 구현이 필요하다.
다중 접속 서버 구현을 위한 방법
1. 멀티프로세스 기반 서버 : 다수의 프로세스를 생성하는 방식
2. 멀티플렉싱 기반 서버: 입출력 대상을 묶어서 관리하는 방식으로 서비스 제공
3. 멀티쓰레딩 기반 서버: 클라이어트의 수만큼 스레드를 생성하는 방식으로 서비스 제공
먼저 멀티 프로세스 기반 서버를 알아본다.
본 예제는 리눅스 기반 코드이다.
프로세스란
(운영체제에 등록되어) 메모리 공간을 차지한 상태에서 실행중인 프로그램을 의미한다.
- fork 함수
pid fork(void);
성공 시 프로세스 ID, 실패 시 -1 반환
fork 함수는 호출한 프로세스의 복사본을 생성한다. 기존 프로세스에 할당한 메모리를 모두 복사하고 자식 프로세스로써 새로운 프로세스를 생성한다.
fork.cpp
#include <cstdio>
#include <unistd.h>
int g_val = 10;
int main(int argc, char**argv)
{
pid_t pid;
int l_val = 20;
pid = fork();
if(pid == 0) // if child process
{
++g_val;
++l_val;
}
else{// if parent process
--g_val;
--l_val;
}
if(pid == 0)
{
printf("Child Proc: [%d, %d]\n", g_val, l_val);
}
else{
printf("Parent Proc: [%d, %d]\n", g_val, l_val);
}
return 0;
}
fork()를 분기로 하여금 if문에서 pid == 0일 경우 자식 프로세스의 실행영역이고 그렇지 않다면 부모 프로세스의 실행영역이다.
위를 실행한다면 다음과 같이 출력되어 각 프로세스의 다른 실행 흐름을 보여준다.
- 좀비 프로세스
좀비 프로세스는 생성된 프로세스가 자원을 반납하고 사라지지 않아 시스템 상에 남아있는 프로세스를 가리킨다.
main()을 엔트리로 갖는 프로세스의 종료조건은 exit() 함수 혹은 return을 통해 프로세스가 종료되는데, 이 때 exit(int) 함수 인자 혹은 return 반환값이 운영체제로 전달되어 부모 프로세스에 전달된다. 만약 종료 함수를 호출했더하더라도 이 값이 부모 프로세스에게 전달되지 않으면 프로세스가 해제되지 않는다.
- wait 함수
pid_t wait(int* startloc);
- 성공 시 종료된 자식 프로세스 ID, 실패 시 -1
- wait 함수는 이미 종료된 자식 프로세스가 있다면, 자식 프로세스의 반환값이 startloc에 저장된다.
- startloc에는 종료된 프로세스의 복합적인 정보가 담겨 있으니 다음 함수로 종료 반환 인자를 추출할 수 있다.
wait 함수는 블로킹 함수로 현재 종료된 자식 프로세스가 없다면, 블로킹 상태로 대기한다.
- WIFEXITED: 자식 프로세스가 정상 종료한 경우 true를 반환
- WEXITSTATUS: 자식 프로세스의 반환값
if(WIFEXITED(status))
{
puts("Normal Termination");
printf("Child pass num: %d", WEXITSTATUS(status));
}
- 두 개의 자식 프로세스를 생성하고 두 프로세스의 반환값을 얻어 출력한다.
wait.cpp
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
int status;
pid_t pid = fork();
// first child process
if(pid == 0)
{
return 3;
}
else
{
printf("Child PID: %d \n", pid);
pid = fork();
// second child process
if(pid == 0)
{
exit(7);
}
// parent process
else
{
printf("Child PID: %d \n", pid);
// catches first child process
wait(&status);
if(WIFEXITED(status))
{
printf("Child send one: %d \n", WEXITSTATUS(status));
}
// catches second child process
wait(&status);
if(WIFEXITED(status))
{
printf("Child send two: %d \n", WEXITSTATUS(status));
}
sleep(30);
}
}
return 0;
}
- waitpid 함수
pid_t waitpid(pid_t pid, int *startloc, int options);
- 성공 시 종료된 자식 프로세스의 pid 실패 시 -1
- pid: 종료를 확인하고자하는 프로세스 ID, -1 전달시 임의의 프로세스를 관측.
waitpid 함수는 wait 함수의 블로킹 상태를 없앨 수 있다. options 인자에 WNOHANG을 전달하면, 종료된 자식 프로세스가 존재하지 않아도 블로킹 상태에 있지 않고, 0을 반환하면서 함수를 빠져나온다.
waitpid.cpp
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
int status;
pid_t pid = fork();
// first child process
if(pid == 0)
{
sleep(5);
return 24;
}
else
{
// 자식 프로세스 종료시 까지 대기
while(!waitpid(-1, &status, WNOHANG))
{
}
if(WIFEXITED(status))
printf("Child send %d\n", WEXITSTATUS(status));
}
return 0;
}
시그널 핸들링
waitpid의 예시 코드에서 블로킹 상태에 있지 않음에도 결국 자식 프로세스가 종료될 때까지 기다려야 한다. 이렇게 현재 자식 프로세스가 종료되었는 지 계속 확인하는 busy-waiting 문제로 부모 프로세스는 CPU 자원을 사용하여 결과를 확인해야한다.
부모 프로세스를 잠시 쉬게했다가 자식 프로세스가 종료되었을 때 신호를 주어 자식 프로세스가 종료되었음을 알려주고 그 때 깨어나도록 하는 것이 시그널 핸들링이라고 한다.
프로세스는 운영체제에게 시그널 핸들링을 유발(Trigger)하는 신호 정보와 콜백 함수을 설정하여 시그널을 등록한다.
- signal 함수
#include <signal.h>
void (*signal(int signo, void (*func)(int))) (int);
시그널 등록 함수 signal은 함수의 반환 값을 void (int) 형의 함수 포인터로 지정하고 있다.
- signo: 시그널 발동 조건
- func: void (int) 형 함수 포인터로 시그널 핸들링으로 호출할 콜백 함수e
대표적이 시그널 발동 조건(상황)으로 전달할 수 있는 값이다.
- SIGALRM: alaram 호출을 통해 등록된 시간이 만료된 상황
- SIGINT: CTRL+C가 입력된 상황
- SIGCHILD: 자식 프로세스가 종료된 상황
자식 프로세스가 종료할 때, void (int)형 함수 myfunc을 호출하고 싶다면 다음과 같이 시그널을 등록한다.
signal(SIGCHLD, myfunc);
- alarm 함수
unsigned int alarm(unsigned int seconds);
등록한 seconds 시간이 지나면 SIGALARM을 발생시키는 함수이다.
signal.cpp
#include <cstdio>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if(sig == SIGALRM)
puts("Time out!");
alarm(2);
}
void keycontrol(int sig)
{
if(sig == SIGINT)
puts("CTRL+C pressed");
}
int main(int argc, char**argv)
{
int i ;
signal(SIGALRM, timeout);
signal(SIGINT, keycontrol);
alarm(2);
for(i=0;i<3;++i)
{
puts("wait...");
sleep(100);
}
return 0;
}
메인 함수에서 wait... 출력 후 100초 동안 sleep 상태에 있을 거라 예상되지만 실제로는 alarm(2)의 3번의 호출로 block 상태에 있던 프로세스가 wakeup 하게 된다. timeout 함수를 보면 내부에서 alarm(2)를 호출하기 때문에 한 번 시그널 핸들링으로 timeout이 호출되면 alaram 설정이 되기 때문에 연쇄적으로 2초마다 프로세스를 깨운다.
- sigaction 함수를 이용한 시그널 핸들링
sigaction 함수를 이용하여 시그널을 등록할 수 있다.
int sigaction(int signo, const struct sigaction* act, struct sigaction* oldact);
- 성공 시 0, 실패 시 -1
- signo: 등록할 신호 정보
- act: 시그널 발생시 호출할 함수
- oldact: 이전에 등록되었던 함수, 없다면 0
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
}
sigaction 구조체에 sa_handler가 siganl 함수에서 등록했던 콜백 함수를 대체하고 있다.
int main(int argc, char**argv)
{
int i ;
struct sigaction act;
act.sa_handler = timeout;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGALRM, &act, 0);
alarm(2);
for(i=0;i<3;++i)
{
puts("wait...");
sleep(100);
}
return 0;
}
- 시그널 핸들링을 통한 좀비 프로세스 소멸
remove_zombie.cpp
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void read_childproc(int sig)
{
int status;
pid_t id = waitpid(-1, &status, WNOHANG);
if(WIFEXITED(status))
{
printf("Removed proc id : %d\n", id);
printf("Child send: %d \n", WEXITSTATUS(status));
}
}
int main(int argc, char *argv[])
{
pid_t pid;
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, & act, 0);
pid = fork();
if(pid == 0) // the first child process
{
puts("Hi! I am the first child proces");
sleep(10);
return 12;
}
else // parent process
{
printf("Child proc id: %d \n", pid);
pid = fork();
if(pid == 0) // the second child process
{
puts("Hi! I am the second child process");
sleep(10);
exit(24);
}
else
{
int i;
printf("Child proc id: %d \n", pid);
for(i=0;i<5;++i)
{
puts("wait...");
sleep(5);
}
}
}
return 0;
}
자식 프로세스가 소멸할 때마다 read_childproc()를 호출하고 waitpid를 통해 함수 반환 인자를 알 수 있게 된다.