MacOS下利用launchd实现守护进程与开机自启


背景

最近在 Mac 上安装了一些软件。重启后发现这些软件会随开机启动。我不喜欢这样,所以想禁止这些启动项。

在中文网络搜索,大多数内容都是在系统偏好设置中,在账户和群组里管理「登录项」。但是目标软件没有出现在登录项的列表中。为此,我不得不在英文网络上搜索,找到了 Apple 关于开发者的一些文档,最终解决了问题。

这篇记录一下如何管理 macOS 系统上的启动项与守护进程。


守护进程与用户代理

启动项的专业称呼是守护进程(Daemon)。守护进程是计算机系统中,运行在后台的程序。在 nix 系统中,守护进程通常没有父进程。一般来说,守护进程完成监听而后作出响应的任务。举例来说,杀毒软件的守护进程监听到有新下载的文件,就给出响应——启动杀毒软件对新下载的文件进行安全扫描。

通常来说,守护进程是系统启动及内核运行后在系统初始化阶段启动的进程。对于 macOS 来说,还有名为用户代理(User Agent)的守护进程类似物。与守护进程相同,用户代理也能实现上述监听而后作出响应的功能。不过,与守护进程不同的是,用户代理是在用户登录系统时启动的,而不是在系统初始化阶段启动的。不过,就本文而言,守护进程与用户代理是一回事,因此除有特殊注明外,一律以守护进程指代,不做区分。


launchd介绍

在 macOS 上,Apple 推荐用 launchd 来启动守护进程与用户代理。

具体来说,launchd 在系统启动及内和运行后,在系统初始化阶段启动守护进程,而在用户登录是启动用户代理。流程大致如下:

  1. 读入属性列表文件(property list files)
  2. 注册守护进程所需的套接字(sockets)和文件描述符(file descriptors)
  3. 启动要求在任何情况下持续运行的守护进程
  4. 对于按需启动的守护进程,在 launchd 收到相应请求时,启动对应的守护进程
  5. 当关机(对于守护进程)或用户登出(对于用户代理)时,launchd 对这些守护进程发出 SIGTERM 信号

launchctl定义

launchctl 经常用来做定时任务,英文解释如下:

In computing, launchd, a unified operating system service management framework, starts, stops and manages daemons, applications, processes, and scripts in macOS. It was introduced with Mac OS X Tiger and is licensed under the Apache License. – Wikipedia

英文官网地址

核心是定义一系列的自启动应用配置文件

Type Lacation Run on behalf of
User Agents ~/Library/LaunchAgents Current logged in user
Global Agents /Library/LaunchAgents Current logged in user
Global Daemons /Library/LaunchDaemons root or the user sepcified with the key UserName
System Agents /System/Library/LaunchAgents Current logged in user
System Daemons /System/Library/LaunchDaemons root or the user sepcified with the key UserName

启动顺序

  1. 电脑启动,在用户未登录时,系统会扫描 /System/Library/LaunchDaemons//Library/LaunchDaemons/ 里面的配置
  2. 用户登录进来时,系统会扫描 /System/Library/LaunchAgents, /Library/LaunchAgents ,以及用户的目录:~/Library/LaunchAgents directory 里面的配置

属性列表文件 ( property list files )

上一节提到,launchd 会去相应目录读取属性列表文件,然后根据属性列表文件中的参数,注册套接字和文件描述符等资源,以及控制守护进程的运行策略。因此,接下来的关键就是这类属性列表文件。

属性列表文件的英文是 Property List files,对应的文件名后缀是 .plist 。说是属性列表文件,其实本质上就是 XML 文件。以下是一个示例文件。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.example.hello</string>
    <key>ProgramArguments</key>
    <array>
        <string>hello</string>
        <string>world</string>
    </array>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>

Apple 在文档里给出了守护进程属性列表文件必选和推荐的字段,这里翻译如下。

关键字 说明
Label <string> 必填;包含用于 launchd 识别守护进程的唯一字符串标识符。
ProgramArguments <array of strings> 必填;包含用于 launchd 启动守护进程时使用的参数。
inetdCompatibility 表示该守护进程对于每个传入的连接启用相互独立的实例。该关键字会让 launchdinetd 那样运作;具体来说,launchd 会将与每个传入连接的客户端建立好的套接字传给相互独立的守护进程实例。
KeepAlive <boolean or dictionary of stuff> 该关键字用于指定相应的守护进程是按需启动还是要一直启动着。Apple 推荐实现按需启动的守护进程。

另外的一些可选字段可参考如下表格:

