服务器入坑指北:进程卡死与杀不掉的进程
进程杀不掉,信号发出去像石沉大海,最后只能重启机器;僵尸进程越堆越多,怎么杀都没用;用会话工具后台起的服务,会话结束了它还活着,服务管理器却管不住它——重启之后,要么它还活着,要么彻底丢了。
这三种情况的共同点是:动作之前必须先读状态。不同状态对信号的反应完全不同——有的根本收不到信号,有的已经结束、没有可杀的对象,还有的压根不归管理器管。不先分清,就会把三件不同的事混成一件事,越处理越乱。
这篇先给一张状态判定表,再把两类"杀不掉"的分野讲清,然后是只读取证、僵尸该找谁、以及"管理方式二选一"的取舍与收编顺序。全文只给命令形态、状态判据与动作顺序。
先读状态:同一句"杀不掉",三种不同的情况
| 状态 | 含义 | 能被信号杀掉吗 | 动作 |
|---|---|---|---|
| 运行 / 可中断睡眠 | 正常工作状态 | 能:可中断睡眠等系统调用返回后就会响应 | 先发礼貌信号,必要时再发强制信号 |
| 停止 | 被作业控制暂停 | 能,但可捕获的终止信号会一直挂起到被继续 | 终止信号之后紧接着发继续信号(服务管理器正是这么做) |
| 不可中断等待 | 进程在内核里等 I/O | 不能:内核屏蔽了所有信号,强制信号也送不进去 | 等它完成 / 让那次 I/O 失败 / 重启 |
| 僵尸 | 已经结束,只剩进程表里一条记录 | 没有可杀的对象 | 处理父进程(或等接管回收) |
两条因果要讲透,否则这张表只是死记硬背。
为什么强制信号在不可中断等待里送不进去:内核在"如果打断这个系统调用,就会让内核自己的数据结构处于不一致状态"的场合,会屏蔽所有信号;进程只有在系统调用返回之后才会响应信号。所以当一个系统调用长期阻塞时,实际上没有杀它的办法——这不是权限问题,也不是命令写错了。
为什么僵尸杀不掉:它已经结束了,只是进程表里的一条记录,等着父进程去读取它的退出状态。没有可杀的对象,对它反复发信号自然不会有任何反应。父进程先退出时,孤儿会被 1 号进程接管并回收。
顺带纠正一个错觉:"强制信号一定立刻生效"是不成立的。信号的投递是异步的,即使没被屏蔽,也要等目标进程拿到时间片;进程可以阻止普通信号,但不能阻止强制信号——唯一的例外正是上面那种"内核代码里屏蔽"的情形。所以"发完没反应"先别怀疑命令和权限,先看状态。
只读取证:状态、父子关系、等待通道、归属
ps -eo pid,ppid,stat,wchan:24,comm # 状态、父进程、内核等待通道、命令名
ps -eo stat,comm | awk '$1 ~ /^Z/' # 只看僵尸(读法示意)
cat /proc/<pid>/status | head -20 # State 等状态信息
cat /proc/<pid>/cgroup # 它属于哪个控制组,也就是"归谁管"
systemctl status <单元> # 这个服务被谁在管、当前什么状态
判据:状态列 + 等待通道回答"它卡在哪一类";控制组那一条回答"它归谁管"。这两件事一起看,动作方向就定了——是等、是让 I/O 失败、是找父进程,还是根本该由管理器处理。
两类"杀不掉"的处理方向相反
等 I/O 的那一类,三条出路,代价各不相同:
- 等它完成。判据:等待通道对应的那个 I/O 有没有可能完成(比如远端是否恢复、对端是否还在)。代价是可能要等很久,业务一直挂着。
- 让那次 I/O 失败。典型做法是强制卸载它所依赖的网络/远程文件系统,从而中止未完成的请求。判据:先确认这条路能接受不完整的 I/O 结果——被中止的操作会留下部分完成的状态,动之前先看有没有副本、数据能不能重取。这是有数据代价的动作,不是"顺手清一下"。
- 重启。它会丢掉现场,是最后手段。
等回收的那一类(僵尸),处理顺序是固定的:
确认它是僵尸 → 找父进程 → 判断父进程是否仍在运行 → 处理父进程(让它正常停止或完成回收)→ 如果父进程已经退出,就等 1 号进程接管后回收。
明确禁止对僵尸反复发信号:那只是在对着一条记录较劲。
为什么会话工具起的服务,管理器管不住
机制在控制组这一层:服务管理器的"管理能力"来自控制组——它把单元里的进程放进一个控制组,停止时默认杀掉该控制组内的所有剩余进程(先发礼貌信号、超时后再发强制信号,并且会在发送前附带一个继续信号,确保处于挂起状态的进程也能被终止)。
而用会话工具后台起的进程(忽略挂断信号、重定向输出那一类做法)不在任何单元的控制组里。这类工具解决的只是"会话结束时不被打断",不解决"被管理"。于是你同时得到两个坏结果:
- 管理器停不掉它——它不在管理器的控制组里;
- 重启后也不受管理——没有单元描述它,机器重启后它的命运取决于当初怎么起的,常见是"要么还在,要么彻底没了"。
还有一条取舍必须写清代价:把停止语义放宽成"只杀主进程"或"什么都不杀",是被明确不推荐的做法。它看起来更温和,实际是让进程逃出服务管理器的生命周期与资源管理——甚至会出现"服务被认为已经停止、进程却还在跑"的状态。需要保留后台进程时,正确做法是把它交给管理器(写成单元),而不是放宽停止语义。
一条可查的事实:一个进程只属于一个控制组。所以"它到底归谁管"是查出来的,不是猜的——看 /proc/<pid>/cgroup 就有答案。
收编野进程:先确认、先停旧实例、再建单元
把在管理器之外跑起来的服务收编进来,顺序不能乱:
- 先确认它是什么:命令行、监听的端口、数据目录、当初怎么起的。不知道它是什么就收编,等于把一个未知的东西交给管理器。
- 先停掉旧实例:否则新旧两份会同时跑——重复提供同一个服务、抢同一个端口、争同一份数据。
- 写单元并让管理器接管:之后它的启动、停止、开机行为都归管理器。
- 复核:管理器能看到它,停止与启动行为符合预期。
# 收编动作的形态示意(不在运行中的服务上直接试验)
# systemctl daemon-reload # 写好单元之后
# systemctl start <单元> # 由管理器拉起
# systemctl is-active <单元> # 判据:管理器认为它在运行
# systemctl stop <单元> # 判据:管理器能停掉它(而野进程停不掉)
回滚考虑:收编失败时,停掉这个单元、回到原来的启动方式,别让两份并存——并存比"暂时没被管理"更危险。
改完怎么验证:每项一条判据
- 先用只读证据判定:服务显示未激活、进程却仍在运行,且该进程不属于任何单元的控制组——确认是野进程之后,再安排维护窗口做停机与重启验证。
- 卡住的进程不再阻塞其它操作:原来被挂住的动作(挂载/卸载、文件访问)能正常完成。
- 僵尸数量不再增长:连续观察一段时间,僵尸计数不上升;父进程被处理后,已有僵尸被回收。
- 管理器能停掉、也能拉起那个服务:两个方向都试,只看其中一个不算。
- 重启一次后行为一致:这是专治"野进程"的一条——重启后服务由管理器拉起,而不是"自己冒出来"或"彻底消失"。
- 归属正确:
/proc/<pid>/cgroup显示它属于预期的单元,而不是会话或根控制组。 - 验证方式固定成一句话:用原来的操作复现一次,进程状态、管理动作与重启后的行为都符合预期,才算通过。
这篇不解决什么
- 不解决连接层问题:《连不上但服务是好的》那篇在连接队列层;本篇在进程状态与信号层。两者都可能表现为"服务没反应",判定入口是队列计数还是进程状态。
- 不展开单元语义:单元的类型与重启策略、就绪判定这些内容属于单元语义层;本篇只讲状态与信号,以及"归谁管"这件事。
- 不讨论资源上限:句柄或监视配额用尽属于另一层(上限类那篇),本篇不重复。
踩坑清单
- 对处于不可中断等待的进程反复发强制信号:内核屏蔽了信号,它收不到,等系统调用返回才有机会响应。
- 对僵尸发信号:它已经结束,没有可杀的对象,动作对象是父进程。
- 只看进程名、不看状态:同一句"杀不掉",三种成因的处置方式完全不同。
- 把"杀不掉"当成权限问题:权限不足和信号被屏蔽是两回事,先看状态。
- 用会话工具起服务,却指望服务管理器去管它:它不在任何单元的控制组里。
- 把停止语义放宽后以为"更安全":进程会逃出生命周期与资源管理,出现"服务停了、进程还在跑"。
- 收编野进程时不停旧实例:新旧两份同时跑,重复服务、抢端口、争数据。
- 一遇到杀不掉就重启:丢现场,而且下次还会遇到同样的问题。
- 不看它属于哪个控制组就下结论:归属是可查的事实,不是猜的。
- 把"信号发出去了"当成"它一定收到了":投递是异步的,可能还没轮到它。
- 让 I/O 失败时不管数据代价:强制卸载会得到不完整的 I/O 结果。
- 收编之后不复核:管理器看不到它,或停不掉的野进程仍在跑,等于没收编成功。
- 点赞
- 收藏
- 关注作者
评论(0)