先分清正在运行、允许被启动、是否随目标启动这三件事。本文以系统服务 example.service 为例;用户服务在命令中加 --user,不要把两套管理器的状态混在一起。
先读状态,再找退出原因
检查一个服务
systemctl status example.service --no-pager
--no-pager 直接输出。先看 Loaded 的路径与错误,再看 Active 和主进程;status 在非 active 时可能返回非零,不能直接当成查询失败。
systemctl show example.service -p ActiveState -p SubState -p Result -p ExecMainStatus -p UnitFileState
ActiveState=failed 与 Result=exit-code 指向进程退出失败;ExecMainStatus 是主进程状态码。Type 与启动阶段不同可能需要检查 ExecStartPre 等,不能只盯主进程。
journalctl -u example.service -b -n 80 --no-pager
-u 选 unit,-b 限本次系统启动,-n 限行数。问题发生在上次启动则选择 -b -1;持续观察用 -f,不是必须再执行的步骤。
查单个服务与列全机清单分开
systemctl --failed
修复原因后才按需 sudo systemctl reset-failed example.service 清除失败状态与启动限速计数;它不会修配置或启动应用。
systemctl list-units --all --type=service
要查安装在磁盘上的 unit 文件及 enable 状态,改查 systemctl list-unit-files --type=service。一个是管理器加载集合,一个是文件清单,范围不同。
| 看到什么 | 能说明什么 | 下一步 |
| active (running) | 管理器认为进程正在运行,不证明业务正常 | 检查服务端口和实际请求 |
| active (exited) | 常见于带 RemainAfterExit 的一次性任务 | 看 Type、ExecStart、退出结果 |
| failed / exit-code | 有失败结果,不能仅靠 reset-failed 修复 | 看 journal 与 Result,不只看最后一行 |
| not-found | 此管理器没找到该 unit | 查名称、安装位置、是否应加 --user |
运行状态与启用状态是两个维度
正在呈现知识画面
正在运行与开机启用:两条独立轴start/stop 改变当前运行状态,enable/disable 改变启动依赖;mask 阻止后续启动但不会自动停止已有进程。
{"id":"ops-systemd-state","title":"正在运行与开机启用:两条独立轴","summary":"start/stop 改变当前运行状态,enable/disable 改变启动依赖;mask 阻止后续启动但不会自动停止已有进程。","height":600,"html":"<main><p class=\"quiet\" data-i18n=\"scope\"></p><div class=\"cards\"><section class=\"card \"><h3 data-i18n=\"runtime\"></h3><strong id=\"run\"></strong></section><section class=\"card \"><h3 data-i18n=\"boot\"></h3><strong id=\"boot\"></strong></section><section class=\"card \"><h3 data-i18n=\"gate\"></h3><strong id=\"gate\"></strong></section></div><div class=\"controls\"><button type=\"button\" data-action=\"start\" data-i18n=\"start\"></button><button type=\"button\" data-action=\"stop\" data-i18n=\"stop\"></button><button type=\"button\" data-action=\"enable\" data-i18n=\"enable\"></button><button type=\"button\" data-action=\"disable\" data-i18n=\"disable\"></button><button type=\"button\" data-action=\"mask\" data-i18n=\"mask\"></button><button type=\"button\" data-action=\"unmask\" data-i18n=\"unmask\"></button></div><div class=\"explain\"><div class=\"stable \"><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"ok\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"blocked\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"enableBlocked\"></p><p id=\"message\" aria-live=\"polite\"></p></div></div></main>\n","css":"main {\n display: grid;\n gap: 18px;\n min-width: 0;\n}\np {\n overflow-wrap: anywhere;\n}\n.controls {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n}\n.presets {\n display: flex;\n flex-wrap: wrap;\n}\n.presets button {\n flex: 1 1 85px;\n font-size: 15px;\n}\n.cards {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 10px;\n}\n.card {\n border: 1px solid var(--rule);\n border-radius: 10px;\n padding: 14px;\n min-width: 0;\n background: var(--surface);\n}\n.card h3 {\n font-size: 15px;\n color: var(--muted);\n margin-bottom: 8px;\n}\n.card strong {\n font-size: 23px;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.code {\n font-family: var(--viz-font-mono);\n font-size: 15px;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.stable {\n display: grid;\n min-width: 0;\n}\n.stable > * {\n grid-area: 1/1;\n margin: 0;\n}\n.stable > .reserve {\n visibility: hidden;\n pointer-events: none;\n}\n.explain {\n border-left: 3px solid var(--accent);\n padding: 12px 14px;\n background: var(--accent-soft);\n}\n.arrow {\n text-align: center;\n color: var(--accent);\n font-size: 25px;\n}\n.row {\n padding: 12px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n min-width: 0;\n}\n.row.on {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n.row.off {\n color: var(--muted);\n}\n.bar {\n height: 18px;\n background: var(--surface);\n border: 1px solid var(--rule);\n border-radius: 5px;\n overflow: hidden;\n margin-top: 8px;\n}\n.bar > i {\n display: block;\n height: 100%;\n background: var(--accent);\n width: 0;\n transition: width 0.22s;\n}\n.checks {\n display: grid;\n gap: 8px;\n}\n.checks label {\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 9px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n font-size: 16px;\n}\n.checks input {\n width: 18px;\n height: 18px;\n accent-color: var(--accent);\n flex-shrink: 0;\n}\nlabel.range {\n display: block;\n font-size: 16px;\n}\nlabel.range output {\n float: right;\n font-variant-numeric: tabular-nums;\n}\n.pair {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n}\n.flow {\n display: grid;\n gap: 6px;\n}\n.quiet {\n font-size: 14px;\n color: var(--muted);\n}\n.badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 5px;\n background: var(--accent-soft);\n color: var(--accent);\n}\nbutton {\n overflow-wrap: anywhere;\n}\n.large {\n font-size: 32px;\n color: var(--accent);\n font-weight: 500;\n}\n.layers {\n display: grid;\n gap: 12px;\n}\n.layer {\n border-left: 3px solid var(--rule);\n padding-left: 12px;\n}\n.layer.on {\n border-color: var(--accent);\n}\n@media (max-width: 520px) {\n .cards,\n .pair {\n grid-template-columns: 1fr;\n }\n .card {\n padding: 12px;\n }\n .card strong {\n font-size: 22px;\n }\n}\n","js":"const $ = (s) => document.querySelector(s);\nconst $$ = (s) => [...document.querySelectorAll(s)];\nconst t = (k) => viz.t(k);\nconst put = (id, v) => ($(\"#\" + id).textContent = v);\nconst say = (k) => put(\"message\", t(k));\nconst state = (v) => ($(\"main\").dataset.state = JSON.stringify(v));\nconst press = (key, v) =>\n $$(\"[data-\" + key + \"]\").forEach((b) =>\n b.setAttribute(\"aria-pressed\", String(b.dataset[key] === v)),\n );\nlet run = false,\n enabled = false,\n masked = false;\nfunction draw() {\n put(\"run\", run ? \"active\" : \"inactive\");\n put(\"boot\", enabled ? \"enabled\" : \"disabled\");\n put(\"gate\", masked ? \"masked\" : \"unmasked\");\n state({ run, enabled, masked });\n}\n$$(\"[data-action]\").forEach(\n (b) =>\n (b.onclick = () => {\n const a = b.dataset.action;\n let k = \"ok\";\n if (a === \"start\") {\n if (masked) k = \"blocked\";\n else run = true;\n }\n if (a === \"stop\") run = false;\n if (a === \"enable\") {\n if (masked) k = \"enableBlocked\";\n else enabled = true;\n }\n if (a === \"disable\") enabled = false;\n if (a === \"mask\") masked = true;\n if (a === \"unmask\") masked = false;\n say(k);\n draw();\n }),\n);\nsay(\"ok\");\ndraw();\n","audio":false,"strings":{"blocked":"mask 阻止 start。先 unmask 才能再次启动;mask 本身不停止运行。","boot":"启动链接","disable":"disable","enable":"enable","enableBlocked":"被 mask 的 unit 不能直接 enable,先 unmask。已有启用链接在此模型中单独保留。","gate":"启动屏蔽","mask":"mask","ok":"命令已作用于对应状态;另一条轴保持不变。","runtime":"当前进程","scope":"普通 service,初始 inactive + disabled;忽略 socket、依赖触发与进程故障。自由点击命令,观察两条轴。","start":"start","stop":"stop","unmask":"unmask"}}| 意图 | systemctl | 作用 |
| 现在启动/停止 | start / stop | 改变当前运行状态 |
| 随目标启动/取消 | enable / disable | 按 [Install] 管理符号链接,不自动 start/stop |
| 同时执行 | enable --now / disable --now | 同时改变安装与运行状态 |
| 禁止任何正常启动请求 | mask / unmask | mask 指向 /dev/null;已运行服务仍需 stop 或 --now |
只改变当前运行状态
sudo systemctl start example.service
成功后用 systemctl is-active example.service 确认管理器状态,再做业务请求。需要停止时另用 sudo systemctl stop example.service;stop 不会取消开机启动。
设置随目标启动
sudo systemctl enable example.service
随后 systemctl is-enabled example.service 应反映 enabled;要同时启动,以 enable --now 代替。取消只用 disable;需同时停止则选 disable --now。这些是不同意图的选项。
阻止启动前先检查谁会拉起它
systemctl list-dependencies example.service --reverse
去掉 --reverse 看该服务依赖谁;同时查关联 socket / timer。该清单不是所有未加载 unit 的完整静态分析。确需屏蔽时用 sudo systemctl mask example.service,解除用 unmask,然后核对 is-enabled。
disabled 的服务仍可手动或经依赖、socket、timer 启动;static 通常没有可供 enable 的 [Install] 信息。mask 对已有本地 unit 文件可能报冲突,先检查 systemctl cat,不要覆盖文件。
修改的是 unit 文件,还是应用配置?
| 改动 | 执行 | 验证 |
| 手工修改 unit / drop-in | daemon-reload | show 查看解析值;必要时 restart 才使用新进程配置 |
| 应用自己的配置 | reload | 仅在该服务支持 reload 时可用;连接影响由应用决定 |
| 需要重新创建进程 | restart | 会停止再启动,安排业务中断窗口 |
读完整配置再添加覆盖
systemctl cat example.service
记录要修改的原值及文件位置,避免直接编辑发行版管理的文件。输出是磁盘文件;是否已加载还要看 show。
sudo systemctl edit example.service
在编辑器提示的有效区域写入下面的 [Service] 片段,保存退出。取消时退出而不保存;回退时只移除或恢复本次改动的 drop-in,再 reload / restart 验证,不清空其他人的覆盖配置。
[Service]
Environment=LOG_LEVEL=debug
Restart=on-failure
RestartSec=5s
systemctl edit 成功保存后通常会自动执行等效的 daemon-reload;手工写文件时需自行执行。替换单条 ExecStart 要先用空的 ExecStart= 清除旧值,再写新命令;不要照抄别的服务启动参数。
手工写文件后的校验与加载
systemd-analyze verify /etc/systemd/system/example.service
路径替换为实际主 unit 路径;若只通过 edit 修改发行版服务,按 cat 的来源定位,不能假设 /etc 下已有完整主文件。校验失败先修复;通过不代表应用自身配置有效。
sudo systemctl daemon-reload
它不重新创建服务进程。systemctl edit 正常保存通常已经完成这一步,手工写文件才需要补做。
systemctl show example.service -p ExecStart -p Environment -p Restart
确认正确后,确需新进程才执行 sudo systemctl restart example.service,再检查 status / journal 和业务请求。仅改应用配置且支持 reload 时,选择 sudo systemctl reload example.service;两条不应机械连续执行。
用户管理器有独立的状态
systemctl --user status example.service --no-pager
对应文件一般在 ~/.config/systemd/user/。同一用户运行 edit / daemon-reload / enable --now,并用 journalctl --user -u example.service 读日志;不要加 sudo 后误操作 root 的用户管理器。
用户服务用 systemctl --user,日志用 journalctl --user -u example.service。需要未登录时也启动用户管理器,再考虑 loginctl enable-linger USER;这与 enable 某个用户服务是两件事。
定时任务:看触发条件与执行结果
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=30s
[Install]
WantedBy=timers.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/example-task
先提供可执行的任务程序,再启用 timer。Persistent=true 对 OnCalendar 生效:timer 停用期间错过了日历触发,可在重新激活时补一次,不逐条重放所有遗漏。OnBootSec、OnUnitActiveSec 属于单调时间计时,不靠 Persistent 补跑。
先校验定时表达式与配套 unit
systemd-analyze calendar daily
核对时区和时间是否符合预期;可换成实际 OnCalendar 表达式。再用 systemd-analyze verify /etc/systemd/system/example.timer /etc/systemd/system/example.service 校验两个文件,确认任务程序存在且可执行。
sudo systemctl daemon-reload
sudo systemctl enable --now example.timer
Persistent 补跑可能在激活后发生,提前确认任务重复执行的行为。
systemctl list-timers --all
NEXT 是计划,LAST 是触发,不是成功记录。用 journalctl -u example.service --since today --no-pager 和 systemctl show example.service -p Result -p ExecMainStatus 核对任务结果。停用调度选择 sudo systemctl disable --now example.timer,已经运行的任务需另外处理。
启动耗时与日志空间是独立任务
systemd-analyze critical-chain example.service
结合 journal 的时间戳判断等待发生在哪里;并行启动和服务类型会影响这些统计。
journalctl --disk-usage
确需删除旧日志时,sudo journalctl --rotate 归档当前文件,再单独 sudo journalctl --vacuum-time=7d 清理过期归档;删除前保存排障所需时间段。持久大小策略应配置 journald 的 SystemMaxUse 等,而不是反复手工 vacuum。
timer active 只表示计时器在等候,不表示上次任务成功;看对应 service 的 Result 与日志。journalctl --vacuum-time=7d 只清理归档日志;必要时先 rotate,不能把它当成总目录大小的硬上限。blame 也不能单独证明谁阻塞了启动关键路径。
systemctl · systemd.timer · journalctl