在日常使用 systemd 管理 Linux 服务时,一个常见但容易踩坑的问题是:当你执行 systemctl stop 时,systemd 会不会把服务 fork 出来的子进程也一并杀死?
答案是:默认情况下,会的。 但背后的机制远比简单的"杀进程"要复杂,而且 systemd 提供了多种方式来控制这一行为。
问题场景
假设你有一个服务 myapp.service,它的主进程启动后会 fork 出多个工作进程:
bash
systemctl start myapp.service
# 主进程 PID 1234,fork 出子进程 1235、1236...
systemctl stop myapp.service
此时,1235 和 1236 这些子进程会怎样?是随主进程一起被清理,还是继续存活变成孤儿进程?
核心机制:cgroup(控制组)
systemd 管理进程的核心武器是 Linux 的 cgroup(Control Group) 。当一个服务启动时,systemd 会为其创建一个独立的 cgroup,并将主进程及其所有子进程都放入这个 cgroup 中。
当你执行 systemctl stop 时,systemd 默认向整个 cgroup 发送终止信号。这意味着无论进程是主进程还是 fork 出来的子进程,只要它还在这个 cgroup 里,都会被 systemd 一并处理。
这种设计的好处是:服务停止时不会留下"孤儿进程",确保资源被彻底清理。
控制行为的关键参数:KillMode
如果你需要改变这种"一刀切"的行为,可以在服务的 unit 文件中通过 KillMode 参数进行精确控制。
四种模式详解
| 模式 | 行为说明 | 适用场景 |
|---|---|---|
control-group |
默认值 。停止服务时,杀死该 cgroup 中的所有进程(主进程 + 所有子进程) | 大多数普通服务 |
process |
只杀死主进程,子进程不会被 systemd 主动终止 | 主进程管理子进程生命周期的服务(如 Docker) |
mixed |
先向主进程 发送 SIGTERM 让它优雅退出;超时后,向 cgroup 内其余进程发送 SIGKILL | 需要主进程先优雅收尾,再强制清理残留 |
none |
不杀死任何进程(systemd 完全不管进程退出) | 已废弃/不推荐,容易产生孤儿进程 |
配置示例
ini
# /etc/systemd/system/myapp.service
[Service]
ExecStart=/usr/bin/myapp
KillMode=mixed
TimeoutStopSec=30
实际案例:Docker 的配置策略
Docker 是一个最典型的需要特殊处理的例子。
Docker daemon(dockerd)会启动大量的容器进程。如果 systemd 在停止 Docker 服务时把这些容器进程全部杀死,那么所有运行中的容器都会异常退出,这显然不是用户想要的。
因此,Docker 的官方 systemd 配置中明确设置了:
ini
[Service]
KillMode=process
这样,当你执行 systemctl stop docker 时,systemd 只会终止 dockerd 主进程,而不会去碰那些容器进程。容器是否继续运行,由 Docker 自身来管理。
如果你不想让子进程被杀死
除了修改 KillMode,还有几种常见的解决方案:
1. 修改 KillMode 为 process
ini
[Service]
KillMode=process
注意:主进程退出后,子进程会变成孤儿进程继续运行,systemd 会认为服务已经停止。你需要确保这些子进程有适当的退出机制,或者后续手动清理。
2. 让子进程脱离当前 cgroup
在启动脚本中使用 setsid 或 nohup,让子进程进入新的 session 和 cgroup:
bash
setsid /usr/bin/background-worker
这样 systemd 就追踪不到这些进程,自然不会去终止它们。
3. 使用独立的 systemd 服务
将子进程放到另一个 .service 单元中管理,通过 PartOf= 或 BindsTo= 建立依赖关系。这样每个进程都有 systemd 的独立生命周期管理,更加规范。
ini
# worker.service
[Unit]
PartOf=myapp.service
[Service]
ExecStart=/usr/bin/worker
信号与超时流程
默认情况下,systemctl stop 的执行流程如下:
- 发送 SIGTERM 给目标进程(优雅终止请求)
- 等待
TimeoutStopSec(默认通常为 90 秒,可在 unit 文件中配置) - 超时后发送 SIGKILL 强制终止仍未退出的进程
如果你使用 KillMode=mixed,流程会变成:
- SIGTERM → 仅发送给主进程,让它有机会优雅地清理子进程
- 等待超时
- SIGKILL → 发送给 cgroup 中剩余的进程(主进程之外的进程)
这种模式在主进程需要"临终遗言"来通知子进程退出时非常有用。
总结
| 问题 | 答案 |
|---|---|
systemctl stop 默认会杀子进程吗? |
会,通过 cgroup 机制一并清理 |
| 如何避免? | 设置 KillMode=process 或使用 setsid 脱离 cgroup |
| 最佳实践是什么? | 大多数服务保持默认 control-group;特殊服务(如 Docker)使用 process;需要优雅退出的考虑 mixed |
理解 systemd 的 cgroup 和 KillMode 机制,能帮助你更精确地控制服务的生命周期,避免"想停主进程却误杀子进程"或者"停了服务却留下一堆孤儿进程"的尴尬局面。