服务器入坑指北:进程在等谁
服务卡住不动:占用不高、也不报错;按"谁 CPU 高"去找,找到的进程一切正常;把它杀掉重启,过一阵又卡住。你始终说不清它到底在等什么。
关键区分是:卡住的进程往往只是"受害者"——它在等一个资源,而持有资源的那一方可能看起来很闲,甚至根本不在"占用榜"上。所以"谁 CPU 高"这个入口天然找不到真凶。
这篇给一条可执行的反查链:读等待位置 → 反查持有者 → 再看持有者自己在等什么 → 才决定动作。三段只读命令,全部可以在生产上直接做;取值用占位符,路径、列名与命令名照写。
先认角色:受害者,还是阻塞来源
卡住的进程通常只是在等一个资源,而持有资源的一方可能看起来很闲——它不需要消耗 CPU,只要"拿着"就够了。于是"谁 CPU 高"这条路筛出来的永远是"正在消耗 CPU 的那一个",而真凶可能正在安静地持有某样东西。
top 这类按占用排序的视图,天生只显示"正在消耗"的那一侧——等锁、等 I/O 的进程在这一侧一个字都不会说。
顺序反了的代价很具体:直接杀掉"看起来卡住的那个",杀掉的是受害者——现场丢了、在途工作丢了,过一阵还会复现,因为持有那一方毫发无伤。
状态字:先读对状态,再谈方向
| 状态 | 含义 | 处置方向 |
|---|---|---|
R |
正在跑或可运行 | 不是等待问题 |
S |
可中断睡眠 | 可能在等锁、等对端、等定时 |
D |
不可中断睡眠(多半等 I/O) | 等它完成 / 让那次 I/O 失败 |
Z |
僵尸,等待父进程回收 | 动作落在父进程上 |
在据此下结论之前,先把两条前提看清:
- 读等待位置受受保护进程的读取权限约束(权限检查基于
ptrace访问模式);读不到,说明这一层不向你开放,而不是"它没在等"; - 远程挂载上的锁状态通常记在对端服务器(例如
NFS挂载),本机看不到:在本机查不到不等于没有。
读状态本身很简单:ps 的 stat 列就是状态字;要看完整字段,读 /proc/<pid>/status 里的 State 行。
读等待位置块
ps -eo pid,stat,wchan:24,comm # 一屏看状态与等待通道:stat 里的 D 就是不可中断等待
cat /proc/<pid>/wchan # 单进程:读它睡在内核的哪个位置(一个符号名)
cat /proc/<pid>/status # 读 State 等字段:确认它在 S 还是 D
判据:wchan 这一列给出的是内核里的一个位置名,它告诉你在等哪一类东西(锁、I/O、对端……)。读到的是一个符号名而不是中文解释——用它是来判断"等哪一类",不是让你背下这些名字;不同内核版本、不同子系统下的名字会有差异,以你在本机读到的为准。
反查持有者块
lslocks -o PID,COMMAND,TYPE,MODE,PATH # 锁类:指定列一次读全(TYPE 与 MODE 决定怎么读)
lslocks # 默认列就是 COMMAND PID TYPE SIZE MODE M START END PATH
lslocks -o +BLOCKER # 追加一列:BLOCKER=阻塞该锁的进程的 PID(默认不显示)
lsof <路径> # 文件 / 套接字类:谁打开了这个对象
fuser -v <路径> # 反查占用者:输出更直接
ss -tnp # 套接字类:哪个进程占着这条连接
这一块是整篇的核心交付,怎么读:
- 锁类资源靠"列定义"读,而不是靠一个叫持有者的列:
lslocks的默认列是COMMAND PID TYPE SIZE MODE M START END PATH,而列定义写着COMMAND就是"持有该锁的进程的命令"——持有者就是该行自身;BLOCKER是"阻塞该锁的进程的 PID",它不在默认输出里,要用-o +BLOCKER显式追加。于是"受害者 → 持有者"从推断变成直接读出来:先在这一行读到持有者,再追加BLOCKER看谁被它挡住; TYPE与MODE要一起看:这把锁是租约(lease)、FLOCK、POSIX还是OFD类型,会影响你怎么读"持有者";对某些锁类型,持有者并非唯一(同一个打开文件描述可被多个进程共享);-o用来指定输出列:PID/COMMAND/PATH这几列是读反查结果的最小集合;fuser的-v是详细输出;- 文件 / 套接字类则用"谁打开了这个对象"反查:
lsof <路径>与fuser -v <路径>一对;套接字用ss -tnp。
判据:能指认"谁持有"——指认不出来,说明还没走到该走的下一环。lsof 的 -p 用来指定进程,需要看"这个进程自己打开了什么"时用它。
顺带一句机制:处于不可中断等待(D)的进程,连强制信号都送不进去——所以"杀掉它"这条路有时根本走不通。这一条的机制与处置在《进程卡死与杀不掉的进程》那篇,本篇不展开,只把它作为"为什么有时只能等"的依据。
三类动作与代价
| 动作 | 何时选 | 代价 | 前提 |
|---|---|---|---|
| 让持有者尽快释放 | 持有者本身有瓶颈、可以修 | 要改持有者,可能影响它的其它调用方 | 你能动到持有者 |
| 隔离受害者 | 需要先止血、根因一时修不了 | 被隔离的那部分业务受损,根因仍在 | 有可用的隔离手段(挪走流量、降级) |
| 直接杀掉 | 持有者已经无法自救 | 会丢现场与在途工作,而且可能复发 | 已确认没有更温和的路 |
为什么先动持有者:持有者通常才是根因;只动受害者,等于把症状按下去,过一阵它还会卡在同一个地方。
ps -eo pid,ppid,stat,cmd # 僵尸(Z)找父进程:ppid 与 cmd 用来确认父子关系
cat /proc/<pid>/wchan # 再看持有者自己在等什么:链条可能还有一层
lsof -p <pid> # 换一个入口看这个进程自己打开了什么
僵尸这一路的动作对象是父进程:父进程先退出时,孤儿的回收会由 PID 1 接管——所以对僵尸本身发信号没有任何意义。
停手判据:查到哪层停哪层
等待链可能跨进程、跨服务,甚至跨到内核态。判据是三条:
- 等待位置读不到(权限不开放)→ 到此为止,说明"这一层不向你开放";
- 持有者在别的层次(宿主侧、对端,或
NFS这类远程挂载的对端)→ 到此为止,把问题交到那一层; - 等待对象是外部依赖 → 到此为止,动作落在"联系对端 / 降级"上。
共同纪律:没有证据时不要假定更深的成因。不要把"锁等待"直接说成"存储故障",也不要把"等对端响应"说成本机内核有问题。能查到哪一层,就停在哪一层,并说明为什么停在这里。
现象到动作的判定表
| 现象 | 结论 | 动作 |
|---|---|---|
| 占用不高却不响应 | 它在等一个资源 | 读 wchan 与 stat,先定"等哪一类" |
| 杀掉重启后又卡住 | 真凶没被碰到 | 走反查链:该行的 COMMAND/PID 就是持有者,再加 -o +BLOCKER 看谁被挡 |
| 读不到等待位置 | 权限受限 | 承认这一层不开放;改用可读的证据 |
| 持有者在远程挂载或对端 | 本机看不到锁状态 | 停在这一层,交到对端处理 |
| 分不清等的是锁还是文件 | 状态与列没读全 | lslocks -o PID,COMMAND,TYPE,MODE,PATH 与 lsof <路径> 各读一次 |
改完怎么验证:每项一条判据
- 反查链能走到"持有者"这一层(能指认出来);
- 持有者被处理后受害者恢复(不是靠重启掩盖);
- 没有再出现同类阻塞(观察一段时间);
- 本次动作与回滚点有记录;
- 若只能止损,明确写清"根因未除"。
这篇不解决什么
- 讲进程卡死与杀不掉的进程的那篇管状态与信号语义(为什么杀不掉、该找谁、会话工具起的进程为什么不受管);本篇管等待的方向性与三类动作,不写信号语义——遇到"为什么杀不掉",请转去那篇。
- 讲负载高不等于 CPU 忙的那篇判断整机是否在排队;本篇判断具体是谁在等——两篇互相指路。
- 讲云盘变慢或报 I/O 错误的那篇是I/O 异常分支的取证与迁移;本篇的"等磁盘 I/O"只作为反查链里的一类等待对象出现,不重复块层取证。
踩坑清单
- 按"谁 CPU 高"找真凶:持有者可能很闲,压根不在榜上。
- 直接杀受害者不看持有者:丢掉现场,还会复发。
- 把"锁等待"直接说成"存储故障":越层归因。
- 不看被阻塞那一方的
MODE标记:把"谁在等"和"谁持有"读反。 - 远程挂载上的锁在本机找不到就以为没有:状态记在对端。
- 忽略"持有者可能不唯一":某些锁类型下同一个打开文件描述可被多进程共享。
- 读到
wchan看不懂也不承认看不懂:符号名随版本与子系统而异。 - 不知道读等待位置受权限约束:读不到被当成"没在等"。
- 拿引导式救援去救阻塞问题:那是另一条路径。
- 只止损不记录根因:下次还得从头查一遍。
- 只看
ps -eo pid,stat,wchan:24,comm不看/proc/<pid>/status:滞后与瞬时状态会混在一起。 - 把
Z状态也当成"在等资源":僵尸已经结束,动作在父进程。
- 点赞
- 收藏
- 关注作者
评论(0)