본문 바로가기

진리는어디에

[Linux] 데몬(Daemon) 프로세스 만들기

이..이 데몬인가?

데몬(daemon)이란?

'데몬(daemon)' 프로세스는 리눅스 운영 체제서 사용하는 프로세스의 일종으로써, 시스템 시작이 시작할 때 그 생명을 시작하여, 우리가 알지 못하는 백그라운드에서 자신의 할 일을 묵묵히 행하다 시스템과 함께 그 생명을 다한다(대충 컴퓨터 켜면 자동으로 실행해서 끌 때까지 종료되지 않는다는 뜻). 이 페이지에서는 데몬의 특징을 살펴 보고 간단한 데몬 프로세스를 만들어 보도록 하겠다.

참고로 윈도우에서는 '서비스'라는 데몬 프로세스와 비슷한 역할을 하는 것이 있다. 윈도우 '서비스'에 대한 설명은 본 포스트 맨 아래 부록으로 추가 되어있다.

데몬(daemon) 프로세스의 특징

일반적으로 Daemon을 단순히 시스템 백그라운드에서 돌아가고 있는 프로세스라 생각하기 쉽다. 아래의 화면을 보고 Daemon과 일반 백그라운 프로세스의 차이를 알아 보도록 하자. (가장 밑에 있는 프로세스가 필자가 띄운 백그라운드 프로세스다)

[Test]$ ps -axj | more
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    1     4     1     1 ?           -1 SW       0   0:04 [keventd]
    1     7     1     1 ?           -1 SW       0   8:25 [kswapd]
 2182  2382  2382  2182 pts/13    2400 S      528   0:00 ./test_proc

위의 결과는 test_proc라는 무한 루프를 도는 프로그램을 백그라운드로 실행 시키고 난후, 현재 시스템 위에서 돌아가고 있는 프로세스들의 PPID와 PID, PGID를 출력 해본 것이다. 결과에서 알 수 있다시피 Daemon들은 첫째 TTY(터미널 장치)를 가지고 있지 않다. 둘째 PPID(parent id)가 1로 세팅되어 있으며 SID(session id)역시 자신의 아이디와 같다.

데몬(daemon) 프로세스 만들기

위에서 언급 했듯이 데몬 프로세스는 일반 프로세스와는 다른 몇 가지 특징을 가지고 있다고 했다. 데몬 프로세스를 만들기 전에 특징을 자세히 살펴 보고 넘어가도록 하자.

파일을 만들 땐 umask 설정

설명에 앞서 아래 결과 화면을 먼저 살펴 보자.

[Test]$ umask
0002
[Test]$ touch test_umask
[Test]$ ls -la
합계 8
drwxrwxr-x    2 kukuta   kukuta       4096 10월 17 21:36 .
drwxr-xr-x    9 kukuta   kukuta       4096 10월 17 21:14 ..
-rw-rw-r--    1 kukuta   kukuta          0 10월 17 21:36 test_umask
[Test]$ umask 0
[Test]$ touch test_umask_zero
[Test]$ ls -la
합계 8
drwxrwxr-x    2 kukuta   kukuta       4096 10월 17 21:37 .
drwxr-xr-x    9 kukuta   kukuta       4096 10월 17 21:14 ..
-rw-rw-r--    1 kukuta   kukuta          0 10월 17 21:36 test_umask
-rw-rw-rw-    1 kukuta   kukuta          0 10월 17 21:37 test_umask_zero

만일 데몬 프로세스가 파일을 생성해야 하는데, 그 파일이 아무도 읽을 수도, 수정 할 수도 없는 파일이라면 문제가 있는 것이다. 데몬 프로세스가 파일을 생성해야 할 필요 성이 있다면 umask 함수를 사용하여 데몬 프로세스가 생성하는 파일의 접근 권한을 미리 설정 해 두자.

fork()를 이용하여 PPID를 1로

위에서 언급했다 싶이 데몬 프로세스의 특징 중에 하나가 PPID가 1(init)라는 것이다. 여기서 우리가 기억해야 할 것은, 한 프로세스가 자식 프로세스를 만들고, 자식 프로세스가 소멸 되기도 전에 부모가 죽어 버린다면, 자식 프로세스는 init프로세스에게 입양되며, 그 init의 PID가 1이라는 것이다. 

