第一次听到“ax调度”这个说法是在一次线上问题复盘会上。有人甩出一行ps ax -o pid,stat,%cpu,cmd --sort-%cpu | head -20说“ax 看调度先看 D 状态多不多”。我当时愣了一下后来才明白这里的 ax 不是某个分布式调度框架而是ps命令的通用参数组合表示“所有进程、包括没有控制终端的进程”。它的价值不在于命令本身而在于把系统里所有进程的状态摊开让你一眼看出哪些进程正在等待调度、哪些已经卡死、哪些在空转。这篇文章把我这些年用ps ax排查调度问题的完整方法整理出来从命令细节讲到实战案例再到自动化脚本和避坑经验。1. 项目起点ax 与“调度”的底层关系1.1 为什么排查调度我第一步永远是ps axps是 Linux 下最基础的进程查看工具ps ax这个参数组合来自 BSD 风格语法a表示显示所有用户的所有进程x表示包含没有控制终端的进程。别小看这个x正是它让那批守护进程、内核线程、后台任务全部现形。很多刚入门的朋友习惯用ps aux这本身没错但如果你想稳定复现一条命令用于脚本和自定义输出ax配合-o指定列会比aux默认格式好处理得多。把ps ax和“调度”放在一起是因为操作系统调度器维护的是一棵进程树里的所有线程而ps ax恰好把这棵树的状态快照完整呈现给你。判断一个系统是不是处于异常调度状态先回答三个问题有多少进程在可运行队列里排队多少进程因为 IO 卡在不可中断睡眠多少进程变成了僵尸没人回收这三个问题全都可以从ps ax输出的STAT列里读出来。我后来把这条命令直接写进了巡检脚本每次线上异常先用它落一份现场再去开top和perf效率高很多。1.2 这套排查方法适合谁能解决什么问题如果你和我一样是后端开发、SRE、DevOps或者经常要和线上 Linux 服务器打交道那么这套以ps ax为入口的调度排查流程非常值得收藏。它能帮你解决这几类高频问题某个 CPU 核被打满导致业务响应变慢、负载load average飙高但 CPU 总利用率很低、进程进入 D 状态后长时间无响应、定时任务重叠执行造成资源浪费、以及进程反复创建销毁导致系统上下文切换过多。需要先说明一下这里的“调度”讨论的是操作系统进程/线程调度而不是 Kubernetes 或 Spark 里的任务调度框架。两者的共同点都是“排队、安排、执行”但在 Linux 主机上排查问题时最底层、最直接的抓手就是进程状态和内核调度队列。下面我会从常用命令的选型拆解开始逐步讲清楚如何用ax定位异常、如何干预调度行为、又如何把经验沉淀成自动化脚本。2. 命令选型与关键输出拆解2.1 别再用 ps aux 一把梭常用工具横向对比我见过不少同事只要排查进程就敲ps aux然后对着输出一行一行看。这样不是不可以但效率太低。实际排查应该组合使用多个工具不同工具有完全不同的时间尺度和解决问题的能力。以下是我常用的工具清单工具场景优点注意点ps ax进程快照、批处理、脚本统计非交互、输出可解析瞬时状态CPU 列是生命周期均值top动态观察整体负载实时刷新、可交互排序需要手动按键不便于留存htop图形化查看进程树/线程直观部分精简系统未预装pidstat按进程周期统计 CPU、IO、上下文切换连续采样看到变化趋势属于 sysstat需安装mpstat按 CPU 核统计快速定位单核热点同样是 sysstat 包perf内核态热点、锁竞争分析深入到调度器内部学习曲线陡权限要求高真心建议把pidstat装到所有服务器上它和ps ax配合一个看瞬时快照一个看采样趋势能覆盖绝大多数排查场景。对于基本的问题定位ps ax加top已经够用不用一上来就上重武器。2.2ps ax的输出里每个字母都在说话ps ax默认输出列不多我平时最常用的是自己组合的-o格式它保留了关键列又能自定义宽度ps ax -o pid,ppid,user,stat,%cpu,%mem,nlwp,wchan:32,cmd这里的列分别代表什么直接决定你看不看得懂调度状态。STAT是首当其冲的状态列。R表示进程正占用 CPU 或在可运行队列中等待调度S是可中断睡眠进程在等待某个事件比如网络数据包或锁释放D是不可中断睡眠最典型的是正在等待磁盘 IO 完成这种状态下进程连kill -9都暂时无法响应T是被暂停Z是僵尸进程I是空闲的内核线程。状态后面还可能跟着高优先级、N低优先级、l多线程、s会话首进程、前台进程组等后缀看到l就说明这个进程内部带了多个线程。%CPU是很多人容易误解的列。它在ps里并不是实时 CPU 占用率而是该进程从启动到当前累计 CPU 时间与存活时间的比值。一个每秒钟规律休眠的进程就算当前刚好在运行ps ax里的%CPU也只是长期平均值。所以要用ps ax找瞬时高峰一定要配合top或pidstat交叉验证否则可能漏掉真凶。wchan列非常有用它显示进程当前阻塞在内核的哪个函数上比如wait_on_page_bit说明在等页缓存do_futex说明在等用户态锁。wchan:32只是限制显示长度实际文件在/proc/PID/wchan嫌列短可以直接读文件。2.3 三个必会的高频命令形态理解了输出列以后我把工作中最常用的三种命令形态贴在下面直接复制就能用。第一个按 CPU 使用率降序看全貌ps ax -o pid,ppid,user,stat,%cpu,%mem,nlwp,cmd --sort-%cpu | head -20--sort-%cpu表示按 CPU 从大到小排队head -20只看前二十条。这条命令最大的好处是稳定——所有列都由-o显式指定不会因为终端宽度变化被截断非常适合写进脚本或留存现场。第二个按状态过滤异常进程ps ax -o pid,ppid,stat,wchan:32,cmd | awk $3 ~ /^D/ {print}把D替换成Z就能统计僵尸进程。awk里匹配的是STAT列因为状态可能是D、Dl这类组合所以用^D从字段开头匹配。第三个把多线程进程的内部线程全部摊开ps -eLf | grep PID-L会显示所有线程行每行的 LWP 列是线程 ID。如果你发现ps ax里某个进程的%CPU很高下一步几乎必然是ps -eLf配合top -Hp找到具体是哪个线程在烧 CPU。3. 调度问题排查从现象到根因3.1 CPU 飙高先定位进程再定位线程这是一个我处理过很多次的场景监控平台告警某台机器 CPU 使用率超过 90%服务接口耗时明显增大。很多人第一反应是top但top在大量进程的环境里不够聚焦。我习惯的路径是这样的。第一步用ps ax --sort-%cpu抓高消耗进程ps ax -o pid,user,stat,%cpu,cmd --sort-%cpu | head -10拿到可疑 PID 后第二步立刻用top带-Hp参数按线程维度刷新top -Hp PID进入交互界面后按P键就能看到该进程内部所有线程的 CPU 占用排序。记下最高的那一行的 PID在top -Hp里线程列显示的其实是 TID。第三步将其从十进制转成十六进制printf %x\n 12345第四步如果你排查的是 Java 服务直接jstackjstack PID | grep -A 30 nid0x3069nid对应的就是刚才的十六进制线程号找到线程栈之后就能看到它到底卡在某次 Full GC、锁竞争还是死循环。这套链路我用了很多年核心思想就一句话调度器调度的对象永远是线程而不是进程所以定位必须从进程下沉到线程。实际案例里我遇到过一个并行计算任务把单核打满进程整体%CPU才百分之十几直接用ps ax按进程排根本看不到。用top -Hp才发现某个线程接近 100%顺藤摸瓜找到是序列化类库版本不一致导致的无限重试。3.2 D 状态进程调度器最无奈的停摆D状态的全程是不可中断睡眠意思是这个线程正在内核态执行一段不能被打断的操作典型场景是等待磁盘 IO、NFS 网络文件系统响应或者是等某些驱动完成请求。这种状态最麻烦的地方在于进程内部正在执行的关键操作不能被打断普通信号无法立刻处理kill -9也只能等内核完成当前工作之后再排队生效。统计系统里有多少 D 状态进程是判断主机是否存在 IO 瓶颈的快方法ps ax -o pid,stat,cmd | awk $2 ~ /^D/ {print}数量超过 10 个尤其集中在某个磁盘分区对应的进程上时基本可以断定存储 IO 出了问题。接下来要做的是定位阻塞点。先看阻塞内核符号cat /proc/PID/wchan如果显示wait_on_page_bit、__blkdev_direct_IO这类符号说明确实卡在块设备读写上。再用iostat -x 1观察各磁盘的%util、await、svctm某块盘的%util长期接近 100%基本就锁定了。这里有一条重要经验看见 D 状态不要急着 kill。它有可能正在写关键数据强行结束可能导致文件系统损坏或数据不一致。正确做法是先确认底层存储是否正常比如磁盘是否被频繁的日志写入压垮、NFS 挂载是否已断连等进程自己恢复。还有一种隐蔽情况系统负载已经被打爆但所有 D 状态进程都在等待同一个内核锁而不是磁盘 IO。此时要看wchan是不是mutex_lock或rwsem_down_read_failed如果是问题可能出在文件系统层的元数据锁竞争比如大量小文件创建删除。这时要走perf lock定位锁的持有者光靠ps ax已经不够了。3.3 负载高但 CPU 不高要看上下文切换Linux 的 load average 统计的是 TASK_RUNNING 和 TASK_UNINTERRUPTIBLE 状态的线程数之和。于是就会出现一个经典场景load 已经 30CPU 使用率却只有 30%看起来非常矛盾。我的经验是这种场景十有八九和上下文切换风暴有关。举个例子一个服务接口需要调用外部系统超时时间设了 2 秒业务高峰期同时有 300 个线程在等第三方响应。这些线程进入睡眠后一旦响应到达就被唤醒唤醒时会判断是否要重新调度。如果大量线程频繁触发这种轻量级唤醒系统会在用户态与内核态之间反复切换这部分切换本身也是要消耗 CPU 时间的。vmstat一眼就能看出问题vmstat 1 5关注r可运行线程数、b阻塞线程数、cs每秒上下文切换次数三列。正常空闲机器cs可能只有几百到几千压测或业务高峰期几万也算正常但如果几十万甚至上百万就值得警惕了。pidstat能从进程维度拆开上下文切换看谁贡献了最多切换次数pidstat -w 1 5输出里的cswch/s是自愿上下文切换nvcswch/s是非自愿上下文切换。自愿切换多说明线程经常主动让出 CPU 等事件非自愿多说明线程的时间片被抢占得很频繁通常意味着运行队列里竞争激烈。对症下药的方向一般是限制并发数、优化锁粒度、把同步等待改为批量异步、缩短外部调用超时。3.4 中断与软中断另一种“调度”说到调度很多人只想到进程调度器忘了 CPU 还有一层中断调度。硬件中断和软中断softirq同样会抢占进程的执行时间而且往往只打满某一个核肉眼排查起来特别迷惑。排查命令有两组。看硬件中断在各个 CPU 核上的分布cat /proc/interrupts看软中断的分布和增长趋势watch -n 1 -d cat /proc/softirqs如果发现某个 CPU 核的数字远高于其他核说明中断没有均匀分散。常见原因是网络设备没有开启多队列或者 irqbalance 没正常工作。典型场景是网卡只启用了单队列大量收包全部落在 CPU0 上导致 CPU0 软中断NET_RX一直很高而其他核还在空闲。这种问题的治理方案一般是调网卡队列和中断亲和性确认硬件支持多队列后用 ethtool 调整 RSS 队列数或者把处理不同队列的中断绑定到不同 CPU 核。需要注意手动设置/proc/irq/N/smp_affinity前要确认 irqbalance 是关掉的否则两者会互相覆盖越调越乱。4. 调度干预与自动化治理4.1 用 taskset、nice、chrt 干预调度行为排查出问题之后有时候需要立刻干预调度行为来恢复服务。Linux 提供了三个常用工具作用各不相同。taskset用于把进程绑定到指定 CPU 核上。绑定核的好处是减少线程在不同核之间的迁移降低缓存失效和切换开销。但不要无脑绑单核很容易造成一个核满载而其他核闲置一般绑到一个 CPU 组如 0-3更稳妥taskset -pc 0-3 PIDnice和renice调整进程优先级范围从 -20最高到 19最低默认 0。对后台批处理任务适当调到 19可以让它只在系统空闲时运行不至于抢占前台业务资源renice -n 10 -p PIDchrt可以设置实时调度策略和优先级比如把关键采集进程设置为SCHED_FIFOchrt -f -p 88 PID这点必须特别谨慎。实时调度策略会让进程以极高优先级运行一旦进程本身有 bug 死循环整个系统可能连 ssh 都进不去。非必要场景不要用用了也要先在测试机验证。4.2 用 cgroup 把资源份额焊死在生产环境光靠 nice 和 taskset 太原始了更可靠的做法是使用 cgroup 对 CPU、内存、IO 进行配额管理。我习惯在 systemd service 里直接写资源限制比如[Service] CPUQuota150% CPUWeight200 MemoryMax2G IOWeight100CPUQuota150%表示该服务最多使用 1.5 个 CPU 核的配额即使机器其他部分空闲它也不能超过这个上限。CPUWeight则是在多个 cgroup 之间分配 CPU 的相对权重适合多个服务共享机器、希望按比例分配而不是硬限制的场景。运行中的服务也可以用命令动态调整systemctl set-property my-service.service CPUQuota100%配置生效后ps ax里看到的进程 CPU 数值可能不高但业务表现依然不好这时要想到是不是 CPUQuota 限得太死。查证方法是用systemd-cgtop它直接展示 cgroup 层级的资源使用和配额比在进程级别猜来猜去直观得多。4.3 定时任务调度与重叠防护cron 引发的资源问题我处理过太多次了。最常见的坑是一个脚本执行时间超过 cron 周期上次还没跑完下次又启动了于是脚本自己和自己抢资源CPU 和负载瞬间飚高ps ax里能看到一堆同名进程。防重叠最好用的工具是flock一行就能解决*/5 * * * * /usr/bin/flock -n /var/lock/myjob.lock -c /opt/scripts/myjob.sh /var/log/myjob.log 21-n表示拿不到锁就直接退出不会排队等待这样到了下一个周期如果上次还没跑完本次会直接跳过。日志里会留下退出记录方便事后排查为什么某次任务没执行。如果用的是 systemd timer也可以把同样的flock套进 ExecStart或者合理设计触发间隔。systemd timer 对于同一个 service如果上一次执行还没结束默认不会再次并行启动一个新实例但这个行为依赖 unit 的状态管理一旦脚本内部有 fork、daemon 化状态跟踪就可能失灵安全起见还是显式加锁。巡检时如果突然发现ps ax中有多个同名 job 进程首先要检查的就是锁有没有生效。4.4 巡检脚本把经验固化排查经验只有固化成脚本才有长期价值。我有一台主机上挂着这样的早检脚本核心逻辑很简单#!/bin/bash d_count$(ps ax -o stat | awk $1 ~ /^D/ {c} END {print c0}) z_count$(ps ax -o stat | awk $1 ~ /^Z/ {c} END {print c0}) r_count$(ps ax -o stat | awk $1 ~ /^R/ {c} END {print c0}) echo [$(date %F %T)] R$r_count D$d_count Z$z_count if [ $d_count -ge 2 ]; then echo 警告D状态进程过多疑似IO阻塞 ps ax -o pid,ppid,stat,wchan:32,cmd | awk $3 ~ /^D/ {print} fi if [ $z_count -ge 3 ]; then echo 警告僵尸进程过多需要检查父进程 ps ax -o pid,ppid,stat,cmd | awk $3 ~ /^Z/ {print} fi echo CPU TOP10: ps ax -o pid,user,stat,%cpu,%mem,cmd --sort-%cpu | head -11脚本本身非常简单但胜过手动敲命令的地方在于每次异常发生时它已经帮我把状态分布和 CPU TOP 都打印好了。把输出重定向到日志文件再用监控系统定期扫描关键字就能覆盖相当一部分调度异常告警。5. 常见问题排查与避坑速查5.1 高 CPU 却抓不到进程的坑有一次线上告警 CPU 高我用ps ax --sort-%cpu | head -20却找不到任何可疑进程每个进程 CPU 都只有百分之几。后来才发现是ps ax的%CPU是生命周期均值瞬时尖峰早被平均掉了快照根本拍不到。这种情况要靠持续采样工具。pidstat可以按固定周期打印实时 CPU把尖峰暴露出来pidstat -u 1 5也可以先用mpstat -P ALL 1确定是不是某个 CPU 核异常再结合/proc/interrupts看是不是中断打在一个核上。还有一类情况是短命线程线程启动、烧几毫秒 CPU、退出再创建新线程ps抓取瞬间线程已经不存在了这时候要么把采样间隔缩到 100ms要么用perf或 eBPF 追踪线程生命周期。5.2 僵尸进程与 init/subreaper僵尸进程的特点是已经退出但进程表项还在等待父进程调用wait()回收。它的STAT状态是Z不消耗 CPU 和内存但会占用 PID 号。大量僵尸堆积时系统可能因为 PID 耗尽而无法创建新进程。排查命令ps ax -o pid,ppid,stat,cmd | awk $3 ~ /^Z/ {print}重点看PPID。在容器环境里如果僵尸进程的 PPID 是 1说明容器内的 init 进程没有正确回收子进程。解决方案是给容器配置一个支持 subreaper 的 init如tini或者在父进程代码里正确注册SIGCHLD处理函数。我自己在写常驻守护进程时会显式调用prctl(PR_SET_CHILD_SUBREAPER, 1)确保孤儿进程不会堆积成僵尸。5.3 负载飚高但 CPU 闲置的几个典型原因load 高但 CPU 不高除了上下文切换还有几个高频原因值得排查。第一D 状态进程多说明 IO 阻塞。看vmstat的b列配合iostat -x定位磁盘。第二内存换页激烈si/so列持续非零大量进程等待 swap 或内存回收free -h和swapon --show能确认交换配置。第三锁竞争导致大量线程在 futex 上排队pidstat -w里非自愿切换奇高perf top里能看到futex_wait或queued_spin_lock_slowpath类的热点。这几种情况都表现为负载高、CPU 忙不起来但根因完全不同处理手段也完全不同。5.4 常用命令速查表最后整理一张我贴在工位上的速查表覆盖上面全部讲到的高频指令目的命令按 CPU 降序看全部进程ps ax -o pid,user,stat,%cpu,%mem,nlwp,cmd --sort-%cpu | head -20只看 D 状态进程和阻塞点ps ax -o pid,stat,wchan:32,cmd | awk $3 ~ /^D/ {print}只看僵尸进程及父进程ps ax -o pid,ppid,stat,cmd | awk $3 ~ /^Z/ {print}看进程内线程top -Hp PID或ps -eLf | grep PID持续采样 CPUpidstat -u 1 5看上下文切换vmstat 1 5/pidstat -w 1 5看单核分布mpstat -P ALL 1看硬件中断分布cat /proc/interrupts看软中断增长watch -n1 -d cat /proc/softirqs查找阻塞内核符号cat /proc/PID/wchan动态调整优先级renice -n -10 -p PID绑定 CPU 核taskset -pc 0-3 PID查看 cgroup 配额systemd-cgtop6. 经验收尾几个我坚持很久的操作习惯6.1 现场优先先留存后分析我处理线上问题有一个雷打不动的习惯不管问题多紧急第一件事永远是先把现场完整留存下来再开始交互式排查。ps ax的快照、top的几秒输出、vmstat的采样、ss的连接状态这些数据成本极低。很多问题在几分钟后就难以复现尤其是瞬时性的调度抖动如果没有当时的快照后续分析只能靠猜。6.2 调度是表象别停在表层最后说一点很深的体会所有看起来是“调度问题”的现象底层几乎都藏着另一个原因。CPU 高不一定是业务太忙可能是锁竞争负载高不一定是线程太多可能是 IO 挂起上下文切换高不一定是内核配置差可能是外部调用超时设置不合理。我的流程永远是先用ps ax锁定状态和进程再顺着状态的语义一路追下去直到找到一个可以行动的根因。这套方法论在我手里解决过的问题比任何单个命令都值钱希望对你也有同样的帮助。