일반적인 서버는 대부분 단일 클라이언트를 받아들이기 보다는 여러개의 클라이언트를 동시에 받아들이는 경우가 대부분이다.
telnet, web, ftp, smtp, nntp 등 대부부분의 서버가 한번에 여러개의 클라이언트 (이하 다중연결)를 처리한다.
이러한 다중연결을 처리하기 위해 4가지 정도의 표준적인 방법이 존재하는데, 이번장에서는 Unix 환경하에서 다중연결처리를 위해서 가장 널리 사용되는 방법인 fork(2) 에 대해서 알아보도록 하겠다.
사실 fork 는 네트웍플밍을 위한 기법이라기 보다는 일반적인 시스템플밍을 위해 사용되는 기법이고, 이를 네트웍플밍에 확대시켜서 사용한것이라고 할수 있다.
fork 는 프로세스를 복사해서 하나의 프로그램이 동시에 여러개의 업무를 처리 할수 있도록 하는 기법으로, fork 를 사용하게 되면 부모프로세스를 복사한 새로운 프로세스를 생성하게 된다. 이때 가장 먼저 실행되는 원본 프로세스를 "부모프로세스"라 하고 이 부모프로세스에게서 복사된 프로세스를 "자식프로세스" 라고 한다.
fork 에 대한 자세한 내용은 다른 문서들을 살펴보도록하고(나중에 이 사이트에도 문서로 올리겠습니다), 이번문서 에서는 간단한 예재로 개념만 살펴보도록 하겠다.
예제: fork.c
프로그램을 실행하면 다음과 같은 결과가 발생할것이다.
이제 ps 를 이용해서 프로세스 상황을 한번 알아보도록 하자.
ps 에 -eo 옵션을 사용하면 일정한 포맷에 준한 레포트출력을 가능하게 도와준다. %U 는 유저이름 %p 는 pid, %P 는 ppid, %c 는 command 를 나타낸다. 이외의 자세한 내용을 알고 싶다면 ps 의 맨페이지를 참조하기 바란다.
아뭏든 위의 내용을 보면 별도의 PID를 가지는 2개의 동일한 이름을 가진 프로 세스가 생겼음을 알수 있다.
그리고 1582 PID를 가지는 프로세스의 PPID를 보면 부모 프로세스의 PID가 1581 임을 알수 있고, 이를 통해서 1581 이 부모 프로세스 1582 가 자식 프로세스임을 알수 있다.
우리는 fork 의 이러한 특성을 이용해서 다중연결을 받아들이는 서버를 제작할수 있다. 다음은 셈플로 알아보는 소켓프로그래밍(1) 에 있는 주소록 프로그램을 다중연결 가능하도록 수정한 코드이다. 클라이언트는 위의 서버를 위해 작성한 클라이언트를 그대로 사용하도록 한다.
예제 : zipcode_multi.c
zipcode_multi.c 를 컴파일한다음, port 번호를 주고 실행을 하고, 여러개의 zipcolde_cl 을 이용해서 접근을 해보자.
동시에 다수의 클라이언트를 처리한다는것을 알수 있을것이다.
fork 는 다중의 클라이언트를 제어하는 매우 확실한 방법을 제공해준다.
하나의 클라이언트를 처리하다가 문제가 생기더라도 해당 자식프로세스에게만 영향을 끼치므로 전체 서비스에 지장이 생길염려도 없고, 코드역시 다른 select, poll, thread 를 사용하는것에 비해서 간단하며, 디버깅이 용이 하다라는 장점도 가지고 있다.
거의 유일하다고 생각되는 단점은 자식프로세스를 생성하는데, 많은 시간과 자원이 소모된다는점과, 어느정도 이상의 자식프로세스를 생성시켰을때 Unix 버젼에 따라서 생성프로세스의 수에 제한이 생긴다는 점이다.
물론 다른 방법들도(select, poll..) 이러한 제한이 있긴하지만, fork 의 경우 좀더 이러한 제한에 신경을 써주어야 한다.
telnet, web, ftp, smtp, nntp 등 대부부분의 서버가 한번에 여러개의 클라이언트 (이하 다중연결)를 처리한다.
이러한 다중연결을 처리하기 위해 4가지 정도의 표준적인 방법이 존재하는데, 이번장에서는 Unix 환경하에서 다중연결처리를 위해서 가장 널리 사용되는 방법인 fork(2) 에 대해서 알아보도록 하겠다.
사실 fork 는 네트웍플밍을 위한 기법이라기 보다는 일반적인 시스템플밍을 위해 사용되는 기법이고, 이를 네트웍플밍에 확대시켜서 사용한것이라고 할수 있다.
fork 는 프로세스를 복사해서 하나의 프로그램이 동시에 여러개의 업무를 처리 할수 있도록 하는 기법으로, fork 를 사용하게 되면 부모프로세스를 복사한 새로운 프로세스를 생성하게 된다. 이때 가장 먼저 실행되는 원본 프로세스를 "부모프로세스"라 하고 이 부모프로세스에게서 복사된 프로세스를 "자식프로세스" 라고 한다.
fork 에 대한 자세한 내용은 다른 문서들을 살펴보도록하고(나중에 이 사이트에도 문서로 올리겠습니다), 이번문서 에서는 간단한 예재로 개념만 살펴보도록 하겠다.
예제: fork.c
#include <unistd.h> #include <stdlib.h> int main(int argc, char **argv) { int pid; pid = fork(); if (pid > 0) { printf("부모 프로세스\n"); pause(); } else if (pid == 0) { printf("자식 프로세스\n"); pause(); } else if (pid == -1) { perror("fork error : "); exit(0); } } |
[yundream@localhost test]# ./fork 부모 프로세스 자식 프로세스 |
[yundream@localhost /root]# ps -eo "%U %p %P %c" | grep fork yundream 1581 1483 fork yundream 1582 1581 fork |
아뭏든 위의 내용을 보면 별도의 PID를 가지는 2개의 동일한 이름을 가진 프로 세스가 생겼음을 알수 있다.
그리고 1582 PID를 가지는 프로세스의 PPID를 보면 부모 프로세스의 PID가 1581 임을 알수 있고, 이를 통해서 1581 이 부모 프로세스 1582 가 자식 프로세스임을 알수 있다.
우리는 fork 의 이러한 특성을 이용해서 다중연결을 받아들이는 서버를 제작할수 있다. 다음은 셈플로 알아보는 소켓프로그래밍(1) 에 있는 주소록 프로그램을 다중연결 가능하도록 수정한 코드이다. 클라이언트는 위의 서버를 위해 작성한 클라이언트를 그대로 사용하도록 한다.
예제 : zipcode_multi.c
#include <sys/types.h> #include <sys/stat.h> #include <sys/socket.h> #include <signal.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char **argv) { int server_sockfd, client_sockfd; int state, client_len; int pid; FILE *fp; struct sockaddr_in clientaddr, serveraddr; char buf[255]; char line[255]; if (argc != 2) { printf("Usage : ./zipcode [port]\n"); printf("예 : ./zipcode 4444\n"); exit(0); } memset(line, '\0', 255); state = 0; // 주소 파일을 읽어들인다. client_len = sizeof(clientaddr); if((fp = fopen("zipcode.txt", "r")) == NULL) { perror("file open error : "); exit(0); } // internet 스트림 소켓을 만들도록 한다. if ((server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket error : "); exit(0); } bzero(&serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(atoi(argv[1])); state = bind(server_sockfd , (struct sockaddr *)&serveraddr, sizeof(serveraddr)); if (state == -1) { perror("bind error : "); exit(0); } state = listen(server_sockfd, 5); if (state == -1) { perror("listen error : "); exit(0); } signal(SIGCHLD, SIG_IGN); while(1) { client_sockfd = accept(server_sockfd, (struct sockaddr *)&clientaddr, &client_len); pid = fork(); if (pid == 0) { if (client_sockfd == -1) { perror("Accept error : "); exit(0); } while(1) { memset(buf, '\0', 255); if (read(client_sockfd, buf, 255) <= 0) { close(client_sockfd); exit(0); } if (strncmp(buf, "quit",4) == 0) { write(client_sockfd, "bye bye", 8); close(client_sockfd); exit(0); break; } while(fgets(line,255,fp) != NULL) { if (strstr(line, buf) != NULL) { write(client_sockfd, line, 255); } memset(line, '\0', 255); } write(client_sockfd, "end", 255); printf("send end\n"); rewind(fp); } } if (pid == -1) { perror("fork error : "); } } close(client_sockfd); } |
동시에 다수의 클라이언트를 처리한다는것을 알수 있을것이다.
fork 는 다중의 클라이언트를 제어하는 매우 확실한 방법을 제공해준다.
하나의 클라이언트를 처리하다가 문제가 생기더라도 해당 자식프로세스에게만 영향을 끼치므로 전체 서비스에 지장이 생길염려도 없고, 코드역시 다른 select, poll, thread 를 사용하는것에 비해서 간단하며, 디버깅이 용이 하다라는 장점도 가지고 있다.
거의 유일하다고 생각되는 단점은 자식프로세스를 생성하는데, 많은 시간과 자원이 소모된다는점과, 어느정도 이상의 자식프로세스를 생성시켰을때 Unix 버젼에 따라서 생성프로세스의 수에 제한이 생긴다는 점이다.
물론 다른 방법들도(select, poll..) 이러한 제한이 있긴하지만, fork 의 경우 좀더 이러한 제한에 신경을 써주어야 한다.