Linux 基础体系 · 第 17/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux CPU 调度与负载:运行队列、上下文切换、Load 和软中断

Linux 中“CPU 很忙”并不是一个单一现象。一个 CPU 可能正在执行用户代码,也可能执行内核代码、处理软中断、等待内存或磁盘 I/O;系统的 Load 可能很高,但 CPU 利用率并不高;上下文切换次数可能很多,却未必说明调度器失效。

要准确分析这些现象,必须把几个层次区分开:

  1. 调度器如何从可运行任务中选择下一个任务
  2. 运行队列如何表示 CPU 当前等待的任务
  3. 任务切换时保存和恢复了什么状态
  4. Load Average 到底统计了哪些任务
  5. 软中断如何把硬中断后的工作延迟执行,以及它为什么会消耗 CPU

一、先建立几个基本概念

1. 进程、线程与调度实体

Linux 内核调度的基本对象是线程,而不是用户通常理解的“进程”。

一个多线程进程包含多个线程,每个线程有自己的:

  • 内核栈;
  • 寄存器上下文;
  • 调度状态;
  • 调度优先级;
  • CPU 亲和性;
  • 调度统计信息。

用户空间中,一个进程的多个线程共享地址空间和大部分资源,但调度器仍然分别调度它们。

因此,下面两种情况不同:

一个进程,1 个线程
一个进程,32 个线程

第二种情况可能同时占用多个 CPU,也可能因为锁、I/O 或 CPU 亲和性限制而只有部分线程真正运行。

2. 任务状态

常见任务状态可以从 /proc/<pid>/statps 或调度器接口中观察到:

状态 含义
R Running 或 Runnable,正在运行或等待运行
S 可中断睡眠,通常等待事件、定时器、管道、网络等
D 不可中断睡眠,常见于等待块设备或某些内核资源
T 被停止或被调试器暂停
Z Zombie,已退出但父进程尚未回收退出状态

R 有两个含义:任务可能正在 CPU 上执行,也可能只是已经具备运行条件、等待调度。

D 并不等价于“正在运行”。但是 Linux 的 Load Average 通常会把可运行任务和不可中断睡眠任务都计入,因此大量 I/O 阻塞也可能导致 Load 升高。

Zombie 不会继续消耗 CPU,也不会因为 Zombie 数量增加而直接提高 Load;但大量 Zombie 说明父进程没有及时调用 wait() 回收子进程,可能耗尽进程表或暴露生命周期管理问题。


二、运行队列:CPU 调度的局部工作集

1. 什么是运行队列

运行队列(run queue,简称 rq)是调度器用于管理任务的数据结构。

在现代 Linux 的常见实现中,每个逻辑 CPU 都有自己的调度队列。可以把它抽象成:

CPU 0 ── rq0: 可运行任务
CPU 1 ── rq1: 可运行任务
CPU 2 ── rq2: 可运行任务
...

任务只有在满足以下条件时,才会进入某个 CPU 的可运行集合:

  1. 任务处于可运行状态;
  2. CPU 允许执行该任务;
  3. 任务没有被暂停、冻结或受其他调度约束限制。

这里的“可运行”不表示任务已经获得 CPU,只表示调度器可以选择它。

2. 一个任务如何进入和离开运行队列

典型状态变化如下:

flowchart LR
    A[睡眠或阻塞] -->|事件到达/系统调用返回| B[唤醒]
    B --> C[加入某个 CPU 的运行队列]
    C -->|被选择| D[在 CPU 上运行]
    D -->|时间片到期或更高优先级任务唤醒| C
    D -->|主动等待 I/O/锁/定时器| A
    D -->|退出| E[结束]

例如,一个线程执行 read()

  1. 用户线程进入内核;
  2. 内核发现数据尚未准备好;
  3. 线程进入睡眠;
  4. 线程从运行队列中移除;
  5. 其他可运行任务获得 CPU;
  6. 网卡收到数据,驱动和网络协议栈处理数据;
  7. 等待中的线程被唤醒;
  8. 线程重新加入某个 CPU 的运行队列;
  9. 调度器稍后再次选择它;
  10. read() 返回用户空间。

如果任务等待的是磁盘 I/O,它在等待期间常见为 D 状态。这个任务不消耗用户代码 CPU,但仍可能计入 Load。

3. 运行队列长度不是 CPU 利用率

假设一台机器有 4 个逻辑 CPU:

运行队列中的可运行线程:4
CPU 利用率:接近 100%

这通常表示每个 CPU 都有工作可做。

如果:

运行队列中的可运行线程:20
CPU 利用率:接近 100%

则意味着平均每个 CPU 有多个线程竞争,线程会排队等待。

但如果:

可运行线程:1
CPU 利用率:25%

这在 4 CPU 系统上可能是一个单线程 CPU 密集任务。该线程已经占满一个 CPU,但整机平均利用率只有约 25%。

反过来:

可运行线程:0
D 状态线程:30
CPU 利用率:10%
Load:较高

