ps ax调度:从进程状态到内核调度器排查实战

发布时间:2026/9/28 17:12:32
ps ax调度:从进程状态到内核调度器排查实战 线上环境一有点风吹草动很多人第一反应就是敲ps ax。在运维和后端这个圈子里ax 这两个字母已经变成肌肉记忆不管是负载飙高、接口超时还是 CPU 打满先刷一眼进程列表总不会错。最近圈里还有个热词叫 ax 调度说的其实不是某种神秘的调度算法而是大家最朴素的直觉——先 ps ax再看调度状态。但多数人刷完 ax 只盯着 PID 和 COMMAND 两列看到几个可疑进程就顺手 kill这恰恰把 ax 最有价值的部分浪费掉了。真正值得读的是 STAT 列、TIME 列以及它们背后那套内核调度器的工作逻辑。这篇就围绕 ax 调度这件事把进程状态、负载计算、优先级、vruntime再到一次典型 D 状态排查串起来适合刚接触 Linux 排查的运维也适合想补一补调度底层知识的后端开发。1. 为什么 ax 会成为排查调度的第一条命令1.1 ps ax 输出里藏着哪些调度信息先把这个命令本身讲透。ps ax是 BSD 风格的进程列表命令a负责列出所有带终端的进程x负责列出所有不带终端的进程两者合起来就约等于整台机器上的全部进程。这也是它和ps -ef的最大区别ps -ef同样能显示全部进程但输出列更偏进程派生关系PPID而ps ax默认带着 STAT、TTY、TIME 这些跟调度强相关的列一眼扫过去就能对系统“当时的状态”有个大致判断。ps ax | head -20拿一台正在压测的机器举例输出会长这样PIDTTYSTATTIMECOMMAND1?Ss0:00/sbin/init2?S0:00[kthreadd]25192pts/0R0:08python app.py25193?D0:00nginx: worker process这里每一列都不是摆设。PID 和 COMMAND 不用多说TTY 的?表示进程没有控制终端通常是守护进程或内核线程STAT 是进程当前调度状态TIME 不是进程启动到现在经过的时间而是它累计消耗的 CPU 时间单位是分钟和秒。如果 TIME 长得很快说明进程一直在抢 CPU如果 TIME 几乎不动但进程活着那它多半在睡眠或者等待状态里泡着。ps ax默认不显示 CPU 占用排序我实际排查时几乎总会加自定义输出ps ax -o pid,ppid,stat,%cpu,%mem,etime,cmd --sort-%cpu | head -20etime是进程已运行时长%cpu是进程对单个逻辑核的占用百分比。这样一出来最吃 CPU 的进程排最前面状态、父进程、运行时长全在一屏里高负载排查第一眼就能抓住重点。顺带说一句ps aux也能看它比ps ax多 USER、%CPU、%MEM 三列日常够用但ps ax配合-o自定义更灵活想看什么列自己拼这是我喜欢它的原因。1.2 从 ax 到调度器先看结果再看原因随手一刷的进程列表其实是一张“调度快照”。Linux 里成千上万个进程不可能同时都在 CPU 上跑CPU 核心就那么几个谁上 CPU、谁排队、谁被换下来都是调度器说了算。我们看到的每个进程状态、累计 CPU 时间本质上都是调度器决策后的输出结果。这就是为什么光看 COMMAND 列没用哪怕看到一个进程 CPU 占比很高你也看不出它是真的在烧计算还是因为优先级太高长期霸占 CPU又或者卡在某个内核函数里出不来。用一个生活化类比CPU 相当于只有一个窗口的食堂进程是排队窗口的队伍调度器是窗口背后维持秩序的那套规则。ps ax只能告诉你“现在谁站在队伍哪个位置”至于队伍为什么这么长、某个人为什么一直卡在窗口不动它不会直接告诉你得看状态和内核等待点。所以接下来要重点拆 STAT 列它是整个 ax 调度体系里最值得练眼力的部分。2. 读懂 STAT 列等于拿到调度状态机的仪表盘2.1 状态字符速查一个字母一个含义STAT 列看起来像一堆奇怪的字母组合其实拆开看很简单。第一个字符是进程主状态后面跟着的是修饰字符。主状态里最常见的是 R、S、D、Z、T、I、X 这几种含义整理成一张表主状态字符含义通俗解释Rrunning / runnable正在运行或排队等待 CPUSinterruptible sleep可中断睡眠常等事件/信号Duninterruptible sleep不可中断睡眠通常卡在内核 IO 路径Zzombie僵尸进程进程已结束但父进程没回收Tstopped被暂停常因收到 SIGSTOPIidle kernel thread空闲内核线程属于 D 状态的特殊成员Xdead即将完全退出正常瞬时状态需要注意R 状态并不是“正在运行”的专属标识它表示进程当前在运行队列里处于可运行状态可能真的在某个核上执行也可能正在排队等核。S 状态则是进程因为等待某个事件比如网络包、锁、定时器主动让出 CPU事件没来就一直睡来一个信号就能醒。所以 ps ax 里出现大量 S 状态很正常出现大量 R 状态通常说明 CPU 资源紧张出现大量 D 状态则要立刻警觉。修饰字符也很关键s 表示该进程是会话首进程通常带终端l 表示多线程 表示位于前台进程组 表示高优先级nice 为负N 表示低优先级nice 为正。比如常见的Ss是会话首进程正在睡眠R是前台进程正在运行Sl是多线程进程正在睡眠。看到或N时顺带用ps -o nice,pri验证一下调度优先级有时候一个任务长期霸占 CPU就是因为它不知被谁调成了负 nice。2.2 D 状态卡在内核路径上的进程有多难缠D 状态是 ax 调度排查里最经典的“坑”。D 全称 TASK_UNINTERRUPTIBLE意思是进程进入了一个不能接收信号的内核等待路径常见场景包括等待块设备 IO、NFS 网络文件系统响应、内存回收、内核锁等。此时你在用户态敲任何 kill 命令都没用因为信号会排在那边但由于进程无法从内核路径返回用户态信号压根没机会被处理表现就是 kill -9 打上去毫无反应。进程不是不响应而是暂时无法响应。很多次线上负载异常都是 NFS 或存储阵列抖动引发的一连串 D 状态进程。排查时可以统计 D 状态进程数量ps ax -o stat | awk $1 ~ /^D/ {n} END {print n}如果数量从 0 突然涨到几十基本可以断定 IO 层面出问题了。接下来要定位具体是哪些进程把 PID 和 COMMAND 打印出来ps ax -o pid,ppid,stat,cmd | awk $3 ~ /^D/再看某个 D 状态进程在内核里到底等什么cat /proc/PID/wchan这个文件会给出当前进程在内核栈中等待的函数名比如nfs_wait_bit_killable、wait_on_page_bit、blkdev_direct_IO等。看到 NFS 相关函数就去查挂载点和网络连通性看到通用块设备等待就查磁盘 IO 和存储。定位方向对了问题就解决了一大半。这里有个操作纪律必须强调出现 D 状态进程千万不要急着 kill -9。内核通常会在 IO 超时或底层恢复后让进程返回你真正要修的是存储、网络、文件系统这类底层依赖。如果遇到极端情况比如远端存储彻底失联只能耐心等内核超时或者评估重启机器的代价。比起瞎 kill 一顿这些动作才是有意义的。2.3 Z 状态父进程没接住的僵尸进程Z 状态通常不会让负载飙升但它比 D 状态更让人头皮发麻。僵尸进程的出现机制其实很简单子进程 exit 之后内核不能立刻把它的 task_struct 删掉必须留一个残骸等父进程调用 wait() 来回收如果父进程一直不回收子进程就永远以僵尸姿态挂在进程表里。用 ps ax 判断僵尸进程的经典姿势ps ax -o pid,ppid,stat,cmd | grep -i defunct输出里 COMMAND 列会出现defunct标记。僵尸进程没法直接 kill因为进程本身已经死了要处理的是父进程先看 PPID确认父进程是不是某个有问题的业务进程或脚本再考虑重启父进程或父进程链让 init 接管并回收。海量僵尸进程还会带来一个隐蔽风险内核虽然会保留僵尸进程占用的 task_struct但 PID 空间会被快速耗光新进程 fork 失败。我见过一台机器因为父进程的 bug短短半天攒了 2800 多个僵尸进程业务进程尝试 fork 时报cannot allocate memory看着像内存问题一查进程表才发现是 PID 耗尽了。所以 monitoring 里不能只看负载和内存僵尸进程数量也是个值得接进告警的指标。3. 从 ps ax 到内核调度负载、nice 与 vruntime 的关系3.1 load average 到底在算什么账之前提到“ax 调度”很多人会顺手敲uptime/top看到 load average 三兄弟。这三个数字1 分钟、5 分钟、15 分钟经常被人误解成 CPU 使用率实际上它们统计的是内核调度队列里处于活跃状态的任务数量TASK_RUNNING可运行与 TASK_UNINTERRUPTIBLE不可中断睡眠的加权滑动平均值。换句话说S 状态进程再多也不会进 loadD 状态进程反而是 load 的重要组成部分。这解释了两种经典场景。场景一一台单核机器跑着一个死循环CPU 使用率顶到 100%load average 在 1.0 附近晃这其实是正常现象因为负载统计的是队列里的任务数量而不是 CPU 占用百分比。场景二8 核机器 CPU 占用只有 30%load average 却稳定在 8 上下这种事一看就要警惕大概率有 8 个左右进程进入不可中断睡眠通通卡在 IO 等待上。后一种情况用 CPU 监控根本发现不了得靠ps ax看 STAT 和vmstat 1 5的 b 列处于不可中断睡眠的进程数。vmstat里 r 列表示正在运行/排队等待 CPU 的进程数b 列表示不可中断睡眠进程数。r 长期接近 CPU 核数说明 CPU 压力大b 常有值就值得去查 IO。这套组合拳比单看 CPU 使用率靠谱得多也是我在第 4 章实战里会反复用到的思路。3.2 nice 与 vruntimeCFS 的公平游戏规则接下来聊最基础也是最重要的调度细节nice 和 vruntime。Linux 默认的完全公平调度器CFS从内核 2.6.23 一路演进到现在其核心设计目标是让每种任务都获得“相对公平”的 CPU 时间。这里的公平不是简单按进程数量平分而是按权重来。nice 值从 -20 到 19数字越小越“高贵”优先级越高默认是 0。比如两个 CPU 密集型进程竞争一个核一个 nice 0、一个 nice 10内核会按权重比例分配。nice 每差 1权重大约差 1.25 倍nice 0 和 nice 10 的权重差距约 1.25^10 ≈ 9.3 倍所以 nice 0 进程大约拿到 90% 的 CPUnice 10 只有 10%。这就是renice的现实意义renice -n 10 -p 25192 # 把目标进程 nice 调高 10让它少抢 CPU进程能否被调度不是按“谁跑得久谁先让”来排而是维护一个虚拟运行时间 vruntime。进程实际运行越久vruntime 涨得越多权重越大的进程vruntime 涨得越慢所以它更容易排在队列前面。调度器每次选 vruntime 最小的先去跑本质上是让所有进程的虚拟运行时间尽量对齐。这里顺带提一句Linux 6.6 引入了 EEVDF 调度器把 CFS 的红黑树改成了更精细的延迟分配算法但虚拟运行时间的核心思想仍然是理解调度行为的地基。实际排查中看到某个后台任务长期抢占 CPU可以用 nice 调低它的份额而不是简单暴力 kill。它比 kill 温和也更符合生产环境“保留业务能力”的原则。至于怎么确认当前进程的 nice 值用ps -o pid,nice,cmd -p PID即可。3.3 实时调度策略需要超越 CFS 的场景CFS 默认解决的是普通任务之间的公平问题但有些场景需要“特殊任务优先”比如实时音频处理、DPDK 网络收包线程这些任务要求严格的低延迟和可预测性不能跟批量计算任务一起排队。Linux 为此保留了实时调度器类 SCHED_FIFO 和 SCHED_RR优先级范围 1 到 99数字越大越优先。查看当前进程的调度策略和优先级chrt -p 25192把某个进程改成 FIFO 实时策略并设置优先级chrt -f -p 50 25192这里必须给新手提个醒不要在生产环境随手把普通进程调成实时策略。实时任务会无条件抢占所有 CFS 普通任务如果你把一个纯空转的死循环进程设为 SCHED_FIFO 且优先级很高它可能把一个核甚至整个系统“焊死”连 SSH 都响应不了到时候你手里 ps ax 再熟练键盘按破也救不回来。我见过的实践经验是确认业务确实需要硬实时能力并且完整评估过调度风险之后才在特定线程上启用实时策略。多数 Web/后端服务根本用不到这一步看看chrt -p的输出知道有这么回事就够了。4. 实战复盘一次 CPU 不高、负载爆表的 ax 调度排查4.1 现场还原先 ps ax 扫一眼全局有一年夏天我收到一台应用服务器的告警load average 连续 15 分钟都在 8 以上但 CPU 使用率只有 30% 左右。这种“CPU 不高负载高”的组合经验告诉我多半不是计算问题而是进程被“卡”住了。第一件事依旧是敲 ps ax先看全局。当时uptime显示 load average 是 8.7 / 8.2 / 7.9vmstat 1 5的 b 列维持在 7~9r 列却只有 0~1。说明有七八个进程因为某种原因进不了运行队列在不可中断睡眠里挂着。随即执行ps ax -o stat | awk $1 ~ /^D/ {n} END {print n}输出 8和 load 基本对得上。到这里基本可以确定这 8 个 D 状态进程就是负载的主要来源而不是 CPU 跑满了。下一步是把 PID 们揪出来ps ax -o pid,ppid,stat,cmd | awk $3 ~ /^D/当时输出集中在几个 nginx worker 和 PHP-FPM worker 上COMMAND 统一指向一个挂载的 NFS 存储目录。这就把范围缩小到了一个很常见的点位NFS 后端卡顿。4.2 顺着 wchan 找到 D 状态进程的内核等待点确认进程是哪些之后为了不漏判我打开了其中一个 PID 的内核等待点cat /proc/28513/wchan输出是nfs_wait_bit_killable另一个进程则是wait_on_page_bit这就足够说明问题了这些 worker 正在等待 NFS 页缓存回写或读取完成网络文件系统响应慢内核路径没返回进程只能挂着。再配合mount看挂载参数和nfsstat看 RPC 统计现场很快锁定了是 NFS 服务端所在存储集群发生了性能抖动。这里有个操作纪律要再次强调出现 D 状态进程千万不要急着 kill -9。D 状态的信号处理路径被内核卡住kill 发过去只会让进程继续挂着还可能让你在恢复窗口内浪费大量时间。正确做法是恢复底层依赖我们当时联系存储侧确认故障NFS 服务端恢复后这 8 个 worker 自动从 D 状态退回 S 状态负载在 3 分钟内回到 1 以下一条进程都没杀。如果你遇到的是无法立即恢复的情况比如远端存储彻底失联只能等内核 IO 超时或者慎重考虑重启机器因为强制重启的代价通常比等待更大。4.3 问题恢复与事后监控加固D 状态进程数量不是一个常规监控指标但它比 CPU 使用率更能反映系统“卡”在哪。这次故障之后我在监控里加了一个针对 D 状态进程数的采集项最简单的脚本思路是这样#!/bin/bash d_count$(ps ax -o stat | awk $1 ~ /^D/ {n} END {print n0}) if [ $d_count -gt 20 ]; then echo D state processes: $d_count | mail -s IO stuck alert opsexample.com fi实际生产环境建议直接把 d_count 上报到 Prometheus Alertmanager 或你团队已有的监控体系阈值可以按机器角色来定普通容器节点我习惯设为 5NFS/存储型节点可以给到 20。同时把vmstat的 b 列也纳入趋势图和刚才的 D 状态数量互验。另外对已知使用 NFS 的挂载点我会特别确认hard/soft与超时参数是否符合业务容忍度。软挂载虽然能更快让进程从 IO 等待中返回但可能会掩盖数据一致性问题生产数据库类路径通常不敢用 soft这些都是要在上线前就定好的策略不能等故障来了才纠结。5. ps ax 的边界调度排查工具矩阵5.1 从瞬间快照到连续采样ps ax 最大的限制是它只给“当前瞬间”的状态快照你看不到一分钟前发生了什么也不会记住任何历史数据。排查负载问题时我通常把它当成第一步而不是唯一一步。与 ps ax 互补的连续采样工具是老牌的 vmstat、pidstat、sar。以 pidstat 为例它来自 sysstat 包可以按进程维度看 CPU、内存、IO 和上下文切换pidstat -u 1 5 # 每 1 秒采一次 CPU采 5 次 pidstat -d 1 5 # 看每进程 IO 读写 pidstat -w 1 5 # 看每进程上下文切换次数ps ax 定位“哪些进程异常”pidstat 定位“如何异常”。比如 ps ax 告诉你 PID 25192 占用 CPU 高pidstat 能进一步告诉你它是在用户态烧计算%usr还是在内核态处理 IO%sys还能看它的主动/被动上下文切换频率。多一个维度判断就准一分。sar 系列则适合事后回溯sar -q 1 5查看运行队列长度和负载sar -w 1 5查看每秒上下文切换次数。在高负载故障发生后如果之前有部署系统监控你完全可以翻出历史曲线还原出 D 状态进程是几点开始聚集的、当时 IO 设备发生了什么。ps ax 解决的是“此时此地”sar 解决的是“彼时彼地”两者配合才是一个完整的排查闭环。5.2 调度延迟和上下文切换怎么量化调度的最终用户感受是“任务有没有延迟”而不仅仅是“CPU 跑没跑满”。上下文切换频繁可能意味着系统在多个进程/线程间反复“翻牌”锁竞争会导致可运行但无法推进的线程增多这些光看 ps ax 的 STAT 列很难量化。对上下文切换先看 vmstat 的 cs 列如果某个进程切得特别多用pidstat -w 1 5就能看到具体进程的 cswch/s自愿切换和 nvcswch/s非自愿切换。自愿切换多通常说明进程在等锁/等 IO非自愿切换多通常说明时间片被抢占或者 CPU 核数不足导致排队严重。再往下可以用 perf 的调度事件分析调度延迟和 CPU 迁移模式perf sched record perf sched latencyperf sched 记录一段时间内的进程调度事件然后统计每个进程的调度等待时间、运行时间、迁移次数。它能解释“为什么我的机器 CPU 没满但某个服务就是慢”这类问题。这一步通常需要 root 权限而且生产环境里 perf 的使用要谨慎建议先在预发环境复现。不过一旦用起来你会发现自己对刚才 ps ax 里那些 STAT 组合的理解会深一个层次。5.3 工具速查表先选对命令再动手把这次实战用到的工具和适用场景整理成一张速查表方便以后直接抄作业。症状首选命令定位思路负载高、CPU 也高ps aux --sort-%cpu找 CPU 占用 TOP 的进程核对业务合理性负载高、CPU 不高ps ax -o stat配合 awk、vmstat 1检查 D 状态进程数量与 b 列转入 IO/存储排查某个服务频繁超时pidstat -w 1、vmstat 1看上下文切换与锁竞争必要时用 perf 深挖出现大量僵尸ps ax -o pid,ppid,stat,cmd配合 grep defunct根据 PPID 定位父进程修复回收逻辑或重启链路某进程占用 CPU 过高ps -o pid,nice,cmd -p PID、renice调整 nice 降低竞争或确认是否业务预期这张表的核心思想是先看状态再动资源。ps ax 的边界就在于它只是一个开始它能告诉你进程现在是什么状态但它说不出这个状态是怎么形成的。要彻底搞明白一次 ax 调度异常你需要把进程状态、负载计算、IO 等待、优先级这些概念连成一条线再用连续采样工具去验证。这也是我把这些内容串起来写的原因。最后说点我自己的习惯。排查这类问题多了之后我已经把 ps ax 的默认输出改成了一个更顺手的别名在 .bashrc 里挂着alias axps ax -o pid,ppid,stat,%cpu,%mem,etime,cmd --sort-%cpu这样每次敲 ax出来的不是默认的简版输出而是按 CPU 占用排好序、带状态、带运行时长的全量视图。第一眼就能抓住最异常的进程再配合 STAT 列判断它是真的在跑还是卡住了。个人最大的体会是高负载从来不是问题本身它只是调度器写给外界的症状清单。先别急着 kill先看懂 ax 那一行行状态背后的等待与竞争往往才是解决问题最短的路径。