TCP/IP Server model:

    여러개의 child 프로세스를 최소 단위로 실행후 모자라면 더 fork( )하는 pattern

 

초당 수 백개의 client가 동시에 접속하여 송수신하고 접속된 socket만 close하고 child는 종료하지 않고 대기하면서 fork( )에 대한 부담을 줄이는 pattern입니다. 부모 프로세스는 혹시라도 child 프로세스가 비정상적으로 종료되었는 지를 체크합니다. child 프로세스가 비정상적으로 종료되었으면 새로운 프로세스를 실행시켜줍니다.

 

만약, 전체 child 프로세스가 작업중이면 프로세스 개수를 늘립니다. 설정된 max child process의 개수를 초과하지 않는 한도 내에서 프로세스가 모자라면 fork( )합니다. 일반적으로 더 이상 프로세스의 갯수가 많이 필요하지 않는 경우에도 줄이지 않고 그대로 유지합니다. 이와 같은 프로그램 pattern은 1회의 요청에 1회 응답하는 pattern의 업무에 좋습니다. 대화형(한번 접속후에 데이터 송수신을 계속하다가 특정 데이터 수신시 close하는 업무)에서 사용해도 상관은 없으나 프로세스를 너무 오랫동안 잡고 있는 업무에서는 바람지하지 않습니다.

 

또한, 수백, 수천 개의 프로세스가 동시에 실행되어 업무처리 후에도 종료를 하지 않기 때문에 프로그램을 다운 시키는 프로그램도 필요합니다. 프로세스간에 상태를 공유하고 전달하기 위하여 shared memory를 사용하였습니다. 이와 같이 동시에 많은 프로세스가 먼저 실행해서 대기하는 프로그램은 웹서버(httpd)와 티맥스, 턱시도 등과 같은 middleware 등이 있습니다.

 


TCP/IP 서버 daemon 프로그램 (ready_fork.c)

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <tcpip_lib.h>

#define     SERVER_PORT         6905
#define     SHD_MEM_KEY         6905

/* child stat */
#define     CHILD_STAT_IDL      0
#define     CHILD_STAT_BUSY     1

/* child shutdown */
#define     CHILD_NORMAL        0
#define     CHILD_SHUTDOWN      1
#define     CHILD_RESTART       2

/* parent control */
#define     PARENT_NORMAL           0
#define     PARENT_WAIT_DOWN        1
#define     PARENT_FORCE_DOWN       2
#define     PARENT_CHILD_RESTART    3

/*
* child의 상태정보를 저장합니다.
* child의 호출건수만 예를들었지만, 
* 실행시작 시간 등 통계에 필요한 정보도 추가할 수 있습니다.
*/
typedef struct {
    int     pid;
    int     stat;
    int     shutdown;
    unsigned long call_count;
} child_info_t;

typedef struct {
    int     status;    /* 부모 프로세스의 상태 통제*/
    child_info_t children[];
} process_info_t;

/*
* server port, shared memory key 들은 환경설정파일에서 읽어 오도록
* 변경하는 것이 좋음
*/
int g_server_port   = SERVER_PORT;
int g_shd_mem_key   = SHD_MEM_KEY;

/*
* child process의 min, max 값은 상수가 아닌 전역변수로 선언하여
* 별도의 환경 설정파일에서 읽어오거나 main parameter로 전달함.
*/
int g_max_child_count = 200; /*최대 실행할 child 프로세스 수*/
int g_min_child_count = 50;  /*최초에 실행할 child 프로세스 수*/
int g_cur_child_count = 0;   /*현재 실행중인 child 프로세스 수*/

int g_binded_socket;    /* 서버 소켓 scriptor*/
int g_accepted_socket;  /* 접속된 소켓 scriptor*/

process_info_t  *g_process_info;
child_info_t    *g_children; /* 전체 child의 상태정보 */
child_info_t    *g_child;    /* child 자신의 상태정보 */

void parent_main(void);
int  create_child(void);
void create_children(void);
void shutdown_all_children(int flag);
void children_control(void);
void child_main(void);
void child_job(void);
 
