如何观察系统的调度情况 —— 从队列长度到上下文切换到底是谁在跑
这篇文档是做什么的
性能排查到"CPU 利用率很高"这一步是卡住最多的分岔路口:是 CPU 真的算不过来?还是调度不合理导致任务排长队?是线程太多频繁切换?还是实时任务在霸占核心?本文从系统调度健康度的视角,给出观测的完整工具箱和判断方法。
可以回答什么问题
| 问题 | 工具和判据 |
|---|---|
| CPU 是不是过载了,任务在排队等 CPU? | vmstat 1 的 r 列;sar -q 的 runq-sz |
| 切换太频繁了吗?什么原因? | pidstat -w 区分自愿/非自愿;vmstat 1 的 cs 列 |
| 任务被调度器在不同核间迁来迁去? | pidstat -u 的 CPU 列是否频繁跳变;perf stat 的 migrations |
| 有时实任务在占着 CPU 不放? | ps -eo class,rtprio;chrt -p |
| 调度器自身开销大吗? | /proc/schedstat 看调度延迟;perf sched |
| 负载均衡在跨 NUMA 拖任务? | numastat 看 remote 比例;perf stat 的 node-loads/node-stores |
一、整机层面:运行队列和负载
1.1 vmstat——调度健康的"血压计"
vmstat 1
# r b ... cs ...
# 2 0 ... 1200 ...| 字段 | 含义 | 判读 |
|---|---|---|
r | 所有 CPU 上可运行 + 正在运行的任务总数(运行队列长度汇总) | 持续 > CPU 核数 = CPU 过载,任务在排队。例如 4 核机器 r 稳定在 6-8 = 有 2-4 个任务一直在等 |
b | 处于 D 状态(不可中断睡眠)的任务数 | 持续 >0 = IO 拖累,不是 CPU 慢 |
cs | 每秒上下文切换次数(全系统) | 经验值:几千/秒正常,几万~几十万/秒偏高(但取决于负载——高吞吐服务器 cs 本来就高)。要结合 pidstat -w 找元凶 |
示例分析:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
8 0 0 512M 4096M 12G 0 0 5 12 200 450 15 5 79 1 0
9 0 0 510M 4096M 12G 0 0 0 1024 310 1200 20 15 64 1 0
7 1 0 508M 4096M 12G 0 0 3 256 280 980 18 12 69 1 0
r稳定在 7-9、cs1200/s、us+sy35%、id 64%。
结论:CPU 不忙(id 64%),但任务在排队(r > 4 核数)。这不矛盾——可能是一个任务拿到 CPU 后只跑一小段就阻塞了(等锁/等 IO),调度器不断切换,平均利用率看着不高但始终有人排队。根因常是锁竞争或短 IO 密集,下一步用
pidstat -w看谁在频繁自愿切换。
1.2 sar -q——回看历史队列长度
sar -q -f /var/log/sa/sa15 -s 14:00:00 -e 15:00:00| 字段 | 含义 |
|---|---|
runq-sz | 平均运行队列长度(同 vmstat 的 r) |
plist-sz | 系统中所有任务数(含睡眠) |
ldavg-1/5/15 | 1/5/15 分钟平均负载(同 top 的 load average) |
blocked | D 状态任务数(同 vmstat 的 b) |
1.3 top load average——负载是否在增长
load average: 1.25, 0.98, 0.75判断法:三个数呈递增趋势(0.75→0.98→1.25)= 负载在上升;递减 = 高峰已过。把这些数和 CPU 核数比,持续 > 核数 = 过载。
二、进程/线程层面:上下文切换与迁移
2.1 pidstat -w——区分自愿/非自愿切换,这是最关键的调度诊断
pidstat -w -p <PID> 1| 字段 | 含义 | 判读 |
|---|---|---|
cswch/s | 自愿上下文切换/秒 | 主动让出 CPU:等 mutex_lock()、等 read() 完成、等条件变量、sleep()。高 = 频繁阻塞在资源上。典型值:IO 密集程序几千到几万 |
nvcswch/s | 非自愿上下文切换/秒 | 时间片耗尽被内核抢占(need_resched)。高 = CPU 竞争激烈、可运行线程远超核数。典型值:计算密集程序如果线程数 > 核数,nvcswch/s 会飙到几百到几千 |
示例分析——两个极端:
场景 A:高 cswch,低 nvcswch
PID cswch/s nvcswch/s
8921 12000 50
→ 该进程频繁自愿切换——在等 IO 或等锁。如果是 IO 密集程序就正常;
如果是"应该纯计算"的程序,说明有意外阻塞。下一步 `strace -c -p <PID>` 看 block 在哪。场景 B:低 cswch,高 nvcswch
PID cswch/s nvcswch/s
8922 5 3200
→ CPU 被不断抢占——该任务的线程数远超可用 CPU 核数,每个线程刚跑一小段就被抢走。
要么减少线程数,要么加核,要么看能不能改成协程/事件驱动。场景 C:两个都高
PID cswch/s nvcswch/s
8923 8000 2500
→ 最差情况:既频繁阻塞又 CPU 竞争。程序可能在线程间频繁同步(锁争用)+ 线程过多。
先降线程数或减小锁粒度,重新设计同步模型。2.2 pidstat -u——看任务在核间迁移
pidstat -u -p <PID> 1看 CPU 列是否频繁跳变:同一进程每秒钟跑在不同的核心上。
CPU
1
2
1
3 ← 4 秒内在 1/2/3 号核间跳动
2频繁迁移 = 每次迁移丢掉旧核的 L1/L2 cache 和 TLB,缓存变冷。虽然调度器负载均衡是自动的,但对延迟敏感的应用应绑核(
taskset -c或numactl --physcpubind),用"放弃自动均衡"换取"缓存命中"。
2.3 perf stat——量化迁移和调度开销
perf stat -e sched:sched_wakeup,sched:sched_switch,sched:sched_migrate_task -p <PID> -- sleep 5| 事件 | 含义 |
|---|---|
sched_wakeup | 唤醒次数——高 = 事件驱动频繁,未必有问题 |
sched_switch | 调度切换次数——高 = 上下文切换多 |
migrations | 跨核迁移次数——高 = 调度器频繁搬 task 做负载均衡,结合 pidstat -u 判断 |
三、调度器内部状态:/proc 深度下钻
3.1 /proc/<PID>/sched——单个任务的调度快照
cat /proc/$(pgrep cpu_demo)/schedcpu_demo (4043, #threads: 1)
-------------------------------------------------------------------
se.exec_start : 18327385.530324
se.vruntime : 1519426.840678
se.sum_exec_runtime : 2456.123456
...
policy : 0
prio : 120
...
nr_switches : 342
nr_voluntary_switches : 280
nr_involuntary_switches : 62| 关键字段 | 含义 | 判读 |
|---|---|---|
policy | 0=OTHER, 1=FIFO, 2=RR, 6=DEADLINE | 实时任务不是故意设置的就是问题 |
prio | 内核优先级(120=普通,<100=实时) | |
se.vruntime | CFS 虚拟运行时间 | 越小越"欠跑",对比同核其他任务 |
se.sum_exec_runtime | 累计 CPU 时间(ms) | |
nr_switches | 累计切换次数 | |
nr_voluntary_switches | 累计自愿切换 | 对比 pidstat -w 的累计值 |
nr_involuntary_switches | 累计非自愿抢占 |
3.2 /proc/<PID>/stat——单行关键字段
cat /proc/<PID>/stat
# 字段太多,关键看这几个:
awk '{print "state="$3" ppid="$4" priority="$18" nice="$19" policy="$41" cpu="$39 \
" utime="$14" stime="$15}' /proc/<PID>/stat| 字段 | 位置 | 含义 |
|---|---|---|
state | 第 3 字段 | R/S/D/T/Z 状态 |
priority | 第 18 字段 | 内核优先级 |
| nice | 第 19 字段 | nice 值(-20~19) |
policy | 第 41 字段 | 调度策略编号 |
cpu | 第 39 字段 | 上次跑在哪个核(每次 pidstat 采样的基础) |
3.3 /proc/schedstat——整机调度器统计
cat /proc/schedstat
# 每 CPU 一行:cpu0 <yld_count> <sched_switch> <sched_count> ...这是 kernel/sched/ 的原始计数器,适合用脚本按增量监控。关键:
sched_switch:该核累计上下文切换次数sched_count:进入schedule()的次数
3.4 perf sched——调度器 tracepoint 分析
完整指南:
perf sched record→latency→timehist→map→script全流程 + 5 大实战场景 → 详见 perf-sched
perf sched record -- sleep 5 # 记录所有调度事件
perf sched latency # 打印每个 task 的平均/最大调度延迟输出示例:
Task | Runtime ms | Switches | Avg delay ms | Max delay ms |
cpu_demo:4043 | 4873.245 | 15 | 0.058 | 2.340 |
kworker/1:0:56 | 2.103 | 42 | 0.012 | 0.089 |
cpu_demo只被切换了 15 次,最大等待延迟 2.34ms——说明调度延迟很低,基本拿到 CPU 就一直在跑。对比大量短任务的kworker,切换 42 次。
四、实时任务的监控
4.1 找出系统中所有实时任务
# 方法 1:ps 按 scheduling class 过滤
ps -eo pid,tid,class,rtprio,ni,comm | grep -v "TS"
# class 列:TS=OTHER, FF=FIFO, RR=RR, DLN=DEADLINE, B=BATCH, IDL=IDLE
# 方法 2:遍历 /proc 逐个看
for pid in $(ls /proc | grep -E '^[0-9]+$'); do
class=$(awk '{print $2}' /proc/$pid/sched 2>/dev/null | head -2 | tail -1)
if [ "$class" = "FIFO" ] || [ "$class" = "RR" ]; then
echo "PID=$pid class=$class $(cat /proc/$pid/comm 2>/dev/null)"
fi
done4.2 实时任务的调度行为观测
# 看实时任务的切换频率——FIFO 任务应该极少被抢占
pidstat -w -p <PID> 1
# 看它跑在哪个 CPU
pidstat -u -p <PID> 1 | awk '{print $NF}' # CPU 列五、整体判断流程
开始:调度健康检查
│
vmstat 1:看 r 和 cs
│ │
r > 核数 r ≤ 核数但 cs 很高
│ │
┌───────┤ pidstat -w 找元凶
│ │ │
CPU 过载 id 低? cswch 高? nvcswch 高?
│ │ │ │
top -H 可能是IO strace 看 线程太多/
找谁烧CPU 阻塞了 在等什么 协程模型重设计
│
pidstat -u
看是否迁移频繁
│
是 → taskset 绑核
否 → perf 看热点函数七、关键指标速查
| 指标 | 层级 | 工具 | 正常 | 异常 |
|---|---|---|---|---|
| 运行队列长度 | 整机 | vmstat 1 r 列 | r ≤ CPU 核数 | r 持续 > 核数 → CPU 过载 |
| D 状态任务数 | 整机 | vmstat 1 b 列 | ≈ 0 | > 0 → IO 阻塞拖累 |
| 上下文切换速率 | 整机 | vmstat 1 cs 列 | 几千/s | 几万~几十万/s → 调度风暴 |
| 负载均值 | 整机 | top / uptime | LA ≈ 核数 | LA >> 核数 + id 低 → CPU 不足 |
| 自愿上下文切换 | 进程 | pidstat -w cswch/s | 视负载 | 很高 → 频繁阻塞(IO/锁) |
| 非自愿上下文切换 | 进程 | pidstat -w nvcswch/s | < 100/s | > 几千/s → 被抢占/线程过多 |
| CPU 核间迁移 | 进程 | pidstat -u CPU 列 | 稳定在某核 | 每秒钟跳核 → 缓存变冷 |
| 调度延迟 | 调度器 | perf sched latency | μs 级 | ms 级 → 实时性差 |
| 调度策略 | 进程 | chrt -p <PID> | SCHED_OTHER | SCHED_FIFO/RR 高优先级 → 可能饿死他人 |
| 调度等待占比 | 调度器 | /proc/<pid>/sched | — | se.sum_exec_runtime / wall_time < 50% → CPU 竞争严重 |
八、常见排查方法与分析
8.1 r 高但 CPU us+sy 不高 → 任务在频繁阻塞后排队
vmstat 1 → r > 核数 但 us+sy 不高、id 充足
→ 说明:任务拿到 CPU 后只跑一小段就阻塞了,调度器不断切换
→ pidstat -w 1 → 找 cswch/s 高的进程
→ 若是 IO 密集 → 正常的自愿切换
→ 若是锁竞争 → strace 看 futex 或 perf lock 分析8.2 cswch 高 + nvcswch 低 → 频繁阻塞
pidstat -w -p <PID> 1 → cswch/s 数万、nvcswch/s 几十
→ 程序在频繁等 IO 或锁
→ strace -c -p <PID> → 看高频阻塞调用
→ futex 多 → 锁竞争,perf lock
→ read/write 多 → IO 密集,iostat + iotop8.3 nvcswch 高 → CPU 竞争激烈
pidstat -w -p <PID> 1 → nvcswch/s 几千
→ 线程数 > CPU 核数 × 合理倍率 → 减少线程或加核
→ 线程数 ≈ 核数但仍高 → 检查是否有实时任务在抢占
→ ps -eo pid,tid,class,rtprio,comm | grep -v TS8.4 核间迁移频繁 → 缓存不热
pidstat -u -p <PID> 1 → CPU 列每秒跳变
→ taskset -cp <PID> → 看当前亲和性
→ taskset -cp 0-3 <PID> → 绑核试试
→ numactl --physcpubind=0-3 也可(NUMA 机器)
→ 绑后对比 perf stat -e cache-misses8.5 实时任务排查
参考 chrt §六(常见排查方法与分析):找出所有非 SCHED_OTHER 任务 → chrt 确认优先级 → 降级或确认合法性。
九、交叉引用
- 调度器原理:scheduling——运行队列、调度类、CFS 选人逻辑
- 上下文切换开销:context-switch——切换到底花了什么代价
- 绑核:thread-affinity——禁止迁移、保住缓存热度
- chrt:chrt——查看/修改调度策略
- 中断亲和性:irq-affinity——中断分布也影响调度
- 观测工具:vmstat 判据(r/b/cs)、pidstat 详解(-w -u)、mpstat 逐核(看单核是否被打满)
十、一句话总结
观察调度:
vmstat 1看r(排队)和cs(切换频率)→pidstat -w区分自愿/非自愿 →pidstat -u看迁移 →perf sched latency看调度延迟 →ps/chrt排查实时任务。调度问题的核心判据就两个:运行队列长度(r)和上下文切换模式(cswchvsnvcswch)。