Skip to content

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 的组成 ​

组件作用
systemdPID 1,系统与服务管理器
systemctl管理 unit、服务、target 等
systemd-journald收集内核、服务、syslog 日志
journalctl查询 journal 日志
logind登录会话与用户管理
networkd / NetworkManager网络配置(不同发行版选择不同)
resolvedDNS 解析
udev设备管理
timedated / hostnamed / localed时间、主机名、区域设置
systemd-coredump收集和分析核心转储

unit 类型 ​

systemd 把各种系统资源抽象成 unit,常见的后缀:

后缀类型说明
.service服务最常用,管理守护进程或一次性任务
.socketsocket按连接激活服务
.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 文件路径、是否 enabled
  • Active:当前状态,如 active (running)、inactive (dead)、failed
  • Main 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 nginx

enable 和 start 是两件事:

  • enable 创建 target 的依赖链接,让服务开机自启
  • start 只影响当前运行状态
  • enable --now 同时做这两件事

is-enabled 常见输出:

text
enabled
disabled
static
masked
indirect
generated
transient

static 表示 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 --failed

list-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.service

systemctl 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.service

systemctl edit 默认创建:

text
/etc/systemd/system/nginx.service.d/override.conf

drop-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.service

disable 只是取消开机自启,手动仍然可以 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.service

is-system-running 可能返回:

text
initializing
starting
running
degraded
maintenance
stopping
offline
unknown

degraded 表示系统已经启动,但有 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.target

Wants 负责把对方拉起来,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启动后执行,可多条
ExecReloadreload 时执行
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/env
  • EnvironmentFile 前加 - 表示文件不存在也不报错
  • 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/user
  • PrivateTmp=true 给服务独立的 /tmp 和 /var/tmp
  • NoNewPrivileges=true 阻止 setuid/setgid 提权
  • CapabilityBoundingSet= 清空 capability,需要时再单独加
  • DynamicUser=yes 可以配合 StateDirectory、CacheDirectory、LogsDirectory 自动创建专用用户和目录

安全选项不是越多越好。加得太严会导致服务无法读写配置或启动失败,建议先用 systemd-analyze security 看基线,再逐项增加并测试。

[Install] 段 ​

ini
[Install]
WantedBy=multi-user.target

WantedBy=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.service

unit 里用 %i 引用实例:

ini
[Service]
ExecStart=/opt/myapp/bin/myapp --profile %i

systemd-escape 可以处理路径和特殊字符:

bash
systemd-escape --path /var/lib/myapp
systemd-escape --path --unescape var-lib-myapp

常用 specifier ​

写法含义
%n完整 unit 名,如 myapp.service
%N去掉后缀的 unit 名
%punit 前缀,如 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.service

systemctl 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.target

RemainAfterExit=yes 让脚本执行完后服务仍显示为 active (exited),适合只需要执行一次的初始化逻辑。

依赖关系与 target ​

依赖与顺序是两件事 ​

ini
[Unit]
Requires=postgresql.service
After=postgresql.service
  • Requires:postgresql 必须启动;它失败,本服务也失败
  • After:本服务在 postgresql 之后启动

只有 After 不会拉起 postgresql;只有 Requires 不保证顺序。常见组合是:

ini
Wants=network-online.target
After=network-online.target

Wants 是弱依赖,适合“希望有网络,但没有网络也尽量启动”的服务;Requires 适合真正不能缺少的依赖。

常见 target ​

target含义
multi-user.target多用户命令行模式
graphical.target图形界面模式
rescue.target单用户救援模式
emergency.target最小紧急模式
network.target网络栈已启动
network-online.target网络已经可用
timers.target定时器
sockets.targetsocket 激活
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.target

isolate 会停止不属于目标 target 的其他 unit,生产环境慎用。

条件与失败处理 ​

ini
[Unit]
ConditionPathExists=/etc/myapp/config.yml
ConditionFileNotEmpty=/etc/myapp/config.yml
OnFailure=myapp-failure.service

Condition... 不满足时 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.sh
ini
# /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.target
ini
# /etc/systemd/system/myapp.service
[Unit]
Description=My App

[Service]
ExecStart=/opt/myapp/bin/myapp
bash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.socket
systemctl status myapp.socket
ss -ltnp | grep 8080

Accept=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.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.path
systemctl list-paths

PathChanged 监视文件内容变化;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,SubState

systemd-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_ID

list-jobs 能看到正在等待的 start/stop 任务以及阻塞原因。

系统处于 degraded ​

bash
systemctl is-system-running
systemctl --failed
journalctl -b -p err --no-pager

degraded 不是“系统坏了”,而是有 unit 失败。先定位失败的 unit,再决定是否修复或屏蔽。

启动耗时分析 ​

bash
systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain
systemd-analyze plot > boot.svg

blame 按耗时排序,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 的容器环境中操作。

安全加固建议 ​

一个务实的加固顺序:

  1. 用专用用户运行:User=、Group=
  2. 禁止提权:NoNewPrivileges=true
  3. 隔离临时目录:PrivateTmp=true
  4. 只读系统:ProtectSystem=strict
  5. 隐藏家目录:ProtectHome=true
  6. 明确可写路径:ReadWritePaths=
  7. 限制 capability:CapabilityBoundingSet=
  8. 限制地址族:RestrictAddressFamilies=
  9. 用 systemd-analyze security 查看评分
  10. 逐项测试,不要一次性全开

示例:

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 nginx

2. 改完 unit 忘记 daemon-reload ​

bash
sudo systemctl daemon-reload
sudo systemctl restart myapp.service

daemon-reload 只重新读取配置,不重启服务。

3. 只写 After 不会拉起依赖 ​

ini
# 错误:postgresql 不会被自动启动
After=postgresql.service

# 正确
Wants=postgresql.service
After=postgresql.service

4. 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:取消开机自启,仍可手动 start
  • mask:彻底禁止启动,手动也不行

排查时如果发现 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 -b

journal 的详细用法见 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 的核心可以概括为:

  1. unit 描述资源:service 管进程,timer 管时间,socket 管按需激活。
  2. systemctl 管生命周期:start/stop/restart、enable/disable、status。
  3. 依赖靠 Wants/Requires 拉取,顺序靠 After/Before 控制。
  4. 改配置先 daemon-reload,再看 journalctl。
  5. 排障先看状态和退出码,再检查权限、路径、沙箱和依赖。

写 service 时,优先保证命令可执行、用户和路径正确、日志能进 journal,然后再逐步加 Restart 和安全加固。遇到启动失败,systemctl status + journalctl -xeu 通常能快速定位大部分问题。

相关文章 ​

参考 ​