这通常指向 I/O、存储设备或内核锁等待,而不是 CPU 算力不足。

4. CPU 亲和性会改变运行队列含义

任务不是一定可以在所有 CPU 上运行。CPU 亲和性可以通过:

taskset -p 1234
taskset -cp 0-3 1234

查看或设置。

示例:

taskset -c 0 sha256sum /dev/zero

这会把命令限制到 CPU 0。即使机器有 32 个 CPU,该进程仍只能在 CPU 0 上运行。

因此分析运行队列时必须区分:

  • 全系统平均负载;
  • 单个 CPU 的运行队列;
  • 任务实际允许使用的 CPU 集合;
  • 容器或 cgroup 的 CPU 配额。

一个线程在 CPU 0 上排队,不代表 CPU 1 到 CPU 31 也繁忙。


三、调度器如何选择任务

1. 调度的触发时机

调度器通常在以下场景被触发:

  • 当前任务主动睡眠或阻塞;
  • 当前任务退出;
  • 更高调度优先级的任务被唤醒;
  • 当前任务的调度时间分配到期;
  • 内核返回用户空间前发现需要重新调度;
  • 某些内核路径主动调用调度点。

调度不是“每隔固定时间强制切换一次”这么简单。任务可能因为 I/O 很快睡眠,也可能长时间保持运行;是否抢占还取决于调度策略、优先级、内核配置和当前执行上下文。

2. 普通调度类与实时调度类

Linux 支持多个调度类。常见类别包括:

  • SCHED_NORMAL:普通任务,通常由桌面和服务器应用使用;
  • SCHED_BATCH:适合不需要交互响应的批处理;
  • SCHED_IDLE:极低优先级任务;
  • SCHED_FIFOSCHED_RR:实时调度策略;
  • SCHED_DEADLINE:基于截止时间的实时调度策略。

可使用以下命令查看任务的调度策略和优先级:

chrt -p 1234
ps -eLo pid,tid,cls,rtprio,pri,ni,stat,comm

示例输出可能类似:

  PID   TID CLS RTPRIO PRI  NI STAT COMMAND
 1234  1234  TS      -  19   0 S    worker
 1234  1235  TS      -  19   0 R    worker
 2000  2000  FF     80 120   - R    realtime-task

这里:

  • TS 通常表示普通时间共享调度;
  • FF 表示 SCHED_FIFO
  • RTPRIO 是实时优先级;
  • PRI 是内核显示的动态优先级,不应简单等同于 nice 值。

实时调度策略可以抢占普通任务。错误地设置高优先级实时线程,可能让普通任务甚至系统服务长期得不到 CPU。生产环境修改实时调度策略必须有明确的 CPU 隔离、退出机制和恢复方案。

3. 普通任务的公平调度

普通任务的目标不是让每个线程获得相同的墙上时间,而是在调度权重意义上尽量公平。

nice 值影响调度权重:

  • nice 值越低,优先级通常越高,权重越大;
  • nice 值越高,优先级通常越低,权重越小。

例如,两个同样持续可运行的普通任务:

任务 A:nice 0
任务 B:nice 5

它们不会简单地各运行 50% 时间。任务 A 的调度权重更高,因此长期获得的 CPU 时间通常更多。

Linux 内核的普通调度实现随版本演进。传统文档和工具经常使用 CFS(Completely Fair Scheduler,完全公平调度器)及虚拟运行时间解释调度行为;较新的内核还引入了 EEVDF 等调度改进。因此,“普通任务按虚拟运行时间最小者选择”适合作为传统 CFS 的解释模型,但不能把某个版本的内部数据结构或精确选择路径视为跨内核版本的稳定 API。

稳定的工程结论是:

  • nice 会影响普通任务的调度权重;
  • 调度会尽量在权重意义上分配 CPU;
  • 具体的调度队列结构和选择细节属于内核实现;
  • 不应依赖未导出的内核内部结构编写应用逻辑。

4. 负载均衡与任务迁移

多 CPU 系统需要避免某个 CPU 的队列过长,而其他 CPU 空闲。调度器会在适当时机进行负载均衡,把任务迁移到其他 CPU。

迁移不是免费的,因为它可能带来:

  • CPU 缓存失效;
  • NUMA 节点间内存访问;
  • 任务运行位置变化;
  • 锁竞争;
  • 额外调度开销。

因此调度器不会在每次队列有轻微差异时都迁移任务。CPU 亲和性、cpuset、cgroup、NUMA 策略和实时任务都会限制迁移。


四、上下文切换:从一个执行上下文切到另一个

1. 什么是上下文

对一个线程来说,CPU 上下文至少包括:

  • 通用寄存器;
  • 指令指针;
  • 栈指针;
  • 标志寄存器;
  • 地址空间相关状态;
  • 浮点、向量寄存器等扩展状态;
  • 内核调度所需的线程状态。

上下文切换是指 CPU 停止执行当前线程,保存其必要状态,再恢复另一个线程的状态并继续执行。

