0%

macOS launchd 不完全指南

launchd 是什么?

launchd 是 macOS 下一个服务管理工具,用于启动、停止和管理守护进程、应用程序、进程和脚本。 我们可以将 launchd 看作是 mac 下的 systemd 或者是 supervisor,如果我们想要在 mac 下启动守护进程,用 launchd 就可以了。

守护进程是在后台运行的不需要用户输入的程序。比如我们常用的 MySQL,往往是以守护进程的方式启动的。 需要注意的是:虽然本文多次提到了 守护进程,但是准确来说,launchd 可以启动的不仅仅是守护进程,还可以启动应用程序、进程和脚本。

如何写 launchd 配置文件?

launchd 的配置文件是通过一个 plist 文件来定义的(plistproperty list 的缩写),一个典型的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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>
<!-- Label 可以看作是守护进程的名称,key 是配置的名称,key 的下一行就是它的值,string 标签表示值的类型是字符串 -->
<key>Label</key>
<string>com.example.app</string>
<key>Program</key>
<string>/Users/Me/Scripts/cleanup.sh</string>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

说明:

  • 配置文件中,除了 dict 里面的那一部分,其他的都是固定的,不需要修改。
  • 三个字段说明:
    • Label:也就是服务的名字,可以随便取,但是不能重复。我们通过 launchctl list 来查看的时候,列出的就是这个名字。上述例子是 com.example.app
    • Program:要启动的程序的路径,需要填写绝对路径。上述例子是 /Users/Me/Scripts/cleanup.sh如果需要加参数的话,需要使用 ProgramArguments 来代替 Program,详情参考下文。
    • RunAtLoad:是否在配置被加载的时候就运行,默认是 false,如果需要在启动的时候就运行,需要设置为 true。上述例子是 true
  • 标签说明:key 就是属性的名称,紧跟着 key 的下一行就是属性的值,属性的值的类型通过其标签反映出来,比如上面的 <string> 表示包裹的是一个字符串类型,而 <true/> 表示是一个布尔类型,而且它的值是 true

mac 中很多配置都是通过 plist 来定义的,

launchd 配置文件放哪里?

macOS 中有两种类型的守护进程,一种是系统级别的(Daemons),一种是用户级别的(Agents),它们的配置文件放的位置是不一样的。 系统级别的守护进程就是不管你用户是谁,都会启动的,而用户级别的守护进程就是只有在对应的用户登录的时候才会启动的(所以会保存在用户主目录下)。

简单来说,就是如果没有用户登录进系统中,那么用户级别的守护进程(Agents)就不会启动。mac 其实跟 linux 一样,都是多用户系统,没有任何用户登录的时候,它依然是在运行的。

下面是 launchd 配置文件的路径:

  • ~/Library/LaunchAgents:用户级别的守护进程配置文件路径。这里保存特定用户的 Agents 配置。(一般情况都是放这里
  • /Library/LaunchAgents:用户级别的守护进程配置文件路径。这里保存所有用户共用的 Agents 配置。
  • /Library/LaunchDaemons:全局的 Daemons 配置。
  • /System/Library/LaunchAgents:所有登录用户共用的 Agents 配置。
  • /System/Library/LaunchDaemons:全局的 Daemons 配置。

可能大家看得有点迷,会有一种想法就是,那我的配置文件应该放哪里?这个问题的答案很简单:如果你的电脑只有你一个人用,那么你就把配置文件放在 ~/Library/LaunchAgents 下面就行了。

如何让 launchd 开机启动我们配置的守护进程?

在我们添加了配置文件之后,还有一件事需要做的就是,修改配置文件的权限,我们可以参考一下上面几个文件夹中文件的权限,然后修改成相同的就可以了。 比如 ~/Library/LaunchAgents 文件夹中的配置文件权限都是 xx:staffxx 是当前登录的用户),那么我们也把我们的配置文件权限修改成 xx:staff 就可以了。

接下来,我们只需要执行 launchctl load ~/Library/LaunchAgents/com.example.app.plist 就可以了。

