像 systemd 一样管理 MacOS 后台常驻任务
引言
想在 MacOS 后台跑一个 frp 服务,想到 MacOS 是类 Unix 系统所以应该跟 Linux 差不多,自然而然的想到使用 systemd 将 frp 服务作为守护进程管理。然鹅输入vim /etc/systemd/sys
之后按了半天 tab 都不自动补全为vim /etc/systemd/system
,一看/etc
目录下压根就没有systemd
这个目录。上网一搜才知道 MacOS 压根就没有 systemd,取而代之的是 launchd。既然都提到了,就简要介绍一下吧。
起源
传统的 Linux 系统使用 System V init 或 BSD init 作为系统的第一个进程,负责启动其他服务和进程。随着系统复杂性的增加,这些 init 系统暴露出一些缺点,比如串行启动服务、启动速度慢等。
随后,upstart 项目引入了并行启动服务等概念用以解决 System V init 存在的问题,但仍然存在一些限制。
接着,systemd 诞生了,它提供更高效、更灵活的系统启动和管理方式。但是由于 systemd 使用了 cgroups 等组件以实现其特性,所以只适用于Linux。
苹果系统是闭源的,但是 launchd 却是开源的,还挺有意思。其最早由 Dave Zarzycki 等人创建,并在Mac OS X Tiger(10.4版本)中首次引入,用于替代传统的 init 脚本、SystemStarter 以及其他一些服务管理工具,如 inetd、atd 和 crond 等。
systemd 实战入门
在本节中,我们首先通过一个小🌰来入门 systemd,然后接下来给出 service 文件(核心)的详细说明。
小例子:后台记录时间
一、首先我们需要编写一个需要后台运行的脚本,这里以每10秒输出一次时间的脚本/root/bin/systemd_test.sh
为例,内容如下
1 | !/bin/bash |
二、赋予脚本可执行权限
1 | chmod +x /root/bin/systemd_test.sh |
三、先执行一下看看有没有问题
四、创建 systemd 服务文件
在/etc/systemd/system/
目录下创建一个服务文件,名字随意,但必须以.service
结尾,本示例为/etc/systemd/system/record_time.service
,内容如下
1 | [Unit] |
五、执行如下命令即可启动服务
1 | systemctl daemon-reload |
成功运行示例如下
如果启动失败,可以使用命令journalctl -u record_time
查看失败日志。比如我最开始就因为忘记创建/var/log/record_time
目录导致启动失败了😂
service 文件详解
看到网上有一篇介绍 service 文件如何编写的文章 OpenSUSE: How to write a systemd service,已经写的很通俗很详细了,这里就不重复造轮子了。
用户级 systemd
上面我们给出的例子是需要管理员权限的,普通用户是无法在/etc/systemd/system
目录下创建.service
文件的
但是我们可以在自己的用户目录下创建,具体的目录为~/.config/systemd/user/xxx.service
,service 文件的内容和普通 service 文件是相同的,相应的管理命令也稍有不同(需要加上--user
参数),如下
1 | systemctl --user daemon-reload |
有一点比较坑的是执行systemctl --user enable xxx
后的后台守护进程只在用户登录后运行,用户退出登录后服务就停止了。有一个解决办法是设置用户登录会话为逗留状态,这样即使用户注销,其会话仍然会保持,也就允许后台服务或定时任务继续运行,命令如下
1 | loginctl enable-linger <username> # 启动逗留状态 |
launchd 实战入门
同上,我们仍然是先给出一个例子,然后再给出具体解释。
例子:后台记录时间
(没错,还是这个例子,我们主要关注操作方式与 systemd 的不同)
一、编写脚本~/Scripts/launchd_test.sh
,内容如下
1 | !/bin/bash |
二、赋予脚本可执行权限
1 | chmod +x launchd_test.sh |
三、先执行一下,看看有没有问题
四、创建 plist 配置文件
在~/Library/LaunchAgents
目录下创建一个服务文件,名字随意,但必须以.plist
结尾,本示例为~/Library/LaunchAgents/com.pushihao.record_time.plist
,内容如下
1 |
|
五、加载/卸载服务
旧:
1 | launchctl load ~/Library/LaunchAgents/com.pushihao.record_time.plist # 加载服务 |
新(更推荐):
1 | launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.pushihao.record_time.plist # 在用户 GUI 域中注册服务 |
六、执行命令启动服务
1 | launchctl list | grep com.pushihao.record_time # 检查服务是否被加载 |
注意:因为 KeepAlive 设置为 true,所以手动 stop 后,launchd 可能很快会再次尝试启动。要想永久停止,需要使用上述卸载服务的 unload/bootout 命令。
成功运行示例如下
还会弹出个这玩意
plist 文件解释
这里去看 Apple 的官方文档即可 Daemons and Services Programming Guide,它介绍的已经非常详细了。这里就简要介绍一下上面例子中各配置项的作用。
Label
: 服务的唯一标识符,launchctl
命令会用到它。ProgramArguments
: 要执行的命令和参数。数组的第一个字符串是要执行脚本的绝对路径。RunAtLoad
:<true/>
表示当服务被加载时立即启动。KeepAlive
:<true/>
这是实现长期运行的关键!如果设为true
,launchd
会监视该进程,一旦它退出(无论是因为错误崩溃还是正常结束),launchd
会立刻重新启动它。StandardOutPath
: (可选) 重定向标准输出(脚本中echo
的内容)到指定文件。StandardErrorPath
: (可选) 重定向标准错误到指定文件。这对于排查脚本错误至关重要。WorkingDirectory
: (可选) 设置脚本运行前的工作目录。
系统级 launchd
注意看,这里我的介绍顺序是和 systemd 是反着的。systemd 我给出的例子是系统级,随后我又给出了用户级 systemd 的使用方法。而该 launchd 的例子我给出的是用户级,接下来我介绍一下系统级 launchd 的使用。
注意:上面我们已经提到了旧版和新版的加载/卸载命令,他们对应的用户级/系统级 launchd 的使用方式也是不同的。
旧版 load/unload 命令:
其区别主要在于 plist 文件的存放位置,系统级 launchd 的 .plist 文件放在/Library/LaunchDaemons/
目录下,而用户级 launchd 的 .plist 文件放在~/Library/LaunchAgents/
或/Library/LaunchAgents/
目录下。他们之间的区别如下:
~/Library/LaunchAgents
- 启动时机:仅当特定用户登录时启动。
- 运行权限:以当前用户的权限运行。
- 访问GUI:可以。因为它们在用户的图形会话中运行,所以这些任务可以创建窗口、显示菜单栏图标或执行其他与用户界面交互的操作。
- 适用场景:适用于仅当前用户需要的服务,例如个人的定时任务或用户特定的后台程序。
/Library/LaunchAgents
- 启动时机:当任何用户登录时启动。
- 运行权限:以当前登录用户的权限运行。
- 访问GUI:可以。同上。
- 适用场景:适用于所有用户共用的用户级服务,例如某些需要在用户登录时启动的系统服务。
/Library/LaunchDaemons
- 启动时机:在系统启动时运行,无需用户登录。
- 运行权限:通常以root用户的权限运行,但也可以指定其他用户。
- 访问GUI:不可以。因为它们独立于任何用户,所以它们无法(也不应该)与用户界面进行任何交互。
- 适用场景:适用于需要在系统启动时运行的与用户无关的全局服务,例如网络服务、数据库服务等。
除此之外,/System/Library/LaunchDaemons/
和/System/Library/LaunchAgents/
目录下也会保存有一些.plist
文件,这些文件是由苹果系统提供的系统守护进程或用户代理。一般不要动它们。
新版 bootstrap/bootout 命令:
bootstrap 与旧的 load 命令最大的不同在于明确性。
launchctl load(旧方式)通过
.plist
文件的存放路径来推断我们想要将服务注册到哪个域。这种方式比较模糊,依赖于文件路径的约定,现在苹果官方已不推荐在新的脚本中使用。launchctl bootstrap(新方式)强制要求我们必须明确指定要注册到哪个域,命令本身就包含了所有信息,不再依赖于文件路径来做判断。这种方式更清晰、更精确,减少了歧义,是目前官方推荐的方式。
通过域信息,我们就可以指定该任务是在哪个级别下运行了。域信息如下
- system 域:类似于老方式的
/Library/LaunchDaemons
。 - gui<uid> 域:在指定用户登录后创建的图形会话中运行,类似于老方式下的
~/Library/LaunchAgents
。 - user<uid> 域,用于注册指定用户后台代理,但不提供GUI访问权限。
虽说使用了新版方式后理论上.plist
文件可以放在任何位置,但是由于系统启动或用户登录时 launchd 进程只会去扫描一组预先定义好的、受信任的标准目录(也就是我们上面所说的5个目录)而并不会去扫描整个硬盘来寻找所有.plist
文件。因此如果我们的.plist
文件放在一个自定义目录(比如/Users/itgrape/Launchd/
),那么在扫描阶段,launchd 根本就不会去查看那个目录,因此它永远发现不了我们的服务,这也就导致了 enable 可能会失效。兼容起见,还是放在上面我提到的三个目录其中之一里面吧。
在读完了上述章节之后,相信读者也已经对 systemd 和 launchd 的基本使用有了一定的了解。接下来我们在设计层面比较一下他们之间的差异。
两大体系对比
它们虽然干着类似的活儿——管理那些开机启动的、默默在后台运行的服务(守护进程),以及响应各种事件触发任务,但它们两个的设计理念以及工作方式却大不相同。
启动方式
Launchd:按需启动
- Mac 上有很多服务(比如打印服务、文件共享服务等)。默认按需启动,也就是说 Launchd 让这些服务平时都在“睡觉”,节省资源。只有当真正有人或系统事件需要它时(比如我们点了“打印”,或者有个网络连接请求进来),它才会迅速唤醒对应的服务。服务干完活,如果没特别要求,它就又回去“睡觉”了。这样电脑就能又快(响应我们的操作时快)又省电。除非指定了 KeepAlive 为 true,服务才会一直运行。
Systemd:开机并行驱动
- 它首要目标就是让 Linux 开机速度大大加快。怎么做到?不是按需启动,而是并行启动。分析好各个服务之间的依赖关系(谁先启动谁后启动),然后尽可能多同时启动没有依赖冲突的服务。结果就是:系统整体启动嘎嘎快。
文件组织
Launchd:所有配置全放在.plist
文件(一种 XML 或二进制格式的文件)里。这个文件描述了服务叫什么名字(Label)、启动命令是什么(ProgramArguments)、监听什么事件(Sockets, WatchPaths, StartCalendarInterval)、要不要一直跑(KeepAlive)等等。简单来说,就这一种文件。
Systemd:配置是按单元(Unit)分的(.ini
风格),有不同的类型,每种单元单独写一个文件:
.service
:定义要运行的后台服务本身(启动命令、重启策略等)。.socket
:定义监听一个网络端口或 Unix Socket(用来按需启动服务)。.timer
:定义定时任务(替代 cron)。.path
:定义监控文件或目录变化(变化了启动服务)。.target
:定义一组单元(类似传统 Linux 的运行级别)。- … 其他(
.mount
,.slice
等)。
日志查看
Launchd:服务自己决定日志写哪。
Systemd:它自己带了个日志系统叫 journald。所有由 systemd 管理的服务的日志(包括它们的输出信息),默认都统一收拢到这里。然后用journalctl
命令就能查、过滤、跟踪所有服务的日志。
快速参考备忘录
接下来我将用一个清晰的表格概括 systemd 和 launchd 的使用。
表格中...
表示带后缀的配置文件的绝对路径。
功能 | systemd (Linux) | launchd (macOS) |
---|---|---|
配置文件 | /etc/systemd/system/my-app.service | ~/Library/LaunchAgents/com.me.myapp.plist |
启用并立即启动 | sudo systemctl enable –now my-app | launchctl load … | launchctl bootstrap … |
禁用并停止 | sudo systemctl disable –now my-app | launchctl unload … | launchctl bootout … |
启动服务 | sudo systemctl start my-app | launchctl start com.me.myapp |
停止服务 | sudo systemctl stop my-app | launchctl stop com.me.myapp |
查看服务状态 | sudo systemctl status my-app | launchctl list |
查看日志 | sudo journalctl -u my-app -f | tail -f /path/to/your.log |
重载配置 | sudo systemctl daemon-reload | 需要先 unload 再 load |
总结
systemd 使用于 Linux,而 launchd 适用于 MacOS,并非替代关系。