[Network] 10. 멀티 프로세스 기반의 서버 구현

2023. 5. 28. 18:17

프로세스의 이해와 활용

다중 접속 서버의 구현 방법들

다중접속 서버 : 둘 이상의 클라이언트에게 동시 접속을 허용하여 동시에 둘 이상의 클라이언트에 서비스를 제공하는 서버를 의미한다.

  • 멀티프로세스 기반 서버 : 다수의 프로세스를 생성하는 방식
  • 멀티플렉싱 기반 서버 : 입출력 대상을 묶어서 관리하는 방식
  • 멀티쓰레딩 기반 서버 : 클라이언트의 수만큼 쓰레드를 생성하는 방식

 

프로세스와 프로세스 IP

프로세스 : 실행 중인 프로그램
멀티 프로세스 OS는 둘 이상의 프로세스를 동시에 생성 가능하다.

운영체제는 생성되는 모든 process에 id(PID)를 할당하며, 이를 ps 명령어를 통해  확인할 수 있다. 

 

fork 함수의 호출을 통한 프로세스의 생성

한 프로그램에서 여러개의 프로세스를 생성할 때에는 fork 명령어를 이용하면 된다.

#include <unistd.h>

pid_t fork(void);

fork를 성공하면 PID를 return 한다. 실패시 -1을 return 한다.

fork 함수가 호출되면 부모 프로세스와 자식 프로세스가 독립적으로 실행되게 되는데, 이때, fork의 반환값을 보고 두 개를 구분할 수 있다. 

부모 프로세스 : fork 의 반환값은 자식의 PID
자식 프로세스 : fork 반환값은 0

이 성질을 이용해 두 프로세스가 할 일을 구분해 코드를 작성할 수 있다.

pit_t pid = fork()
if (pid == 0) // 자식 프로세스
{
	// 자식 프로세스가 할 일
}
else
{
	// 부모 프로세스가 할 일
}

 

 

프로세스 & 좀비 프로세스

좀비 프로세스

실행이 완료되었음에도 소멸하지 않은 프로세스이다. 프로세스는 main 함수가 반환되었을 때 소멸되는데 반환이 되지 않았다는 것은 리소스가 메모리 공간에 존재하고 있다는 의미이다.

좀비 프로세스의 생성 원인

자식 프로세스가 종료될 때의 반환 값이 부모프로세스에 전달되지 않으면 좀비 프로제스가 된다. 

  • 인자를 전달하면서 exit 전달
  • main 함수에서 return 값 전달

위 두 상황이 자식 프로세스의 종료 상태 값이 OS 에 전달되는 방법이다. 

자식 프로세스는 부모 프로세스가 wait() 시스템 함수를 호출해야 종료가 된다. 보통 wait()는 부모 프로세스가 소멸될 때 발생하고 이렇게 되면 할 일이 모두 끝난 상태인데도 부모 프로세스가 종료될 때까지 프로세스 시스템 테이블에 남아있게 된다. 이 것이 많이 누적되다보면 문제가 발생된다.

그래서 좀비 프로세스가 발생하는지 판단하고 이를 회수해주어야하며, wait를 사용하는 방법과 waitpid를 사용하는 2가지 방법이 있다. 

 

좀비 프로세스의 소멸 : wait 함수

int status

pid_t pid = fork();

if (pid == 0)
{
	return 3;
}
else
{
	printf("Child PID: %d\n" , pid);
    
    pid = fork();
    if(pid == 0)
    {
    	exit(7);
    }
    else
    {
    	printf("Child PID : %d\n", pid);
        
        wait(&status); //child process 가 종료될때까지 기다림
        if(WIFEXITED(status))
        	printf("Childe send one : %d\n", WEXITSTATUS(status));
            
        wait(&status);
        if(WIFEXITED(status))
        	printf("Child send two : %d\n", WEXITSTATUS(status));
        sleep(30);
     }
 }

wait 함수의 경우 자식 프로세스가 종료가 되지 않은 경우 블로킹 상태에 빠진다.

