首页 / 内容指南 / 当前文章

systemd显示active (exited)但程序没运行:Type、RemainAfterExit与PID排查

发布于 2026-08-26 · Content Fleet 编辑部

systemd显示active (exited)但程序没运行:Type、RemainAfterExit与PID排查

active与有进程不是同一概念

systemd 的 active 是 unit 状态,不总等于当前存在 MainPID。对 Type=oneshot 配合 RemainAfterExit=yes 的服务,启动动作成功退出后仍可保持 active;这适合防火墙规则、挂载准备或一次性初始化,但不适合用来证明 Web 服务、队列 worker 或数据库守护进程仍在运行。

第一步:读取systemctl show的结构化字段

不要只看彩色的 status 摘要。使用以下命令读取 unit 的加载来源、类型、状态、主进程和退出信息:

systemctl show demo.service \
  -p FragmentPath -p DropInPaths -p Type -p RemainAfterExit \
  -p ActiveState -p SubState -p MainPID -p ControlPID \
  -p ExecMainCode -p ExecMainStatus -p Result

MainPID=0SubState=exitedRemainAfterExit=yes,应优先检查 unit 设计,而不是盲目重启。

确认实际生效的unit内容

执行 systemctl cat demo.service 查看主文件和所有 drop-in。systemctl status 显示的路径不一定包含覆盖配置。发行版 unit、/etc/systemd/system 覆盖和临时 drop-in 可能共同决定最终值。修改前保存输出,并使用 systemd-analyze verify 检查 unit 语法。

Type=oneshot适合什么任务

上游 systemd.service 文档说明,oneshot 会等待启动命令执行完,再进入后续状态。它适合执行一次就结束的动作,例如初始化、清理或应用静态规则。默认不保留 active 时,命令成功后会回到 inactive;配合 RemainAfterExit=yes 时,unit 可在没有运行进程的情况下保持 active。

RemainAfterExit=yes为什么造成误解

该选项的语义正是“即使所有进程退出,也把服务视为 active”。如果长期服务误配它,启动脚本只要返回 0,systemd 就会显示成功,即使后台程序从未启动或随后退出。不要用它掩盖错误的守护进程跟踪。只有 unit 表示的状态在命令结束后仍真实存在时才使用,例如规则已经加载到内核。

Type=simple与Type=exec怎样选择

长期程序如果能在前台持续运行,通常应让 ExecStart 直接执行主进程,不要自行 daemonize。Type=simple 在进程启动后较早认为服务已开始;Type=exec 会等待执行程序本身成功完成 exec 阶段,能发现部分可执行文件或用户切换错误。具体选择需结合当前 systemd 版本和依赖启动语义,但两者都要求主进程不要立即把工作丢给未跟踪的后台 shell。

shell中的&会破坏进程跟踪

类似 ExecStart=/bin/sh -c '/opt/app/server &' 的命令让 shell 启动后台进程后立即退出。systemd 可能认为主进程结束,并根据 unit 类型停止剩余 cgroup 进程或把状态判断错。应删除 &,使用应用的前台或 foreground 选项,让 systemd 直接拥有主进程。

nohup和daemon命令也通常不需要

systemd 已负责生命周期、标准输出、信号和重启。再套 nohupsetsiddaemon 或自制 start 脚本会让 PID、日志和退出码变得不透明。优先把真实二进制直接写入 ExecStart=,并让日志输出到 stdout/stderr,由 journal 收集。

Type=forking用于传统后台化程序

无法关闭自我 daemonize 的旧程序可以使用 Type=forking。systemd 会等待父进程退出,再跟踪留下的守护进程。上游文档建议这类服务尽可能提供 PIDFile=,帮助 systemd 可靠确定 main process。不要对前台程序误用 forking,否则 systemd 可能一直等待它“完成后台化”。

PIDFile必须写对路径与时机

PIDFile= 应指向守护进程实际写入的文件,内容是当前主进程 PID,且必须在初始化父进程退出前准备好。检查 RuntimeDirectory、权限、chroot 和路径是否一致。旧 PID 文件可能指向已退出或被复用的 PID;启动脚本应原子更新并在停止后清理,而不是让 systemd 读取历史值。

GuessMainPID为何不可靠

没有 PIDFile 时,systemd 可能尝试从 cgroup 中猜测主进程。如果启动脚本产生多个长期子进程,猜测可能失败或选择错误对象,导致 Restart=ExecReload=$MAINPID 不可靠。更好的方案是拆分多个 daemon 为独立 unit,或改造应用使用前台/notify 模式。

Type=notify提供明确就绪信号

如果程序支持 systemd 通知协议,可使用 Type=notify,在初始化和监听真正完成后发送 READY 信号。这样依赖 unit 不会只因为进程刚创建就开始。必须正确设置通知访问范围,并验证应用确实发送信号;否则服务会停在 activating 直到超时。

一个脚本启动多个daemon的问题

