네트워크/개념

[Network] - 멀티 프로세싱 서버

빗방울소리 2022. 3. 4. 01:14

 다중접속 서버를 구현하는 방법으로는 여러 가지가 있는데,

멀티 프로세싱, 멀티 플렉싱, 멀티 쓰레딩 등 여러 방법이 있습니다.

 

 이런 방법을 쓰지 않더라도 while, for등의 반복문을 통해서도 여러명의 클라이언트가 

접속할 수 있는 서버를 구현할 수 있지만, 성능면에서 많이 차이가 납니다.

클라이언트 한명과 통신이 끝날때 까지 서버는 다른 클라이언트를 상대할 수 없으니

그런 서버는 솔직히 1대1이 아닌이상 의미가 없는 서버일 것 입니다.

 

 이 글에는 첫 번째로 멀티 프로세싱에 대해 정리를 해보고자 합니다.

대부분의 내용은 윤성우 저자의 '열혈 TCP/IP 소켓 프로그래밍' 를 읽은 내용과

인터넷에서 자료를 찾아서 읽고 최종적으로 정리 하였습니다.

 

- 서론

 일단 서버 프로그램을 실행하면 그에대한 프로세스가 띄워집니다.

여기서 서버 프로그램은 예제의 코드를 컴파일하고 실행한 프로그램으로 보면 됩니다.

 

 일반적으로 프로그램 하나당 하나의 프로세스를 생성하므로 그림과 같이 서버에 대해 하나의

프로세스가 생성이 될 것입니다.

 

 하지만 위에서 말했다시피, 별다른 기법없이 하나의 프로세스로는 동시에 한 명의 클라이언트만

서버에 접속할 수 있습니다. 순서를 기다리면 한명씩 서버에 접속할 순 있겠지만, 대기시간이 무한정 길어집니다.

 

 그렇다면 어떻게 해야 여러명의 클라이언트들이 동시에 서버에 접속하여 사용자의 만족감을 높일 수 있을까?

그렇게 사용되는 방법이 멀티 프로세싱, 멀티 플렉싱, 멀티 쓰레딩 기법입니다.

 

- 멀티 프로세싱이란?

멀티프로세싱 기반의 다중 접속 서버

 이 글에서는 멀티 프로세싱에 대해 정리하였습니다. 멀티 프로세싱은 말 그대로 프로세스를 여러개 생성합니다.

서버 프로그램을 실행하면 먼저 부모 프로세스가 생성이 되고,

클라이언트가 접속할 때마다 자식 프로세스를 생성하여 클라이언트와 통신하도록 하는 기법입니다.

 

 이렇게 되면 클라이언트와의 연결은 새로 생성한 프로세스가 유지, 관리하므로 부모 프로세스는

계속해서 다음 클라이언트와의 연결에 대기, 수락 할 수 있는 상태로 있을 수 있는 것입니다.

 

#include <unistd.h>

pid_t fork(void); // 성공 시 프로세스 ID, 실패시 -1 반환

 

 프로세스의 복제는 fork() 라는 함수를 사용합니다.

해당 함수는 unistd.h 라이브러리를 include 해야 사용가능하기 때문에 윈도우 에서는 멀티 프로세싱 기법을

사용할 수 없습니다.

 

 참고로 프로세스는 생성 될 때마다 운영체제로 부터 PID(process ID) 라는 것을 부여 받기 때문에

fork 함수의 반환 값을 이용해서 부모 프로세스만 실행할 영역과 새로 생성된 자식 프로세스만 실행할

영역을 구분하는 식으로 서버를 구현 할 수 있습니다.

 

- 좀비 프로세스?

 fork 함수에 의해 복제된 자식 프로세스들은 return이나 exit함수를 통해 종료 됩니다.

하지만 프로세스가 종료 되었음에도 메모리에서 내려가지 않고, 대기 하게 됩니다.

 

 이 상태에 놓인 프로세스가 좀비 프로세스 입니다.

이미 프로세스가 종료 되었으나 메모리를 차지 하고 있으니 이는 메모리 낭비로 이어지게 됩니다.

그럼 이 프로세스를 완전히 없애는 방법은 무엇일까?

 

 부모 프로세스에 의해 생성된 자식 프로세스들은 return이나 exit함수에 의해 종료 되면, 반환 값을 부모 프로세스

에게 전달한 후에 완전히 메모리상에서 내려 갑니다. 헌데 이 값을 부모 프로세스가 자동으로 받는 것이 아니라

