1. 先从理解进程状态开始
如果你刚开始接触 Linux,或者已经敲了一段时间的命令但还是对系统里那一堆"状态标记"似懂非懂,这篇笔记应该能帮上忙。Linux 的进程状态,说到底是操作系统对"每个程序当前正在干什么、能不能调度"的一种抽象分类,它在 ps、top、htop 这些工具里以单个字母的形式出现,比如 R、S、D、Z 等。内核通过这套状态机来管理 CPU 时间分配、内存回收、信号响应,甚至系统崩溃时的行为。对我这种做运维和嵌入式开发的人来说,看懂进程状态不是考试知识点,而是排查线上问题最基础的一环。
先给个直观的结论:Linux 进程状态可以粗略分成"可运行、睡眠、暂停、僵尸、不可中断"这几大类。其中大多数状态你平时一眼就能带过,但 D 状态和 Z 状态这两个坑,几乎每个运维都踩过——一个对应 IO 卡死,一个对应资源没回收。这篇文章会以学习笔记的形式,从内核视角出发,把每个状态的原理、转换关系、排查方法、常见误判都过一遍,配合命令实操和案例分析。适合刚入门 Linux 的新手,也适合想系统性梳理进程状态的老手。
我刚接触 Linux 的时候,以为进程状态就是个简单的"运行和停止"开关,后来发现自己错得离谱。进程状态不只是给用户看的,它直接关系到内核调度器的决策、虚拟内存的换入换出、文件系统锁的等待,甚至连关机时系统能不能正常退出都跟进程状态有关。比如你执行 kill -9 杀不掉一个进程,如果它在 D 状态,你会发现信号根本不起作用,这种场景在 NFS 挂载故障时特别常见,新手很容易懵。
这篇笔记不是简单罗列状态字母,而是我会从一个项目的角度来拆:先讲设计思路,再讲每个状态的具体含义和转换路径,然后完整演示用命令查状态、分析状态的实操流程,最后把我在真实环境里踩过的坑整理成速查表。你就算从零开始,也能顺着这条线把进程状态这块硬骨头啃下来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计思路:为什么 Linux 要把进程分成这么多状态
2.1 状态机的基本逻辑
进程在 Linux 内部其实是一个 task_struct 结构体,里面有一个字段叫 state,它的取值就是我们在命令行里看到的那些字母的老家。这整套设计背后的核心逻辑,是调度器需要快速判断"哪个进程现在能上 CPU,哪个进程在等什么资源"。
想象一个场景:系统里同时跑着几百个进程,每个进程都在争抢 CPU、内存、磁盘 IO。如果内核不把它们分类,而是每次调度时挨个去问"你能跑吗",效率会差到无法想象。状态机就是把"能不能被调度"这件事预先算好,调度器看一眼状态,立刻就能做出决策。这部分我强烈建议配合《深入理解 Linux 内核》或者内核源码中 sched.h 里的定义一起读,光看命令输出很难建立完整认知。
Linux 的主要状态定义在 include/linux/sched.h 中,下面这几个宏是核心:
c复制#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define __TASK_STOPPED 4
#define __TASK_TRACED 8
#define EXIT_ZOMBIE 16
#define EXIT_DEAD 32
注意这些值不是随意指定的,它们以二进制位标志的方式存在,这样设计的好处是:内核可以通过位运算快速组合和判断状态,比如 TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE 就能同时表示"可被信号唤醒"和"不可被信号唤醒"两种睡眠类型。我之前读源码时觉得这纯粹是节省内存的小技巧,后来才发现它在状态转换时的判断逻辑里特别优雅。
2.2 命令输出里的状态字母背后的对应关系
我们在 ps 里看到的状态字母,跟内核里的 task_struct->state 直接对应,但做了一层用户态映射。对应关系大致如下:
| 命令输出 | 内核状态标志 | 含义 | 可否被信号中断 |
|---|---|---|---|
| R | TASK_RUNNING | 正在运行或等待运行 | — |
| S | TASK_INTERRUPTIBLE | 可中断睡眠,等某个条件 | 可 |
| D | TASK_UNINTERRUPTIBLE | 不可中断睡眠,通常等 IO | 不可 |
| T | __TASK_STOPPED | 被暂停,如收到 SIGSTOP | 可恢复 |
| t | __TASK_TRACED | 被调试器跟踪暂停 | 可恢复 |
| Z | EXIT_ZOMBIE | 僵尸态,等父进程收尸 | — |
| I | — | 空闲内核线程 | — |
这里有个我早期经常忽略的点:ps 输出里的大小写是有讲究的。T 表示被作业控制暂停(比如按了 Ctrl+Z),t 表示被 ptrace 跟踪暂停(比如正在 gdb 调试)。两者表面上都是"暂停",但来源不同,处理方式也不一样。有一次我在排查脚本卡死问题时,发现进程状态显示 t,我以为是 T,结果用 kill -CONT 怎么都恢复不了,后来仔细看才发现是小写,是调试器在控制。
2.3 为什么系统不能没有 D 状态
很多人不理解:为什么 Linux 要设计一个连信号都杀不死的 D 状态?这不是给自己找麻烦吗?其实这要从内核同步机制说起。
当进程发起磁盘读写时,CPU 发出 IO 请求后,进程就得等硬件把数据准备好。如果允许信号中断这段等待,就会面临一个严重问题:信号处理函数可能想访问那份还没就绪的数据,或者进程被强制杀掉时,内核的 IO 子系统还在用这个进程的内存缓冲区,直接释放会引发数据竞争甚至内核崩溃。所以内核把这种等待标记为 TASK_UNINTERRUPTIBLE,意思是"内核正在替你做一件不能半途而废的事,你暂时不能被打断"。
用生活例子类比:你正在银行柜台办转账,柜员已经把你的钱从账户里划出去了,这时候你突然接到电话说要退单,银行不能立刻让你走,因为账已经做了一半。必须先把这笔操作完整走完,才能响应你的退单请求。D 状态就是这种"事务中途不可打断"的状态。所以当你看到进程处于 D 状态时,第一反应不应该是"杀掉它",而是先找到它到底在等什么设备资源。
3. 核心细节解析:每个状态怎么理解,怎么排查
3.1 R 状态:真正在跑和排队等跑的混合体
R 状态的全称是 TASK_RUNNING,但注意,进程在这个状态不代表它正占用 CPU 执行。在多核系统里,只有部分 R 状态的进程真正在 CPU 上跑,其余的其实在运行队列里排队,等调度器分配时间片。
你要区分这两者,光看 ps 不够,得配合 top 里的 %CPU 列。如果一个进程 %CPU 是 100% 以上(多核时常见),说明它确实占着核在计算;如果 %CPU 是 0 但状态是 R,那它多半是个忙等进程——代码里写了死循环,没在等任何资源,纯粹把 CPU 时间片耗完又继续排队。这种进程是最让运维头大的之一,因为系统负载可能飙升到几十,但具体看 CPU 使用率又不高,因为你看到的只是它在排队,没真正执行。
排查 R 状态进程的常用命令是 top 然后按 P 键按 CPU 排序,再按 M 键按内存排序,组合着看。如果想看某个进程到底跑在哪个 CPU 核上,可以用 taskset -pc PID 来查看和设置 CPU 亲和性。
3.2 S 状态:最常见的可中断睡眠
S 状态是 Linux 中最常见的进程状态,你会发现 ps aux 里绝大多数进程都是 S。它的含义是:进程主动让出 CPU,在等待某个事件发生,而这个事件可以是被信号唤醒,也可以是等待的条件满足。
典型的场景包括:等待用户键盘输入、等待网络数据包到达、等待定时器超时、等待锁释放。处于 S 状态的进程如果收到 SIGTERM 或 SIGINT,可以被唤醒并执行信号处理逻辑。这也是为什么你可以用 kill 命令正常关闭一个处于睡眠中的服务进程——它的睡眠是可中断的。
我在实际项目中常用 cat /proc/PID/status 查看进程的具体状态和更多细节,比如 voluntary_ctxt_switches(自愿上下文切换次数)这个字段。如果一个进程的上下文切换次数增长得异常快,说明它可能频繁在睡眠和运行之间切换,存在锁竞争问题。
很多新手会混淆"进程在睡眠"和"系统休眠",其实 S 状态只是进程层面的让出 CPU,跟系统电源管理完全无关。服务器上几百个 S 状态进程非常正常,你不需要去干预它们。
3.3 D 状态:不可中断睡眠,IO 卡死的重灾区
D 状态是运维排查里最让人头疼的状态之一。前面说了它不可被信号中断,这意味着:kill -9 杀不掉,kill 系列命令全部无效,进程只能等内核完成它正在做的 IO 操作才能自行退出。如果 IO 一直不返回,这个进程就会一直卡在 D 状态,严重时会导致大量进程堆积,系统负载持续走高,甚至触发系统假死。
最常见的触发场景有这几种:
- NFS 挂载的网络文件系统失去响应,进程访问挂载点文件时陷入不可中断等待
- 磁盘硬件故障或 RAID 阵列正在重建
- 设备驱动中的 IO 操作丢失,比如某些 USB 设备异常拔插
- Ceph、iscsi 等分布式存储链路故障
我遇到过一个典型案例:一台数据库服务器挂载了 NFS 用来做备份目录,某天存储节点网络断开,但 TCP 连接还没超时,所有写备份的进程跑进了 D 状态,持续了快 20 分钟。因为不能直接 kill,我只能先处理网络链路恢复,等内核的 IO 超时机制触发,进程才陆续恢复。如果你遇到这种场景,第一原则是恢复底层存储链路,而不是盯着进程杀。
有一种比较实用的排查手段:ps aux 里状态栏显示 D+,+ 表示进程在前台进程组。然后通过 cat /proc/PID/stack 查看内核调用栈,可以看出它具体阻塞在哪个函数上。/proc/PID/stack 需要 root 权限,但能给你最直接的线索。
3.4 Z 状态:僵尸进程,父进程的失职
Z 状态,即僵尸态,是另一种常见但本质完全不同的状态。僵尸进程其实已经死亡,它的代码段、数据段、内存都已经释放,唯一残留的是 task_struct 结构体,目的是让父进程通过 wait() 系统调用获取它的退出状态码。如果父进程没有调用 wait(),这个残留结构体就永远挂在进程表里,变成"僵尸"。
僵尸进程不占 CPU 也不占内存(保留的内核结构非常小),但如果大量堆积,会耗尽进程表项,导致系统无法创建新进程。常见的情况是父进程写了循环创建子进程的脚本,但忘记回收子进程的退出状态。
处理僵尸进程不是杀掉僵尸本身——你杀不掉一个已经死了的进程。核心思路是处理它的父进程。如果父进程还在运行,可以发 SIGCHLD 信号提醒它回收子进程,但多数情况下是没用的;实在不行只能杀掉父进程,让僵尸进程被过继给 init(PID 1),由 init 进程回收。
我自己的习惯是每隔一段时间用下面的命令扫描全局僵尸进程数量:
bash复制ps aux | awk '$8=="Z" {print}'
再把父进程一起打印出来:
bash复制ps -o pid,ppid,stat,cmd -p $(pgrep -f your_service)
如果你写的是长期运行的服务进程,务必在代码里正确调用 waitpid() 或设置 SIGCHLD 的信号处理器,否则你的服务跑上几个月,僵尸进程一定会冒出来。
3.5 T 和 t 状态:暂停与跟踪
T 状态主要是作业控制产生的,你按 Ctrl+Z 把前台进程挂起,它就会进入 T 状态。恢复用 fg 或 bg 命令,也可以通过 kill -SIGCONT <PID> 来手动恢复。
t 状态则是因为进程被调试器(如 gdb)附加而暂停。当调试器调用 ptrace(PTRACE_ATTACH) 后,目标进程默认会暂停,等待调试器指令。这里的字母是小写 t,提醒你这个暂停是"被跟踪"导致的,不是普通的作业控制暂停。
两者的共同点是进程都处于暂停状态,不消耗 CPU,但区别在于:T 状态可以用 SIGCONT 恢复,t 状态必须由调试器接管后主动执行 PTRACE_CONT 才能继续。如果你在用脚本批量管理进程时不小心把某个服务变成了 t 状态,可以先检查是否有 gdb attach 残留,用 gdb -p PID 再 detach 即可恢复。
4. 实操过程:用一条命令把系统进程状态"扒个底朝天"
4.1 快速上手 ps 和 top
我先给出一套完整的实操流程,这套流程我在排查线上问题时反复用到,可以算是压箱底的招。
第一步,先用 ps aux 看整体概貌:
bash复制ps aux
输出里第二列是 PID,第三列是 CPU 占用,第四列是内存占用,第八列是 STAT(进程状态)。STAT 列就是你观察状态的主战场。常见状态字母在前一节已经列过。有时你会看到 Ss、R+、D+ 这种组合,第一个字母是主状态,第二个字符是附加信息。s 表示该进程是会话首进程,+ 表示它位于前台进程组,l 表示多线程进程(有线程)。
然后是动态刷新版:
bash复制top
top 默认按 CPU 使用率排序,第一行能看系统平均负载,第二行看进程总数和各状态统计。top 里的 S 列就是进程状态列。如果你要看某个特定进程,过滤一下会更清晰:
bash复制top -p PID1,PID2,PID3
第二步,针对每个进程深挖。/proc 文件系统是 Linux 给我们的"后门",每个运行中的进程都有一个对应目录 /proc/PID/。查看进程状态的完整信息:
bash复制cat /proc/PID/status
重点关注 State: 那一行,它显示 R (running)、S (sleeping) 这种带完整状态描述的形式。还有个非常有用的字段是 voluntary_ctxt_switches 和 nonvoluntary_ctxt_switches,用来判断进程上下文切换的活跃程度。比如一个进程的 nonvoluntary_ctxt_switches 数值非常大,说明它经常被调度器强制抢占,可能是在忙等循环中。
4.2 实测:模拟各种状态并观察变化
纯粹读命令输出很难形成直观感受,我建议你动手制造几个状态的进程来观察。这一节我带你从零做一遍。
先让终端进入 R 状态。运行一个死循环:
bash复制while :; do :; done
因为是前台进程,你会看到终端卡住,CPU 占用飙升。开另一个终端执行:
bash复制ps -o pid,stat,cmd -C bash
找到对应的 bash 进程(注意循环是在 bash 子进程里执行的,或者你用 C 语言写个死循环更精确)。这个进程的状态会是 R+。
接着制造 S 状态。最简单的办法是运行 sleep 命令:
bash复制sleep 100
然后在另一个终端查看:
bash复制ps -o pid,stat,wchan:32,cmd -p $(pgrep -f "sleep 100")
这里我加了一个 wchan 输出列,它表示进程当前在内核中等待的内核函数名。对 sleep 来说,通常会显示 hrtimer_nanosleep,意思是在等定时器触发。对于处于某种状态但不知道在等什么的进程,wchan 是关键排查线索。
造 D 状态比较麻烦,因为正常环境下很难模拟。最接近的方式是挂载一个 NFS 或 CIFS 共享后强行断开网络。比如在 Linux 机器上挂载一个 Windows 共享文件夹,然后拔掉网线,再去访问挂载目录里面的文件。操作系统会尝试读取,然后进程就会卡进 D 状态。我当年在实验环境里就是这么复现的。配置一个本地 NFS server 也可以做同样测试。不过要注意不要在重要的生产环境上试,容易把系统搞到需要重启的地步。
造 Z 状态最容易:
bash复制sh -c 'sleep 100 & exit'
这个脚本启动了一个子进程 sleep 100,然后 shell 直接退出,没有回收子进程的退出状态。由于父进程已经死了,sleep 进程会被过继给 init,但在此之前,它会有一个很短的僵尸窗口期。如果你看不到它,就用下面这个命令检查系统当前所有僵尸进程:
bash复制ps aux | awk '$8=="Z" {print $2, $11}'
为了让僵尸持续存在,你需要一个没写 wait 的父进程循环创建子进程,网上很多示例代码可以抄,但我就偷懒直接用 python -c 造一个:
python复制import os, time
for _ in range(100):
pid = os.fork()
if pid == 0:
exit(0)
time.sleep(0.1)
time.sleep(1000)
运行这个 Python 脚本后,马上就能在 ps 里看到大量 Z 状态进程。因为父进程一直活着且不调用 wait(),子进程就一直是僵尸。
4.3 查看进程状态转换的历史:pidstat 和性能排查
ps 和 top 给出的是某个时间点的快照,你想要看一段时间的状态变化趋势,推荐用 pidstat,它是 sysstat 包里的工具,专门按进程输出 CPU、上下文切换、状态等信息。
bash复制pidstat -p ALL -l 1 10
上面的命令每秒采样一次,总共输出 10 次,-l 显示完整命令行。对比多次采样的 STAT 列,你可以看出进程在 R、S 等状态之间的动态变化。
还有一个核心概念要提:load average 和 R 状态的关系。系统平均负载在 Linux 里不是简单的 CPU 使用率,而是所有处于 TASK_RUNNING 或 TASK_UNINTERRUPTIBLE 状态的进程数的平均值。这就是为什么一个 IO 卡死的 D 状态进程会直接把系统负载拉到很高,但 top 里的 CPU 使用率却很低。理解这个关系之前,我一直以为负载高就是 CPU 忙,直到一次 NFS 故障才彻底想明白。
5. 常见问题与排查技巧实录
5.1 常见问题速查表
| 状态 | 表面现象 | 常见原因 | 处理建议 |
|---|---|---|---|
| R | 负载偏高,进程持续占用 CPU | 死循环、计算密集任务、调度异常 | 用 top 找 CPU 占用居高不下的进程,检查是否为正常业务,必要时调优或限流 |
| S | 大量进程处于睡眠,正常现象 | 等待输入输出、锁、定时器 | 如果 S 状态进程异常多,查锁竞争、连接池耗尽 |
| D | kill -9 无效,进程卡住 | NFS 故障、磁盘故障、驱动问题 | 先恢复底层存储/网络链路,再用 /proc/PID/stack 看内核栈 |
| Z | 进程数多,可能耗尽 PID | 父进程未调用 wait | 定位父进程,处理父进程或让其正确回收子进程 |
| T | 进程暂停,无法提供服务 | Ctrl+Z、作业控制 | kill -SIGCONT 或 fg 恢复 |
| t | 进程被调试器锁定 | gdb attach | 进入 gdb 执行 detach |
我单独强调一下表里第一行的场景。有一次线上 Java 应用 CPU 居高不下,top 显示进程状态是 R,而且负载已经飙到 40 多。最常见的套路是用 top -H -p PID 找到是哪个线程占用最高,然后用 jstack 输出线程栈就能定位到代码行。对 C/C++ 服务,可以用 gdb attach 或 perf top 来定位热点。R 状态不一定是坏事,但它长时间持续且 CPU 占用与业务预期不符时,大概率有代码层面的问题。
5.2 经验分享:用 /proc 做状态深挖
/proc 目录是排查进程细节的宝藏。进程的状态、内存映射、打开的文件、IO 统计、上下文切换次数全都在里面。我常用的几个文件:
bash复制/proc/PID/status # 状态、内存、线程数、上下文切换等信息
/proc/PID/stack # 内核栈(需要 root)
/proc/PID/wchan # 进程当前等待的内核函数
/proc/PID/io # IO 读写字节数
/proc/PID/fd # 打开的文件描述符列表
排查 D 状态进程时,/proc/PID/stack 是最直接的内核视角证据。比如进程阻塞在 wait_on_page_bit 上,说明它正在等内存页回写完成,大概率是磁盘读写响应不及时;阻塞在 nfs_wait_bit_killable,说明是 NFS 访问挂起。
不过要注意一点,/proc/PID/stack 对权限要求高,而且内核开启 CONFIG_STACKTRACE 才能看到完整调用栈。不少生产环境的内核默认是关闭的,只能看到一行 <0> 或空输出。这时候可以改用 cat /proc/PID/syscall 查看当前系统调用号,再对照系统调用表猜它的行为。这是相对冷门但有效的办法。
5.3 关于 kill 信号与状态的棘手场景
信号与进程状态的关系经常让人困惑。SIGKILL(9号)和 SIGSTOP(19号)是两种特殊的信号:它们不能被捕获,也不能被阻塞。但前面有个前提条件——进程必须处于可以接收信号的状态。对于 D 状态的进程,内核根本不会去尝试投递信号,所以 kill -9 没有效果。
这里的底层原因是:信号投递发生在进程从内核态返回用户态的时间点。如果进程一直沉睡在内核中的不可中断等待里,它根本没有机会回到用户态去检查信号队列。这就解释了为什么 NFS 故障时的 D 状态进程杀不掉。
另一个常见但反直觉的操作是:用 kill 发送信号修复状态。比如一个暂停的进程,可以发送 SIGCONT 让它继续;一个正常的运行中进程,发送 SIGSTOP 可以让它进入 T 状态。这在调试某些"失去了响应但又不愿意退出"的进程时很实用。不过千万别在生产环境随意对一个你不知道用途的进程做这种操作,容易引起连锁故障。
5.4 防止僵尸进程的正确姿势
最后聊一下开发层面的问题。写长期运行的守护进程时,子进程退出后父进程必须处理 SIGCHLD 信号。正确做法有两种:要么父进程主动调用 wait() / waitpid() 阻塞等待子进程退出;要么注册 SIGCHLD 的信号处理函数,在函数里调用 waitpid(-1, &status, WNOHANG) 循环回收所有已退出的子进程。
c复制#include <signal.h>
#include <sys/wait.h>
#include <stdio.h>
void sigchld_handler(int sig) {
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("reaped child %d\n", pid);
}
}
int main() {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
// 业务逻辑...
return 0;
}
我自己写过不少 C 语言服务,早期偷懒不处理 SIGCHLD,结果每隔一段时间就要手动清一次僵尸进程,后来养成了习惯:所有会 fork 子进程的模块,必须同时写回收逻辑。Python 项目里用 multiprocessing 也要注意 join() 方法别漏掉,subprocess.Popen 要用 communicate() 或者 wait() 收尾。
6. 进程状态背后的深层内核机制
很多资料讲进程状态只会告诉你这个字母什么含义,但不会解释这些状态在内核调度里到底起到什么作用。既然这篇是学习笔记,我把那层"为什么"也补上。
Linux 调度器(目前主流内核用的是 CFS / EEVDF 调度器)在挑选下一个要运行的进程时,扫描的是各个 CPU 运行队列中的 TASK_RUNNING 进程。S、D、T、Z 状态的进程根本不在调度队列里,也就是不会参与 CPU 时间分配。这样做有几大好处:
- 运行队列只包含"真的可以跑"的进程,调度器查找效率高
- 睡眠进程不会浪费 CPU 时间片
- 状态转换时有明确的唤醒机制(如等待队列
wait_queue),而不是让进程反复轮询
睡眠状态的进程通过"等待队列"机制挂在内核对象上。比如进程等一个锁时,它会被加入这把锁的等待队列,锁释放时内核会遍历这个队列,把里面的进程状态重新标记为 TASK_RUNNING,然后扔回调度器。这套机制搭配上 wake_up() 系列函数,构成了 Linux 的事件驱动模型底层。
如果你对 D 状态与内核 IO 的关系感兴趣,可以继续往下挖:TASK_UNINTERRUPTIBLE 通常和内核的 IO 等待页、磁盘请求队列深度、SCSI/NVMe 驱动等强相关。当存储设备本身很慢或者故障时,即使文件系统上层已经设置了 IO 超时,底层驱动可能还在重试,导致进程长时间停留在 D 状态。这也是为什么 SSD 和 NVMe 在企业存储上有时候反而比机械盘更难排查故障——因为它们被设计为永不放弃重试。
另外进程状态还跟 CPU 热插拔、系统休眠/唤醒有关系。系统进入休眠时,内核会冻结所有用户态进程,把它们标记成类似 T 的冻结状态,但命令输出里你可能看到的还是 D 或 S 的变体。这个知识点在排查"进程怎么突然都不动"时会冒出来,知道有这回事就行。
7. 最后分享一点个人经验
进程状态这块,我前后学了三遍才算是真正"内化"。第一遍背字母,第二遍看源码,第三遍是在生产环境踩了坑之后才真正融会贯通。印象最深的还是那次 NFS 故障,数据库备份进程全体 D 状态,kill 不掉、重启不能,最后只能等网络恢复。从那次以后,我接手任何系统都会先看一眼挂载列表,把 NFS、Ceph、iSCSI 这些外部存储明确的故障风险记在运维手册里。
给你几个实用的小建议:排查任何"进程不动"的问题,第一件事永远是 ps -o pid,stat,wchan:32,cmd,把状态和等待的内核函数一起打出来,不要只看 top 里的状态列;看到 Z 状态先查父进程是谁,而不是想办法杀僵尸本身;看到 D 状态先查存储链路健康度,而不是盲目重启服务;看到 T 状态就想想作业控制,八成是有人按了 Ctrl+Z 忘了恢复。
最后一个冷门但好用的技巧:ps 里加 -L 参数可以把进程下的线程一起列出来,配合 nlwp 字段可以快速判断一个进程是不是多线程卡死。比如 Java 的 GC 线程如果卡在 D 状态,整个 JVM 都会停顿。排查的时候把线程维度的状态也纳入视野,很多时候真正的问题隐藏在线程层而不是进程层。