单个 start.sh 同时拉起 Web、worker 和 scheduler 时,systemd 只能可靠地把一个 PID 视作 main process,某个组件退出未必改变 unit 状态。应拆成多个 .service,再用 target 或依赖关系组合。这样每个服务都有独立 MainPID、Restart、资源限制和日志。

查看unit的整个cgroup

systemctl status 通常会列出 CGroup 中的进程,也可结合 systemd-cgls/proc 验证。若 MainPID 为 0 但 cgroup 还有进程,说明进程模型或 ExitType 需要进一步检查;若 cgroup 完全空而 unit 仍 active (exited),则多半是 RemainAfterExit 的预期语义。

journalctl要查看完整启动窗口

使用 journalctl -u demo.service -b --no-pager -o short-iso-precise 查看本次启动从头到尾的日志,而不是 status 最后十行。关注启动脚本退出码、子进程 stderr、超时和 systemd 的状态转换。若日志来自自定义文件,也要确认应用在后台化前是否重定向到了错误路径。

ExecStart成功不代表应用健康

脚本可能无论子命令失败都执行 exit 0,例如缺少 set -e、错误被 || true 吞掉,或只检查 PID 文件存在。让启动程序返回真实退出码,并用应用级 health check 验证端口和关键依赖。systemd 状态可作为进程证据,但不能替代业务健康检查。

Restart为何没有生效

如果 unit 使用 oneshot 和 RemainAfterExit,systemd 可能认为启动动作已成功且 unit 仍 active,没有“主进程失败”可触发 Restart=on-failure。改正 Type 和前台运行模型后,再配置适合的 Restart 策略。不要用 timer 不断 start 一个已经 active 的 RemainAfterExit unit,因为 start 请求可能不会再次执行动作。

修改unit后的正确加载步骤

systemctl edit 创建可追踪 drop-in,修改后执行 systemctl daemon-reload,再重启服务。先运行 systemd-analyze verify,并比较 systemctl catsystemctl show 的最终结果。不要直接编辑 /usr/lib 下的供应商文件,升级会覆盖这些修改。

推荐的前台服务模板

[Unit]
Description=Demo application
After=network-online.target

[Service]
Type=exec
User=demo
WorkingDirectory=/opt/demo
ExecStart=/opt/demo/bin/server --foreground
Restart=on-failure
RestartSec=3s

[Install]
WantedBy=multi-user.target

模板中的用户、路径、依赖和参数必须按实际应用调整。不要复制 RemainAfterExit=yes,也不要在命令末尾加 &

迁移旧SysV脚本的注意点

由 sysv generator 转换的旧服务可能使用 forking、PIDFile 或猜测主进程。先查看生成后的 unit 和原脚本行为,再逐步替换为原生 unit。不要同时启用旧 init 脚本和新 unit,避免两个管理器重复启动同一 daemon。

验收时必须检查什么

重启后确认 ActiveState=activeSubState=running(对长期服务)、MainPID 非零且对应真实二进制;检查 cgroup 只有预期进程,端口监听和健康接口正常。杀掉主进程验证 Restart 策略,再执行 stop,确认所有进程退出且 PIDFile 清理。最后重启机器验证开机行为。

常见问题

active (exited)一定是错误吗?

不一定。对 oneshot 加 RemainAfterExit 的一次性状态服务,这是正常设计;对必须持续运行的 daemon,通常说明 Type 或启动方式错误。

能否只删除RemainAfterExit=yes?

要先确认业务语义。长期 daemon 还需改成前台运行或正确 forking/PIDFile;一次性服务删除后会回到 inactive,可能影响依赖或重复执行行为。

为什么ps能看到进程但MainPID是0?

进程可能脱离 unit cgroup、由脚本后台化、属于另一个 unit,或 systemd 无法从多个子进程确定主进程。核对 cgroup、PPID 和 unit 配置。

Type=forking是否适合所有后台服务?

只适合确实自行 fork/daemonize 的传统程序。能前台运行时,直接使用前台模式通常更易跟踪和诊断。

status正常还要健康检查吗?

需要。systemd 主要证明 unit 和进程状态,不能保证应用已连接数据库、能处理请求或返回正确业务结果。

总结

active (exited) 的核心不是“systemd 显示错了”,而是 unit 状态与应用进程语义没有对齐。一次性动作使用 oneshot,并仅在状态应持续时启用 RemainAfterExit;长期 daemon 优先前台运行,由 systemd 直接跟踪,传统后台化程序则正确配置 forking 和 PIDFile。结合 MainPID、cgroup、journal 与业务健康检查,才能确认服务真正可用。

来源资料

  • systemd 上游,systemd.service:https://github.com/systemd/systemd/blob/main/man/systemd.service.xml
  • systemd 上游,systemd.exec:https://github.com/systemd/systemd/blob/main/man/systemd.exec.xml
  • systemd 上游,systemctl:https://github.com/systemd/systemd/blob/main/man/systemctl.xml
  • systemd 上游,journalctl:https://github.com/systemd/systemd/blob/main/man/journalctl.xml