DAEMON(7)

NAME
daemon - 編寫與打包系統守護進程

描述

"守護進程"的意思是在後臺運行的服務進程， 常用於監督系統的運行或者提供某種功能。 在傳統的 SysV Unix 系統上， 多個守護進程必須嚴格按照特定的順序依次啓動。 在"新型"的 systemd(1) 系統上， 守護進程的啓動順序非常簡單且非常強大。 本手冊同時解說了上述兩種不同的啓動方案， 並特別推薦了應該包含在 systemd 系統中的守護進程。

  傳統的SysV守護進程

傳統的SysV守護進程在啓動的時候， 應該在初始化階段執行下面的步驟：

    1. 關閉除 STDIN STDOUT STDERR 之外的所有文件描述符

    2. 重置所有信號處理器

    3. 重置所有信號掩碼

    4. 清理環境變量(重置一部分，移除一部分)

    5. 調用 fork() 創建一個後臺進程

    6. 在子進程中調用 setsid() 從終端脫離並創建一個獨立的會話

    7. 在子進程中再一次調用 fork() 以確保守護進程永遠無法獲取任何終端。

    8. 第一個子進程主動退出， 只有第二個子進程(實際的守護進程)保持運行， 並且以 init(PID=1) 爲父進程。

    9. 守護進程(第二個子進程)將 STDIN STDOUT STDERR 連接到 /dev/null 虛擬設備

    10. 守護進程將 umask 設爲 0

    11. 守護進程將當前目錄切換到根目錄(/)

    12. 守護進程將自身的PID記錄到例如 /run/foobar.pid 這樣的文件中

    13. 守護進程丟棄自己不需要的權限(如果可以)

    14. 守護進程通知最初的父進程：初始化工作已完成

    15. 最初的父進程自身退出

注意，這些步驟對於下文講述的新型守護進程是不需要的， 除非爲了刻意兼容傳統的SysV系統。

  新型守護進程

Linux 系統上的新型守護進程更容易被監控也更容易實現。

守護進程無需實現前文所描述的複雜步驟， 即可直接在 systemd 提供的乾淨的上下文環境中運行：

環境變量已經被清理、信號處理器與信號掩碼已經被重置、沒有遺留的文件描述符、守護進程自動在其專屬的會話中執行、 標準輸入(STDIN)已被連接到 /dev/null 虛擬設備(除非另有配置)、 標準輸出(STDOUT)與標準錯誤(STDERR)已被連接到 systemd-journald.service(8) 日誌服務(除非另有配置)、umask 已經被重置 ... 等等

新型守護進程只需要遵守如下要求：

    1. 收到 SIGTERM 信號後 關閉進程並確保乾淨的退出

    2. 收到 SIGHUP 信號後 重新加載配置文件(若需要)

    3. 主守護進程在退出時應該按照 LSB recommendations for SysV init scripts[1] 的要求返回恰當的退出碼， 以便於 systemd 判斷服務的退出狀態。

    4. 若可行，在初始化的最後一步， 通過 D-Bus 創建進程的控制接口， 並在 D-Bus 上註冊一個總線名稱。

    5. 提供一個 .service 單元文件， 包含如何啓動/停止/維護該服務的配置。 詳見 systemd.service(5) 手冊。

    6. 儘可能依賴於 systemd 的資源控制與權限剝奪功能 (CPU與內存佔用/文件訪問等等)， 而不要自己實現它們。 詳見 systemd.exec(5) 手冊。

    7. 若使用了 D-Bus ， 則強烈推薦使用基於 D-Bus 的啓動機制。 這樣做有許多好處： 守護進程可以按需延遲啓動； 可以和依賴於它的進程並行啓動(提升啓動速度)； 守護進程可以在失敗時被自動重啓 而不丟失D-Bus總線上的請求(詳見下文)

    8. 若守護進程通過套接字提供服務， 則強烈推薦使用基於套接字的啓動機制(詳見下文)。 這樣做有許多好處： 守護進程可以按需延遲啓動； 可以和依賴於它的進程並行啓動(提升啓動速度)； 對於無狀態協議(例如 syslog, DNS)， 守護進程可以在失敗時被自動重啓而不丟失套接字上的請求(詳見下文)

    9. 若可能，守護進程應該通過 sd_notify(3) 接口通知 systemd "啓動已完成"或"狀態已更新"這樣的消息。

    10. 不要使用 syslog() 記錄日誌， 只需簡單的使用 fprintf() 向 STDERR 輸出日誌即可。 如果必須指明日誌等級， 則可以在日誌的 行首加上類似 "<4>" 這樣的前綴即可(這裏表示4級"WARNING")。 詳見 sd-daemon(3) 與 systemd.exec(5) 手冊。

上述要求與 Apple MacOS X Daemon Requirements[2] 類似， 但並不完全相同。

啓動