2. 线程切换和地址空间切换不是同一件事

两个线程可能属于同一个进程,共享同一地址空间:

线程 A → 线程 B
同一进程

这仍然是线程上下文切换,但通常不需要像切换到另一个进程那样更换整个用户地址空间。

如果切换到另一个进程:

进程 A 的线程 → 进程 B 的线程

则可能涉及地址空间切换、TLB 行为和更多缓存影响。现代处理器和 Linux 会使用各种优化,因此不能简单地认为每次上下文切换都必然刷新全部 TLB 或造成固定成本。

3. 上下文切换的两种统计分类

Linux 常把上下文切换分为:

自愿上下文切换

当前任务主动放弃 CPU,常见原因包括:

  • 等待锁;
  • 等待 I/O;
  • sleep()
  • 等待条件变量;
  • 等待管道、套接字或 futex。

非自愿上下文切换

当前任务仍可运行,但被调度器换下,常见原因包括:

  • 更高优先级任务唤醒;
  • 时间分配到期;
  • 任务被抢占;
  • CPU 亲和性或调度策略发生变化。

可以用 pidstat 观察:

pidstat -w -p 1234 1

典型输出:

Linux 6.x (...)
08:30:01   UID       PID   cswch/s nvcswch/s  Command
08:30:02  1000      1234     12.00      3.00  worker

字段含义:

  • cswch/s:每秒自愿上下文切换数;
  • nvcswch/s:每秒非自愿上下文切换数。

如果自愿切换很高,通常应调查锁、I/O、条件等待和系统调用;如果非自愿切换很高,通常应调查 CPU 竞争、线程数量、优先级和 CPU 亲和性。

但是“高”没有通用阈值。一个网络服务器可能因为大量短请求而有较多自愿切换,这是正常的;一个计算线程出现大量非自愿切换,则更可能说明它与其他线程竞争同一 CPU。

4. 上下文切换的成本来自哪里

切换成本不只有保存寄存器,还可能包括:

  1. 调度器更新任务统计;
  2. 运行队列出队和入队;
  3. 选择下一个任务;
  4. 切换内核栈;
  5. 处理地址空间变化;
  6. 破坏部分 CPU 缓存局部性;
  7. 影响分支预测和 TLB;
  8. 触发锁竞争或跨 CPU同步。

因此不能用“上下文切换次数 × 固定纳秒数”精确计算性能损失。切换成本取决于:

  • 是否同一进程;
  • 是否跨 CPU;
  • 是否涉及 NUMA;
  • 寄存器扩展状态使用情况;
  • 缓存工作集;
  • 调度器实现;
  • 当前 CPU 频率和微架构。

5. 系统级上下文切换统计

可以使用:

vmstat 1

输出中的 cs 是每秒上下文切换数,r 是可运行任务数,b 是阻塞任务数。

也可以查看 /proc/stat

grep '^ctxt ' /proc/stat

例如:

ctxt 184392817

这个值是系统启动以来累计的上下文切换次数。要获得速率,至少需要间隔两次采样:

a=$(awk '$1=="ctxt"{print $2}' /proc/stat)
sleep 1
b=$(awk '$1=="ctxt"{print $2}' /proc/stat)
echo "$((b-a)) context switches/s"

该方法简单直接,但只能得到系统总量,不能告诉你是哪一个线程造成的。定位到任务级别时应使用 pidstatperf 或调度 tracepoint。


五、Load Average:它统计的不是 CPU 百分比

1. Load 的定义

Linux 的 Load Average 通常表示一段时间内处于以下状态的任务数量的指数加权平均:

  1. 可运行任务;
  2. 不可中断睡眠任务。

在传统描述中,分别对应:

TASK_RUNNING
TASK_UNINTERRUPTIBLE

因此 Load 不是:

  • CPU 使用率;
  • CPU 队列长度的瞬时值;
  • 正在执行的线程数;
  • 单纯的用户态工作量。

uptime 可以查看:

uptime

输出类似:

08:40:12 up 12 days,  3:15,  4 users,  load average: 3.20, 2.10, 1.50

三个数分别是:

过去 1 分钟、5 分钟、15 分钟的平均负载

/proc/loadavg 提供原始接口:

cat /proc/loadavg

可能得到:

3.20 2.10 1.50 4/812 27193

前 3 个字段是 Load Average;4/812 通常表示当前可运行任务数与系统任务总数;最后一个数字是最近创建的进程 ID。具体字段应以目标内核和 procfs 文档为准,不应把 /proc 的所有内部字段当作长期稳定的应用 API。

2. Load 的指数衰减

Load Average 不是简单算术平均,而是指数加权移动平均。可用以下连续形式理解:

L(t+Δt)=L(t)eΔt/τ+A(t)(1eΔt/τ)L(t+\Delta t)=L(t)e^{-\Delta t/\tau} +A(t)\left(1-e^{-\Delta t/\tau}\right)

