惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

披萨盒的赛博日志

使用策略模式重构复杂业务分支 以ORM看封装的边界 Git Merge VS Git Rebase: 如何优雅地合并分支? 修改Linux内核模块以支持WG OpenLDAP折腾日记 非特权模式容器 ssh 登录问题 在 Linux 开发环境中使用网络代理 白嫖 Aseprite 像素绘图软件 MongoDB 增删改查 Python数据分析工具包-Numpy 解决 CLion 中文乱码问题 搭建 RLCraft 服务器 SpringBoot读取配置文件 Centos 配置 LNMP 环境 部署项目时遇到的坑 浅谈 xhr 请求跨域问题 JavaScript 学习笔记 Eclipse配置Web开发环境 Vue2 基本知识 Ribbon 简单使用 Nacos 简单使用 Spring Cloud Alibaba 环境搭建 什么是RSS?什么是Feed?它们有什么关系? Docker基本使用 TensorFlow启用GPU加速 如何进行内网穿透 Hello World! Git基本使用 Butterfly常用标签外挂
像 systemd 一样管理 MacOS 后台常驻任务
2025-06-14 · via 披萨盒的赛博日志

引言

想在 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,然后接下来给出 service 文件(核心)的详细说明。

小例子:后台记录时间

一、首先我们需要编写一个需要后台运行的脚本,这里以每10秒输出一次时间的脚本/root/bin/systemd_test.sh为例,内容如下

1
2
3
4
5
6
7
#!/bin/bash

while true; do
CURRENT_TIME=$(date +"%Y-%m-%d %H:%M:%S")
echo "$CURRENT_TIME"
sleep 10
done

二、赋予脚本可执行权限

1
chmod +x /root/bin/systemd_test.sh

三、先执行一下看看有没有问题

image-20250614192113838

四、创建 systemd 服务文件

/etc/systemd/system/目录下创建一个服务文件,名字随意,但必须以.service结尾,本示例为/etc/systemd/system/record_time.service,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=Record Time Service
After=network.target

[Service]
Type=simple
ExecStart=/root/bin/systemd_test.sh
Restart=on-failure
RestartSec=10s
StandardOutput=append:/var/log/record_time/output.log
StandardError=append:/var/log/record_time/error.log

[Install]
WantedBy=multi-user.target

五、执行如下命令即可启动服务

1
2
3
systemctl daemon-reload
systemctl start record_time
systemctl enable record_time

成功运行示例如下

image-20250614195034781

如果启动失败,可以使用命令journalctl -u record_time查看失败日志。比如我最开始就因为忘记创建/var/log/record_time目录导致启动失败了😂

image-20250614195313254

service 文件详解

看到网上有一篇介绍 service 文件如何编写的文章 OpenSUSE: How to write a systemd service,已经写的很通俗很详细了,这里就不重复造轮子了。

用户级 systemd

上面我们给出的例子是需要管理员权限的,普通用户是无法在/etc/systemd/system目录下创建.service文件的

image-20250614202237287

但是我们可以在自己的用户目录下创建,具体的目录为~/.config/systemd/user/xxx.service,service 文件的内容和普通 service 文件是相同的,相应的管理命令也稍有不同(需要加上--user参数),如下

1
2
3
systemctl --user daemon-reload
systemctl --user start record_time
systemctl --user enable record_time

有一点比较坑的是执行systemctl --user enable xxx后的后台守护进程只在用户登录后运行,用户退出登录后服务就停止了。有一个解决办法是设置用户登录会话为逗留状态,这样即使用户注销,其会话仍然会保持,也就允许后台服务或定时任务继续运行,命令如下

1
2
loginctl enable-linger <username>    
loginctl disable-linger <username>

launchd 实战入门

同上,我们仍然是先给出一个例子,然后再给出具体解释。

例子:后台记录时间

(没错,还是这个例子,我们主要关注操作方式与 systemd 的不同)

一、编写脚本~/Scripts/launchd_test.sh,内容如下

1
2
3
4
5
6
7
#!/bin/bash

while true; do
CURRENT_TIME=$(date +"%Y-%m-%d %H:%M:%S")
echo "$CURRENT_TIME"
sleep 10
done

二、赋予脚本可执行权限

1
chmod +x launchd_test.sh

三、先执行一下,看看有没有问题

image-20250614201839000

四、创建 plist 配置文件

~/Library/LaunchAgents目录下创建一个服务文件,名字随意,但必须以.plist结尾,本示例为~/Library/LaunchAgents/com.pushihao.record_time.plist,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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.pushihao.record_time</string>

<key>ProgramArguments</key>
<array>
<string>/Users/itgrape/Scripts/launchd_test.sh</string>
</array>

<key>RunAtLoad</key>
<true/>

<key>KeepAlive</key>
<true/>

<key>StandardOutPath</key>
<string>/Users/itgrape/logs/record_time/output.log</string>

<key>StandardErrorPath</key>
<string>/Users/itgrape/logs/record_time/error.log</string>

<key>WorkingDirectory</key>
<string>/Users/itgrape/Scripts/</string>

</dict>
</plist>

五、加载/卸载服务

旧:

1
2
launchctl load ~/Library/LaunchAgents/com.pushihao.record_time.plist    
launchctl unload ~/Library/LaunchAgents/com.pushihao.record_time.plist

新(更推荐):

1
2
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.pushihao.record_time.plist    
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.pushihao.record_time.plist

六、执行命令启动服务

1
2
3
4
5
launchctl list | grep com.pushihao.record_time            
launchctl start com.pushihao.record_time
launchctl stop com.pushihao.record_time
launchctl enable gui/$(id -u)/com.pushihao.record_time
launchctl disable gui/$(id -u)/com.pushihao.record_time

注意:因为 KeepAlive 设置为 true,所以手动 stop 后,launchd 可能很快会再次尝试启动。要想永久停止,需要使用上述卸载服务的 unload/bootout 命令。

成功运行示例如下

image-20250614205709768

还会弹出个这玩意

image-20250614210112192

plist 文件解释

这里去看 Apple 的官方文档即可 Daemons and Services Programming Guide,它介绍的已经非常详细了。这里就简要介绍一下上面例子中各配置项的作用。

  • Label: 服务的唯一标识符,launchctl命令会用到它。
  • ProgramArguments: 要执行的命令和参数。数组的第一个字符串是要执行脚本的绝对路径
  • RunAtLoad: <true/>表示当服务被加载时立即启动。
  • KeepAlive: <true/>这是实现长期运行的关键!如果设为truelaunchd会监视该进程,一旦它退出(无论是因为错误崩溃还是正常结束),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,并非替代关系。