創建 SysV 風格的 linux daemon 程序

本文介紹如何使用 C 語言創建 Linux 系統中 SysV 風格的 daemon 程序。注意:這是一種舊式的 daemon 程序寫法,進入 systemd 時代後是不需要通過這樣的方式創建 daemon 程序的。 本文的演示環境為 ubuntu 18.04。

創建 daemon 程序的流程

通過前文《Linux session(會話)》我們了解到,如果要讓程序運行在後台,必須處理好進程的 session。所以在創建 daemon 程序的過程中處理 session 問題是很重要的一步,當然除此之外還需要其它的步驟。下面是在 Linux 系統中創建一個 SysV 風格的 daemon 的基本流程:

  1. 從父進程 fork 出一個子進程
  2. 為子進程創建新的 session ID
  3. 在子進程中再 fork 一次
  4. 修改 umask
  5. 修改進程的當前工作目錄
  6. 關閉進程中的文件描述符

接下來我們通過代碼來介紹這些操作的含義。

創建 daemon 程序

從父進程 fork 出一個子進程
創建一個子進程,如果成功就讓父進程退出,此時的子進程已經成為了 init 進程的子進程:

pid_t pid;

pid = fork();
if (pid < 0)
    exit(EXIT_FAILURE);
if (pid > 0)
    exit(EXIT_SUCCESS);

為子進程創建新的 session ID
運行在後台的進程需要擺脫 session 終端的束縛,通過 setsid() 函數為進程設置新的 session ID 可以做到這一點:

pid_t pid;

pid = fork();
if (pid < 0)
    exit(EXIT_FAILURE);
if (pid > 0)
    exit(EXIT_SUCCESS);

if (setsid() < 0)
    exit(EXIT_FAILURE);

********************************
執行到這裡時,PID==PGID==SID

********************************

在子進程中再 fork 一次
這次 fork 的目的是防止進程再次獲得終端。因為只有 session leader 才能獲得終端,而這次 fork 使子進程變成了非 session leader:

pid_t pid;

pid = fork();
if (pid < 0)
    exit(EXIT_FAILURE);
if (pid > 0)
    exit(EXIT_SUCCESS);

if (setsid() < 0)
    exit(EXIT_FAILURE);
    
/* 第二次 fork */
pid = fork();
if (pid < 0)
    exit(EXIT_FAILURE);

if (pid > 0)
    exit(EXIT_SUCCESS);

********************************
執行到這裡時,PGID==SID 但是已經不等於 PID 了,說明進程已經不是 session leader

********************************

修改 umask
為了能夠向 daemon 進程創建的任何文件中寫入內容(包括日誌),必須重置 umask(file mode mask, umask),以確保能夠正確地寫入或讀取這些文件:

umask(0);

修改進程的當前工作目錄
必須保證進程的當前工作目錄是存在的。因為眾多的 Linux 發行版中很多都沒有完全遵守標準的文件目錄結構,所以最好是把進程的當前工作目錄設置為 /,這樣可以避免因設置了某個目錄而導致它無法被 unmount:

chdir("/");

關閉進程中的文件描述符
關閉進程中所有打開的文件描述符:

int x;
for (x = sysconf(_SC_OPEN_MAX); x>=0; x--)
{
    close (x);
}

把日誌寫入 syslog
Daemon 程序的日誌非常重要,我們可以通過 openlog、syslog 和 closelog 三個函數把日誌內容寫入到 syslog  中:

openlog ("daemondemo", LOG_PID, LOG_DAEMON);
syslog (LOG_NOTICE, "Daemon demo is running, number: %d", count);
closelog();

本文 demo 輸出的日誌如下所示:

完整的代碼

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <syslog.h>

static void demo_daemon()
{
    pid_t pid;

    /* Fork off the parent process */
    pid = fork();

    /* An error occurred */
    if (pid < 0)
        exit(EXIT_FAILURE);

    /* Success: Let the parent terminate */
    if (pid > 0)
        exit(EXIT_SUCCESS);

    /* On success: The child process becomes session leader */
    if (setsid() < 0)
        exit(EXIT_FAILURE);

    /* Catch, ignore and handle signals */
    //TODO: Implement a working signal handler */
    //signal(SIGCHLD, SIG_IGN);
    //signal(SIGHUP, SIG_IGN);

    /* Fork off for the second time*/
    pid = fork();

    /* An error occurred */
    if (pid < 0)
        exit(EXIT_FAILURE);

    /* Success: Let the parent terminate */
    if (pid > 0)
        exit(EXIT_SUCCESS);

    /* Set new file permissions */
    umask(0);

    /* Change the working directory to the root directory */
    /* or another appropriated directory */
    chdir("/");

    /* Close all open file descriptors */
    int x;
    for (x = sysconf(_SC_OPEN_MAX); x>=0; x--)
    {
        close (x);
    }

    /* Open the log file */
    openlog ("daemondemo", LOG_PID, LOG_DAEMON);
}

int main()
{
    int count = 0;
    demo_daemon();

    while (1)
    {
        //TODO: Insert daemon code here.
        count ++;
        syslog (LOG_NOTICE, "Daemon demo is running, number: %d", count);
        sleep (5);
        if(count > 5)
        {
            break;
        }
    }

    syslog (LOG_NOTICE, "Daemon demo terminated.");
    closelog();

    return EXIT_SUCCESS;
}

把上面的代碼保存到文件 daemondemo.c 中(也可以從這裡下代碼),然後執行下面的命令進行編譯就可以得到可執行文件 daemondemo:

$ gcc -Wall daemondemo.c -o daemondemo

關於 fork 兩次

這是一個很有意思的話題,有人說需要 fork 兩次,有人說第二次是可選的,究竟該如何做呢?當我們理解了第二次 fork 的用途後就可以自行決定是否需要第二次 fork 了。
這還需要從 session 的控制終端說起。控制終端是進程的一個屬性,通過 fork 系統調用創建的子進程會從父進程那裡繼承控制終端。這樣,session 中的所有進程都從 session 領頭進程那裡繼承控制終端。前面已經說過了,要把程序變成 daemon,就得讓進程擺脫 session 的終端。而這些在第一次 fork 後調用 setsid() 函數就搞定了。那麼如果接下來不小心再給進程添加了終端該怎麼辦?答案是不讓你添加!這就是第二次 fork 的作用。只有 session leader 才能獲得終端,而第二次 fork 使子進程變成了非 session leader,你想犯錯也不給你機會了。

像 nginx 和 gblic 的 daemon 函數的實現都是 fork 一次,所以說第二次 fork 是可選的,你可以根據自己的實際情況來決定。

參考:
Linux Daemon Writing HOWTO
Creating a daemon in Linux
daemon man page
daemon 函數
Unix Daemon Server Programming
glibc daemon.c