其中:

  • L(t)L(t):当前 Load;
  • A(t)A(t):当前采样窗口中的 active task 数量;
  • Δt\Delta t:采样间隔;
  • τ\tau:衰减时间常数;
  • 1 分钟、5 分钟、15 分钟 Load 使用不同的衰减参数。

直觉是:最近发生的任务活动权重更高,较早的活动逐渐被遗忘。Linux 内核实际使用定点数和固定采样逻辑实现该计算,公式是理解行为的近似表达,而不是要求用户手工复现内核位运算。

算例

假设:

当前 Load = 2
在接下来的 5 分钟中,active task 稳定为 6

以 5 分钟窗口的连续近似计算:

Lnew=2e1+6(1e1)L_{\text{new}}=2e^{-1}+6(1-e^{-1})

因为:

e10.3679e^{-1}\approx0.3679

所以:

Lnew2×0.3679+6×0.63210.736+3.7934.529L_{\text{new}}\approx2\times0.3679+ 6\times0.6321 \approx0.736+3.793 \approx4.529

这说明即使任务数量立刻从 2 变为 6,5 分钟 Load 也不会瞬间变成 6,而是逐步靠近 6。

3. 如何解释 Load 与 CPU 数量

对于一台具有 NN 个逻辑 CPU 的机器,可以把 Load 与 NN 作一个粗略比较:

Load < N:平均来看,CPU 竞争可能不严重
Load ≈ N:平均来看,CPU 接近饱和
Load > N:平均来看,有任务在等待 CPU,或有不可中断等待

例如 8 个逻辑 CPU:

load average: 8.0, 8.0, 8.0

通常表示长期接近满负载,但不能据此断定所有负载都是 CPU 密集型。

以下反例很重要:

反例一:单线程任务

CPU 数量:16
Load:1.0
CPU 总利用率:约 6%

如果只有一个单线程计算任务,它可以占满一个 CPU,但 Load 只有约 1,整机平均 CPU 利用率约为 1/161/16

反例二:磁盘阻塞

CPU 数量:16
Load:20
CPU 利用率:30%
大量任务处于 D 状态

这更可能是存储延迟、文件系统、块设备队列或内核 I/O 路径问题。增加 CPU 通常不能直接解决。

反例三:容器 CPU 配额

容器看到的 CPU 数量、宿主机 CPU 数量和 cgroup 可用 CPU 可能不同。宿主机有 64 个 CPU,但容器只获得 2 个 CPU 配额时,容器内 Load 与“64 个 CPU”比较会产生误导。

应同时检查:

nproc
cat /sys/fs/cgroup/cpu.max 2>/dev/null
cat /sys/fs/cgroup/cpu.stat 2>/dev/null

cgroup v2 中,cpu.max 的形式通常是:

quota period

例如:

200000 100000

表示每 100000 微秒周期最多使用 200000 微秒,即约 2 个 CPU 的配额。max 表示没有该项配额限制。不同发行版可能使用 cgroup v1 或 v2,路径和字段会不同。

4. Load 高但 CPU 不高时的诊断路径

先观察整体状态:

vmstat 1

典型字段:

字段 含义
r 可运行任务数
b 阻塞任务数
us 用户态 CPU 时间
sy 内核态 CPU 时间
si 软件中断 CPU 时间
wa I/O wait
st 被虚拟化平台偷走的时间

可以按以下逻辑解释:

r 高,us/sy 高
→ CPU 竞争或 CPU 密集计算

b 高,wa 高
→ I/O 等待倾向

b 高,wa 不一定高
→ 可能是内核不可中断等待、锁或设备路径问题

si 高
→ 软中断处理消耗明显

st 高
→ 虚拟机获得的物理 CPU 时间不足

wa 是 CPU 空闲期间等待 I/O 的统计表现,不能机械地把它等同于“某个进程正在 D 状态”。必须结合进程状态、磁盘延迟和 I/O 设备指标判断。


六、软中断:把中断后的工作延迟执行

1. 为什么需要软中断

硬件设备产生中断时,CPU 需要尽快响应。硬中断处理程序应尽量短,否则会阻塞其他中断和正常任务。

但设备事件往往需要做更多工作,例如:

  • 处理收到的网络数据包;
  • 完成块设备请求;
  • 更新定时器;
  • 处理任务队列;
  • 执行 RCU 回调。

因此 Linux 会将部分工作延迟到软中断上下文执行。

抽象流程如下:

sequenceDiagram
    participant D as 设备
    participant CPU as CPU
    participant HI as 硬中断处理程序
    participant SI as 软中断处理
    participant K as ksoftirqd/NAPI

    D->>CPU: 产生硬件中断
    CPU->>HI: 进入硬中断上下文
    HI->>CPU: 标记待处理软中断
    CPU-->>SI: 从中断返回路径处理软中断
    alt 软中断工作量过大或需要让出
        SI->>K: 唤醒 ksoftirqd
        K->>CPU: 在线程上下文继续处理
    end