键值 描述(作用)
Program <string> 可选, 要运行的程序, 如果省略这个选项,会把 ProgramArguments 的第一个元素作为要运行的程序, 任务执行绝对路径
WatchPaths <array of strings> 可选, 作用: 监听路径. 当任何一个被监听的路径 ( array 中指定的 string ) 被修改时, 都会重启任务
QueueDirectories <array of strings> 可选, 与 WatchPaths 的作用很像, 作用是监听路径的修改. 不同之处是, 只有这个路径是一个目录并且这个目录不为空时 , 任务才会被重启.
StartOnMount <boolean> This optional key causes the job to be started every time a filesystem is mounted. 具体作用未明
StartInterval <integer> 可选, 每间隔 N 秒 重启任务. 当系统是睡眠状态时,任务会在下次系统被唤醒时重启, 睡眠时的多个任务重启事件会被合并成一个事件 ( 系统睡眠时,无论多少个N秒, 系统被唤醒时只会执行一次 )
StartCalendarInterval <dictionary of integers or array of dictionary of integers> 可选, 在指定的日历格式的时间点 重启任务, 缺少的参数会被当做通配符(对应参数时间段内任意时间点), 这个命令和 crontab 命令很像(在系统休眠是也会执行任务), 和 corn ( 系统休眠时不执行任务,系统被唤醒时, 多次执行命令合并成一次.同 StartInterval <integer> ) 不一样. 参数格式详见下附表1
StandardInPath <string> 可选, 执行任务的标准数据输入的文件路径
StandardOutPath <string> 可选, 任务执行时保存标准输出的文件路径
StandardErrorPath <string> 可选, 任务执行时, 保存错误日志的文件路径
Debug <boolean> 可选, 当任务执行, launchd 将临时的调整它的日志 mask 到LOG_DEBUG
附表1
键值 描述
Minute <integer> 第几分钟时执行任务
Hour <integer> 第几个小时执行任务(采用24时制)
Day <integer> 每个月的第几天执行任务
Weekday <integer> 周几执行任务, ( 0 ~ 7 )
Month <integer> 第几个月执行任务

此外,Apple 在技术笔记中还提及了其他两个关键字,这两个关键字也可能影响守护进程的运行策略(当然还有其他一些关键字可能影响,但主要还有这两个):

  • RunAtLoad: 在属性列表文件加载时启动守护进程;
  • SuccessfulExit:KeepAlive 联合使用。当 SuccessfulExit = true 时表示若进程正常退出 ( Exit at 0 ),则 launchd 应当尝试将其重启;当 SuccessfulExit = false 时表示若进程异常退出,则 launchd 应当尝试将其重启。

因此我们可以构建一些场景:

预期的行为 相应的属性配置
完全地按需启动 KeepAlive = false; RunAtLoad = false
在属性列表文件加载时启动,而后按需启动 KeepAlive = false; RunAtLoad = true
在守护进程异常退出之前,按需启动 KeepAlive = { SuccessfulExit = false }; RunAtLoad = false
在守护进程正常退出之前,按需启动 KeepAlive = { SuccessfulExit = true }; RunAtLoad = false

管理启动项

有了这些知识,管理 macOS 上的启动项就很容易了。你需要做的就是在上述 5 个目录下,找到相应程序的属性列表文件,而后按你的意图修改即可。当然,有些属性列表文件需要使用 root 权限来修改。有必要的话,你需要在终端(Terminal.app)当中使用 sudo vim /path/to/your.plist 来修改目标文件。

查看进程清单

# 罗列系统当前运行的进程清单
launchctl list

# 查看特定服务的配置信息
launchctl list com.adobe.AdobeCreativeCloud

加载守护进程

# 加载特定的服务配置
launchctl load <file_path>

# 例如:
launchctl load /Library/LaunchAgents/com.adobe.AdobeCreativeCloud.plist

移除守护进程

如果要移除的话,可以通过如下命令执行:

# 移除 xyz.hanks.spider
launchctl unload /Library/LaunchAgents/your.plist

# 卸载特定的服务配置
launchctl unload -w /Library/LaunchAgents/com.adobe.AdobeCreativeCloud.plist
# 特此说明,-w 参数的作用是,如果自动执行了 load 命令尝试去恢复服务注册,则让其无效

唯有一点需要注意,在做任何修改之前做好备份


最佳实践

开机自启

接下来我们自己自制一个自启动应用,用来每次启动电脑打印一次 ls -al /Users/nestealin/Downloads

注意: 在开机启动或守护进程配置中,应尽量保持使用绝对路径,如果使用相对路径可能会提示路径不存在。