그리고 fork()가 될경우 자식 프로세스는 새로운 프로세스 아이디를 받지만, 그룹 아이디는 부모의 것을 그대로 상속 받게 된다. 한마디로 그룹의 리더가 아니라는 말이다. 이렇게 부모 프로세스를 죽이는 것은 다음에 나올 그룹의 리더가 되기 위한 setsid()호출의 필수불가결한 조건이다. (비정한 프로세스의 세상이다. 그룹의 리더가 되기 위해 부모를 죽이다니...ㅠㅠ)

새로운 Session을 위해 setsid()를..

setsid 함수는 호출하는 프로세스가 그룹의 리더가 아닐때 새로운 세션을 생성하여 다음과 같은 세 가지 일을 한다.

  1. 호출한 프로세스는 새로운 세션의 리더가 된다.
  2. 호출한 프로세스는 새로운 그룹의 리더가 된다.
  3. 프로세스는 컨트롤 터미널을 잃어 버리게 된다.
※ session 이란 ?
하나 또는 이상의 프로세스 그룹들의 집합이다.

새로운 디렉토리를 찾아서..

chdir을 이용하여 현재 워킹 디렉토리를 루트 디렉토리(/)로 변경한다. 부모로 물려 받은 워킹 디렉토리는 파일 시스템에 마운트 되어 있는 것일 수도 있고, 시스템이 정지 할때 까지 살아 있는 Daemon의 특성 때문에 파일 시스템이 언마운트 되지 않을 수도 있다.

필요 하지 않은 파일 디스크립터는 모두 죽인다

데몬 프로세스의 특징 중에 하나는 컨트롤 터미널을 가지지 않는 것이다. 이는 시스템이 시작할 때 아무도 몰래 실행되어 시스템이 죽을 때 그 운명을 같이 하는 데몬 프로세스를 사용자들이 궂이 애써 알 필요도 없고, 컨트롤 해줘야 할 필요도 없는 것이기 때문이다. 데몬에게 여러 필요 없는 파일 디스크립터들을 물린다는 것은 자원의 낭비요, 행여 모를 오류의 원인이 된다.

죽지 않는 파일 디스크립터는 /dev/null에 물린다

Daemon은 터미널 디바이스 자체를 가지고 있지 않다. 그렇기에 0, 1, 2 번 파일 디스크립터 처럼, 터미널과 통신을 하는 것들은 아무런 영향을 줄 수 없는 /dev/null로 보내 버린다.

데몬 프로세스 예제 코드

#include <fcntl.h>
#include <iostream>
#include <signal.h>
#include <string>
#include <syslog.h>
#include <unistd.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>

void daemonize(const char* cmd)
{
    /*
     * set file creation mask to 0
     */
    umask(0);

    /*
     * Get maximum number of file descriptors
     */
    rlimit rl;
    if(getrlimit(RLIMIT_NOFILE, &rl) < 0) {
        std::cerr << "error getlimit" << std::endl;
    }

    pid_t pid;
    /*
     * Become a session leader to loase controlling TTY
     */
    if((pid = fork()) < 0)
    {
        std::cerr << "error fork" << std::endl;
    }
    else if(pid != 0) // parent process
    {
        exit(0);
    }
    setsid();

    struct sigaction sa;
    /*
     * ensure future open won't allocate controlling TTYs.
     */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if(sigaction(SIGHUP, &sa, NULL) < 0)
    {
        std::cerr << "can't ignore SIGHUP" << std::endl;
    }
    
    if(chdir("/") < 0)
    {
        std::cerr << "can't change directory " << std::endl;
    }

    /*
     * Close all file descriptors
     */
    if(rl.rlim_max == RLIM_INFINITY)
    {
        rl.rlim_max = 1024;
    }
    for(int i=0; i<rl.rlim_max; i++)
    {
        close(i);
    }

    /*
     * Attach file descriptors 0, 1, and 2 to /dev/null
     */
    int fd0, fd1, fd2;
    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);
    }
    closelog();
}

부록 1. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!