4、Systemd入门:unit文件概念、service unit结构、target与runlevel映射
聊到系统启动,绕不开 Systemd。说实话,我早年还在用 SysV init 的时候,觉得那套 rc.d 脚本也挺好。直到有一次线上服务器启动花了快两分钟,排查下来发现一堆串行启动的脚本在互相等。后来切到 Systemd,启动时间直接砍到 20 秒。嗯,从那以后我就再也没回头了。
这一节,咱们把 Systemd 最核心的三个概念讲透:unit 文件是什么、service unit 怎么写、target 和传统 runlevel 怎么对应。你搞懂了这些,后面写定制 init 脚本就顺手多了。
4.1 Unit 文件:Systemd 的“零件”
Systemd 把系统里的每一个可管理对象都抽象成一个 unit。你可以把它理解成乐高积木——每个积木块负责一件事,拼在一起就是整个系统。
常见的 unit 类型有这些:
| 类型 | 文件后缀 | 作用 |
|---|---|---|
| service | .service | 管理后台守护进程(最常用) |
| socket | .socket | 监听网络或本地 socket,按需启动服务 |
| target | .target | 逻辑分组,相当于运行级别 |
| timer | .timer | 定时任务,替代 cron |
| mount | .mount | 管理挂载点 |
| path | .path | 监控文件或目录变化,触发其他 unit |
每个 unit 对应一个 .conf 风格的文本文件。Systemd 从这些目录加载:
/usr/lib/systemd/system/—— 软件包自带的 unit 文件/run/systemd/system/—— 运行时生成的 unit 文件/etc/systemd/system/—— 管理员自定义的 unit 文件(优先级最高)
/usr/lib/ 下的文件。需要覆盖配置时,在 /etc/systemd/system/ 下创建同名文件或使用 systemctl edit 生成 override 片段。这样系统升级时不会被覆盖。
4.2 Service Unit 结构:三段式配置
一个典型的 service unit 文件长这样。我拿一个实际项目里用过的例子:
[Unit]
Description=My Custom App Server
Documentation=https://docs.example.com/myapp
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=myapp
Group=myapp
ExecStart=/usr/local/bin/myapp-server --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
咱们拆开来看。
[Unit] 段:依赖与顺序
- Description —— 给人看的说明,
systemctl status时会显示。 - After —— 启动顺序约束。表示本 unit 在哪些 unit 之后启动,但不强制依赖。
- Requires —— 强依赖。如果 postgresql.service 启动失败,我这个服务也会被停掉。
- Wants —— 弱依赖。依赖的 unit 启动失败不影响本 unit。
After 和 Requires 搞混了。有个服务写了 After=network.target 但没写 Requires,结果网络没起来服务就启动了,连 socket 都绑不上。记住:After 只管顺序,Requires 才管依赖。
[Service] 段:行为控制
这是最核心的部分。先说 Type,它决定了 Systemd 怎么判断服务是否启动成功:
| Type | 适用场景 | 行为说明 |
|---|---|---|
| simple | 主进程持续运行(如 web 服务) | ExecStart 启动后立即认为服务已启动 |
| forking | 传统 daemon(如 nginx、sshd) | 父进程 fork 后退出,子进程继续运行 |
| oneshot | 一次性任务(如初始化脚本) | 执行完就退出,可配合 RemainAfterExit |
| notify | 支持 sd_notify 协议的服务 | 服务主动发信号通知 Systemd 已就绪 |
| dbus | 依赖 D-Bus 注册的服务 | 等待 D-Bus 名字出现在总线上 |
其他关键字段:
- ExecStart —— 启动命令,必须写绝对路径。
- ExecReload —— 重载命令,通常发信号。
- Restart —— 重启策略。我一般用
on-failure,避免服务因为临时错误就挂了。 - RestartSec —— 重启前等待秒数,防止频繁重启打满 CPU。
- LimitNOFILE —— 文件描述符上限。高并发服务一定要设,默认 1024 根本不够用。
关于 Type=forking 的注意事项:
如果你写的是传统 daemon 脚本,必须指定 Type=forking,同时最好加上 PIDFile 告诉 Systemd 子进程的 PID 文件位置。否则 Systemd 找不到子进程,重启时可能会出问题。我见过有人把 nginx 写成 Type=simple,结果 systemctl stop nginx 永远杀不掉主进程。
[Install] 段:启用与目标
这个段只有 systemctl enable 和 disable 时才会被解析。最常用的就是 WantedBy:
- WantedBy=multi-user.target —— 系统进入多用户模式时启动本服务。
- RequiredBy= —— 强依赖方式加入 target,一般不常用。
- Also= —— enable 本 unit 时同时 enable 其他 unit。
执行 systemctl enable myapp.service 后,Systemd 会在 /etc/systemd/system/multi-user.target.wants/ 下创建一个符号链接指向我们的 unit 文件。说白了,enable 就是建个软链接。
4.3 Target 与 Runlevel 映射
SysV init 时代有 0-6 七个运行级别。Systemd 用 target 替代了这套机制。target 本身不做事,它只是把一堆 service unit 组织在一起。
映射关系如下:
| SysV Runlevel | Systemd Target | 说明 |
|---|---|---|
| 0 | poweroff.target | 关机 |
| 1 | rescue.target | 单用户救援模式 |
| 2 | multi-user.target | 多用户,无图形界面 |
| 3 | multi-user.target | 多用户,无图形界面(同 2) |
| 4 | multi-user.target | 多用户,无图形界面(同 2) |
| 5 | graphical.target | 多用户,带图形界面 |
| 6 | reboot.target | 重启 |
你想想看,SysV 里 2、3、4 三个级别其实没区别,但很多人配置时容易搞混。Systemd 直接合并成 multi-user.target,清爽多了。
查看当前默认 target:
systemctl get-default
切换默认 target:
systemctl set-default multi-user.target
临时切换(仅当前会话):
systemctl isolate rescue.target
systemctl set-default multi-user.target 就行。想恢复图形界面就 systemctl set-default graphical.target。比改 inittab 方便多了。
4.4 知识结构图
下面这张图把 unit 文件、service 结构、target 映射串在一起,方便你理解整体脉络:
4.5 实战:写一个简单的 service unit
光说不练假把式。咱们写一个最简单的 service unit,用来启动一个 Python 写的 HTTP 服务。
假设你的程序在 /opt/myapp/server.py:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Python HTTP Server
After=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/server.py
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
部署步骤:
- 把文件放到
/etc/systemd/system/myapp.service - 执行
systemctl daemon-reload让 Systemd 重新加载配置 - 执行
systemctl enable myapp.service设置开机自启 - 执行
systemctl start myapp.service启动服务 - 用
systemctl status myapp.service查看状态
排错小贴士:
如果服务启动失败,先看 journalctl -u myapp.service -n 50 --no-pager。这是 Systemd 的日志,比去 /var/log/ 翻文件快多了。我排查问题时 90% 的情况靠这条命令就能定位。
嗯,到这里 Systemd 的入门三件套就讲完了。unit 文件是基础,service 段是核心,target 是组织方式。你把这些搞明白,后面写定制 init 脚本时就能直接上手,不用再对着 Systemd 文档发愁了。