데몬 프로세스 (daemon process)

본 포스트는 APUE chap.13 을 참고하여 작성함.

데몬은 오래사는 프로세스를 말하고, 보통은 시스템이 부팅될 때 같이 켜지고 종료될 때 같이 끝남.
Controlling 터미널이 없어서 background에서 돈다라고 표현한다.
데몬의 구조, 작성법, 오류 로깅에 대해 설명하려고 함!

데몬의 특징

ps -efj 와 같은 명령어로 확인 controlling 터미널이 없는 프로세스 (데몬)을 확인할 수 있다.

보통 parent pid 가 0 이면 커널 프로세스임. (참고, init은 예외. 부팅때 커널이 시작한 user level commane 이다.)
위에 캡쳐된 화면에서 대괄호에 있는 애들 모두 커널프로세스. 해당 버전에서는 kthreadd 가 다 생성하기 때문에 parent pid 가 2임.
이러한 커널프로세스들은 시스템과 lifetime 이 같고, 수퍼유저 권한으로 실행되고, controlling 터미널과 command line 이 없다.

커널의 component 중에서, process context 에서 수행되어야 하지만, user-level process가 invoke 하지 않을 component 들은 보통 kernel daemon을 가지고 있다. linux의 예를 보면,

  • kswapd : pageout 데몬. dirty page나 free page를 swapping out 해서 회수(reclaim)되도록 함.
  • flush , sync_supers, jbd 등의 page 관리, file system 관리하는 데몬들.

이 외에도 많은 데몬들이 있음. 커널 프로세스가 아닌 것들의 예로는 rpcbind, inetd, cron, atd, cupsd, sshd 등등.

데몬의 특성을 정리하면,

  • 대부분의 데몬은 superuser 권한으로 실행됨.
  • controlling 터미널을 가진 데몬은 없음. 위의 캡처에서 확인하면, tty 항목이 ? 로 표시된 것을 볼수 있음. (아마도 setsid 를 호출했을 것)
  • 대부분의 user-level 데몬은 프로세스 그룹 리더 이자 세션 리더 이고, 그룹과 세션에 혼자 있음.
  • user-level 데몬의 부모는 init 프로세스임.

참고로, controlling terminal를 가진다면 아래와 같은 문제가 발생할 수 있음.

  • 사용자가 터미널 escape 문자를 써서 (대표적으로 CONTROL-c, CONTROL-z 등) 원치 않게 프로세스를 끝내 버리거나, suspend시킬 수 있다.
  • 원격 터미널 연결이 끊길 경우, 원치 않게 프로세스가 종료될 수 있다.
Coding Rules

기본 규칙들을 지켜서 코딩하면 "unwanted interactions from happening" 을 막을 수 있음.

  1. unmask 를 호출해서 파일 모드 생성값 (보통은 0, 생성된 파일은 777)을 지정하자. 데몬이 상속받은 mask는 특정 권한이 막혀있을 수 있으니, 풀어두자. 필요하면 막아 두는 것도 가능 (ex. 007로 생성되도록)

  2. fork를 호출하고 부모가 exit을 호출한다. 데몬이 셸 명령어로 실행된 경우, 부모가 종료되면 셸은 끝났다고 생각함. 자식은 해당 그룹의 리더가 아니게 되는데, setsid를 호출해서 성공하려면 리더가 아니어야 함.

  3. setsid를 호출해서 새 세션을 생성한다. 그러면, 새 세션의 리더가 되고, 프로세스 그룹의 리더가 되고, controlling 터미널과 끊어진다.

이 상태에서 한 번 더 fork⁠를 하고, 이 때 부모 프로세스를 종료하면 현재 프로세스는 (sid != pid) 세션 리더가 아니기 때문에, (혹시 실수로라도) controlling 터미널을 얻을 수 없는 상태가 됨

  1. 현재 working directory를 root로 변경. 부모로 부터 물려받은 working directory는 mount된 file system일 수 있기 때문에, (해당 file system은 unmount할 수 없음) 특정 작업 디렉토리로 working directory 변경

  2. 불필요한 fd들을 다 닫자. 부모로부터 상속받은 fd를 계속 들고 있는 것을 방지하도록

  3. 어떤 데몬은 fd 0,1,2를 /dev/null로 open할 것. 특정 library routine에서 standard in/out/error를 사용해도 영향 받지 않도록

#include "apue.h"
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
 