int main(int argc, char *argv[])
{
    int shmid;
    child_info_t    *child;
    int     idx;
    int     fd;
    int     mem_size;
    struct  sigaction   act_ignore;

    /* 
    * background program으로 실행과 terminal에 의한 signal 발생을 배제함. (Control-C 등) 
    * 최초실행 프로그램은 종료하여 부모 process id가 1인 child 프로세스가 됨.
    * 또한 session 그룹의 leader가 아닌 프로세스가 됨
    */
    if(fork() != 0) {
        /* child process가 아니면 종료 */
        exit(0);
    }
    
    /*
    * 새로운 session을 생성하여 session그룹의 leader가 됨
    * 새로운 session은 terminal의 영향을 받지 않음.
    */
    if(setsid() == -1) {
        fprintf(stderr, "NEW SESSION ERROR: %s\n", strerror(errno));
        exit(1);
    }
    
    /*
    * 프로세스의 home directory를 ROOT디렉토리로 변경함.
    */
    if(chdir("/") == -1) {
        fprintf(stderr, "CHDIR ERROR: %s\n", strerror(errno));
        exit(1);
    }

    /*
    * 표준입력/표준출력/표준오류 처리를 할 수 없도록 하기 위하여 /dev/null 파일을 open합니다.
    */
    if((fd = open("/dev/null", O_RDWR)) == -1) {
        exit(1);
    }
    
    /*
     * 표준입력의 동작을 못하도록 redirect 처리함.   
     * scanf(), fgets(..., stdin) 등의 함수는 정상동작하나 입력은 안됨.
     */
    dup2(fd, 0); 
 
    /*
     * 표준출력을 출력하지 못하도록 redirect 처리함. 
     * printf() 등의 함수는 정상동작하나 출력은 안됨
     */
    dup2(fd, 1); 
    
    /*
     * 표준오류를 출력하지 못하도록 redirect 처리함. 
     * fprintf(stderr, ...)등의 함수는 정상동작하나 출력은 안됨
     */
    dup2(fd, 2); 
 
    close(fd);   /* 불필요해진 fd는 close함 */

    memset(&act_ignore, 0x00, sizeof(act_ignore));
    act_ignore.sa_handler    = SIG_IGN;
    sigaction(SIGBUS,  &act_ignore, NULL);
    sigaction(SIGSEGV, &act_ignore, NULL);
    sigaction(SIGILL,  &act_ignore, NULL);
    sigaction(SIGFPE,  &act_ignore, NULL);

   /*
     * 이미 프로그램이 실행중이면 shared memory를 지우고 재생성하면 안되기 때문에
     * 먼저 socket 생성부터 진행함.
     */
    if((g_binded_socket = TCPIPserver(g_server_port)) == -1) {
        /* TO-DO : 오류 로그 생성 */
        exit(1);
    }

    /* accept할 socket은 프로세스의 상태도 체크해야하기 때문에 NON-BLOCKING으로 설정함 */
    fcntl(g_binded_socket, F_SETFL, O_NONBLOCK);
    
    /* shared memory를 생성함 */
    mem_size = sizeof(process_info_t) + sizeof(child_info_t) * g_max_child_count;
    shmid = shmget((key_t)g_shd_mem_key, mem_size, 0600 | IPC_CREAT);
    if (shmid == -1) {
        perror("shmget failed : ");
        exit(0);
    }
 
    /* 생성된 shared memroy를 참조함 */
    g_process_info = shmat(shmid, (void *)0, 0);
    if (g_process_info == (void *)-1) {
        perror("shmat failed : ");
        exit(0);
    }
    
    memset(g_process_info, 0x00, mem_size);
    g_children = g_process_info->children;
 
    create_children();

    parent_main();
}

/*
 * 1. child process의 main으로 부모 프로세스가 종료 요청을 한 경우에는 
 *    업무 처리후에 스스로 종료처리함.
 * 2. child process의 main으로 client의 접속을 대기하고 child_job( )함수를 호출하여
 *    client요청을 처리하도록 함.
 */
void child_main(void)
{
    int acceted_socket;
    struct  sockaddr_in client;
 
    /* 
    부모 프로세스가 CHILD_SHUTDOWN으로 설정하거나 CHILD_RESTART로 설정하면 프로세스를 종료함.
    CHILD_SHUDOWN인 경우는 전체 프로세스를 종료하거나 임시로 생성된 프로세스를 정리하기 위함.
    CHILD_RESTART인 경우는 변경된 라이브러리를 재반영하거나 기타 이유로 재기동이 필요한 경우
    */
    while(g_child->shutdown != CHILD_SHUTDOWN && g_child->shutdown != CHILD_RESTART) {
        g_child->stat     = CHILD_STAT_IDL;
        /* 부모프로세스가 종료되었으면 종료함. */
        if(getppid() == 1) {
            exit(0);
        }
 
        if((g_accepted_socket = TCPIPaccept(g_binded_socket, &client)) == -1) {
            /* TO-DO : 오류 로그 생성 */
            continue;
        }
 
        /* 
        * 혹시라도 g_accept_socket은 전역변수라 child_job에서 다른 값을 대입하여  
        * socket이 close되지 않는 것을 막기 위하여 accept_socket을 보관하여 close함
        */
        acceted_socket = g_accepted_socket;
        g_child->stat  = CHILD_STAT_BUSY;
        g_child->call_count++;

        child_job();
 
        close(acceted_socket);
    }
 
    return;     
}

/*
 * 접속한 client와 실제로 데이터를 주고 받는 로직을 구현해야 합니다.
 */
void child_job(void)
{
    /* TO-DO: 이 함수는 실제 업무 로직을 구현합니다. */

#if 0    
    /* 
    * TO-DO: 변수는 업무에 맞게 데이터 송수신 구조체를 정의하여 사용합니다. 
    * 필요시 전역변수를 선언하여 사용합니다.
    */
    int recv_len;
    char recv_buf[4096];
    char send_buf[4096];
    
    if((recv_len = TCPIPrecv(g_accepted_socket, recv_buf, 2048, 0)) == -1) {
        /* TO-DO : 오류 로그 생성 */
        return ;
    }
    
    /* TO-DO : 입력한 데이터로 처리하는 로직  */
    /* ...... */
    
    if((TCPIPsend(g_accepted_socket, send_buf, 2048, 0)) == -1) {
        /* TO-DO : 오류 로그 생성 */
        return ;
    }
#endif
}

/*
 * 새로운 child process를 생성합니다.
 * 새로운 child process는 전역변수 g_child에 설정되므로 
 * create_child 함수를 실행하기 전에 g_child에 child 위치를 설정해야 함.
 */
int create_child(void)
{
    pid_t   child_pid;
    
    if((child_pid = fork()) == -1) {
        return -1;
    } else if(child_pid == 0) {
        g_child->stat     = CHILD_STAT_IDL;
        g_child->shutdown = CHILD_NORMAL;
        child_main();
        exit(0);
    } else if(child_pid > 0) {
        g_child->pid = child_pid;
        return 0;
    }   
}

/*
 * 비정상 종료된 child process가 있으면 
 * 새로운 프로세스를 실행합니다.
 */
void killed_children_check(void)
{
    int     idx;
    pid_t   child_pid;
    int     status;
    
    while(1) {
        if((child_pid = waitpid(-1, &status, WNOHANG)) == 0) {
            return;
        }
        if(child_pid == -1) {
            return;
        }
        for(idx = 0; idx < g_cur_child_count; idx++) {
            if(g_children[idx].pid != child_pid) {
                continue;
            }
            /* 비정상적으로 종료되어 재실행되어야 할 프로세스이면 */
            if(g_children[idx].shutdown == CHILD_NORMAL || g_children[idx].shutdown == CHILD_RESTART) {
                g_child = &g_children[idx];
                create_child();
                break;
            }
        }
    }   
}

/*
 * child 프로세스가 모두 작업중이면, 프로세스가 더 필요한 것으로 판단하여
 * 새로운 프로세스를 실행합니다.
 */
void busy_children_check(void)
{
    int idx;
    int running_child;
    child_info_t    *child;
    
    /* 부모 포르세스는 child process 상태를 감시하면서 process 개수를 늘릴지 검사함.*/
    running_child = 0;
 
    for(idx = 0; idx < g_cur_child_count; idx++) {
        child = &g_children[idx];
        /* process가 실행중이면 */
        
        if(child->stat == CHILD_STAT_BUSY) {
            running_child++;
        }
        
    }
 
    /* 모두 실행중이면 */
    if(running_child == g_cur_child_count) {
        /* 아직 max child count에 도달하지 않았으면, */
        if(g_cur_child_count < g_max_child_count) {
            g_child = &g_children[g_cur_child_count];
            /* 새로운 child를 생성함 */
            if(create_child() == 0) {
                g_cur_child_count++;
            }
        }
    }
}

/*
* 부모프로세스는 1초마다 
* 1. 프로세스를 통제하는 프로그램에서 프로세스 다운 명령을 내렸는 지를 체크하여
*    child reboot, 강제종료, 업무처리 대기 후 종료 등을 수행함.
* 2. child process가 비정상적으로 종료되었는 지 
*    확인하여 비정상 종료된 프로세스는 다시 생성함.
* 3. 놀고있는 프로세스가 없는 경우에는 프로세스 수가 모자라므로
*    새로운 프로세스를 실행시킴(g_max_child_count 보다 적게 실행중이면)
*/
void parent_main(void)
{
    /* 부모 포르세스는 child process 상대 감시하면서 process 개수를 늘릴지 확인함.*/
    while(1) {
        sleep(1);
		
        children_control();
        killed_children_check();
        busy_children_check();
    }
}
 