如何移除守护进程呢?

如果我们想要移除守护进程,只需要执行 launchctl unload ~/Library/LaunchAgents/com.example.app.plist 就可以了。

如果要执行的命令有参数怎么配置?

在很多时候,我们都是需要加上某些参数来启动我们的命令的,要实现这种效果可以使用 ProgramArguments 配置,下面是另外一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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>KeepAlive</key>
<true/>
<key>Label</key>
<string>my-privoxy</string>
<key>ProgramArguments</key>
<array>
<string>/Users/ruby/Code/proxy/privoxy</string>
<string>--no-daemon</string>
<string>privoxy.config</string>
</array>
<key>StandardErrorPath</key>
<string>/Users/ruby/Library/Logs/my-privoxy.log</string>
<key>StandardOutPath</key>
<string>/Users/ruby/Library/Logs/my-privoxy.log</string>
<key>WorkingDirectory</key>
<string>/Users/ruby/Code/proxy/</string>
</dict>
</plist>

这个例子中,我们添加了更多的元素进去,比如日志、工作目录等。

说明:

  • ProgramArguments 属性的作用是指定要执行的命令以及其参数,它的值是数组类型。
  • StandardErrorPath 配置了错误输出的日志路径。
  • StandardOutPath 配置了标准输出的日志路径。
  • WorkingDirectory 设置了我们程序运行时候所在的工作目录。

launchd 配置项说明

Label - 指定名字

1
2
<key>Label</key>
<string>com.example.app</string>

Program、ProgramArguments - 指定要执行的程序

这两个二选一,如果不需要指定参数,用 Program。如果需要指定参数,那么使用 ProgramArguments

1
2
<key>Program</key>
<string>/path/to/program</string>
1
2
3
4
5
6
7
8
<key>ProgramArguments</key>
<array>
<string>/usr/bin/rsync</string>
<string>--archive</string>
<string>--compress-level=9</string>
<string>/Volumes/Macintosh HD</string>
<string>/Volumes/Backup</string>
</array>

EnvironmentVariables - 环境变量

我们可以为 Program 设置环境变量,比如下面这个例子:

1
2
3
4
5
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/bin:/usr/bin:/usr/local/bin</string>
</dict>

这样我们就可以在程序运行的时候读取到这些环境变量。

StandardInPath、StandardOutPath、StandardErrorPath - 重定向输入输出

1
2
3
4
5
6
<key>StandardInPath</key>
<string>/tmp/test.stdin</string>
<key>StandardOutPath</key>
<string>/tmp/test.stdout</string>
<key>StandardErrorPath</key>
<string>/tmp/test.stderr</string>

具体含义:

  • StandardInPath 标准输入的路径。
  • StandardOutPath 标准输出的路径。
  • StandardErrorPath 标准错误输出的路径。

大多时候我们可能不需要,但是如果我们的服务跑不起来,加上 StandardErrorPathStandardOutPath 我们就可以看到错误信息了。

WorkingDirectory - 指定工作目录

1
2
<key>WorkingDirectory</key>
<string>/tmp</string>

这样我们可以在程序运行的时候,直接使用相对路径。

RunAtLoad、StartInterval、StartCalendarInterval - 指定什么时候启动

这三个属性我们可以在配置中选择一个来配置:

  • RunAtLoad:如果设置为 true,那么 Program 会在系统启动的时候执行。(对于 Daemons 来说就是系统启动的时候启动,对于 Agents 来说就是用户登录的时候启动)
  • StartInterval:指定启动的时间间隔,单位是秒。也就是每隔多少秒执行一次。
  • StartCalendarInterval:指定启动的时间,可以指定每天的某个时间启动。(类似定时任务),可以指定多个时间。
1
2
<key>RunAtLoad</key>
<true/>

上面两行的作用是,在系统启动或者用户登进的时候执行命令。

1
2
<key>StartInterval</key>
<integer>3600</integer>

上面两行的作用是,每隔 3600 秒执行一次。

1
2
3
4
5
6
7
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>0</integer>
</dict>