부모 프로세스에서 운영체제에 요청을 해야 반환 값을 전달 받고, 자식 프로세스를 완전히 종료 시킬 수 있는 것입니다.

 

 반환 값을 요청하는 방법에는 wait(), waitpid() 함수 등이 있으나 대부분 signal을 이용하기에 생략하겠습니다.

 

#include <signal.h>

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

 

 시그널 함수의 원형입니다. 함수 포인터를 사용하기에 좀 복잡해 보일 수 있어서

풀어서 쓰면,

 

typedef void (*handler_type)(int);
handler_type signal(int signum, handler_type handler);

 

 이와 같이 선언 되어 있는것을 한줄로 쓴 것이라고 이해 할 수 있습니다.

signal 함수의 첫 번째 인자는 특정 상황에 대한 정보를 나타냅니다.

 

* SIGALRM : alarm 함수호출을 통해서 등록된 시간이 된 상황

* SIGINT : CTRL + C 가 입력된 상황 ( 강제종료 )

* SIGCHLD : 자식 프로세스가 종료된 상황

 

 따라서 멀티 프로세싱을 구현하기 위해 SIGCHLD를 이용하여 자식프로세스의 종료를 부모프로세스가

인지할 수 있습니다.

 

#include <signal.h>

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

 

 signal 함수를 이용해 시그널핸들링을 할 수도 있지만, 일반적으로는

sigaction 함수를 이용합니다. 이 함수는 singnal 함수의 역할을 대신 하면서도 이식성이

더 높기 때문에 signal 함수보다는 sigaction 함수를 씁니다.

 

 signal 함수와 다르게 두번째 인자로 sigaction 이라는 구조체가 들어가는데

이 구조체의 구조는 다음과 같습니다.

 

struct sigaction {
	void(*sa_handler)(int);
    	sigset_t sa_mask;
    	int sa_flags;
}

 

 첫 번째 멤버로 시그널 핸들러 함수 포인터를 가지고 있는것을 확인 할 수 있습니다.

두 번째 멤버인 sa_mask 는 모든 비트를 0으로 초기화 해줘야 합니다. 참고로 두 번째, 세 번째 두 멤버는 시그널과 관련된 옵션을 설정하는데 쓰이지만, 좀비 프로세스 방지를 위한 코드에서는 별다른 설정이 필요 없습니다. 

 

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

void timeout(int sig) {
    if(sig==SIGALRM)
    	    puts("Time out!");
    
}    

int main(void) {

    struct sigaction act;
    act.sa_handler = timeout;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sig action(SIGALRM, &act, 0);
	
    ...
    
    return 0;
}

 

 위에서 설명한 내용을 바탕으로 sigaction 구조체를 설정하고

시그널을 등록하는 코드 입니다.

 

 위와 같이 시그널을 등록하면 alarm 함수가 호출 될 때마다

해당 시그널의 핸들러 함수인 timeout이 호출이 됩니다.

 

 이제 시그널 핸들링에 대한 내용을 바탕으로 좀비 프로세스를

방지할 수 있게 되었고, 이를 바탕으로 멀티프로세싱을 기반으로한 다중 접속 서버를 구현할 수 있습니다.

 