关键点是:软中断不是普通用户线程,也不是硬中断本身。它通常在内核中断返回路径上运行,也可能由每 CPU 的 ksoftirqd/N 内核线程继续处理。

2. 常见软中断类型

可以通过以下命令查看各 CPU 的累计软中断计数:

cat /proc/softirqs

常见行包括:

类型 常见用途
TIMER 定时器相关工作
NET_TX 网络发送
NET_RX 网络接收
BLOCK 块设备相关处理
IRQ_POLL 某些块设备轮询机制
TASKLET tasklet 机制
SCHED 调度相关软中断
HRTIMER 高精度定时器
RCU RCU 回调处理

这些计数是累计值,不是每秒速率。观察变化量:

awk '
/^[A-Z_]+:/ {
    gsub(":", "", $1)
    printf "%s", $1
    for (i = 2; i <= NF; i++) printf " %s", $i
    printf "\n"
}' /proc/softirqs > /tmp/softirqs.before

sleep 1

awk '
/^[A-Z_]+:/ {
    gsub(":", "", $1)
    printf "%s", $1
    for (i = 2; i <= NF; i++) printf " %s", $i
    printf "\n"
}' /proc/softirqs > /tmp/softirqs.after

diff -u /tmp/softirqs.before /tmp/softirqs.after

这只能粗略看到计数变化。更适合人类观察的方式是:

mpstat -I SUM 1

mpstat 来自 sysstat 软件包,部分最小化发行版默认未安装。

3. 网络接收路径与 NET_RX

以网卡接收数据为例:

  1. 网卡收到数据;
  2. 网卡通过 DMA 将数据放入内存;
  3. 网卡触发硬中断,或触发 MSI-X 中断;
  4. 驱动在硬中断中确认事件并安排后续处理;
  5. NAPI 机制通常禁止或抑制部分中断,改为轮询网卡队列;
  6. 网络包处理在软中断路径或相关内核线程中继续;
  7. 协议栈处理 IP、TCP/UDP、socket 缓冲区;
  8. 等待数据的用户线程被唤醒;
  9. 用户线程重新进入运行队列。

高网络包速率不一定意味着高带宽。大量小包可能造成更高的:

  • 硬中断频率;
  • NET_RX 软中断工作量;
  • 协议栈处理次数;
  • socket 唤醒和调度次数;
  • CPU cache 和锁竞争。

因此“网络吞吐量不高但 CPU 很高”时,应检查包速率、软中断分布和单 CPU 是否过载。

4. ksoftirqd 是什么

每个 CPU 通常有对应的 ksoftirqd/N 内核线程,例如:

ps -eLo pid,tid,psr,stat,comm | grep '\[ksoftirqd'

它的作用不是替代所有软中断处理,而是在软中断工作量过大、不能一直占用中断返回路径时,承接待处理的软中断工作。

因此看到:

ksoftirqd/3

CPU 使用率较高,通常说明 CPU 3 上有较多软中断工作积压或持续到达。常见来源包括:

  • 网络收包;
  • 网络发包;
  • 定时器;
  • 块设备;
  • RCU 回调。

但不能仅凭 ksoftirqd 高就断定是网络问题,必须结合 /proc/softirqs、网卡统计和块设备统计。

5. 软中断在哪个 CPU 上运行

软中断通常与触发它的 CPU、设备中断亲和性和 NAPI 调度有关。中断和软中断分布不均时,可能出现:

CPU 0:软中断很高
CPU 1-15:相对空闲

此时整机平均 CPU 利用率可能不高,但 CPU 0 上的应用线程会明显延迟。

查看硬中断分布:

cat /proc/interrupts

查看网卡队列和中断信息:

ethtool -l eth0
ethtool -S eth0

需要把 eth0 替换为实际接口。ethtool -S 的统计名称由网卡驱动决定,不同硬件差异很大。

还可以查看中断亲和性:

for f in /proc/irq/*/smp_affinity_list; do
    printf '%s: ' "$f"
    cat "$f"
done

修改 IRQ 亲和性会改变系统级 CPU 分布,生产环境应记录原值,并确认不会与应用 CPU 隔离、NUMA 设计或自动 IRQ 均衡服务冲突。

6. 软中断对 CPU 统计的影响

top 中常见:

%Cpu(s): 60.0 us, 20.0 sy, 15.0 si, 5.0 id

其中:

  • us:用户态;
  • sy:内核态;
  • si:软中断;
  • hi:硬中断;
  • wa:I/O wait;
  • st:虚拟化环境中的 steal time。

si 较高表示软中断消耗明显,但不同工具的统计口径和显示精度应以工具文档及 /proc/stat 数据为准。

/proc/stat 的 CPU 行类似:

head -n 1 /proc/stat

可能得到:

cpu  12345 20 6789 100000 300 80 1200 0 0 0

传统字段依次包括:

user nice system idle iowait irq softirq steal guest guest_nice

这些是自启动以来累计的时间单位,需要两次采样才能计算区间比例。softirq 是软中断时间,irq 是硬中断时间。


七、把运行队列、Load、上下文切换和软中断串起来

一个网络服务在高峰期可能经历以下完整路径:

网卡接收数据
  ↓
硬中断确认事件
  ↓
NET_RX/NAPI 处理数据包
  ↓
唤醒等待 socket 的线程
  ↓
线程加入运行队列
  ↓
调度器选择线程执行
  ↓
线程处理请求
  ↓
等待数据库、锁或磁盘
  ↓
线程睡眠并离开运行队列

这条路径中:

  • 网卡和协议栈可能提高 si
  • 用户线程被唤醒和切换会增加上下文切换;
  • 同时可运行的线程增加会提高瞬时运行队列长度;
  • 线程处于 R 时可能提高 Load;
  • 如果数据库或磁盘导致线程处于 D,也可能提高 Load;
  • 用户线程 CPU 低,不代表网络软中断 CPU 低;
  • 整机平均 CPU 低,不代表某个 CPU 没有被软中断打满。

因此必须用证据链,而不是只看一个指标。


八、一个可重复的观察实验

以下命令适合在测试机执行。不要直接在生产服务器上制造大量负载。

1. 准备观察窗口

vmstat 1

在另一个终端运行:

mpstat -P ALL 1

再开一个终端:

pidstat -w -u -d 1

分别观察:

  • vmstat:运行队列、阻塞、CPU 总体;
  • mpstat:每个逻辑 CPU 的利用率和软中断;
  • pidstat:进程级 CPU、I/O 和上下文切换。

2. 创建一个单线程 CPU 负载

yes > /dev/null

该命令会持续生成文本并丢弃输出,通常占用一个 CPU。停止:

Ctrl-C

在 8 CPU 机器上,预期现象可能是:

单个 CPU 接近 100%
整机 CPU 利用率约为 1/8
Load 逐渐增加约 1

实际数值会受到其他任务、容器限制和 CPU 计数方式影响。

3. 创建多个 CPU 负载

for i in $(seq 1 8); do
    yes > /dev/null &
done

查看任务:

jobs -l

停止这些任务:

kill $(jobs -p)

在拥有 8 个以上可用 CPU 的测试机上,多个进程通常会填满更多 CPU;如果只允许使用少量 CPU,任务则会在受限 CPU 集合中竞争。

4. 制造睡眠而非 CPU 竞争

while :; do sleep 0.01; done

这个任务频繁睡眠和唤醒,但不会持续占满 CPU。它可能增加自愿上下文切换,不过单个任务的切换速率和实际 CPU 消耗取决于系统调用、定时器精度和调度行为。

5. 观察任务状态

ps -eLo pid,tid,stat,psr,pri,ni,comm --sort=-pri | head -n 20

字段包括:

  • PID:进程 ID;
  • TID:线程 ID;
  • STAT:状态;
  • PSR:最近运行的 CPU,不能视为永久绑定 CPU;
  • PRI:显示的调度优先级;
  • NI:nice 值。

若要查看每个线程的 CPU 时间:

ps -eLo pid,tid,stat,pcpu,psr,comm --sort=-pcpu | head -n 20

停止实验产生的任务后,再确认:

pgrep -a yes

没有残留进程。


九、如何区分几类常见故障

情况一:Load 高、r 高、CPU 接近满

可能原因:

  • CPU 密集型任务;
  • 线程过多导致竞争;
  • cgroup CPU 配额不足;
  • 虚拟机 vCPU 被过度分配;
  • CPU 被软中断或内核工作消耗。

诊断:

uptime
vmstat 1
mpstat -P ALL 1
pidstat -u -w 1

如果某些 CPU 的 us 高,优先查用户程序;如果 sysi 高,优先查内核路径和软中断;如果 st 高,优先查虚拟化平台。

情况二:Load 高、CPU 不高、b

可能原因:

  • 磁盘或网络文件系统延迟;
  • 块设备队列拥塞;
  • 文件系统或驱动路径等待;
  • 内核锁或设备状态导致的不可中断等待。

查看 D 状态任务:

ps -eLo pid,tid,stat,wchan:32,comm | awk '$3 ~ /D/'

wchan 显示任务当前等待的内核等待点,但它依赖内核配置、符号信息和权限,名称不能独立作为结论。

继续检查磁盘:

iostat -xz 1

iostat 同样属于 sysstat。重点关注:

  • await:I/O 平均等待时间;
  • %util:设备忙碌比例;
  • r/sw/s:读写请求速率;
  • 队列长度和吞吐量。

生产环境中不要为了“清理 D 状态进程”直接强制杀进程。不可中断睡眠的任务通常无法立即响应普通信号,强制重启或卸载设备可能造成数据损坏,应先确认设备、文件系统和业务恢复方案。

情况三:CPU 总体不高,但单个 CPU 的 si 很高

可能原因:

  • 网卡接收队列集中在一个 CPU;
  • RSS/RPS/XPS 配置不均;
  • 单个设备中断亲和性集中;
  • 高包速率或小包流量;
  • 某类定时器或 RCU 工作集中。

检查:

mpstat -P ALL 1
cat /proc/softirqs
cat /proc/interrupts
ip -s link
ethtool -S eth0

优化方向可能包括:

  • 调整网卡队列;
  • 检查 RSS 和中断亲和性;
  • 让 IRQ 与应用线程进行合理 CPU 分工;
  • 降低无意义的小包速率;
  • 调整批处理、队列和协议栈参数。

这些改变不是通用“调大参数”操作。错误地把 IRQ 和应用放到同一个过载 CPU 上,可能进一步恶化延迟;过度分散也可能增加跨 NUMA 访问和缓存失效。

情况四:上下文切换很多,但 CPU 利用率不高

可能原因:

  • 大量短生命周期线程;
  • 线程频繁等待锁;
  • 事件循环和工作线程之间频繁唤醒;
  • 短 I/O 操作;
  • 任务在多个 CPU 间迁移;
  • 定时器过于频繁。

先区分自愿和非自愿切换:

pidstat -w -p <PID> 1

如果自愿切换高,应检查:

  • 锁竞争;
  • futex 等待;
  • I/O;
  • 条件变量;
  • 线程之间的同步协议。

如果非自愿切换高,应检查:

  • 线程数是否远超可用 CPU;
  • 是否有高优先级任务;
  • CPU 是否受限于 cgroup;
  • 是否存在多个服务争抢同一组 CPU。

上下文切换高不是必然的性能问题。异步网络服务可能有合理的等待和唤醒;真正需要关注的是切换是否与延迟、吞吐或 CPU 消耗同时恶化。


十、使用 perf 建立调度证据

在允许使用 perf 且内核开放相应性能计数器时,可以执行:

perf stat -a -e context-switches,cpu-migrations,cpu-clock,task-clock sleep 5

可能输出:

   2,100,000      context-switches
      12,000      cpu-migrations
   ...

含义:

  • context-switches:上下文切换;
  • cpu-migrations:任务迁移;
  • task-clock:任务实际运行的 CPU 时间;
  • cpu-clock:CPU 时钟相关统计。

也可以针对一个进程:

perf stat -p 1234 -e context-switches,cpu-migrations,task-clock sleep 10

不同发行版可能限制 perf_event_paranoid,普通用户可能遇到权限错误。不要为了临时采样而在生产系统随意降低全局安全限制;应使用受控权限、临时策略和明确的恢复步骤。

进一步查看调度事件:

sudo perf sched record -a -- sleep 10
sudo perf sched latency

这可以帮助观察任务被唤醒后等待多久才运行。调度延迟比单纯的上下文切换次数更接近“线程是否及时获得 CPU”这个问题。

对于更细粒度分析,可以使用 ftrace、trace-cmd 或 eBPF 观察:

  • sched_switch
  • sched_wakeup
  • irq_handler_entry
  • softirq_entry
  • softirq_exit
  • 网络收包和 socket 唤醒路径。

不同发行版对这些工具的打包、权限和默认启用状态不同;生产采集应控制事件范围、采样时长和缓冲区大小,避免观测本身制造明显开销。


十一、几个容易混淆的边界

1. Load 高不等于 CPU 已经满

Load 统计中包含不可中断等待任务。必须同时查看:

vmstat 1
mpstat -P ALL 1
ps -eLo stat,wchan:32,comm

只有在 r 高且 CPU 接近饱和时,才更像 CPU 竞争。

2. CPU 利用率低不等于没有调度延迟

如果任务集中在一个 CPU、受到 CPU 亲和性限制,或者某个 CPU 被软中断占满,平均 CPU 利用率可能不高,但特定任务仍然排队严重。

应查看每 CPU 数据,而不是只看第一行总和。

3. sy 高不等于软中断高

内核时间可能来自:

  • 系统调用;
  • 文件系统;
  • 锁;
  • 内存管理;
  • 网络协议栈;
  • 中断处理;
  • 其他内核工作。

软中断通常在 si 中单独体现,但具体工具的百分比展示仍应结合 /proc/stat/proc/softirqs 验证。

4. 上下文切换高不等于调度器有 bug

高切换次数可能是设计结果:

  • 大量短请求;
  • 高频事件驱动;
  • 合理的 I/O 等待;
  • 多个线程之间的同步。

需要把切换次数与以下指标关联:

  • 请求延迟;
  • CPU 使用率;
  • 运行队列;
  • 锁等待;
  • 任务迁移;
  • cache miss;
  • I/O 延迟。

5. psPSR 不是 CPU 绑定信息

PSR 通常表示任务最近在哪个 CPU 上执行。它可能随后迁移到另一个 CPU。要查看允许的 CPU 集合,应使用:

taskset -pc 1234

或查看:

grep -E 'Cpus_allowed|Mems_allowed' /proc/1234/status

6. 虚拟机中的 Load 需要结合 steal time

虚拟机内看到:

Load 高
CPU 利用率表面上不高
st 高

可能是虚拟机 vCPU 被宿主机调度器延迟执行。此时来宾系统内部减少线程未必能解决问题,需要检查宿主机超售、CPU 争用、虚拟机 vCPU 配置和云平台指标。


十二、生产环境中的调度修改风险

1. 修改 nice 值

renice +5 -p 1234

这会降低进程的普通调度权重。权限不足时会失败;提高优先级通常需要更高权限。

风险包括:

  • 低优先级任务延迟增长;
  • 后台任务长期得不到 CPU;
  • 任务之间的相对公平性改变;
  • 业务延迟改善但吞吐下降。

2. 修改 CPU 亲和性

taskset -cp 2-5 1234

这会限制任务只能使用 CPU 2 到 5。

风险包括:

  • 可用 CPU 太少,运行队列增加;
  • 与 IRQ 或软中断争用同一 CPU;
  • 破坏 NUMA 局部性;
  • 任务迁移减少,但热点更集中;
  • 配置重启后丢失或被 systemd/cgroup 覆盖。

执行前应保存原始配置:

taskset -pc 1234

变更后观察:

mpstat -P ALL 1
pidstat -w -p 1234 1

3. 使用实时调度

例如:

chrt -f -p 80 1234

这类操作会显著改变调度优先级。实时线程如果不阻塞、不睡眠、也没有退出机制,可能长期占用 CPU,使普通任务无法运行。

恢复普通调度策略需要明确指定策略和参数,例如:

chrt -o -p 0 1234

具体操作前应确认目标进程、权限和发行版工具行为,避免误操作关键系统线程。

4. 调整 IRQ 亲和性和中断均衡

修改 /proc/irq/<IRQ>/smp_affinity_list 或停用自动 IRQ 均衡服务,会影响整台机器的设备处理路径。风险包括:

  • 网络中断集中到业务 CPU;
  • RPS/RSS 与 IRQ 配置相互冲突;
  • NUMA 跨节点访问增加;
  • 重启、热插拔或服务重启后配置失效;
  • 与内核自动负载均衡机制重复调节。

任何变更都应具备:

原配置记录
变更命令记录
业务指标对照
回滚命令
重启后的持久化方案

十三、一个实用的分析顺序

遇到“机器变慢、Load 升高、CPU 指标异常”时,可以按以下顺序建立证据:

第一步:确认时间尺度

uptime
cat /proc/loadavg

观察 1、5、15 分钟 Load 的方向:

  • 1 分钟高、15 分钟低:近期突发;
  • 15 分钟持续高:长期问题;
  • 1 分钟下降但 5 分钟仍高:负载正在恢复,但历史平均值尚未下降。

第二步:区分可运行与阻塞

vmstat 1

重点比较:

r 与 b
us、sy、si、wa、st

第三步:定位到 CPU

mpstat -P ALL 1

不要只看总 CPU 行。单核热点、软中断热点和 IRQ 热点都可能被总平均值掩盖。

第四步:定位到线程

pidstat -u -w -d -t 1
ps -eLo pid,tid,stat,pcpu,psr,wchan:32,comm --sort=-pcpu | head -n 30

-t 用于显示线程级数据。线程级观察对于多线程服务尤其重要。

第五步:如果怀疑 I/O,检查设备

iostat -xz 1

结合 D 状态任务和具体设备延迟,不要只看 %util 一个字段。

第六步:如果怀疑网络或软中断,检查中断路径

cat /proc/softirqs
cat /proc/interrupts
ip -s link
ethtool -S eth0
mpstat -I SUM 1

第七步:如果需要因果证据,记录调度事件

sudo perf sched record -a -- sleep 10
sudo perf sched latency

或使用 eBPF、ftrace 观察唤醒、切换、软中断和网络事件之间的时间关系。


结语

运行队列回答的是“有哪些任务等待或正在竞争 CPU”;上下文切换回答的是“CPU 如何从一个线程转到另一个线程”;Load Average 回答的是“可运行和不可中断等待任务在一段时间内有多少”;软中断回答的是“硬件事件后的内核工作如何延迟执行”。

四者必须联合解释:

Load 高 + r 高 + CPU 满
→ 更像 CPU 竞争

Load 高 + b 高 + CPU 不满
→ 更像 I/O 或不可中断内核等待

CPU 某核 si 高 + 网络中断集中
→ 更像软中断/IRQ 局部热点

上下文切换高 + 自愿切换高
→ 更像锁、I/O 或线程同步

上下文切换高 + 非自愿切换高
→ 更像 CPU 竞争、优先级或亲和性问题

真正可靠的性能判断,不是从某一个数字直接得出结论,而是把任务状态、运行队列、每 CPU 利用率、调度切换、I/O、IRQ 和软中断沿着同一条时间线关联起来。只有这样,Load 才不会被误读成 CPU 百分比,软中断也不会被误判为普通用户进程负载。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。