上面的例子中,我们指定了每天的 3 点执行一次。

还有些不太常用的选项:StartOnMountWatchPathsQueueDirectories,通过这几个配置也可以指定命令执行的时机,本文不做介绍。

KeepAlive - 指定是否保持运行

这个默认其实是 true,我们可以测试一下:kill 掉我们 launchd 启动的进程,我们会发现那个进程马上又会被启动。

这个选项中,我们可以配置一些额外的条件来让 launchd 知道什么时候需要重启进程:

  • SuccessfulExit 如果上一次退出是正常退出,那么就在进程退出的时候启动它。
1
2
3
4
5
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<true/>
</dict>
  • Crashed 如果上一次退出是异常退出,那么就在进程退出的时候启动它。
1
2
3
4
5
<key>KeepAlive</key>
<dict>
<key>Crashed</key>
<true/>
</dict>
  • NetworkState 如果网络连接断开,那么就在网络连接恢复的时候启动它。
1
2
3
4
5
<key>KeepAlive</key>
<dict>
<key>Crashed</key>
<true/>
</dict>

KeepAlive 的其他不常用选项:PathStateOtherJobEnabledAfterInitialDemand,本文不做介绍。

UserName、GroupName - 指定运行的用户和用户组

我们可以指定以什么用户、用户组来运行这个命令:

1
2
3
4
5
6
<key>UserName</key>
<string>nobody</string>
<key>GroupName</key>
<string>nobody</string>
<key>InitGroups</key>
<true/>

RootDirectory - 指定根目录

这允许我们在一个 jail root 中执行我们的命令。

1
2
<key>RootDirectory</key>
<string>/var/jail</string>

AbandonProcessGroup - 进程被终止的时候是否终止其子进程

当我们给 launchd 启动的进程发送 SIGTERM 信号的时候,这个 SIGTERM 信号也会同时被发送给它的子进程。 我们可以将 AbandonProcessGroup 设置为 true 来禁止这种行为:

1
2
<key>AbandonProcessGroup</key>
<true/>

ExitTimeOut - 优雅终止

在我们停止 launchd 启动的进程的时候,会先发送一个 SIGTERM 信号,我们的进程可以在接收到这个信号后做一些清理操作。 直到 ExitTimeOut 秒后,如果进程还没退出,那么就会发送一个 SIGKILL 信号来强行终止进程的运行:

1
2
<key>ExitTimeOut</key>
<integer>30</integer>

ThrottleInterval - 命令调用的时间间隔

可与 KeepAlive 配合使用,在进程异常退出之后,间隔 ThrottleInterval 秒后再尝试启动。

launchd 常用操作

launchctl list - 列出所有 launchd 管理的服务

1
2
➜ launchctl list | grep my-ss-local
89201 0 my-ss-local

输出的第一列是进程 id,如果是 0 说明没有在运行状态。第二列的 0 表示的是进程上一次的退出状态码,0 一般表示成功。第三列表示的是我们在 plist 配置文件中配置的 Label 的值。

加载一个 plist(服务/job)

我们可以通过下面的命令加载一个 plist

1
launchctl load ~/Library/LaunchAgents/com.example.app.plist

移除一个 plist(服务/job)

我们可以通过下面的命令来移除 launchd 配置:

1
launchctl unload ~/Library/LaunchAgents/com.example.app.plist

启动一个 job

下面的 com.example.app 是我们在 plist 中配置的 Label 的值:

1
launchctl start com.example.app

停止一个 job

下面的 com.example.app 是我们在 plist 中配置的 Label 的值:

1
launchctl stop com.example.app

一些实例

下面是个人使用中的一些配置文件,供大家参考。下面的例子涵盖了常用的一些配置,我们复制改改就可以用了。

frpc

文件 ~/Library/LaunchAgents/frp.plist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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>KeepAlive</key>
<true/>
<key>Label</key>
<string>frpc</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/frpc</string>
<string>-c</string>
<string>/etc/frpc.ini</string>
</array>
</dict>
</plist>

