转载来自:http://blog.yorkxin.org/posts/2011/08/04/osx-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 Daemon Launch 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:
    • 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档,反正本文讲到的 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:

  • /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 。


See also: