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 映射串在一起,方便你理解整体脉络:

Systemd 核心概念结构图 Systemd 核心概念结构图 Unit 文件(/etc/systemd/system/) .service | .socket | .target | .timer | .mount | .path Service Unit 三段式结构 [Unit] Description After / Requires [Service] Type / ExecStart Restart / LimitNOFILE [Install] WantedBy RequiredBy Target 与 Runlevel 映射 runlevel 0 → poweroff.target | 1 → rescue.target | 2/3/4 → multi-user.target | 5 → graphical.target | 6 → reboot.target systemctl isolate / set-default 控制启动目标 WantedBy 建立软链接 到 .wants/ 目录

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

部署步骤:

  1. 把文件放到 /etc/systemd/system/myapp.service
  2. 执行 systemctl daemon-reload 让 Systemd 重新加载配置
  3. 执行 systemctl enable myapp.service 设置开机自启
  4. 执行 systemctl start myapp.service 启动服务
  5. 用 systemctl status myapp.service 查看状态

排错小贴士:

如果服务启动失败,先看 journalctl -u myapp.service -n 50 --no-pager。这是 Systemd 的日志,比去 /var/log/ 翻文件快多了。我排查问题时 90% 的情况靠这条命令就能定位。

嗯,到这里 Systemd 的入门三件套就讲完了。unit 文件是基础,service 段是核心,target 是组织方式。你把这些搞明白,后面写定制 init 脚本时就能直接上手,不用再对着 Systemd 文档发愁了。