Mac OS X 的 Launch Daemon / Agent

因為想做某個應用,今天研讀了 Apple Developer 網站上的 Daemons and Services Programming Guide ,終於懂了 Mac OS X 的 Launch Daemon / Agent 是做什麼用的,筆記一下。為了避免專有名詞翻譯不同造成誤解,我試著統統不翻譯。不過我對 Mac OS X 的 system programming 涉世(?)未深,要是有解釋不對的地方,路過的大俠請不吝指教。

以下的操作全是在 Mac OS X 10.6.8 完成的。


What is launchd ?

Mac OS X 從 10.4 開始,採用 launchd 來管理整個作業系統的 services 及 processes 。傳統的 UNIX 會使用 /etc/rc.* 或其他的機制來管理開機時要啟動的 startup services ,而現在的 Mac OS X 使用 launchd 來管理,它的 startup service 叫做 Launch DaemonLaunch Agents 。而視為 service 的程式,就該是 background process ,不應該提供 GUI ,也不應該跳到 (console 的)foreground 。當然有些例外,例如聽快速鍵之後跳出視窗的程式。

launchd 管理的 background process 有四種:

  1. Launch Daemon: 在開機時載入 (load) 。
  2. Launch Agent: 在使用者登入時載入。
  3. XPC Service: 好像是 10.7 才有的,我還沒灌 10.7 ,先跳過。
  4. Login Items: 在 User 登入時執行。有兩種方法可以用程式新增項目到 Login Item:
    1. Shared File List:會出現在 Account 偏好設定的 Login Item 清單。
    2. Service Management Framework:這個就不會出現在 Login Item 清單。
(以下把重點放在 Launch Daemon / Agent 。至於 XPC 和 Login Item 就留待其他比較在行的大大來解釋。)

Launch Daemon & Launch Agent

Launch Daemon 和 Launch Agent 是同一種東西在不同 scopes 的異名。Launch Daemon 是 system-wide 的 service ,稱為 daemon,Launch Agent 是 per-user 的 service ,稱為 agent,前者在開機時會載入 (load) ,後者在使用者登入時(才)會載入。

如果你打開 Activity Monitor ,並切換到 Hierarchy view ,你會發現有個 launchd 會在最上層,跟它同層的只有 kernel_task ,它下面有很多 child processes 的 user 都是 root ,其中還有一個 launchd ,啟動的 user 是你自己,它底下的 child processes 的 user 也幾乎都是你自己。當這些 processes 是由 launchd 載入 launchd property list file 來執行的時候,前者由 root 執行的稱為 Launch Daemons ,後者由使用者執行的稱為 Launch Agents 。

launchd property list file 就是你會在 LaunchDaemon 或 LaunchAgents 目錄中看到的 *.plist 檔案(以下統稱 plist 檔,反正本文講到的 plist 檔也只有這種用途)。它是 XML 格式,不過咱們別這麼糾結手刻 XML ,你直接按兩下打開就是 Property List Editor ,滑鼠點一點就好,不糾結。

launchd Service Process Lifecycle

由 launchd 所管理的 services (Launch Daemon 、 Launch Agent)是要先由 launchd 載入(load)以後才會執行(run),但載入之後並不一定馬上執行。在蘋果的官方文件說明了 kernel 載入完成後會發生的事,用來說明 Launch Daemons 、Launch Agents 及其 processes 的生命週期。

開機時,會先載入 OS Kernel ,載入完成後就執行 launchd ,用來載入 system-wide services (daemons)。這個 system-wide launchd 在開機時會做這些事:

  1. 載入 (load) 存放在這些目錄下的 plist
    • /System/Library/LaunchDaemons
    • /Library/LaunchDaemons
  2. 註冊那些 plist 裡面設定的 sockets (port) 和 file descriptors
  3. 執行 (run) KeepAlive = true 的 daemons ,當然 RunAtLoad = true 的也會啟動。
該 run 的 run 好後, loginwindow 就出現了,提示使用者登入。有設定自動登入的話,就會跳過這關。

在使用者登入以後,會執行屬於該使用者的 launchd ,負責處理 Launch Agent ,做的事跟上面載入 Launch Daemon 很像,差別在於它從以下的目錄載入 plist

由使用者執行的任何程式也都是 launchd 來執行的,所以 launchd 也是該使用者的所有 processes 之母。

在使用者登出、關機或重新開機時,會觸發 Termination event。接受登出、關機、重新開機使用者指令的 process 是 loginwindow 。它會先向使用者確認,一但確認,就會對每個由該使用者的 launchd 所啟動的 processes 送出 termination signal,如果是 Cocoa 則送出 Cocoa API 的 event,其他的就送出 SIGTERM 要他們自我了斷,45 秒之後,除了 Cocoa 的應用程式可以丟出某個 error 來取消這整個 termination process,其他還沒結束的都會被 kill 掉。