systemd 提供了多種啓動機制(見下文)， 而服務單元也經常同時使用其中的幾種。 例如 bluetoothd.service 可以在插入藍牙硬件時被啓動， 也可以在某進程訪問其 D-Bus 接口時被啓動。 又如打印服務可以在IPP端口有流量接入時被啓動， 也可以在插入打印機硬件時被啓動， 還可以在有文件進入打印機 spool 目錄時被啓動。 甚至對於必須在系統啓動時無條件啓動的服務， 爲了儘可能併發啓動， 也應該使用某些啓動機制。 如果某守護進程實現了一個 D-Bus 服務或者監聽一個套接字， 那麼使用基於 D-Bus 或基於套接字的啓動機制， 將允許該進程與其客戶端同時並行啓動(從而加快啓動速度)。 因爲所有的通信渠道都已事先建立， 並且不會丟失任何客戶端請求， 同時 D-Bus 總線或者內核會將客戶端請求排入隊列等候， 直到完成啓動。

  系統啓動時啓動

傳統的守護進程一般是在系統啓動時通過SysV初始化腳本自動啓動， systemd 也支持這種啓動方式。

對於 systemd 來說， 如果希望確保某單元在系統啓動時自動啓動， 那麼最佳的做法是在默認啓動目標 (通常是 multi-user.target 或 graphical.target)的 .wants/ 目錄中爲該單元建立軟鏈接。 參見 systemd.unit(5) 手冊以瞭解 .wants/ 目錄， 參見 systemd.special(7) 手冊以瞭解上述兩個特殊的啓動目標。

  基於套接字的啓動

爲了儘可能提高並行性與健壯性， 以及簡化配置與開發， 對於需要監聽套接字的服務， 強烈推薦使用基於套接字的啓動機制。 使用此機制後， 守護進程不再需要創建和綁定套接字， 而是由 systemd 接管這個工作。 systemd 將會根據單元文件的設置， 預先創建所需的套接字， 並在第一個客戶端請求接入的時候啓動該服務， 以實現服務的按需啓動。 該機制的好處還在於， 預先創建好套接字之後， 所有使用此套接字通信的進程可以並行啓動(包括客戶端和服務端)。 此外，重啓服務只會導致丟失最低限度的客戶端連接， 甚至不丟失任何客戶端請求 (例如對於 DNS 或 syslog 這樣的無狀態協議)。 因爲套接字在服務重啓期間始終保持有效並且可被訪問， 同時所有客戶端請求也都被排入隊列等候處理。

使用此機制之後， 守護進程必須要從 systemd 接收已創建好的套接字， 而不能自己創建並綁定套接字。 關於如何使用該機制，參見 sd_listen_fds(3) 與 sd-daemon(3) 手冊。 只需要小小的修改， 即可在原有啓動機制的基礎上添加基於套接字的啓動機制， 至於如何移植，詳見後文。

systemd 通過 .socket 單元實現該機制，詳見 systemd.socket(5) 手冊。 必須確保所有爲支持基於套接字啓動而創建的監聽 socket 單元都被包含在 sockets.target 中。 建議在 socket 單元的 "[Install]" 小節加入 WantedBy=sockets.target 設置， 以確保在啓用該單元時能夠自動添加上述依賴關係。 除非明確設置了 DefaultDependencies=no ， 否則會爲所有 socket 單元隱含的創建必要的順序依賴。 有關 sockets.target 的解釋，詳見 systemd.special(7) 手冊。 如果某 socket 單元已被包含在 sockets.target 中， 那麼不建議在其中再添加任何額外的依賴關係(例如 multi-user.target 之類)。

  基於 D-Bus 的啓動

如果守護進程使用 D-Bus 與客戶端通信， 那麼它應該使用基於 D-Bus 的啓動機制， 這樣當客戶端訪問其 D-Bus 接口時， 該服務將被自動啓動。 該機制是通過 D-Bus service 文件實現的(不要與普通的單元文件混淆)。 爲了確保讓 D-Bus 使用 systemd 來啓動與維護守護進程， 必須在這些 D-Bus service 文件中使用 SystemdService= 指明其匹配的服務單元。 例如，對於文件名爲 org.freedesktop.RealtimeKit.service 的 D-Bus service 來說， 爲了將其綁定到 rtkit-daemon.service 服務單元， 必須確保在該文件中設置了 SystemdService=rtkit-daemon.service 指令。 注意，必須明確設置 SystemdService= 指令， 否則當服務單元同時使用多種啓動機制時， 可能會導致競爭條件的出現。

  基於設備的啓動