~/Library/LaunchAgents 新建一个配置文件,名字叫做 com.startup.test.plist ,配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
      <key>Label</key>
      <string>com.startup.test</string>
      <key>UserName</key>
      <string>nestealin</string>
    <key>KeepAlive</key>
    <true/>
      <key>ProgramArguments</key>
      <array>
          <string>/bin/sh</string>
          <string>ls -al /Users/nestealin/Downloads</string>
      </array>
      <key>RunAtLoad</key>
      <true/>
      <key>OnDemand</key>
      <false/>
      <key>LaunchOnlyOnce</key>
      <true/>
      <key>StandardErrorPath</key>
      <string>/Users/nestealin/com.startup.test.err</string>
      <key>StandardOutPath</key>
      <string>/Users/nestealin/com.startup.test.out</string>
  </dict>
</plist>

接着输入命令

launchctl load /Users/nestealin/Library/LaunchAgents/com.startup.test.plist
sudo chown root:wheel /Users/nestealin/Library/LaunchAgents/com.startup.test.plist
sudo chmod 600 /Users/nestealin/Library/LaunchAgents/com.startup.test.plist

注意:

设置在 /Library/LaunchAgents 的 plist 文件

  • 用户属组权限需要是:root:wheel
  • 文件权限应该是:600

不然的话设置不会生效。

接着,查看已装载的列表:

launchctl list

最后,重启电脑然后去查看 /Users/nestealin/com.startup.test.out 有没有正确的输出。

守护进程

以守护 Mos 应用进程为例,实现应用进程在手动退出崩溃退出后3秒自动重新拉起。

关联issue: https://github.com/Caldis/Mos/issues/574

编写配置文件

cd ~/Downloads
vim application.com.caldis.Mos.plist

内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>application.com.caldis.Mos</string>
    <key>OnDemand</key>
    <true/>
    <key>ProgramArguments</key>
    <array>
        <string>/Applications/Mos.app/Contents/MacOS/Mos</string>
    </array>
    <key>StartInterval</key>
    <integer>3</integer>
</dict>
</plist>

加载守护进程

cd /Library/LaunchAgents
sudo mv ~/Download/application.com.caldis.Mos.plist ./
sudo chmod +x application.com.caldis.Mos.plist
launchctl load application.com.caldis.Mos.plist

# 检查加载项
launchctl list | grep -i mos

定时任务1

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>

    <key>Label</key>
    <string>xyz.hanks.spider</string>

    <!-- 要运行的程序, 如果省略这个选项,会把ProgramArguments的第一个
    元素作为要运行的程序 -->
    <key>Program</key>
    <string>/Users/zhanks/work/Bookshelf/run.sh</string>

    <!-- 每天18:30 -->
    <key>StartCalendarInterval</key>
    <dict>
        <key>Minute</key>
        <integer>30</integer>
        <key>Hour</key>
        <integer>18</integer>
    </dict>
    <!-- 运行间隔,与StartCalenderInterval使用其一,单位为>秒 -->
    <!-- <key>StartInterval</key>-->
    <!-- integer>5</integer>-->

    <!-- 标准错误输出文件,错误日志 -->
    <key>StandardErrorPath</key>
    <string>/Users/zhanks/run-err.log</string>

  </dict>
</plist>

定时任务2

<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">  
<plist version="1.0">  
  <dict>
    
    <key>Label</key>
    <!-- 名称,要全局唯一 -->
    <string>xyz.hanks.spider</string> 

    <!-- 要运行的程序, 如果省略这个选项,会把ProgramArguments的第一个
    元素作为要运行的程序 -->
    <key>Program</key>
    <string>/Users/hanks/run.sh</string>

    <!-- 命令, 第一个为命令,其它为参数-->
    <key>ProgramArguments</key>
    <array>
      <string>/Users/hanks/run.sh</string>
    </array>

    <!-- 运行时间 -->
    <key>StartCalendarInterval</key>
    <dict>

      <key>Minute</key>
      <integer>30</integer>

      <key>Hour</key>
      <integer>9</integer>

      <key>Day</key>
      <integer>1</integer>

      <key>Month</key>
      <integer>5</integer>

      <!-- 0和7都指星期天 -->
      <key>Weekday</key>
      <integer>0</integer>

    </dict>

    <!-- 运行间隔,与StartCalenderInterval使用其一,单位为秒 -->
    <key>StartInterval</key>
    <integer>30</integer>

    <!-- 标准输入文件 -->
    <key>StandardInPath</key>
    <string>/Users/hanks/run-in.log</string>

    <!-- 标准输出文件 -->
    <key>StandardOutPath</key>
    <string>/Users/hanks/run-out.log</string>

    <!-- 标准错误输出文件 -->
    <key>StandardErrorPath</key>
    <string>/Users/hanks/run-err.log</string>
  </dict>  
</plist>

– EOF –


Reference

mac launchctl 守护进程

mac守护进程

管理 macOS 系统上的启动项

[Mac]使用 launchd 管理系统后台服务


文章作者: NesteaLin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 NesteaLin !
  目录