#keywords programming,daemon,daemonize,fork,setsid,pid,pidfile,background,noclose,nochdir,dup2,umask,chdir,TEMP_FAILURE_RETRY,open,close,write,O_NOCTTY,STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO #title 프로그래밍 [wiki:Home 대문] / [wiki:CategoryProgramming 프로그래밍] / [wiki:DaemonizeProcess Daemon process를 정확히 구현하기 위한 가이드] ---- == [wiki:DaemonizeProcess Daemon process를 정확히 구현하기 위한 가이드] == [[TableOfContents]] * 작성자 조재혁([mailto:minzkn@minzkn.com]) * 고친과정 2017년 2월 9일 : 처음씀 === Daemonize 의 목표 === * Process 를 Fore-ground에서 Background 로 전환합니다. 이를 위해서는 fork()함수를 사용하여 부모프로세스(Parent process)는 곧바로 종료하고 자식프로세스(Child process)가 실행되는 형태로 동작합니다. {{{#!enscript c pid_t s_pid; s_pid = fork(); if(s_pid == ((pid_t)(-1))) { /* ERROR: fork failed */ } else if(s_pid == ((pid_t)0)) { /* daemon main loop */ } else { /* 모든 자원을 반환하도록 구현 (종료시 기본적으로 자원이 반환되지 않는 자원들의 경우 반드시 필요) */ exit(EXIT_SUCCESS); } }}} * 완전히 Background로 실행되는 프로세스가 되기 위해서는 다음과 같은 조건을 선택적(강력히 권고하는 사항)으로 구현해야 합니다. * umask(0) 함수를 호출하여 기본 파일 사용권한을 돌려놓거나 제한을 하는게 좋습니다. {{{#!enscript c umask(0); }}} * 부모프로세스가 곧 종료되므로 Session leader가 되어야 되며 이를 위해서 setsid() 함수가 호출되어야 합니다. (이렇게 함으로써 ppid가 init process를 가르키게 되어 온전한 Process group을 형성할 수 있습니다.) {{{#!enscript c pid_t s_new_session_id; s_new_session_id = setsid(); if(s_new_session_id == ((pid_t)(-1))) { perror("create session and sets the process group id FAILED"); } }}} * 현재 위치한 디렉토리를 최상위 변경하여 실행시점에 있던 디렉토리가 unmount 불가한 것을 방지합니다. (이것으로 인하여 현재 디렉토리가 변경되기 때문에 daemon내에서는 절대경로로 파일을 접근하는 방법 또는 상대경로를 적절한 절대경로로 바꾸는 일련의 구현이 필요합니다. 이를 위해서 realpath() 함수의 사용도 경우에 따라서는 고려할 수 있습니다.) {{{#!enscript c if(chdir("/") == (-1)) { perror("could not change to root directory"); } }}} * stdin, stdout, stderr 을 모두 close 하거나 null로 바꾸어 TTY group으로부터 벗어나도록 합니다. (이것을 하지 않으면 flow control 제어문자를 받으면 process가 stop될 수 있습니다. 그래서 close하지 않으려면 TTY ixon flag 를 끄도록 구현하는것을 권장합니다.) {{{#!enscript c int s_fd_null; for(;;) { s_fd_null = open("/dev/null", O_RDWR | O_NOCTTY); if(s_fd_null != (-1)) { if(dup2(s_fd_null, STDIN_FILENO) == (-1)) { perror("dup stdin failed"); } if(dup2(s_fd_null, STDOUT_FILENO) == (-1)) { perror("dup stdout failed"); } if(dup2(s_fd_null, STDERR_FILENO) == (-1)) { perror("dup stderr failed"); } TEMP_FAILURE_RETRY(close(s_fd_null)); break; } if(errno == EINTR) { continue; } perror("could not open null !"); break; } }}} * 위의 모든 구현이 포함된 daemon(...) 함수를 사용하는것도 좋습니다. {{{#!enscript c pid_t s_pid; s_pid = daemon(0 /* nochdir */, 0 /* noclose */); if(s_pid == ((pid_t)(-1))) { perror("daemonize failed"); } }}} === PIDFIle 의 목표 === PIDFile은 외부프로세스로부터 해당 Process를 제어하기 위해서 PID값을 기록해둔 파일을 말합니다. PIDFile을 생성하고 관리하기 위해서는 생성시점과 기록시점, 그리고 삭제시점을 명확히 그 의미하는 위치에 구현하는게 중요하며 프로그램의 버그나 전원차단등에 의한 비정상 종료시 어떻게 다뤄야 하는지에 대한 시나리오가 중요하다고 볼 수 있습니다. * PIDFile 은 해당 daemon이 실행되고 있는 동안 자신의 pid값이 기록되어 있는 파일을 말합니다. * daemon이 정상종료하는 경우는 만들었던 PIDFile 을 삭제하도록 구현해야 합니다. * 비정상 종료에서는 PIDFile이 삭제되지 않고 남게 되며 다시 daemon을 띄울 때 PIDFile 이 남아 있으면 daemon을 띄우지 않고 경고하는게 필요합니다. 단, 남아있는 PIDFile 에 기록된 pid 에 해당하는 process가 없는 경우 PIDFile을 지우고 정상적으로 daemon을 띄워도 됩니다. * daemon을 종료하고자 할 때 daemon의 실행시점에서 만들어진 PIDFile 을 읽어서 해당 pid로 SIGTERM 또는 SIGQUIT signal을 전송하여 정상적인 종료를 할 수 있습니다. * PIDFile은 크게 다음과 같은 단계에 해당하는 함수를 설계하고 적절한 목적에 맞도록 daemonize 할때 적용해야 합니다. * open_pidfile : PIDFile을 생성합니다. 만약 기존에 PIDFile이 있다면 저장되어 있는 pid 값과 함께 에러를 반환합니다. * open된 PIDFile은 크기가 0 bytes 인 상태로 놓입니다. * write_pidfile : open된 PIDFile 에 실제 자신의 pid값을 getpid()함수를 이용해서 얻어서 정수로 저장합니다. * 파일의 선두부터 pid값을 저장하도록 적절한 seek 후에 저장합니다. (즉, 반드시 Append write 하지 않도록 합니다.) * pid정수값 외에 '\r', '\n' 등이 뒤에 붙는 것은 선택적입니다. (pid값은 10진수이며 뒤에 isspace(x)가 1을 반환하는 모든 문자는 뒤에 붙어도 무시하도록 구현해야 합니다.) * pid 값을 저장후 ftruncate() 등의 함수를 이용하여 저장한 내용 이외의 뒷 부분은 모두 잘라버립니다. * close_pidfile : open된 PIDFile을 close 합니다. * remove_pidfile :open된 PIDFile을 close 하고 PIDFile을 삭제합니다. === Daemonize와 PIDFile 의 생성을 포함한 정확한 Daemon의 구현 === * 실제 daemonize 와 pidfile 의 생성관계를 다음과 같이 구현하면 되겠습니다. {{{#!enscript c #define def_my_pidfile "/var/run/myprocessname.pid" int main(...) { int s_pid_fd; pid_t s_pid; s_pid_fd = open_pidfile(def_my_pidfile); if(s_pid_fd == (-1)) { perror("could not create pidfile"); return(EXIT_FAILURE); } #if 0L s_pid = daemon(0, 0); if(s_pid == ((pid_t)(-1)) { perror("daemonize failed"); remove_pidfile(s_pid_fd); return(EXIT_FAILURE); } #else s_pid = fork(); if(s_pid == ((pid_t)(-1))) { perror("fork failed"); remove_pidfile(s_pid_fd); return(EXIT_FAILURE); } else if(s_pid == ((pid_t)0)) { /* daemon main loop */ } else { /* 모든 자원을 반환하도록 구현 (종료시 기본적으로 자원이 반환되지 않는 자원들의 경우 반드시 필요) */ close_pidfile(s_pid_fd); exit(EXIT_SUCCESS); } (void)umask(0); s_pid = setsid(); if(s_pid == ((pid_t)(-1))) { perror("create session and sets the process group id FAILED"); } /* if(nochdir == 0) */ if(chdir("/") == (-1)) { perror("could not change to root directory"); } /* if(noclose == 0) */ for(;;) { int s_fd_null; s_fd_null = open("/dev/null", O_RDWR | O_NOCTTY); if(s_fd_null != (-1)) { if(dup2(s_fd_null, STDIN_FILENO) == (-1)) { perror("dup stdin failed"); } if(dup2(s_fd_null, STDOUT_FILENO) == (-1)) { perror("dup stdout failed"); } if(dup2(s_fd_null, STDERR_FILENO) == (-1)) { perror("dup stderr failed"); } TEMP_FAILURE_RETRY(close(s_fd_null)); break; } if(errno == EINTR) { continue; } perror("could not open null !"); break; } #endif if(write_pidfile(s_pid_fd, s_pid) == (-1)) { perror("could not write pidfile"); remove_pidfile(s_pid_fd); exit(EXIT_FAILURE); } /* main-loop */ /* normal exit - daemon의 정상종료 */ remove_pidfile(s_pid_fd); return(EXIT_SUCCESS); } }}} === Flow control 제어문자에 의한 Process stop 방지 기술 === * Flow control 제어문자는 Terminal 장치콘솔에 의해서 내부 버퍼를 초과하는 경우 Process를 대기하도록 하여 입/출력를 놓치지 않도록 하는 일련의 제어를 말합니다. 이러한 제어는 process에 SIGTTIN, SIGTTOU, SIGSTOP, SIGCONT 등을 발생시키며 process는 일시정지 및 재개를 반복하기도 하게 됩니다. * 하지만 제어터미널이 proccess를 stop시킨 후에 제어권한을 잃어버리는 일련의 상황이 발생하면 process는 printf, fscanf 등이 누적되어 일시정지상태에 진입하여 다시 재개하지 않는 문제가 발생할 수 있습니다. * 때문에 daemonize 구현시 noclose == 1 인 상태, 즉 STDIN, STDOUT, STDERR을 close 하지 않게 되면 디버깅하는데 유용하기는 하지만 제어문자로 인한 일시정지상태에서 멈추게 될 수 있습니다. * 다음과 같은 구현을 main 시작시에 호출하면 이러한 상태를 방지할 수 있습니다. (또는 프로그램 실행전에 "stty -ixon" 명령으로도 달성 할 수 있습니다.) {{{#!enscript c #include int flow_control(int s_fd, int s_ixon, int s_ixoff, int s_ixany) { struct termios s_termios; if(tcgetattr(s_fd, (struct termios *)(&s_termios)) == (-1)) { return(-1); } if(s_ixon == 0) { s_termios.c_iflag &= (tcflag_t)(~(IXON)); } else if(s_ixon != (-1)) { s_termios.c_iflag |= (tcflag_t)(IXON); } if(s_ixoff == 0) { s_termios.c_iflag &= (tcflag_t)(~(IXOFF)); } else if(s_ixoff != (-1)) { s_termios.c_iflag |= (tcflag_t)(IXOFF); } #if defined(IXANY) /* This is not POSIX : IXANY */ if(s_ixany == 0) { s_termios.c_iflag &= (tcflag_t)(~(IXANY)); } else if(s_ixany != (-1)) { s_termios.c_iflag |= (tcflag_t)(IXANY); } #endif if(tcsetattr(s_fd, TCSANOW, (struct termios *)(&s_termios)) == (-1)) { return(-1); } return(0); } int main(...) { (void)flow_control( STDIN_FILENO, 0 /* s_ixon */, 0 /* s_ixoff */, 0 /* s_ixany */ ); (void)flow_control( STDOUT_FILENO, 0 /* s_ixon */, 0 /* s_ixoff */, 0 /* s_ixany */ ); (void)flow_control( STDERR_FILENO, 0 /* s_ixon */, 0 /* s_ixoff */, 0 /* s_ixany */ ); /* ... */ } }}} === Zombie process === * Zombie process 란 해당 프로세스가 종료되었으나 해당 PID(Process ID) 가 반환되지 않은 상태를 말합니다. * 예제소스: [attachment:make-zombie-source-20181106.tar.gz] * {{{#!enscript c pid_t s_pid; s_pid = fork(); if(s_pid == ((pid_t)(-1))) { perror("fork"); } else if(s_pid == ((pid_t)0)) { /* 자식프로세스는 종료. 그러나 이 자식프로세스는 부모프로세스로부터 Process ID 자원의 회수를 위한 처리(waitpid)가 되지 않으면 Zombit process 상태가 됩니다. 만약 부모프로세스가 waitpid호출없이 종료된다면 Process ID 자원의 회수는 init process 가 처리하게 됩니다. */ exit(0); } else { printf("make a zombie process (pid=%ld)\n", (long)s_pid); for(;;) { /* no waitpid call and forever exit */ sleep(10); } /* waitpid */ } }}} === 참고자료 === * [wiki:LauncherProcess 죽어도 죽지 않는 프로세스를 위한 launcher 만들기] * [^http://cinsk.github.io/articles/daemon.html Daemon 만들때 알아둘 점] * [^http://tdoodle.tistory.com/entry/%EC%8B%9C%EA%B7%B8%EB%84%90%EC%9D%98-%EB%AA%A8%EB%93%A0%EA%B2%83-All-about-Linux-signals 시그널의 모든것 (All about Linux signals)]