用於管理特定類型硬件的守護進程， 只應該在符合條件的硬件變爲可用或者被插入時，才需要啓動。 爲了達到上述目的， 可以將服務的啓動/停止與硬件的插入/拔出事件綁定。 當帶有 "systemd" 標籤的設備出現在 sysfs/udev 設備樹中時， systemd 將會自動爲其創建對應的 device 單元。 通過向這些單元中添加對其他單元的 Wants= 依賴， 就可以實現當該 device 單元被啓動(也就是硬件被插入)時， 連帶啓動其他單元，從而實現基於設備的啓動。 這可以通過向 udev 規則庫中添加 SYSTEMD_WANTS= 屬性來實現， 詳見 systemd.device(5) 手冊。 通常，並不是將 service 單元直接添加到設備的 Wants= 依賴中， 而是通過專用的 target 單元間接添加。 例如，不是將 bluetoothd.service 添加到各種藍牙設備的 Wants= 依賴中， 而是將 bluetoothd.service 添加到 bluetooth.target 的 Wants= 依賴中， 同時再將 bluetooth.target 添加到各種藍牙設備的 Wants= 依賴中。 通過引入 bluetooth.target 這個抽象層， 系統管理員無需批量修改 udev 規則庫， 僅通過 systemctl enable|disable ... 命令 修改 bluetooth.target.wants/ 目錄中的軟鏈接， 即可控制 bluetoothd.service 的使用。

  基於路徑的啓動

對於處理 spool 文件或目錄的守護進程(例如打印服務)來說， 僅在 spool 文件或目錄狀態發生變化或者內容非空時， 才需要啓動。 通過 .path 單元實現的、 基於路徑的啓動機制正好適用於這種場合， 詳見 systemd.path(5) 手冊。

  基於定時器的啓動

對於週期性的操作(例如垃圾文件清理或者網絡對時)， 可以通過基於定時器的啓動機制來實現。 這種機制通過 .timer 單元實現，詳見 systemd.timer(5) 手冊。

  其他啓動方式

在其他操作系統上還存在着其他的啓動機制， 不過這些機制都可以被前述的各種機制的組合替代。 因此在這裏不再贅述。

與 SYSTEMD 整合

  編寫 systemd 單元文件

在編寫單元文件時應當考慮下列建議：

    1. 儘可能不用 Type=forking 。 若非用不可，則必須正確設置 PIDFile= 指令。參見 systemd.service(5) 手冊。

    2. 若守護進程在 D-Bus 上註冊了一個名字， 則應儘可能使用 Type=dbus

    3. 設置一個易於理解的 Description=

    4. 確保 DefaultDependencies=yes ， 除非該單元必須在系統啓動的早期啓動或者必須在系統關閉的末期關閉。

    5. 通常無需顯式定義依賴關係。 不過，如果確實需要顯式定義依賴關係， 爲了確保單元文件不侷限於特定的發行版，僅應該依賴於 systemd.special(7) 中列出的單元以及自身所屬軟件包中提供的單元。

    6. 確保在 "[Install]" 小節中包含完整的啓用信息(參見 systemd.unit(5) 手冊)。 若希望自動啓動該單元， 則應該設置 WantedBy=multi-user.target 或 WantedBy=graphical.target 若希望自動啓動該單元的套接字，則應該設置 WantedBy=sockets.target 。 通常你還希望在啓用該單元時， 一起啓用對應的套接字單元(假定爲 foo.service)， 因此還應該設置 Also=foo.socket

  安裝 service 單元文件

當從源代碼編譯安裝(make install)軟件包時， 其中的系統服務單元文件會被默認安裝到 pkg-config systemd --variable=systemdsystemunitdir 命令返回的目錄中(通常是 /usr/lib/systemd/system)； 而其中的用戶服務單元文件會被默認安裝到 pkg-config systemd --variable=systemduserunitdir 命令返回的目錄中(通常是 /usr/lib/systemd/user)； 但並不應該使用 systemctl enable ... 命令啓用它們。 當從包管理器安裝(rpm -i)二進制軟件包時， 其中的單元文件應該同樣安裝到上述位置。 但不同之處在於， 還應該使用 systemctl enable ... 命令啓用它們， 因此安裝的單元有可能會在開機時自動啓動。

移植已有的守護進程

雖然 systemd 兼容傳統的 SysV 初始化系統， 但是移植舊有的守護進程可以更好的利用 systemd 的先進特性。 建議對舊有的 SysV 守護進程做如下改進： ...[省略]...

放置守護進程的數據

建議遵守 file-hierarchy(7) 所建議的通用準則。

參見

systemd(1), sd-daemon(3), sd_listen_fds(3), sd_notify(3), daemon(3), systemd.service(5), file-hierarchy(7)

NOTES

1.  LSB recommendations for SysV init scripts
    http://refspecs.linuxbase.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/iniscrptact.html

2.  Apple MacOS X Daemon Requirements
    https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html

跋
本頁面中文版由中文 man 手冊頁計劃提供。

翻譯人員：金步國
金步國作品集：http://www.jinbuguo.com
中文 man 手冊頁計劃：https://github.com/man-pages-zh/manpages-zh