- echo_mpserv.c

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <unistd.h>
  5 #include <signal.h>
  6 #include <sys/wait.h>
  7 #include <arpa/inet.h>
  8 #include <sys/socket.h>
  9 
 10 #define BUF_SIZE 30
 11 void error_handling(char *message);
 12 void read_childproc(int sig);
 13 
 14 
 15 int main(int argc, char *argv[]) {
 16 
 17         int serv_sock, clnt_sock;
 18         struct sockaddr_in serv_adr, clnt_adr;
 19 
 20         pid_t pid;
 21         struct sigaction act;
 22         socklen_t adr_sz;
 23         int str_len, state;
 24         char buf[BUF_SIZE];
 25 
 26         if(argc!=2) {
 27                 printf("Usage : %s <port>\n", argv[0]);
 28                 exit(1);
 29         }
 30         // 시그널 등록
 31         act.sa_handler=read_childproc; 
 32         sigemptyset(&act.sa_mask);
 33         act.sa_flags=0;
 34         state=sigaction(SIGCHLD, &act, 0);
 35 
 36         serv_sock=socket(PF_INET, SOCK_STREAM, 0);
 37         memset(&serv_adr, 0, sizeof(serv_adr));
 38         serv_adr.sin_family=AF_INET;
 39         serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
 40         serv_adr.sin_port=htons(atoi(argv[1]));
 41 
 42         if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
 43                 error_handling("bind() error");
 44         if(listen(serv_sock, 5)==-1)
 45                 error_handling("listen() error");
 46 
 47         while(1) {
 48                 adr_sz=sizeof(clnt_adr);
 49                 clnt_sock=accept(serv_sock, (struct sockaddr*) &clnt_adr, &adr_sz);
 50                 if(clnt_sock==-1) // 클라이언트 접속 실패
 51                         continue;
 52                 else // 클라이언트 접속 성공
 53                         puts("new client connected..."); 
 54                 pid = fork(); // 프로세스 복제
 55                 if(pid==-1){ // 프로세스 복제 실패
 56                         close(clnt_sock);
 57                         continue;
 58                 }
 59                 if(pid==0){ // 자식 프로세스 실행 영역
 60                         close(serv_sock);
 61                         while((str_len=read(clnt_sock, buf, BUF_SIZE))!=0)
 62                                 write(clnt_sock, buf, str_len);
 63 
 64                         close(clnt_sock);
 65                         puts("client disconnected...");
 66                         return 0;
 67                 }
 68                 else // 부모 프로세스 실행 영역
 69                         close(clnt_sock);
 70 
 71         }
 72         close(serv_sock);
 73         return 0;
 74 }
 75 
 76 void read_childproc(int sig) { // 시그널 핸들러 ( 좀비 프로세스 방지 )
 77         pid_t pid;
 78         int status;
 79         pid=waitpid(-1, &status, WNOHANG);
 80         printf("removed proc id: %d \n", pid);
 81 }
 82 void error_handling(char *message) {
 83 
 84         fputs(message, stderr);
 85         fputc('\n', stderr);
 86         exit(1);
 87 }

 

  지금까지 설명한 내용을 바탕으로 구현한 멀티프로세싱 기반 다중 접속 서버입니다.

책에 있는 예제 코드로, 코드 의미 해석을 위해 주석을 달았습니다.

실행시 루프백 ip와 임의의 포트번호를 넘겨서 서버를 작동 시킬 수 있습니다.

 

ex) ./(프로그램 이름) 127.0.0.1 9190

-> 프로그램을 실행한 컴퓨터로 9190포트를 이용해 서버 가동

 

 코드에서 보이듯이 while(1) 반복문에서 끊임없이 클라이언트 들의 접속을 받고 있는 것을 확인 할 수 있습니다.

echo 서버이므로, 접속자가 서버에게 보낸 내용을 그대로 클라이언트에게 보내주는 단순한 서버 입니다.

 

 기능이 특별하진 않지만 멀티프로세싱을 이용해서 다중접속 서버를 구현하는게 목적이므로

이 코드를 이해 하는것이 매우 중요하다고 생각합니다.

 

- echo_client.c

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <unistd.h>
  5 #include <arpa/inet.h>
  6 #include <sys/socket.h>
  7 
  8 #define BUF_SIZE 1024
  9 void error_handling(char *message);
 10 
 11 int main(int argc, char * argv[]) {
 12 
 13         int sock;
 14         char message[BUF_SIZE];
 15         int str_len;
 16         struct sockaddr_in serv_adr;
 17 
 18 
 19         if(argc!=3) {
 20                 printf("Usage : %s <ip> <port>\n", argv[0]);
 21                 exit(1);
 22         }
 23 
 24         sock=socket(PF_INET, SOCK_STREAM, 0);
 25         if(sock == -1)
 26                 error_handling("socket() error");
 27 
 28         memset(&serv_adr, 0, sizeof(serv_adr));
 29         serv_adr.sin_family = AF_INET;
 30         serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
 31         serv_adr.sin_port = htons(atoi(argv[2]));
 32 
 33         if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
 34                 error_handling("connect() error!");
 35         else
 36                 puts("Connected........");
 37 
 38 
 39         while(1) {
 40 
 41                 fputs("Input message(Q to quit): ", stdout);
 42                 fgets(message, BUF_SIZE, stdin);
 43 
 44                 if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
 45                         break;
 46 
 47                 write(sock, message, strlen(message));
 48                 str_len=read(sock, message, BUF_SIZE-1);
 49                 message[str_len]=0;
 50                 printf("Message from server: %s\n", message);
 51         }
 52 
 53         close(sock);
 54         return 0;
 55 
 56 }
 57 
 58 
 59 
 60 void error_handling(char *message) {
 61         fputs(message, stderr);
 62         fputc('\n', stderr);
 63         exit(1);
 64 }

 

 에코 클라이언트 코드입니다. 특별한 내용은 없으며,

서버에 접속해서 메시지를 보내고 서버로부터 수신받는 코드입니다.

 

 

- 실행 화면

실제 실행 화면

 세명의 클라이언트가 차례로 접속하였고, 모든 클라이언트가 접속 종료한 후에 서버를 내린 모습입니다.