systemd 与信号 / 僵尸进程:Linux 进程管理的底层逻辑与排错

一、引言

你管 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 # 看启动报错细节

常见根因:

  1. Type 配错:老 daemon 用 forking 但没给 PIDFile= → systemd 等不到主 PID,判定启动失败。或反之,simple 的进程却自己 fork 后台化,systemd 以为它退出了。
  2. 路径/权限:ExecStart 文件不存在或不可执行、User 无权限读配置、WorkingDirectory 不存在。
  3. 依赖未就绪:After/Requires 没配,网络没起来就绑端口失败;或依赖的 postgresql.service 没起来。
  4. 环境变量缺失:EnvironmentFile 路径错、变量没加,应用读配置报空。
  5. 改完 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)。
    处理:
  1. 不要 kill -9 僵尸------无效。
  2. 找 ppid,重启那个父进程;父退出后僵尸被 PID 1 收养并回收。
  3. 若父进程短时间内无法重启,可临时 kill -TERM 让其退出触发收养。
  4. 治本:修复父进程,确保子进程退出时被 wait();或改用 systemd 托管、设置 KillMode 让其统一回收。
  5. 罕见但严重:若 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 / 信号 / 僵尸 吃透,记住这几条:
  1. systemd 就是 PID 1,用 unit 管理一切;Type/Restart/KillMode/TimeoutStopSec 是 service 文件里最该抠的四个旋钮。
  2. 改了 unit 文件一定 daemon-reload;启动失败看 journalctl -u,别只看 status 摘要。
  3. 信号有「优雅」和「强制」之分:SIGTERM 给清理窗口,SIGKILL/SIGSTOP 不可捕获;先 TERM 后 KILL。
  4. 僵尸 = 父进程不 wait(),只耗 PID、不可被 kill -9:治理靠重启父进程(systemd 收养回收)或修代码;孤儿会被 PID 1 自动回收,不必担心。
  5. 停不掉多是 SIGTERM 不响应或子进程泄漏,KillMode=control-group 能收走整棵进程树;紧急用 systemctl kill -s KILL。
  6. OOM 是 SIGKILL 形式的内核处刑,用 MemoryMax 做 cgroup 级隔离比全局 OOM 友好。
相关推荐
whyutianict_vv1 小时前
零基础转 AI 运维:投入清单、技术路线与核对表
linux·运维·人工智能·ai
DevOpenClub2 小时前
高考一分一段 API:分数查位次、科类枚举与历史对照
linux·数据库·高考
小贾要学习2 小时前
【C++后端开发项目】Reactor 模型 / C++ 仿写 muduo HTTP 服务器:让底层网络库真正跑起网页
linux·服务器·c++·http·网络编程·muduo·reactor模型
g5zhu58962 小时前
没有公网ip的情况下,本地能ping服务器服务器不能ping本地,让服务器能访问本地某端口的简单方法
服务器·网络协议·tcp/ip
Blockbuater_drug2 小时前
OpenMM分子动力学模拟02 安装:Linux:conda 一条命令装通 GPU 加速的分子模拟环境
linux·conda·cuda·gpu驱动·pythonpath·openmm安装
SDWAN_Cheap2 小时前
BGP是什么,广播IP是什么?两个容易混淆的概念拆解
网络·tcp/ip·bgp·广播ip
程序员JerrySUN2 小时前
PCIe Gen5 SSD 为什么读得快、写得慢?从存储原理到 Linux 性能实测
linux·服务器·网络
Android系统攻城狮2 小时前
Linux FFmpeg深度解析之avcodec_parameters_copy调用流程与实战(二十五)
linux·运维·服务器·ffmpeg·音视频·车载音频
guizhoumen2 小时前
net core下WebSocket请求总是断开的解决方案
网络·websocket·网络协议