void
daemonize(const char *cmd)
{
    int                 i, fd0, fd1, fd2;
    pid_t               pid;
    struct rlimit       rl;
    struct sigaction    sa;
    /*
     * Clear file creation mask.
     */
    umask(0);
 
    /*
     * Get maximum number of file descriptors.
     */
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
        err_quit("%s: can't get file limit", cmd);
 
    /*
     * Become a session leader to lose controlling TTY.
     */
    if ((pid = fork()) < 0)
        err_quit("%s: can't fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    setsid();
 
    /*
     * Ensure future opens won't allocate controlling TTYs.
     */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
        err_quit("%s: can't ignore SIGHUP");
    if ((pid = fork()) < 0) /* 한번 더 fork해서 제어터미널을 아예 갖지 못하도록 만들어 버림 */
        err_quit("%s: can't fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
 
    /*
     * Change the current working directory to the root so
     * we won't prevent file systems from being unmounted.
     */
    if (chdir("/") < 0)
        err_quit("%s: can't change directory to /");
 
    /*
     * Close all open file descriptors.
     */
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; i < rl.rlim_max; i++)
        close(i);
 
    /*
     * Attach file descriptors 0, 1, and 2 to /dev/null.
     */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
 
    /*
     * Initialize the log file.
     */
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
          fd0, fd1, fd2);
        exit(1);
    }
}

이거 main에서 call 하고 sleep 하는거 만들어서 돌려보면,

잘 나옴. 9054는 없어져서 세션리더가 없는 데몬이 되었음을 알 수 있다.

Error Logging

데몬이 가지고 있는 문제 중 하나는 error message를 다룰 방법이 없음
각자 파일에 따로쓰면 볼때 머리아픔
4.2BSD로 부터 사용된 BSD syslog facility가 널리 쓰임.

BSD syslog facility 구조

로그 메세지를 남기는 데에는 3가지 방법이 있음.

  • kernel routine에서 log 함수 호출
  • user process에서 syslog 함수 호출
  • 이 host나 연결된 다른 host의 user process에서 UDP port 514로 log message를 send

이 메세지들은 syslogd 데몬이 읽어주는데, 부팅될때 conf 파일 (보통은 /etc/syslog.conf)를 통해서 어디로 메세지를 보낼지 결정함. (tos는 "local7.info /var/log/tos.log" 라고 써있음) 보통 시스템은 admin한테 급한메세지는 보내도록 한다던지, 콘솔에 출력되도록 한다던지 써져있다고 함.

#include <syslog.h> 
void openlog(const char *ident,int option,i nt facility); 
void syslog(int priority,const char *format,...); 
void closelog(void); 
int setlogmask(int maskpri);

위 인터페이스들을 통해서 로깅 시스템을 사용할 수 있음.

  • openlog: 호출은 optional. 호출되지 않았으면 최초 syslog 호출시 openlog가 자동 호출 됨
    ident : 각각의 log msg에 추가할 문장 (보통 program 이름)
    option : 471쪽의 표 13.3에 있는 option중에 하나를 선택 가능
    facility : 472쪽의 표 13.4 에 있는 것중 선택 가능. facility (출처라고 보면 됨...) 마다 log가 다뤄지는 방법을 다르게 할 수 있음 (config file을 통해)
  • syslog
    priority : facility + level(472쪽의 표 13.5에서 선택가능)
    format : vsprintf로 전달될 인자
    format에서 처음 만난 %m으로 errno에 해당되는 error message string이 들어감
  • closelog 호출은 optional. 이 함수는 단지 syslogd daemon과 통신에 사용한 fd를 닫음
  • setlogmask : maskpri에 set된 priority만 logging됨
    maskpri에 0을 넣으면 아무 영향 없음
    기존 mask를 return

예시. 프린터 스풀러 데몬 (lpd) 에서 볼 수 있을 만한 코드.

openlog("lpd", LOG_PID, LOG_LPR);
syslog(LOG_ERR, "open error for %s: %m", filename);

위와 같은 코드

syslog(LOG_ERR|LOG_LPR, "open error for %s: %m", filename);

참고, 실제로 cups에 있는 비슷한 코드 ㄷㄷㄷ

openlog("cups-lpd", LOG_PID, LOG_LPR);
syslog(LOG_ERR, "Unable to open temporary control file \"%s\" - %s", control, strerror(errno));

이 외에도, 많은 시스템에서 logger 프로그램을 제공. 구현에 따라 추가적인 기능이 있음 (vsyslog 등)

대부분의 syslogd 구현은 짧은 시간동안 msg를 queue 해서, 같은 message가 그 사이에 중복으로 오면 기록하지 않는 기능이 있음 (대신 "last message repeated N times." 같은걸 찍어줌)

Single-Instance 데몬

어떤 데몬들은 혼자 있어야 잘 동작하는 애들이 있음.
예를 들어, (1) exclusive access to a device가 필요한 경우 (2) cron daemon (주기적으로 특정 프로그램을 실행시키는 daemon) 같은 경우 여러개가 있을 경우 중복적으로 program을 수행시킴.
데몬이 driver 단에서 다중 접근이 막힌 특정 device에 접근해야 한다면 자동으로 데몬이 하나만 떠 있게 됨.

