﻿# 如何观察系统的调度情况 —— 从队列长度到上下文切换到底是谁在跑

## 这篇文档是做什么的

性能排查到"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`——调度健康的"血压计"

```bash
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` 找元凶 |

**示例分析**：

```bash
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、`cs` 1200/s、`us+sy` 35%、`id` 64%。

> **结论**：CPU **不忙**（id 64%），但任务在**排队**（r > 4 核数）。这不矛盾——可能是一个任务拿到 CPU 后只跑一小段就阻塞了（等锁/等 IO），调度器不断切换，平均利用率看着不高但始终有人排队。根因常是**锁竞争或短 IO 密集**，下一步用 `pidstat -w` 看谁在频繁自愿切换。

### 1.2 `sar -q`——回看历史队列长度

```bash
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——负载是否在增长

```bash
load average: 1.25, 0.98, 0.75
```

判断法：三个数呈递增趋势（0.75→0.98→1.25）= 负载在上升；递减 = 高峰已过。把这些数和 CPU 核数比，持续 > 核数 = 过载。

## 二、进程/线程层面：上下文切换与迁移

### 2.1 `pidstat -w`——区分自愿/非自愿切换，这是最关键的调度诊断

```bash
pidstat -w -p <PID> 1
```

| 字段 | 含义 | 判读 |
|------|------|------|
| `cswch/s` | **自愿**上下文切换/秒 | 主动让出 CPU：等 `mutex_lock()`、等 `read()` 完成、等条件变量、`sleep()`。**高 = 频繁阻塞在资源上**。典型值：IO 密集程序几千到几万 |
| `nvcswch/s` | **非自愿**上下文切换/秒 | 时间片耗尽被内核抢占（`need_resched`）。**高 = CPU 竞争激烈、可运行线程远超核数**。典型值：计算密集程序如果线程数 > 核数，`nvcswch/s` 会飙到几百到几千 |

**示例分析——两个极端**：

```bash
场景 A：高 cswch，低 nvcswch
  PID    cswch/s nvcswch/s
 8921     12000        50
→ 该进程频繁自愿切换——在等 IO 或等锁。如果是 IO 密集程序就正常；
  如果是"应该纯计算"的程序，说明有意外阻塞。下一步 `strace -c -p <PID>` 看 block 在哪。
```

```bash
场景 B：低 cswch，高 nvcswch
  PID    cswch/s nvcswch/s
 8922         5      3200
→ CPU 被不断抢占——该任务的线程数远超可用 CPU 核数，每个线程刚跑一小段就被抢走。
  要么减少线程数，要么加核，要么看能不能改成协程/事件驱动。
```

```bash
场景 C：两个都高
  PID    cswch/s nvcswch/s
 8923      8000      2500
→ 最差情况：既频繁阻塞又 CPU 竞争。程序可能在线程间频繁同步（锁争用）+ 线程过多。
  先降线程数或减小锁粒度，重新设计同步模型。
```

### 2.2 `pidstat -u`——看任务在核间迁移

```bash
pidstat -u -p <PID> 1
```

看 `CPU` 列是否频繁跳变：同一进程每秒钟跑在不同的核心上。

```bash
CPU
  1
  2
  1
  3    ← 4 秒内在 1/2/3 号核间跳动
  2
```

> **频繁迁移** = 每次迁移丢掉旧核的 L1/L2 cache 和 TLB，缓存变冷。虽然调度器负载均衡是自动的，但对延迟敏感的应用应绑核（`taskset -c` 或 `numactl --physcpubind`），用"放弃自动均衡"换取"缓存命中"。

### 2.3 `perf stat`——量化迁移和调度开销

```bash
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`——单个任务的调度快照

```bash
cat /proc/$(pgrep cpu_demo)/sched
```

```bash
cpu_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`——单行关键字段

```bash
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`——整机调度器统计

```bash
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.md](/concepts/tools/perf-sched.md)

```bash
perf sched record -- sleep 5      # 记录所有调度事件
perf sched latency                # 打印每个 task 的平均/最大调度延迟
```

输出示例：

```bash
  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 找出系统中所有实时任务

```bash
# 方法 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
done
```

### 4.2 实时任务的调度行为观测

```bash
# 看实时任务的切换频率——FIFO 任务应该极少被抢占
pidstat -w -p <PID> 1
# 看它跑在哪个 CPU
pidstat -u -p <PID> 1 | awk '{print $NF}'  # CPU 列
```

## 五、整体判断流程

```bash
                      开始：调度健康检查
                             │
                   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 不高 → 任务在频繁阻塞后排队

```bash
vmstat 1 → r > 核数 但 us+sy 不高、id 充足
  → 说明：任务拿到 CPU 后只跑一小段就阻塞了，调度器不断切换
  → pidstat -w 1 → 找 cswch/s 高的进程
    → 若是 IO 密集 → 正常的自愿切换
    → 若是锁竞争 → strace 看 futex 或 perf lock 分析
```

### 8.2 cswch 高 + nvcswch 低 → 频繁阻塞

```bash
pidstat -w -p <PID> 1 → cswch/s 数万、nvcswch/s 几十
  → 程序在频繁等 IO 或锁
    → strace -c -p <PID> → 看高频阻塞调用
      → futex 多 → 锁竞争，perf lock
      → read/write 多 → IO 密集，iostat + iotop
```

### 8.3 nvcswch 高 → CPU 竞争激烈

```bash
pidstat -w -p <PID> 1 → nvcswch/s 几千
  → 线程数 > CPU 核数 × 合理倍率 → 减少线程或加核
  → 线程数 ≈ 核数但仍高 → 检查是否有实时任务在抢占
    → ps -eo pid,tid,class,rtprio,comm | grep -v TS
```

### 8.4 核间迁移频繁 → 缓存不热

```bash
pidstat -u -p <PID> 1 → CPU 列每秒跳变
  → taskset -cp <PID> → 看当前亲和性
    → taskset -cp 0-3 <PID> → 绑核试试
    → numactl --physcpubind=0-3 也可（NUMA 机器）
    → 绑后对比 perf stat -e cache-misses
```

### 8.5 实时任务排查

参考 [chrt.md](/tools/cpu/chrt.md) §六（常见排查方法与分析）：找出所有非 SCHED_OTHER 任务 → chrt 确认优先级 → 降级或确认合法性。

## 九、交叉引用

- **调度器原理**：[scheduling.md](/concepts/process/scheduling.md)——运行队列、调度类、CFS 选人逻辑
- **上下文切换开销**：[context-switch.md](/concepts/process/context-switch.md)——切换到底花了什么代价
- **绑核**：[thread-affinity.md](/concepts/process/thread-affinity.md)——禁止迁移、保住缓存热度
- **chrt**：[chrt.md](/tools/cpu/chrt.md)——查看/修改调度策略
- **中断亲和性**：[irq-affinity.md](/concepts/process/irq-affinity.md)——中断分布也影响调度
- **观测工具**：[vmstat 判据](/tools/cpu/vmstat.md)（r/b/cs）、[pidstat 详解](/tools/cpu/pidstat.md)（-w -u）、[mpstat 逐核](/tools/cpu/mpstat.md)（看单核是否被打满）

## 十、一句话总结

> **观察调度：`vmstat 1` 看 `r`（排队）和 `cs`（切换频率）→ `pidstat -w` 区分自愿/非自愿 → `pidstat -u` 看迁移 → `perf sched latency` 看调度延迟 → `ps/chrt` 排查实时任务。调度问题的核心判据就两个：运行队列长度（`r`）和上下文切换模式（`cswch` vs `nvcswch`）。**

