主题
systemd 使用详解
简介
systemd 是现代 Linux 发行版中最常见的系统与服务管理器,通常作为 PID 1 运行。它负责在开机时拉起系统服务、管理服务生命周期、处理依赖关系,并提供了定时器、socket 激活、日志、设备管理等一整套基础设施。
很多发行版已经用 systemd 取代了传统的 SysVinit 和 Upstart。你日常接触到的 systemctl、journalctl、loginctl、timedatectl、hostnamectl、localectl 等命令,都是 systemd 项目的一部分。
systemd 并不是所有 Linux 环境都使用:Alpine 等发行版使用 OpenRC,普通 Docker 容器、chroot 和一些精简系统通常没有以 systemd 作为 PID 1。
配套阅读
systemd 负责“服务怎么跑”,日志排查交给 journalctl。如果是分析日志内容,还可以配合 grep 和 awk。
systemd 的组成
| 组件 | 作用 |
|---|---|
systemd | PID 1,系统与服务管理器 |
systemctl | 管理 unit、服务、target 等 |
systemd-journald | 收集内核、服务、syslog 日志 |
journalctl | 查询 journal 日志 |
logind | 登录会话与用户管理 |
networkd / NetworkManager | 网络配置(不同发行版选择不同) |
resolved | DNS 解析 |
udev | 设备管理 |
timedated / hostnamed / localed | 时间、主机名、区域设置 |
systemd-coredump | 收集和分析核心转储 |
unit 类型
systemd 把各种系统资源抽象成 unit,常见的后缀:
| 后缀 | 类型 | 说明 |
|---|---|---|
.service | 服务 | 最常用,管理守护进程或一次性任务 |
.socket | socket | 按连接激活服务 |
.timer | 定时器 | 按时间或启动后延迟触发服务 |
.target | 目标 | 把一组 unit 组织成一个启动阶段 |
.mount / .automount | 挂载 | 文件系统挂载 |
.path | 路径 | 文件或目录变化时触发服务 |
.slice | 资源切片 | 对进程组做资源限制 |
.scope | 作用域 | 管理外部创建的进程组 |
.swap | 交换分区 | swap |
.device | 设备 | udev 设备 |
日常使用中,90% 以上的时间都在和 .service、.timer、.socket 打交道。
unit 文件放在哪里
系统级 unit 的主要搜索路径:
text
/etc/systemd/system/ # 管理员自定义,优先级最高
/run/systemd/system/ # 运行时生成
/usr/lib/systemd/system/ # 发行版/软件包提供用户级 unit:
text
~/.config/systemd/user/ # 当前用户自定义
/etc/systemd/user/ # 系统级用户配置
/usr/lib/systemd/user/ # 软件包提供同名 unit 会按优先级覆盖。修改软件包自带的 unit 时,推荐用 drop-in,而不是直接改 /usr/lib/systemd/system/ 下的文件;软件包升级会覆盖后者。
不要直接改 /usr/lib
/usr/lib/systemd/system 属于软件包管理范围。自定义配置放在 /etc/systemd/system,或者用 systemctl edit 生成 drop-in。
systemctl 基础
查看状态
bash
systemctl status sshd
systemctl status sshd --no-pager -l典型输出:
text
● sshd.service - OpenSSH Daemon
Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; preset: enabled)
Active: active (running) since Mon 2026-10-19 09:12:31 CST; 6h ago
Main PID: 812 (sshd)
Tasks: 1 (limit: 18814)
Memory: 6.2M (peak: 7.1M)
CPU: 81ms
CGroup: /system.slice/sshd.service
└─812 "sshd: /usr/bin/sshd -D [listener]"重点看:
Loaded:unit 文件路径、是否 enabledActive:当前状态,如active (running)、inactive (dead)、failedMain PID:主进程CGroup:服务包含的进程- 末尾几行:最近的日志(完整日志用
journalctl)
启动、停止、重启
bash
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx
sudo systemctl try-restart nginx
sudo systemctl reload-or-restart nginx区别:
start:启动stop:停止restart:停止再启动reload:让服务重新加载配置,需要 unit 提供ExecReload,或服务实现Type=notify-reload协议try-restart:只有服务正在运行时才重启reload-or-restart:能 reload 就 reload,否则 restart
对支持平滑重载的服务(如 Nginx),reload 通常不会中断现有连接;如果服务不支持 reload,会报错,此时用 restart。
开机自启
bash
sudo systemctl enable nginx
sudo systemctl disable nginx
sudo systemctl enable --now nginx
sudo systemctl disable --now nginx
sudo systemctl is-enabled nginxenable 和 start 是两件事:
enable创建 target 的依赖链接,让服务开机自启start只影响当前运行状态enable --now同时做这两件事
is-enabled 常见输出:
text
enabled
disabled
static
masked
indirect
generated
transientstatic 表示 unit 没有 [Install] 段,通常由其他 unit 或 target 拉起,不能直接 enable。
查看 unit 列表
bash
# 当前已加载的 unit
systemctl list-units
systemctl list-units --type=service
systemctl list-units --type=service --state=running
systemctl list-units --all
# 已安装的 unit 文件
systemctl list-unit-files
systemctl list-unit-files --type=service --state=enabled
# 失败的 unit
systemctl --failed
systemctl list-units --failedlist-units 默认只显示已加载且处于 active、等待任务或 failed 状态的 unit;加 --all 显示全部已加载 unit(包括 inactive)。未加载的 unit 文件用 list-unit-files 查看。
查看 unit 文件
bash
# 显示 unit 文件及 drop-in
systemctl cat nginx.service
# 显示合并后的有效属性
systemctl show nginx.service
systemctl show nginx.service -p ActiveState,SubState,MainPID,ExecStart
# 显示依赖关系
systemctl list-dependencies nginx.service
systemctl list-dependencies --reverse nginx.servicesystemctl cat 会按加载顺序列出所有相关文件,包括 /etc/systemd/system/nginx.service.d/*.conf。systemctl show 展示的是最终生效的属性,排查覆盖关系时很有用。
修改 unit
bash
# 创建 drop-in,推荐
sudo systemctl edit nginx.service
# 编辑完整副本,谨慎使用
sudo systemctl edit --full nginx.service
# 撤销本地修改,恢复软件包版本
sudo systemctl revert nginx.servicesystemctl edit 默认创建:
text
/etc/systemd/system/nginx.service.d/override.confdrop-in 只需要写要覆盖的字段:
ini
[Service]
Environment=DEBUG=1修改 unit 文件或 drop-in 后必须执行:
bash
sudo systemctl daemon-reload否则 systemd 仍然使用旧配置。daemon-reload 不会重启服务,需要再 restart 才让新配置生效。
启用与屏蔽
bash
sudo systemctl mask nginx.service
sudo systemctl unmask nginx.servicedisable 只是取消开机自启,手动仍然可以 start;mask 会把 unit 链接到 /dev/null,连手动启动也会被拒绝。排查“服务被谁拉起来”时,mask 比 disable 更彻底。
其他常用命令
bash
# 当前系统状态
systemctl is-system-running
# 默认 target
systemctl get-default
sudo systemctl set-default multi-user.target
# 进入救援/紧急模式
sudo systemctl rescue
sudo systemctl emergency
# 查看任务队列
systemctl list-jobs
sudo systemctl cancel JOB_ID
# 向服务发送信号
sudo systemctl kill -s SIGUSR1 nginx.service
# 清理失败状态
sudo systemctl reset-failed nginx.serviceis-system-running 可能返回:
text
initializing
starting
running
degraded
maintenance
stopping
offline
unknowndegraded 表示系统已经启动,但有 unit 失败。先看 systemctl --failed。
Unit 文件结构
一个 service unit 通常由三段组成:
ini
[Unit]
Description=My Application
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/opt/myapp/bin/myapp
Restart=on-failure
[Install]
WantedBy=multi-user.target[Unit] 段
| 指令 | 作用 |
|---|---|
Description | 人类可读的描述 |
Documentation | 文档链接,可写多个 |
After | 仅在指定 unit 之后启动,不负责拉起对方 |
Before | 在指定 unit 之前启动 |
Wants | 弱依赖,对方启动失败不影响自己 |
Requires | 强依赖,对方失败时自己也失败 |
BindsTo | 比 Requires 更紧,对方停止时自己也停止 |
PartOf | 停止/重启对方时,自己也跟着停止/重启 |
Conflicts | 与指定 unit 冲突,不能同时运行 |
OnFailure | 失败时启动另一个 unit |
StartLimitIntervalSec | 启动频率限制的时间窗口 |
StartLimitBurst | 时间窗口内允许的启动次数 |
Condition... | 条件不满足时跳过 unit,不算失败 |
Assert... | 条件不满足时让 unit 失败 |
After 和 Wants 经常一起用:
ini
[Unit]
Wants=network-online.target
After=network-online.targetWants 负责把对方拉起来,After 负责保证顺序。只写 After 不会自动启动依赖。
[Service] 段
Type 的选择
| Type | 适用场景 |
|---|---|
simple | 默认值,ExecStart 启动后立即认为服务已启动 |
exec | 等 ExecStart 成功 exec 后再认为服务已启动 |
forking | 传统守护进程,父进程 fork 后退出,需配合 PIDFile |
oneshot | 一次性任务,进程退出后才算启动完成 |
notify | 服务通过 sd_notify() 发送 READY=1 |
dbus | 服务在 D-Bus 上取得名字后才算启动完成 |
idle | 等所有任务完成后再启动,较少使用 |
大多数自己写的长期运行程序用 Type=simple 或 Type=exec。传统 daemon 用 Type=forking;脚本、备份、初始化任务用 Type=oneshot。
oneshot 如果需要在执行完后保持 active 状态,加:
ini
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/init.sh否则任务执行完,服务会变成 inactive (dead),这是正常行为。
Exec 系列
| 指令 | 作用 |
|---|---|
ExecStart | 启动命令 |
ExecStartPre | 启动前执行,可多条 |
ExecStartPost | 启动后执行,可多条 |
ExecReload | reload 时执行 |
ExecStop | 停止时执行 |
ExecStopPost | 停止后执行,可多条 |
ExecStart 不会经过 Shell。下面这些写法不会按预期工作:
ini
# 错误:管道、重定向、通配符不会被 Shell 解释
ExecStart=/usr/bin/foo --log /var/log/foo.log > /dev/null 2>&1
ExecStart=/usr/bin/foo | /usr/bin/bar需要 Shell 特性时显式调用:
ini
ExecStart=/bin/sh -c 'exec /opt/myapp/bin/myapp >> /var/log/myapp.log 2>&1'但更推荐让程序自己写日志,或者使用 StandardOutput=journal,交给 journald 管理。
命令前加 - 表示忽略失败:
ini
ExecStartPre=-/usr/bin/rm -f /run/myapp.pid重启策略
| Restart 值 | 行为 |
|---|---|
no | 不重启(默认) |
on-success | 正常退出时重启 |
on-failure | 非零退出、信号、超时、watchdog 时重启 |
on-abnormal | 信号、超时、watchdog 时重启 |
on-abort | 因未捕获信号退出时重启 |
always | 总是重启 |
ini
[Service]
Restart=on-failure
RestartSec=3注意:
Restart=on-failure不会在程序正常退出(exit 0)时重启- 短时间反复重启会触发
StartLimit,unit 进入 failed - 触发限制后需要
systemctl reset-failed,或者调整StartLimitIntervalSec/StartLimitBurst
用户、环境与工作目录
ini
[Service]
User=app
Group=app
WorkingDirectory=/opt/myapp
Environment=APP_ENV=production
Environment=DEBUG=0
EnvironmentFile=-/etc/myapp/envEnvironmentFile前加-表示文件不存在也不报错EnvironmentFile的格式是KEY=VALUE,不支持 Shell 展开WorkingDirectory不存在会导致启动失败(常见200/CHDIR)- 使用
User=后,服务以该用户身份运行,文件和目录权限要提前配好
日志与文件描述符
ini
[Service]
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
LimitNOFILE=65535默认情况下,服务的 stdout/stderr 会进入 journal。也可以用:
ini
StandardOutput=append:/var/log/myapp.log
StandardError=append:/var/log/myapp.log但这样会绕过 journald,需要自己处理日志轮转。大多数场景直接用 journal 更省事。
安全加固
常见指令:
ini
[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/myapp /var/log/myapp
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=ProtectSystem=strict会把大部分系统目录挂成只读,必须用ReadWritePaths显式放开可写路径ProtectHome=true隐藏/home、/root、/run/userPrivateTmp=true给服务独立的/tmp和/var/tmpNoNewPrivileges=true阻止 setuid/setgid 提权CapabilityBoundingSet=清空 capability,需要时再单独加DynamicUser=yes可以配合StateDirectory、CacheDirectory、LogsDirectory自动创建专用用户和目录
安全选项不是越多越好。加得太严会导致服务无法读写配置或启动失败,建议先用 systemd-analyze security 看基线,再逐项增加并测试。
[Install] 段
ini
[Install]
WantedBy=multi-user.targetWantedBy=multi-user.target 表示 enable 时创建一个从 multi-user.target.wants 指向本 unit 的链接。用户服务通常写:
ini
[Install]
WantedBy=default.target其他指令:
RequiredBy=:强依赖Alias=:给 unit 增加别名Also=:同时 enable/disable 其他 unit
没有 [Install] 的 unit 通常是 static,需要由其他 unit 或 target 拉起。
模板 unit
文件名里带 @ 的是模板:
text
myapp@.service启动时传入实例名:
bash
sudo systemctl start myapp@blue.serviceunit 里用 %i 引用实例:
ini
[Service]
ExecStart=/opt/myapp/bin/myapp --profile %isystemd-escape 可以处理路径和特殊字符:
bash
systemd-escape --path /var/lib/myapp
systemd-escape --path --unescape var-lib-myapp常用 specifier
| 写法 | 含义 |
|---|---|
%n | 完整 unit 名,如 myapp.service |
%N | 去掉后缀的 unit 名 |
%p | unit 前缀,如 myapp |
%i | 实例名 |
%I | 未转义的实例名 |
%u | 用户名 |
%U | 用户 UID |
%h | 用户家目录 |
%t | 运行时目录,通常是 /run |
%S | 状态目录,通常是 /var/lib |
%C | 缓存目录,通常是 /var/cache |
%H | 主机名 |
%v | 内核版本 |
%% | 字面量 % |
在 unit 文件里写 % 时要注意转义,普通百分号写成 %%。
编写一个 service
示例:长期运行的 Web 服务
假设程序在 /opt/myapp/bin/myapp,以 app 用户运行,监听 8080。
ini
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
Documentation=https://example.com/
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=30
StartLimitBurst=5
[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/myapp
Environment=APP_ENV=production
EnvironmentFile=-/etc/myapp/env
ExecStart=/opt/myapp/bin/myapp --listen 0.0.0.0:8080
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=3
TimeoutStopSec=15
LimitNOFILE=65535
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/myapp /var/log/myapp
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target示例中的 /var/lib/myapp、/var/log/myapp 需要提前创建,并给 app 用户写权限;也可以改用 StateDirectory=myapp、LogsDirectory=myapp 让 systemd 自动创建。
安装并启动:
bash
sudo install -m 0644 myapp.service /etc/systemd/system/myapp.service
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service查看日志:
bash
journalctl -u myapp.service -b -n 100 --no-pager
journalctl -u myapp.service -f常见检查:
bash
systemctl cat myapp.service
systemctl show myapp.service -p ExecStart,ActiveState,SubState,MainPID
systemd-analyze verify /etc/systemd/system/myapp.service用 drop-in 覆盖配置
不要直接改 myapp.service,而是:
bash
sudo systemctl edit myapp.service在打开的编辑器中写:
ini
[Service]
Environment=DEBUG=1
RestartSec=5保存后:
bash
sudo systemctl daemon-reload
sudo systemctl restart myapp.servicesystemctl cat 会看到 vendor unit 和 drop-in 的组合。需要恢复时:
bash
sudo systemctl revert myapp.service示例:一次性初始化脚本
ini
[Unit]
Description=Initialize My Application
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/myapp-init.sh
RemainAfterExit=yes
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.targetRemainAfterExit=yes 让脚本执行完后服务仍显示为 active (exited),适合只需要执行一次的初始化逻辑。
依赖关系与 target
依赖与顺序是两件事
ini
[Unit]
Requires=postgresql.service
After=postgresql.serviceRequires:postgresql 必须启动;它失败,本服务也失败After:本服务在 postgresql 之后启动
只有 After 不会拉起 postgresql;只有 Requires 不保证顺序。常见组合是:
ini
Wants=network-online.target
After=network-online.targetWants 是弱依赖,适合“希望有网络,但没有网络也尽量启动”的服务;Requires 适合真正不能缺少的依赖。
常见 target
| target | 含义 |
|---|---|
multi-user.target | 多用户命令行模式 |
graphical.target | 图形界面模式 |
rescue.target | 单用户救援模式 |
emergency.target | 最小紧急模式 |
network.target | 网络栈已启动 |
network-online.target | 网络已经可用 |
timers.target | 定时器 |
sockets.target | socket 激活 |
default.target | 默认启动目标 |
network.target 只表示网络管理服务已经启动,不保证网卡拿到地址;需要网络真正可用时用 network-online.target。
查看和切换
bash
systemctl get-default
systemctl list-dependencies multi-user.target
systemctl list-dependencies --reverse myapp.service
sudo systemctl set-default multi-user.target
sudo systemctl isolate rescue.targetisolate 会停止不属于目标 target 的其他 unit,生产环境慎用。
条件与失败处理
ini
[Unit]
ConditionPathExists=/etc/myapp/config.yml
ConditionFileNotEmpty=/etc/myapp/config.yml
OnFailure=myapp-failure.serviceCondition... 不满足时 unit 会被跳过,不算失败;Assert... 不满足时会让 unit 进入 failed。排查“为什么服务没启动”时,先看条件是否被跳过:
bash
systemctl status myapp.service
systemctl show myapp.service -p ConditionResult,AssertResult定时器 timer
timer 用来替代 cron 做定时任务,和 service 配合使用。timer 和 service 通常同名:
ini
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.shini
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup every night
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=10m
AccuracySec=1m
[Install]
WantedBy=timers.target启用 timer:
bash
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers --all查看下次触发时间和上次触发时间:
bash
systemctl list-timers backup.timer
systemd-analyze calendar daily
systemd-analyze calendar '*-*-* 03:30:00'常用 [Timer] 指令:
| 指令 | 说明 |
|---|---|
OnCalendar= | 日历时间,如 daily、*-*-* 03:30:00 |
OnBootSec= | 开机后多久触发 |
OnUnitActiveSec= | 上一次服务激活后多久再触发 |
OnUnitInactiveSec= | 上一次服务停止后多久触发 |
Persistent=true | 错过触发时间后,下次启动补跑 |
RandomizedDelaySec= | 随机延迟,避免同时触发 |
AccuracySec= | 允许的时间误差,默认 1 分钟 |
Unit= | 指定触发的 service,默认同名 |
Persistent=true 只对 OnCalendar 有意义,补跑时间记录在 /var/lib/systemd/timers。如果机器经常关机,备份、清理这类任务建议打开。
timer 的 service 通常不需要 [Install],由 timer 拉起即可。只 enable service 而不 enable timer,不会定时执行。
socket 激活
socket 激活让服务在有连接时才启动,适合按需运行的服务。
ini
# /etc/systemd/system/myapp.socket
[Unit]
Description=My App Socket
[Socket]
ListenStream=127.0.0.1:8080
Accept=no
[Install]
WantedBy=sockets.targetini
# /etc/systemd/system/myapp.service
[Unit]
Description=My App
[Service]
ExecStart=/opt/myapp/bin/myappbash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.socket
systemctl status myapp.socket
ss -ltnp | grep 8080Accept=no 表示由同一个服务进程处理所有连接;Accept=yes 会为每个连接启动一个模板实例(myapp@.service),适合简单的 per-connection 服务。
socket 激活需要程序支持从继承的文件描述符读取,或者使用 Accept=yes 让 systemd 建立连接。不是所有程序都能直接套用。
path 单元
path 单元监视文件或目录变化,触发对应 service:
ini
# /etc/systemd/system/myapp.path
[Unit]
Description=Watch myapp spool
[Path]
PathChanged=/var/spool/myapp
Unit=myapp.service
[Install]
WantedBy=paths.targetbash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.path
systemctl list-pathsPathChanged 监视文件内容变化;PathExists 在路径出现时触发;PathModified 还会监视属性变化。底层通常使用 inotify,递归目录和网络文件系统要实际测试。
用户服务
用户服务由当前用户的 systemd 实例管理,不需要 root:
bash
mkdir -p ~/.config/systemd/user
# 写入 ~/.config/systemd/user/myapp.service
systemctl --user daemon-reload
systemctl --user enable --now myapp.service
systemctl --user status myapp.service
journalctl --user-unit myapp.service用户 unit 的 [Install] 通常写:
ini
[Install]
WantedBy=default.target默认情况下,用户服务会随登录会话结束而停止。希望用户退出登录后继续运行:
bash
loginctl enable-linger "$USER"
loginctl show-user "$USER" -p Linger用户服务不能使用需要特权的操作(例如绑定 80 端口、修改系统目录),也不支持所有系统级指令。需要系统级权限时,还是写成系统服务。
排障方法
先看状态和日志
bash
systemctl status myapp.service --no-pager -l
journalctl -xeu myapp.service -b --no-pager
journalctl -u myapp.service -b -p err --no-pager-x 增加 catalog 解释,-e 跳到末尾,-u 指定 unit,-b 限定本次启动。
检查 unit 文件
bash
systemctl cat myapp.service
systemd-analyze verify /etc/systemd/system/myapp.service
systemctl show myapp.service -p FragmentPath,DropInPaths,ExecStart,ActiveState,SubStatesystemd-analyze verify 能发现语法错误、不存在的可执行文件、循环依赖等问题。
常见退出状态
| 状态 | 含义 | 常见原因 |
|---|---|---|
203/EXEC | 执行失败 | 可执行文件不存在、没有执行权限、shebang 解释器缺失 |
200/CHDIR | 切换工作目录失败 | WorkingDirectory 不存在或没权限 |
217/USER | 用户不存在 | User= 指定的用户没有创建 |
208/STDIN | 标准输入设置失败 | StandardInput= 指向的文件有问题 |
209/STDOUT | 标准输出设置失败 | StandardOutput= 路径不可写 |
226/NAMESPACE | 命名空间设置失败 | 沙箱选项、ProtectSystem、ReadWritePaths 等配置冲突 |
1/FAILURE | 程序自身返回 1 | 应用启动失败,需要看应用日志 |
bash
systemd-analyze exit-status 203
systemd-analyze exit-status 200 217 226启动太快被限制
日志中出现:
text
start request repeated too quickly说明触发了 StartLimit。处理:
bash
sudo systemctl reset-failed myapp.service
sudo systemctl start myapp.service然后检查服务为什么反复退出,调整 RestartSec、StartLimitIntervalSec、StartLimitBurst,不要只靠加大限制掩盖问题。
服务找不到
text
Failed to start myapp.service: Unit myapp.service not found.检查:
bash
systemctl list-unit-files | grep myapp
systemctl cat myapp.service
ls -l /etc/systemd/system/myapp.service
sudo systemctl daemon-reload常见原因:文件名不是 .service、放错目录、软链接断开、修改后没有 daemon-reload。
依赖失败
bash
systemctl list-dependencies --reverse myapp.service
systemctl status myapp.service
journalctl -u myapp.service -b -p err如果依赖 unit 失败,先解决依赖;Wants 失败不一定拖垮本服务,Requires 会。
卡在启动任务
bash
systemctl list-jobs
sudo systemctl cancel JOB_IDlist-jobs 能看到正在等待的 start/stop 任务以及阻塞原因。
系统处于 degraded
bash
systemctl is-system-running
systemctl --failed
journalctl -b -p err --no-pagerdegraded 不是“系统坏了”,而是有 unit 失败。先定位失败的 unit,再决定是否修复或屏蔽。
启动耗时分析
bash
systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain
systemd-analyze plot > boot.svgblame 按耗时排序,critical-chain 看关键路径,plot 生成启动时间线 SVG。
调试 systemd 自身
bash
sudo systemctl log-level debug
# 复现问题
sudo systemctl log-level info也可以设置 SYSTEMD_LOG_LEVEL=debug 运行单个 systemd 工具。调试完记得恢复,避免日志暴涨。
容器和 chroot 中无法使用 systemctl
如果看到:
text
System has not been booted with systemd as init system (PID 1). Can't operate.
Failed to connect to bus: Host is down说明当前环境没有以 systemd 作为 PID 1,例如普通 Docker 容器、chroot、WSL 的某些模式。这不是 unit 配置问题,需要在宿主机或使用 systemd 的容器环境中操作。
安全加固建议
一个务实的加固顺序:
- 用专用用户运行:
User=、Group= - 禁止提权:
NoNewPrivileges=true - 隔离临时目录:
PrivateTmp=true - 只读系统:
ProtectSystem=strict - 隐藏家目录:
ProtectHome=true - 明确可写路径:
ReadWritePaths= - 限制 capability:
CapabilityBoundingSet= - 限制地址族:
RestrictAddressFamilies= - 用
systemd-analyze security查看评分 - 逐项测试,不要一次性全开
示例:
ini
[Service]
DynamicUser=yes
StateDirectory=myapp
CacheDirectory=myapp
LogsDirectory=myapp
RuntimeDirectory=myapp
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
CapabilityBoundingSet=DynamicUser=yes 会自动创建临时用户;StateDirectory 等会自动创建目录并设置权限。程序需要写 /var/lib/myapp 时,用 StateDirectory=myapp 比手写 ReadWritePaths 更省事。
常见坑
1. enable 不等于 start
bash
sudo systemctl enable nginx # 只是开机自启,当前不一定运行
sudo systemctl start nginx # 当前启动
sudo systemctl enable --now nginx2. 改完 unit 忘记 daemon-reload
bash
sudo systemctl daemon-reload
sudo systemctl restart myapp.servicedaemon-reload 只重新读取配置,不重启服务。
3. 只写 After 不会拉起依赖
ini
# 错误:postgresql 不会被自动启动
After=postgresql.service
# 正确
Wants=postgresql.service
After=postgresql.service4. ExecStart 不是 Shell
ini
# 错误:管道和重定向不会被解释
ExecStart=/usr/bin/foo | /usr/bin/bar
# 正确:显式调用 Shell
ExecStart=/bin/sh -c '/usr/bin/foo | /usr/bin/bar'5. Type 选错
- 长期运行的程序用
Type=simple/exec - 传统 fork 的 daemon 用
Type=forking,并设置PIDFile - 一次性脚本用
Type=oneshot - 使用
sd_notify的程序用Type=notify
Type=simple 的服务如果主进程立刻退出(exit 0),systemd 会认为服务已结束,变成 inactive,这是正常行为。需要保持 active 的一次性任务用 oneshot + RemainAfterExit=yes。
6. 用户和权限不对
User=app 要求 app 用户存在;WorkingDirectory、日志目录、配置文件都要对 app 可读/可写。可以先用 sudo -u app 手动执行 ExecStart 验证。
7. 沙箱选项导致启动失败
ProtectSystem=strict、ProtectHome=true、PrivateTmp=true 会改变服务看到的文件系统。出现 226/NAMESPACE 或“Permission denied”时,检查:
bash
systemctl show myapp.service -p ProtectSystem,ProtectHome,PrivateTmp,ReadWritePaths按需增加 ReadWritePaths,或者暂时去掉对应限制定位问题。
8. 重启循环
Restart=always 加上程序启动即崩溃,会造成快速重启。设置合理的 RestartSec,并检查 StartLimit。不要用无限重启掩盖配置错误。
9. 停止超时后被 SIGKILL
默认停止超时是 90 秒(由 DefaultTimeoutStopSec 控制)。程序收到 SIGTERM 后需要更长时间清理时,可以针对单个服务增加:
ini
[Service]
TimeoutStopSec=180
KillSignal=SIGTERM如果程序忽略 SIGTERM,systemd 最终会发 SIGKILL,未保存的数据可能丢失。
10. timer 没有生效
检查:
bash
systemctl list-timers --all
systemctl status backup.timer
systemctl status backup.service
journalctl -u backup.timer -u backup.service -b
systemd-analyze calendar daily常见原因:只 enable 了 service、没有 enable timer;OnCalendar 写错;修改后没有 daemon-reload;timer 处于 inactive。
11. network.target 不等于网络可用
需要网络真正可用时:
ini
[Unit]
Wants=network-online.target
After=network-online.target而不是只写 After=network.target。
12. mask 与 disable 混淆
disable:取消开机自启,仍可手动 startmask:彻底禁止启动,手动也不行
排查时如果发现 masked,用 systemctl unmask 恢复。
13. 百分号没有转义
unit 文件里的 % 是 specifier。普通百分号写成 %%:
ini
ExecStart=/usr/bin/foo --percent=100%%14. 用户服务退出登录就停
需要常驻时:
bash
loginctl enable-linger "$USER"否则用户服务会随最后一个会话结束而停止。
15. 只看 systemctl status 的日志不够
systemctl status 只显示末尾几行。完整日志用:
bash
journalctl -u myapp.service -b --no-pager
journalctl -xeu myapp.service -bjournal 的详细用法见 journalctl 日志排查。
速查表
bash
# 查看状态
systemctl status myapp.service
# 启动 / 停止 / 重启
sudo systemctl start myapp.service
sudo systemctl stop myapp.service
sudo systemctl restart myapp.service
# 重载配置
sudo systemctl reload myapp.service
# 开机自启
sudo systemctl enable --now myapp.service
# 取消自启
sudo systemctl disable --now myapp.service
# 是否启用
systemctl is-enabled myapp.service
# 是否运行
systemctl is-active myapp.service
# 查看 unit 文件
systemctl cat myapp.service
# 查看有效属性
systemctl show myapp.service -p ActiveState,SubState,MainPID
# 查看依赖
systemctl list-dependencies myapp.service
systemctl list-dependencies --reverse myapp.service
# 编辑 drop-in
sudo systemctl edit myapp.service
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
# 屏蔽 / 恢复
sudo systemctl mask myapp.service
sudo systemctl unmask myapp.service
# 失败列表
systemctl --failed
sudo systemctl reset-failed myapp.service
# 查看任务队列
systemctl list-jobs
# 退出状态解释
systemd-analyze exit-status 203
# 校验 unit
systemd-analyze verify /etc/systemd/system/myapp.service
# 安全评分
systemd-analyze security myapp.service
# 启动耗时
systemd-analyze blame
systemd-analyze critical-chain
# 定时器
systemctl list-timers --all
systemd-analyze calendar daily
# 用户服务
systemctl --user status myapp.service
journalctl --user-unit myapp.service
loginctl enable-linger "$USER"总结
systemd 的核心可以概括为:
- unit 描述资源:service 管进程,timer 管时间,socket 管按需激活。
- systemctl 管生命周期:start/stop/restart、enable/disable、status。
- 依赖靠 Wants/Requires 拉取,顺序靠 After/Before 控制。
- 改配置先 daemon-reload,再看 journalctl。
- 排障先看状态和退出码,再检查权限、路径、沙箱和依赖。
写 service 时,优先保证命令可执行、用户和路径正确、日志能进 journal,然后再逐步加 Restart 和安全加固。遇到启动失败,systemctl status + journalctl -xeu 通常能快速定位大部分问题。