2018年7月19日 星期四

TLPI: Creating a Daemon, Guidelines for Writing Daemons

初步了解 daemon 後,來看 daemon 建立的步驟:
  1. fork() 結束親行程,繼續的子行程呼叫 setsid() (TLPI §34.3) 成立新的行程群組。
    • 假設 daemon 是從指令行執行,親行程結束讓 shell 得以繼續,留下子行程在背景執行。
    • 親行程可能是行程群組領導,可能帶有其它子行程。fork() 後子行程就不會是領導,呼叫 setsid() 後成立新的獨立行程群組。
  2. 如果 daemon 之後需要開啟終端 device,需要動作確保不會變成控制終端,有兩種方式:
    • open() 任何終端 device 指定 O_NOCTTY 旗標。
    • 另一種較簡易的方式是進行第二次 fork(),一樣只讓子行程繼續執行。如此一來,子行程不會是行程群組領導,依據 Linux 跟隨的 the System V 只有領導取得控制終端機的傳統,可避免取得控制終端機 (TLPI §34.4)。如果是跟隨 BSD 傳統的系統,行程只有明確用 ioctl() TIOCSCTTY 設定後,才會得到控制終端機,第二次 fork() 是多餘的但也沒什麼傷害。
  3. 清除行程的 umask (TLPI §15.4.6),確保 daemon 建立檔案或目錄時,有需要的 permissions.
  4. 在 daemon 結束前,其工作目錄無法卸載 (TLPI §14.8.2),改變工作目錄到根目錄 ( / ) 或其它適當的目錄,可方便在 daemon 結束前卸載。
  5. 繼承的 file descriptor,沒用到的都關閉,以節省資源。 例如 daemon 已經失去控制終端,保留 file descriptors 0、1、和 2 並無意義。例外,開啟的檔案所在的檔案系統無法卸載。
  6. 將 file descriptors 0、1、和 2 都導到 /dev/null。
    • 避免 daemon 呼叫函式庫有輸出無法輸出,造成非預期的錯誤。
    • 避免 daemon 後續開啟檔案使用到 1 和 2,函式庫輸出而造成破壞。
glibc 提供不是標準的函數 daemon() 可轉換呼叫的程式為 daemon,下面是另一個實作的範例。

#include <sys/stat.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
int                             /* Returns 0 on success, -1 on error */
becomeDaemon(int flags)
{
    int maxfd, fd;
    switch (fork()) {                   /* Become background process */
        case -1: return -1;
        case 0:  break;                 /* Child falls through... */
        default: _exit(EXIT_SUCCESS);   /* while parent terminates */
    }
    if (setsid() == -1)                 /* Become leader of new session */
        return -1;
    switch (fork()) {                   /* Ensure we are not session leader */
        case -1: return -1;
        case 0:  break;
        default: _exit(EXIT_SUCCESS);
    }
    if (!(flags & BD_NO_UMASK0))
        umask(0);                       /* Clear file mode creation mask */
    if (!(flags & BD_NO_CHDIR))
        chdir("/");                     /* Change to root directory */
    if (!(flags & BD_NO_CLOSE_FILES)) { /* Close all open files */
        maxfd = sysconf(_SC_OPEN_MAX);
        if (maxfd == -1)                /* Limit is indeterminate... */
            maxfd = BD_MAX_CLOSE;       /* so take a guess */
        for (fd = 0; fd < maxfd; fd++)
            close(fd);
    }
    if (!(flags & BD_NO_REOPEN_STD_FDS)) {
        close(STDIN_FILENO);            /* Reopen standard fd's to /dev/null */
        fd = open("/dev/null", O_RDWR);
        if (fd != STDIN_FILENO)         /* 'fd' should be 0 */
            return -1;
        if (dup2(STDIN_FILENO, STDOUT_FILENO) != STDOUT_FILENO)
            return -1;
        if (dup2(STDIN_FILENO, STDERR_FILENO) != STDERR_FILENO)
            return -1;
    }
    return 0;
}

寫一個程式呼叫 becomeDaemon(0),然後 sleep() 一陣子,可以用指令 ps 來看程式的一些屬性:
$ ./test_become_daemon
$ ps -C test_become_daemon -o "pid ppid pgid sid tty command"
  PID  PPID  PGID   SID TT       COMMAND
24731     1 24730 24730 ?        ./test_become_daemon
TT 下顯示 ? 表示沒有控制終端機。SID 不同於 PID,表示不是 session leader,不會再取得控制終端機。

由於 daemons 是長時間執行。需要特別注意可能的記憶體洩漏 (TLPI §7.1.3) 和 file descriptor 洩漏 (沒有關閉所有開啟的 file descriptor。程式結束不是全部自動關閉嗎?)。

daemon 通常是系統結束時呼叫其 shutdown script,沒有的則由 init process 送 SIGTERM,5 秒後再送 SIGKILL。

許多 daemon 需要確保一個時間只有一個在執行,方法見 TLPI §55.6。

參考來源

  1. TLPI §37.2 & §37.3

沒有留言:

張貼留言

SIP header Via

所有 SIP 訊息 都要有 Via,縮寫 v。一開始的 UAC 和後續途經的每個 proxy 都會疊加一個 Via 放傳送的位址,依序作為回應的路徑。 格式 sent-protocol sent-by [ ;branch= branch ][ ; 參數 ...] s...