/* 최초 min 갯수만큼 child process 생성함 */
void create_children(void)
{
    int idx;
 
    for(g_cur_child_count = 0; g_cur_child_count < g_min_child_count; g_cur_child_count++) {
        g_child = &g_children[g_cur_child_count];
        if(create_child() == -1) {
            break;
        }
    }

    /* 프로세스가 생성이 제대로 되지 않았으면, 전체 종료함 */
    if(g_cur_child_count < g_min_child_count) {
        g_process_info->status = PARENT_FORCE_DOWN;
        children_control();
        exit(0);
    }
}

/*
* child process 전체 종료 및 restart 관리 
* daemon 관리 프로그램(작성 필요)에서 shared memory의 상태를 변경하였을 경우
* 그에 따라서 child reboot, 강제종료, 업무처리 대기 후 종료 등을 수행함.
*/
void children_control(void)
{
    int idx;
    child_info_t    *child;
 
    if(g_process_info->status == PARENT_NORMAL) {
        return;
    }
 
    for(idx = 0; idx < g_cur_child_count; idx++) {
        child = &g_children[idx];
        
        if(g_process_info->status == PARENT_CHILD_RESTART) {
            /* child process의 작업이 완료되면, 종료시키고 다시 실행시킴 */
            child->shutdown = CHILD_RESTART;
        } else if(g_process_info->status == PARENT_FORCE_DOWN) {
            /* child process의 작업을 완료하기를 기다리지 않고 강제 종료함 */
            child->shutdown = CHILD_SHUTDOWN;
            kill(child->pid, 9);
        } else if(g_process_info->status == PARENT_WAIT_DOWN) {
            /* child process의 작업이 종료되면, 종료시킴 */
            child->shutdown = CHILD_SHUTDOWN;
        } else {
            g_process_info->status = PARENT_NORMAL;
            return;
        }
    }
    if(g_process_info->status != PARENT_CHILD_RESTART) {
        exit(0);
    }
    g_process_info->status = PARENT_NORMAL;
}

 


 

많은 프로세스를 관리하기 위해서는 프로세관리 프로그램이 필요합니다.

이 명령어 한번으로 수백/수천개의 전체 daemon이 관리하는 프로세스를 종료시키거나 restart시킵니다.

 

프로세스관리 프로그램 (dm_ctl.c)

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> 
#include <sys/ipc.h> 
#include <sys/shm.h> 

#define     SHD_MEM_KEY         6905

/* parent control */
#define     PARENT_NORMAL           0
#define     PARENT_WAIT_DOWN        1
#define     PARENT_FORCE_DOWN       2
#define     PARENT_CHILD_RESTART    3

typedef struct {
    int     status;    /* 부모 프로세스의 상태 통제*/
} process_info_t;

/*
* shared memory key 들은 환경설정파일에서 읽어 오도록
* 변경하는 것이 좋음
*/

int g_shd_mem_key   = SHD_MEM_KEY;

process_info_t  *g_process_info;

void help(int argc, char *argv[])
{
    fprintf(stderr, "%s {-fdown | -wdown | -restart}\n", argv[0]);
    fprintf(stderr, "\tchild 및 process를 관리합니다.\n\n");
    fprintf(stderr, "options\n");
    fprintf(stderr, "\t-fdown   : child process의 업무처리를 기다리지 않고 강제 종료\n");
    fprintf(stderr, "\t-wdown   : child process의 업무처리를 기다린 후 종료\n");
    fprintf(stderr, "\t-restart : child process의 업무처리를 기다린 후 child 재시작\n");
}

int main(int argc, char *argv[])
{
    int shmid;
    int mem_size;
    
    if(argc == 1) {
        help(argc, argv);
        return;
    }
    /* shared memory를 다시 생성함 */
    mem_size = sizeof(process_info_t);
    shmid = shmget((key_t)g_shd_mem_key, mem_size, 0600);
    if (shmid == -1) {
        perror("shmget failed : ");
        exit(0);
    }
 
    /* 생성된 shared memroy를 참조함 */
    g_process_info = shmat(shmid, (void *)0, 0);
    if (g_process_info == (void *)-1) {
        perror("shmat failed : ");
        exit(0);
    }
    if(strcmp(argv[1], "-fdown") == 0) {
        g_process_info->status = PARENT_FORCE_DOWN;
    } else if(strcmp(argv[1], "-wdown") == 0) {
        g_process_info->status = PARENT_WAIT_DOWN;
    } else if(strcmp(argv[1], "-restart") == 0) {
        g_process_info->status = PARENT_CHILD_RESTART;
    } else {
        help(argc, argv);    
    }
    return 0;
}

 

소스 다운로드

ready_fork.c
0.01MB
dm_ctl.c
0.00MB

 


 

see also :  TCP/IP 통신 프로그램   Socket 통신 관련 Library  

 

 

 

 

블로그 이미지

사용자 자연&사람

행복한 개발자 programmer since 1995.

Tag , , ,

댓글을 달아 주세요