一、引言
你管 AWS EC2、SAP 主机时,十有八九会撞上这几类问题:服务起不来、停不掉卡在 deactivating、日志里刷一堆 Z 僵尸进程把 PID 耗光、或者 kill -9 之后留下文件锁/孤儿进程收拾不干净。这些看似零散的现象,背后是同一套机制------systemd(PID 1)如何管理进程生命周期、信号如何通知/终止进程、以及内核如何回收退出的子进程。
这篇把这三件事讲透,并给出可直接抄的命令和排错路径。读完你会发现:前面聊的 Cgroup、Namespace 在 systemd 里被统一用起来了------每个 service 就是一个 cgroup,资源限制、进程树回收都挂在 systemd 上。
二、原理解析
2.1 systemd 是什么:它就是 PID 1
systemd 是现代 Linux 的 init 系统(替代 SysV init / Upstart),开机第一个进程,PID 固定为 1,负责拉起系统上所有其他进程和服务。
它的核心抽象是 unit(单元),常见类型:
类型
后缀
用途
service
.service
后台服务/守护进程
socket
.socket
按需激活(socket 监听触发 service)
target
.target
一组 unit 的集合(类似「运行级别」),如 multi-user.target
timer
.timer
定时任务(cron 的现代替代)
mount / path
.mount / .path
挂载点 / 文件路径监听触发
unit 文件优先级路径:/etc/systemd/system/(管理员,最高)> /run/systemd/system/(运行时)> /usr/lib/systemd/system/(软件包默认)。
2.2 service unit 文件的结构
一个最小可用的 service 文件:
Unit
Description=My App
After=network.target # 在网络就绪后再启动
Wants=network.target
Service
Type=simple # 关键:见下文
ExecStart=/opt/app/bin/server --config /etc/app/app.yaml
ExecStartPre=/opt/app/bin/check-config # 启动前校验
ExecStop=/opt/app/bin/graceful-stop # 自定义停止
User=app
WorkingDirectory=/opt/app
Environment=LOG_LEVEL=info
EnvironmentFile=/etc/app/env
Restart=on-failure # 非 0 退出才重启
RestartSec=5s
LimitNOFILE=65535 # 文件描述符上限(呼应 Nginx 那篇)
Install
WantedBy=multi-user.target # enable 后软链到该 target
Type 是最容易配错的一项:
- simple(默认):ExecStart 进程就是主进程,前台运行,systemd 直接管它。
- forking:传统 daemon 会 fork() 后台运行并退出父进程。必须配 PIDFile=,否则 systemd 不知道「真正的」主 PID,会误判启动失败或停不掉。
- oneshot:一次性任务(如初始化脚本),配 RemainAfterExit=yes 可让它结束后仍标记为 active。
- notify:进程就绪后向 systemd 发 READY=1(通过 sd_notify),systemd 才认为启动成功------适合有就绪检查的服务。
- dbus / idle:较少用。
2.3 信号(signal):进程间的异步通知
信号是内核发给进程的软件中断,用于通知「发生了一件事」。关键特性:大部分信号进程可以捕获或忽略,但有两个例外------SIGKILL 和 SIGSTOP 既不可捕获也不可忽略。
信号
编号
默认行为
典型用途
SIGHUP
1
终止
终端断开 / 重载配置(nginx、sshd 用它)
SIGINT
2
终止
Ctrl+C,交互式中断
SIGQUIT
3
终止+core
类似 INT,但产生 core dump
SIGKILL
9
强制终止
不可捕获,最后手段
SIGTERM
15
终止
优雅终止请求(默认 stop 信号)
SIGSTOP
19
暂停
不可捕获,挂起进程
SIGCHLD
17
忽略
子进程退出/停止时通知父进程
黄金法则:先发 SIGTERM(给进程清理缓冲、关连接、释放锁的机会),等几秒无响应再 SIGKILL。kill -9 是「拔电源」,不留清理窗口。
2.4 僵尸进程(zombie)与孤儿进程
僵尸进程是怎么来的:子进程退出后,内核不会立刻销毁它的 task_struct,而是保留「PID + 退出码 + 状态」等待父进程调用 wait()/waitpid() 来「收尸」。如果父进程一直不调用 wait,这个子进程就停留在 Z(zombie)状态。 - 僵尸不占内存、不占 CPU,只占用一个 PID 槽。大量僵尸会耗尽 PID(/proc/sys/kernel/pid_max 默认 32768),导致系统无法创建新进程。
- 僵尸无法被 kill -9 杀死------它已经死了,只是没被回收。kill -9 对僵尸无效。
孤儿进程:父进程先于子进程退出,子进程被 PID 1(systemd/init)收养。systemd 会持续 wait() 收养的孤儿并回收,所以正常的孤儿不会变僵尸。这也解释了为什么说「PID 1 必须正确处理 SIGCHLD」------它肩负着兜底回收的职责。
结论:僵尸的根因永远是「父进程没 wait」,治理要么修复父进程代码,要么让父进程退出(由 systemd 收养子进程后自动回收)。
2.5 systemd 怎么用信号停服务
systemctl stop 的默认流程:给主进程发 SIGTERM → 等 TimeoutStopSec(默认 90s)→ 超时则发 SIGKILL。配合 KillMode 决定杀谁: - control-group(默认):杀掉整个 cgroup(主进程 + 所有子孙),能收走泄漏的子进程。
- process:只杀主进程,子进程变孤儿。
- mixed:主进程 SIGTERM、其余 SIGKILL。
- none:不杀(极少用)。
三、实战演示
场景一:写一个生产级 service 文件Unit
Description=Order API Service
After=network-online.target postgresql.service
Wants=network-online.target
Service
Type=simple
ExecStart=/opt/order/bin/api --config /etc/order/api.yaml
ExecStartPre=/opt/order/bin/migrate check
User=order
Group=order
WorkingDirectory=/opt/order
EnvironmentFile=/etc/order/env
LimitNOFILE=65535
Restart=on-failure
RestartSec=3s
防重启风暴:300s 内最多重启 5 次
StartLimitIntervalSec=300
StartLimitBurst=5
停服务时给 30s 优雅退出窗口
TimeoutStopSec=30
Install
WantedBy=multi-user.target
部署:放 /etc/systemd/system/order-api.service → systemctl daemon-reload → systemctl enable --now order-api。
场景二:日常运维命令
systemctl status order-api # 状态 + 最近日志 + 是否 enabled
systemctl start/stop/restart order-api
systemctl enable/disable order-api # 开机自启(软链到 WantedBy target)
systemctl daemon-reload # 改了 unit 文件后必须执行!否则不生效
systemctl cat order-api # 看最终生效的完整配置(含覆盖)
journalctl -u order-api -f # 实时跟踪该服务日志
journalctl -u order-api --since "1 hour ago" -p err # 近 1 小时错误级
场景三:信号实战
优雅停止(等同 systemctl stop 的第一步)
kill -TERM
强制(最后手段)
kill -9
重载配置(进程需实现 SIGHUP 处理,如 nginx)
kill -HUP
给整个进程组发信号(杀主进程连带子孙)
kill -TERM - # 注意负号表示 pgid
场景四:僵尸进程诊断与清理
找出所有僵尸
ps -eo pid,ppid,stat,cmd | awk '$3=="Z"'
ps aux | awk '$8 ~ /Z/'
看 pid_max 余量
cat /proc/sys/kernel/pid_max
ps -e | wc -l
治理:找到僵尸的父进程(ppid),若父是业务进程 bug → 重启父进程
父进程退出后,僵尸被 systemd 收养并自动回收
kill -TERM
四、排错实战(重点)
4.1 service 启动失败(failed)
systemctl status 只有一行摘要,真实错误看 journalctl:
journalctl -u -n 100 --no-pager # 看启动报错细节
常见根因:
- Type 配错:老 daemon 用 forking 但没给 PIDFile= → systemd 等不到主 PID,判定启动失败。或反之,simple 的进程却自己 fork 后台化,systemd 以为它退出了。
- 路径/权限:ExecStart 文件不存在或不可执行、User 无权限读配置、WorkingDirectory 不存在。
- 依赖未就绪:After/Requires 没配,网络没起来就绑端口失败;或依赖的 postgresql.service 没起来。
- 环境变量缺失:EnvironmentFile 路径错、变量没加,应用读配置报空。
- 改完 unit 忘了 daemon-reload:这是最高频的「明明改对了却不生效」。
4.2 重启风暴(无限重启)
Restart=always + 应用启动即崩 → 每秒重启,CPU/磁盘日志打满。
治理:
Restart=on-failure # 只在异常退出时重启,正常退出不重启
StartLimitIntervalSec=300
StartLimitBurst=5 # 300s 内最多 5 次,超了就置 failed,不再猛重启
先 journalctl -u 找到崩溃原因(缺配置、连不上依赖、段错误),修根因而不是调高重启频率。
4.3 停不掉 / 卡在 deactivating
systemctl stop 后状态长时间 deactivating:
- 进程不响应 SIGTERM:应用忽略 TERM 或卡在 IO/锁,90s 后 systemd 自动 SIGKILL。可临时缩短 TimeoutStopSec。
- 子进程泄漏:主进程退出,但子进程没跟着死、也没被 cgroup 收走 → 确认 KillMode=control-group(默认)能带走整个进程树;若设成了 process,子进程变成孤儿游离。
- 紧急手动清理:systemctl kill -s KILL 直接发 SIGKILL 给该 unit 的 cgroup。
4.4 僵尸进程堆积
现象:ps 里一堆 Z、系统无法起新进程、fork: retry: Resource temporarily unavailable(PID 耗尽)。
根因:某父进程不 wait() 子进程(典型是业务代码 bug,或父进程自己卡死无法执行 wait)。
处理:
- 不要 kill -9 僵尸------无效。
- 找 ppid,重启那个父进程;父退出后僵尸被 PID 1 收养并回收。
- 若父进程短时间内无法重启,可临时 kill -TERM 让其退出触发收养。
- 治本:修复父进程,确保子进程退出时被 wait();或改用 systemd 托管、设置 KillMode 让其统一回收。
- 罕见但严重:若 PID 1 自身产生僵尸且未回收,需检查 systemd 版本/异常(正常 systemd 会回收孤儿)。
4.5 误用 SIGKILL 的代价
kill -9 不给进程任何清理机会:不刷 I/O 缓冲、不关文件、不释放文件锁、不通知子进程。后果:
- 数据损坏(缓冲未落盘)、文件锁残留导致下次启动失败;
- 子进程变孤儿,继续占用资源或端口。
正确顺序:kill -TERM → 等待(看日志/端口释放)→ 仍无响应才 kill -9。生产止损用 systemctl stop(自带 TERM→KILL 超时)比手动 kill -9 稳妥。
4.6 OOM 与信号
进程被 OOM Killer 杀时,内核直接发 SIGKILL,应用来不及清理。dmesg 可见:
Out of memory: Killed process 12345 (java) total-vm:...
systemd 给每个 unit 配 cgroup 内存上限(MemoryMax=),超了由内核在该 cgroup 内挑进程杀,比全局 OOM 更可控。排 OOM 先看dmesg grep -i "killed process",再查是哪个 unit 超内存。
五、总结
把 systemd / 信号 / 僵尸 吃透,记住这几条:
- systemd 就是 PID 1,用 unit 管理一切;Type/Restart/KillMode/TimeoutStopSec 是 service 文件里最该抠的四个旋钮。
- 改了 unit 文件一定 daemon-reload;启动失败看 journalctl -u,别只看 status 摘要。
- 信号有「优雅」和「强制」之分:SIGTERM 给清理窗口,SIGKILL/SIGSTOP 不可捕获;先 TERM 后 KILL。
- 僵尸 = 父进程不 wait(),只耗 PID、不可被 kill -9:治理靠重启父进程(systemd 收养回收)或修代码;孤儿会被 PID 1 自动回收,不必担心。
- 停不掉多是 SIGTERM 不响应或子进程泄漏,KillMode=control-group 能收走整棵进程树;紧急用 systemctl kill -s KILL。
- OOM 是 SIGKILL 形式的内核处刑,用 MemoryMax 做 cgroup 级隔离比全局 OOM 友好。