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.targetWantedBy=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
2.
Apple MacOS X Daemon Requirements

本頁面中文版由中文 man 手冊頁計劃提供。
翻譯人員:金步國
 
金步國作品集:http://www.jinbuguo.com
 
 
中文 man 手冊頁計劃: https://github.com/man-pages-zh/manpages-zh