背景:
在Mac下沒有像Linux那樣有很多的關於init方面的工具,從init的發展歷史https://en.wikipedia.org/wiki/Init上可以知道,Mac使用的是Launchd作為init管理工具,對應的命令工具為launchctl。
如果在Linux下創建一個自啟動服務可以使用Upstart、Systemd、Sysvinit,其中最簡單和最古老的方式應該是Sysvinit,畢竟其支持Shell腳本,非常方便。而在Mac下,與Linux的做法不太一樣,采用Launchd進行管理,其設置服務采用了plist文件進行對服務來描述,並通過配置好后放在/System/Library/LaunchDaemons
或者/Library/LaunchDaemons,最后通過launchctl命令行使其生效,期間也可以直接通過launchctl來對服務進行操作,比如啟動、停止等。
詳細的plist編寫規范及介紹,參考:https://en.wikipedia.org/wiki/Launchd
以下是關於Launchd的詳細解釋:
Launchd是什么?
Mac OS X從10.4開始,采用Launchd來管理整個作業系統的Services及Processes 。傳統的UNIX會使用/etc/rc.*或其他的機制來管理開機時要啟動的Startup Services,而現在的Mac OS X使用launchd來管理,它的Startup Service叫做Launch Daemon和Launch Agents 。而視為Service的程序,就該是Background Process,不應該提供GUI,也不應該跳到(Console的)Foreground 。當然有些例外,例如聽快速鍵之后跳出視窗的程序。
Launchd管理的Background Process有四種:
- Launch Daemon:在開機時加載(Load)。
- Launch Agent:在使用者登錄時加載。
- XPC Service:好像是10.7才有的。
- Login Items:在User登錄時執行。有兩種方法可以用程序新增項目到Login Item:(Shared File List:會出現在Account偏好設定的Login Item清單。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檔)。它是XML格式。
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在開機時會做這些事:
- 加載(load)存放在這些目錄下的plist:(/System/Library/LaunchDaemons,/Library/LaunchDaemons)
- 注冊那些plist里面設定的sockets(port)和file descriptors
- 執行(run)KeepAlive = true的Daemons,當然RunAtLoad = true的也會啟動。
Run好后,Loginwindow就出現了,提示使用者登錄。有設定自動登錄的話,就會跳過這關。
在使用者登錄以后,會執行屬於該使用者的Launchd,負責處理Launch Agent,做的事跟上面加載Launch Daemon很像,差別在於它從以下的目錄加載plist:
- /System/Library/LaunchAgents
- /Library/LaunchAgents
- ~/Library/LaunchAgents
由使用者執行的任何程序也都是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,它就會在plist被Launchd加載(load)時執行(run)起來。要是那個Process死掉,Launchd會知道,馬上再把它開起來。所以如果你試着去Activity Monitor砍掉這種Daemon,它就馬上會復活。
No fork or exec
傳統的System Programming會教你用exec、fork等等的POSIX API來做Daemon,但配合Launchd時,由於Daemon的生命周期是由Launchd來控制的,除非強制要求Kepp-Alive,否則要生要死是Launchd決定,更何況Keep-Alive還要考慮Daemon Process在結束以后自動重新執行,所以在配合Launchd寫Daemon時,蘋果建議你不要用傳統的fork和exec*。當然,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幫你完成,參考UserName、GroupName、RootDirectory、WorkingDirectory的keys 。
No pipe redirection hell for fd 0, 1 or 2
在寫Log或輸出信息時可以設定StandardOutPath、StandardErrorPath,只管輸出到stdout或stderr就好了。而StandardInPath也可以讓你的Process一執行就從stdin指定path的內容。也就是說,Launchd幫你把fd = 0, 1, 2的東西都傳了一遍。
其他應用
定時任務
Launch Daemon/Agent的設置項可以指定該Service的執行周期及執行時間,也就是說,它可以替代傳統的at、periodic和cron。這些設定值的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。
參考:
https://en.wikipedia.org/wiki/Init
https://en.wikipedia.org/wiki/Launchd
https://stackoverflow.com/questions/15735320/osx-s-etc-init-d-equivalent
https://nathangrigg.com/2012/07/schedule-jobs-using-launchd#launchctl
https://blog.yorkxin.org/2011/08/04/osx-launch-daemon-agent(以上內容轉自此篇博客,由於繁體翻譯成簡體,有些地方可能語義存在問題)
https://developer.apple.com/library/content/technotes/tn2083/_index.html