這就是為什麼 loginwindow 這個 process 會一直存在,它要負責把該使用者執行起來的 processes 統統清掉。而 per-user services 都關掉以後,就回到 loginwindow ,或是執行關機、重新開機的流程,後兩者就是照著差不多的流程去關掉所有 system-wide services 。


launchd-compatible Daemon Programming Guide

以下是該文件中提及關於配合 launchd 開發 daemon 時應注意的事,提到關於 plist 的 key 就請參考 man 5 launchd.plist 。以下的 daemon 指的是 Launch Daemon 所要運行的 process ,所以 Launch Agent 也一併適用。

Listen to SIGTERM

如上文所提及的,由於 loginwindow 這個 process 在要關掉你的 daemon 時會先送 SIGTERM ,要你自我了斷,等太久沒關掉才會 SIGKILL 。如果你的程式需要在結束之前做什麼事,一定要聽 SIGTERM 這個 signal 。

On-Demand Daemon

Launch Daemon / Agent 預設不會讓某個 process 一直執行,當它的設定沒有 KeepAlive = true 時,它會根據被執行的 process 的 CPU usage 和 requests (如 TCP/IP service)來決定要不要送出 SIGTERM 叫他自盡。

當該 service 需要被使用時,而相對應的 program 沒有跑成 process 時,會自動把該 service 給跑起來。例如某個 TCP/IP service 聽某個 port,當這個 port 有封包進來時, launchd 會把相對應的 service 給啟動,這種行為叫做 on-demand

當然,也有 non-on-demand daemon (好繞舌),其實也就是 keep-alive daemon ,這也是傳統意義上的 daemon ,我是說那種一直躲在牆角默默執行,直到有人找他,他才跳出來回一下話,回完了以後又繼續躲在牆角的那種。只要把 KeepAlive 這個 key 設成 true ,它就會在 plistlaunchd 載入 (load) 時執行 (run) 起來。要是那個 process 死掉,launchd 會知道,馬上再把它開起來。所以如果你試著去 Activity Monitor 砍掉這種 daemon ,它就馬上會復活。

No fork or exec

傳統的 system programming 會教你用 execfork 等等的 POSIX API 來做一隻 daemon ,但配合 launchd 時,由於 daemon 的生命週期是由 launchd 來控制的,除非強制要求 Kepp-Alive,否則要生要死是 launchd 決定,更何況 Keep-Alive 還要考慮 daemon process 在結束以後自動重新執行,所以在配合 launchd 寫 daemon 時,蘋果建議你不要用傳統的 forkexec* 。當然, plist 檔案中的 ProgramArguments 就是 exec* 系列 subroutine 的參數。

當一個 process 跑起來 10 秒內就死掉, launchd 會判定為 crash ,然後試著重新執行。要是你用傳統的 fork-exec style ,就可能會造成無限迴圈。

No setuid / setgid / chroot / chdir etc.

為了安全性的考量,蘋果強烈建議你不要自己呼叫 setupd, setgid, chroot, chdir 等等 system subroutines ,而是透過 plist 檔的設定值來讓 launchd 幫你完成,參考 UserNameGroupNameRootDirectoryWorkingDirectory 的 keys 。

No pipe redirection hell for fd 0, 1 or 2

在寫 log 或輸出訊息時也不用煩惱開檔等等問題,你可以設定 StandardOutPathStandardErrorPath ,只管輸出到 stdoutstderr 就好了。而 StandardInPath 也可以讓你的 process 一執行就從 stdin 吃指定 path 的內容。也就是說, launchd 幫你把 fd = 0, 1, 2 的東西都傳便便。

其他應用

工作排程

Launch Daemon / Agent 的設定檔可以指定該 service 的執行週期及執行時間,也就是說,它可以替代傳統的 at, periodiccron 。這些設定值的 key 請參考 StartInterval 和 StartCalendarInterval

搭配 LaunchOnlyOnce 的話可以模擬 at ,但對我來說,如果要用 launchd 只臨時做一件事,還不如直接 at 方便。

監視檔案或目錄異動

Launch Daemon / Agent 可以監視某個 path 的異動,設定在 WatchPaths 這個 key。這裡所說的 path 可以是 directory 或是某個特定的檔案,只要該 path 有異動,就會執行你的 job 。

也可以用來清 queue ,只要 directory 裡面有東西,就會執行 job 直到空為止,可以用來做 mail server 或 notification 。設定在 QueueDirectories 這個 key 。


See also: