建立 SysV 風格的 linux daemon 程式

sparkdev發表於2020-04-17

本文介紹如何使用 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

相關文章