WIFEXITED : 자식 프로세스가 정상 종료일 때 true
WEXITSTATUS : 자식 프로세스의 전달 값을 반환함.

 

좀비 프로세스의 소멸 : waitpid 함수

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int * statloc, int options);

waitpid 함수는 wait함수와 다르게 블로킹 상태에 빠지지 않는다는 장점이 있다.

 

if(pid == 0)
{
	sleep(15);
   	return 24;
 }
 else
 {
 	while(!waitpid(-1, &status, WHOHANG))
    {
    	sleep(3);
        puts("sleep 3 sec");
    }
    if(WIFEXITED(status))
    	printf("Child send %d \n", WEXITSTATUS(status));
    }
    return 0;
}

 

 

시그널 핸들링

시그널이란?

특정 상황이 되었을 때 운영체제가 프로세스에게 해당 상황이 발생했음을 알리는 일종의 메시지.

-> 시그널을 미리 등록해두면 프로세스가 OS로부터 시그널을 받을 수 있다. 

 

signal 함수

#include <signal.h>

void (*signal(int signo, void(*func)(int)))(int);

signal은 매개변수가 int 이고 반환형이 void 인 함수 포인터이다.

 

sigaction 함수

#include <signal.h>

int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact);

sigaction 구조체 변수에 함수의 정보를 채워서 인자로 전달한다. 

sa_mask 와 sa_flags 는 모두 0으로 초기화한다. (좀비 소멸 목적으로 쓰는 변수가 아니라서)

 

void timeout(int sig)
{
	if(sig == SIGALRM)
    	puts("time out")
    alarm(2);
}

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;
    
}

 

sa_handler에 handler 함수를 등록하고 sigaction의 인자로 전달한다. 

 

우리는 이 시그널 핸들링 기법을 통해 좀비 프로세스를 자동으로 소멸 시킬 것이다. 

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", SEXITSTATUS(status));
	}
}
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;

sigaction(SIGCHLD, &act, 0);

SIGCHID는 자식 프로세스가 소멸될 때 발생한다. 즉, 자식 프로세스가 소멸될 때 read_childproc을 실행해 자동으로 waitpid가 실행될 수 있도록 한다.

 

 

멀티태스킹 기반의 다중접속 서버

프로세스 기반 다중접속 서버 모델

연결이 하나 생성될 때마다 프로세스를 생성해서 해당 클라이언트에 대해 서비스를 제공하는 것이다.

작동 방식은 아래와 같다.

while(1)
{
	adr_sz = sizeof(clnt_adr);
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
    if(clnt_sock == -1)
    	continue; //클라이언트가 연결될 때 까지 반복
    else
    	puts("new client connected...");
    
    //클라이언트가 들어오면 새 프로세스 생성
    pid=fork(); 
    if(pid==-1) //error ( fork 실패 )
    {
    	close(clnt_sock);
        continue;
    }
    if(pid == 0) // 자식 프로세스 => client
    {
    	close(serv_sock); //서버 소켓은 닫아준다.
        
        // 클라이언트에게 서비스 제공
        while((str_len = read(clnt_sock, buf, BUF_SIZE))!=0)
        	write(clnt_sock, buf, str_len);
        close(clnt_sock);
        
        puts("client disconnected...");
        return 0;
    }
    else
    	close(clnt_sock); // 클라이언트의 소켓은 닫는다.
}

 

fork 함수를 호출하면 디스크립터가 복사된다. 그래서 fork를 실행하고 난 뒤 직접 닫아줘야한다.

 

 

TCP의 입출력 루틴 분할

입출력 루틴 분할의 이점과 의미

소켓은 양방향 통신이 가능하다. 따라서 아래 그림과 같이 입력을 담당하는 프로세스와 출력을 담당하는 프로세스를 각각 생성하면, 입력과 출력을 각각 별도로 진행시킬 수 있다. 

두 개를 분할하면 보내고 받는 과정을 한번에 할 수 있다. 

 

BELATED ARTICLES

more