这个配置文件的作用是,以守护进程的方式来启动 /usr/local/bin/frpc -c /etc/frpc.ini 这个命令。启用方式为:

1
launchctl load ~/Library/LaunchAgents/frp.plist

我们可以通过 ps 来查看进程是否成功启动。

ss-local

文件 ~/Library/LaunchAgents/ss-local.plist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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>KeepAlive</key>
<true/>
<key>Label</key>
<string>my-ss-local</string>
<key>ProgramArguments</key>
<array>
<string>/Users/ruby/Code/proxy/ss-local</string>
<string>-c</string>
<string>ss-local-config.json</string>
</array>
<key>StandardErrorPath</key>
<string>/Users/ruby/Library/Logs/my-ss-local.log</string>
<key>StandardOutPath</key>
<string>/Users/ruby/Library/Logs/my-ss-local.log</string>
<key>WorkingDirectory</key>
<string>/Users/ruby/Code/proxy/</string>
</dict>
</plist>

这个配置文件的作用是,以守护进程的方式来启动 /Users/ruby/Code/proxy/ss-local -c ss-local-config.json 这个命令, 并且将标准错误输出和标准输出都重定向到 /Users/ruby/Library/Logs/my-ss-local.log 文件中。 ss-local 进程的工作目录是 /Users/ruby/Code/proxy/

启用方式为:

1
launchctl load ~/Library/LaunchAgents/ss-local.plist

homebrew 的 service - MySQL

文件 ~/Library/LaunchAgents/homebrew.mxcl.mysql@5.7.plist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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>KeepAlive</key>
<true/>
<key>Label</key>
<string>homebrew.mxcl.mysql@5.7</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/opt/mysql@5.7/bin/mysqld_safe</string>
<string>--datadir=/usr/local/var/mysql</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>WorkingDirectory</key>
<string>/usr/local/var/mysql</string>
</dict>
</plist>

我们在使用 mac 的时候最常用的 homebrew 也是通过 launchd 来管理服务的。我们可以通过 brew services list 来查看当前启动的服务。 brew 管理的服务的配置文件也会被放在 ~/Library/LaunchAgents 目录中。

上面的配置文件中,做了以下配置:

  • KeepAlive - 设置为 true,那么当进程退出的时候,launchd 会自动重启这个进程。
  • Label - 这个配置文件的唯一标识。我们除了通过 brew services list 查看到 MySQL 服务,还可以通过 launchctl list | grep mysql 来看到这个进程的状态。
  • ProgramArguments - 指定要执行的命令。这里的完整命令是 /usr/local/opt/mysql@5.7/bin/mysqld_safe --datadir=/usr/local/var/mysql
  • RunAtLoad - 设置为 true,那么当用户进入系统的时候,会自动启动这个进程。
  • WorkingDirectory - 指定 MySQL 进程的工作目录。

总结

  • mac 下可以使用 launchd 来配置一些守护进程、定时任务等。类似的工具有 systemdsupervisor 等,但是 launchd 是 mac 自带的,不用安装其他依赖就能使用。
  • launchd 配置文件类型为 plist,也就是 property list,用户级别的配置文件一般存放在 ~/Library/LaunchAgents 目录中。
  • launchd 配置的几个关键属性:
    • Label - 用来标识这个 job 的唯一标识符。
    • Program - 指定要执行的命令。(如果有参数,我们需要使用 ProgramArguments 来代替 Program
    • StandardErrorPathStandardOutPath - 指定标准错误输出和标准输出的路径。
    • WorkingDirectory - 指定命令执行的工作目录。
  • launchd 可以通过 launchctl 命令来管理:
    • launchctl list - 列出所有 launchd 管理的服务。
    • launchctl load - 加载一个 plist
    • launchctl unload - 移除一个 plist
    • launchctl start - 启动一个 job
    • launchctl stop - 停止一个 job

launchd 的使用还是挺简单的,命令也就那么几个,所以如果想在你的 mac 中启动一些守护进程的话,可以尝试一下。