직접 구현하는 방법
file-locking 이나 record-locking을 통해 한번에 하나만 돌아가게 구현가능
daemon이 특정 위치에 file을 만들고 전체에 대해 write lock을 잡음
두번째 실행된 daemon은 fail, 이미 daemon이 존재함을 알 수 있음
daemon이 종료되면 자동적으로 lock이 풀림.

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)
 
extern int lockfile(int);

int
already_running(void)
{
    int     fd;
    char    buf[16];
 
    fd = open(LOCKFILE, O_RDWR|O_CREAT, LOCKMODE);
    if (fd < 0) {
        syslog(LOG_ERR, "can't open %s: %s", LOCKFILE, strerror(errno));
        exit(1);
    }
    if (lockfile(fd) < 0) {
        if (errno == EACCES || errno == EAGAIN) {
            close(fd);
            return(1);
        }
        syslog(LOG_ERR, "can't lock %s: %s", LOCKFILE, strerror(errno));
        exit(1);
    }
    ftruncate(fd, 0);
    sprintf(buf, "%ld", (long)getpid());
    write(fd, buf, strlen(buf)+1);
    return(0);
}

lockfile 은 파일이 잠겨있을 경우, 에러 설정하고 끝나도록 되어있음.

데몬의 관례 (Conventions)

많은 Unix system의 daemon관련 관례

  1. daemon이 lock file을 이용하면, 그 file은 /var/run 폴더 아래에 저장
    여기에 file 만들라면 superuser permission이 필요
    file 이름은 일반적으로 name.pid

  2. 만약 daemon이 configuration options을 제공하면, 그 파일은 /etc 폴더에 저장
    configure file 이름은 name.conf
    ex) /etc/syslog.conf

  3. daemon이 command line에서 시작될 수 있지만 일반적으로 initialize script중 하나에서 시작
    /etc/rc* or /etc/init.d/*
    종료시 자동시작 되야 한다면, /etc/inittab의 respawn entry에 추가

  4. 보통 configure file이 있다면 시작할 때만 확인
    그래서 다시읽을려면 daemon을 재시작해야하는데
    이게 싫으면 SIGHUP signal을 catch하여 config file을 다시 읽게 함
    (SIGHUP : 터미널 인터페이스의 연결이 종료되면 받는 시그널.)

예제
  • multi-thread 데몬에서 configuration 파일을 다시 읽는 경우
#include "apue.h"
#include <pthread.h>
#include <syslog.h>
 
sigset_t    mask;
 
extern int already_running(void);
 
void
reread(void)
{
    /* ... */
}
 
void *
thr_fn(void *arg)
{
    int err, signo;
 
    for (;;) {
        err = sigwait(&mask, &signo);
        if (err != 0) {
            syslog(LOG_ERR, "sigwait failed");
            exit(1);
        }
 
        switch (signo) {
        case SIGHUP:
            syslog(LOG_INFO, "Re-reading configuration file");
            reread();
            break;
 
        case SIGTERM:
            syslog(LOG_INFO, "got SIGTERM; exiting");
            exit(0);
 
        default:
            syslog(LOG_INFO, "unexpected signal %d\n", signo);
        }
    }
    return(0);
}
 
int
main(int argc, char *argv[])
{
    int                 err;
    pthread_t           tid;
    char                *cmd;
    struct sigaction    sa;
 
    if ((cmd = strrchr(argv[0], '/')) == NULL)
        cmd = argv[0];
    else
        cmd++;
 
    /*
     * Become a daemon.
     */
    daemonize(cmd);
 
    /*
     * Make sure only one copy of the daemon is running.
     */
    if (already_running()) {
        syslog(LOG_ERR, "daemon already running");
        exit(1);
    }
 
    /*
     * Restore SIGHUP default and block all signals.
     */
    sa.sa_handler = SIG_DFL;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
        err_quit("%s: can't restore SIGHUP default");
    sigfillset(&mask);
    if ((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0)
        err_exit(err, "SIG_BLOCK error");
 
    /*
     * Create a thread to handle SIGHUP and SIGTERM.
     */
    err = pthread_create(&tid, NULL, thr_fn, 0);
    if (err != 0)
        err_exit(err, "can't create thread");
    /*
     * Proceed with the rest of the daemon.
     */
    /* ... */
    exit(0);
}

돌려보면 아래처럼 두 process 가 죽고 세번째 프로세스가 데몬이 잘 되어있음.
그리고, pthread를 통해 thread가 2개가 되어 sigwait 하고 있음.

kill 명령어를 통해 sighup을 직접 줘보니 thread3.2 에 reread 가 호출됨을 알 수 있다.

Client - Server 모델

일반적으로 데몬은 server역할이고, 이때 server라 함은 client의 연결을 기다리는 프로세스이다.
위에서 본 syslogd process도 서버로 log msg를 기다림
하지만 msg를 받기만 하지 뭔가 돌려주거나 하진 않음
뒷 chapter 에서 server client의 양방향 통신 모델이 많이 나올 것임