Linux 进程管理深度剖析(五):调度诊断与性能分析实战

进程调度是 Linux 内核中最复杂也最关键的子系统之一。在生产环境中,”CPU 使用率 100% 但响应很慢”、”任务唤醒后等了几十毫秒才运行”、”某个进程长期卡在 D 状态”——这些问题的根因往往深藏在调度层。本文是本系列第五篇,聚焦于调度诊断的完整方法论:从 /proc 接口读取原始数据,到 perf sched 分析调度延迟,再到 bpftrace 精确追踪内核路径,最后结合四个典型生产案例给出可落地的排查流程与修复建议。


一、进程状态与 /proc 接口

1.1 /proc/PID/status 完整字段解读

/proc/PID/status 是进程的”身份证”,包含 40+ 个字段,几乎涵盖进程所有关键属性。以一个 nginx worker 进程为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
$ cat /proc/$(pgrep -n nginx)/status
Name: nginx
Umask: 0022
State: S (sleeping)
Tgid: 12345
Ngid: 0
Pid: 12345
PPid: 12344
TracerPid: 0
Uid: 33 33 33 33
Gid: 33 33 33 33
FDSize: 256
Groups: 33
NStgid: 12345 1
NSpid: 12345 1
NSpgid: 12344 1
NSsid: 12344 1
Kthread: 0
VmPeak: 65432 kB
VmSize: 63108 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 18256 kB
VmRSS: 16384 kB
RssAnon: 9216 kB
RssFile: 7168 kB
RssShmem: 0 kB
VmData: 8192 kB
VmStk: 132 kB
VmExe: 1024 kB
VmLib: 6144 kB
VmPTE: 72 kB
VmSwap: 0 kB
HugetlbPages: 0 kB
CoreDumping: 0
THP_enabled: 1
Threads: 4
SigQ: 0/63693
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000040001000
SigCgt: 0000000198016eff
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
NoNewPrivs: 0
Seccomp: 0
Seccomp_filters: 0
Speculation_Store_Bypass: thread vulnerable
SpeculationIndirectBranch: conditional enabled
Cpus_allowed: ff
Cpus_allowed_list: 0-7
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000003
Mems_allowed_list: 0-1
voluntary_ctxt_switches: 18423
nonvoluntary_ctxt_switches: 342

关键字段含义对照表:

字段 含义 诊断价值
State 进程状态(R/S/D/Z/T/I) 判断进程是否阻塞
Tgid 线程组 ID(即主线程 PID) 区分线程和进程
NSpid 命名空间内的 PID 列表 容器场景下的 PID 映射
Threads 线程数 多线程程序监控
VmPeak 虚拟内存峰值 内存泄漏初判
VmRSS 实际物理内存使用量 内存压力分析
RssAnon 匿名页占用(堆、栈) 区分文件缓存与堆内存
RssFile 文件映射页占用 mmap 使用分析
VmSwap 被 swap 出去的内存量 内存不足告警
SigBlk 被屏蔽的信号掩码(十六进制) 信号处理问题排查
SigIgn 被忽略的信号掩码 信号处理问题排查
SigCgt 设置了 handler 的信号掩码 确认程序注册了哪些信号
CapEff 有效 capability 位掩码 权限排查,容器安全
CapBnd capability bounding set 进程最大权限上限
Cpus_allowed_list 允许运行的 CPU 核心列表 CPU 亲和性设置确认
Mems_allowed_list 允许访问的 NUMA 节点 NUMA 内存策略
voluntary_ctxt_switches 主动上下文切换总次数 切换频率异常诊断
nonvoluntary_ctxt_switches 被动上下文切换总次数 时间片用尽频率

状态字段详解

  • R(Running):正在 CPU 上运行或在运行队列中就绪
  • S(Sleeping):可中断睡眠,等待事件(如 IO、信号、锁)
  • D(Disk Sleep):不可中断睡眠,通常等待 IO 或内核锁,无法被信号杀死
  • Z(Zombie):已退出但父进程未调用 wait(),占用 PID
  • T(Stopped):被 SIGSTOP/SIGTSTP 暂停
  • I(Idle):内核线程的空闲状态(kernel 4.14+ 引入,区别于 D 状态)

1.2 /proc/PID/sched:CFS 调度统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ cat /proc/$(pgrep -n nginx)/sched
nginx (12345, #threads: 4)
-----------------------------------------------------------
se.exec_start : 4823719.123456
se.vruntime : 123456.789012
se.sum_exec_runtime : 45678.901234
se.nr_migrations : 1024
nr_switches : 19823
nr_voluntary_switches : 18423
nr_involuntary_switches : 1400
se.load.weight : 1048576
se.avg.load_sum : 7654321
se.avg.util_sum : 6543210
se.avg.load_avg : 256
se.avg.util_avg : 198
se.avg.last_update_time : 4823719.000000
se.avg.util_est : 210
policy : 0
prio : 120
clock-delta : 35
mm->numa_scan_seq : 12
numa_pages_migrated : 128
numa_preferred_nid : 0
total_numa_faults : 256

关键字段解读:

  • **se.sum_exec_runtime**:该进程的累计 CPU 运行时间(纳秒),单调递增
  • **se.vruntime**:CFS 虚拟运行时间,是调度决策的核心依据。vruntime 最小的任务优先被调度
  • **nr_voluntary_switches**:主动让出 CPU 的次数。进程调用 sleep()mutex_lock()read() 等阻塞时发生,对应内核路径 schedule()__schedule(SM_NONE)prev_state != 0,switch_count 指向 prev->nvcsw
  • **nr_involuntary_switches**:被抢占的次数。时间片用尽或高优先级任务唤醒时发生,对应 __schedule(SM_PREEMPT),switch_count 指向 prev->nivcsw
  • **se.nr_migrations**:任务在 CPU 之间迁移的次数,迁移过多说明负载不均衡或 CPU 亲和性约束太宽松
  • **policy**:调度策略(0=SCHED_NORMAL, 1=SCHED_FIFO, 2=SCHED_RR, 6=SCHED_DEADLINE)
  • **prio**:调度优先级(100-139 对应普通进程,nice -20~19 映射到此范围)

nr_involuntary_switches 偏高的诊断意义:如果该值持续快速增长(通过两次采样相减),说明进程不断被抢占,可能是时间片太短(sched_min_granularity_ns 过小)或同 CPU 上存在更高优先级的竞争者。

1.3 /proc/PID/schedstat:运行时间与等待时间

1
2
$ cat /proc/$(pgrep -n nginx)/schedstat
45678901234 12345678901 19823

三个数字含义:

  1. 运行时间(nanoseconds):进程实际在 CPU 上执行的累计时间,对应 se.sum_exec_runtime
  2. 等待时间(nanoseconds):进程在运行队列中就绪但等待 CPU 的累计时间(即调度延迟之和)
  3. 切换次数:总上下文切换次数

等待时间与运行时间的比值是调度延迟的重要指标。如果等待时间远大于运行时间,说明系统 CPU 资源争用严重,进程长期”就绪但得不到 CPU”。

1.4 /proc/schedstat:全局调度统计

1
2
3
4
5
6
7
8
$ cat /proc/schedstat
version 15
timestamp 4823751234
cpu0 0 0 0 0 0 0 45678901 23456789 18234
cpu1 0 0 0 0 0 0 43215678 21987654 17456
cpu2 0 0 0 0 0 0 41234567 19876543 16789
cpu3 0 0 0 0 0 0 44321098 22345678 17923
domain0 cpu0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

每行 cpuN 后面的字段(version 15 格式):

  • 字段 7:运行队列总运行时间(ns)
  • 字段 8:运行队列总等待时间(ns)
  • 字段 9:该 CPU 的总上下文切换次数

通过对比各 CPU 的切换次数,可以快速发现负载不均衡的情况。


二、上下文切换分析

2.1 主动切换 vs 被动切换的内核路径差异

理解两种切换的本质区别,是诊断调度问题的基础。

主动切换(Voluntary Context Switch)的内核路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
进程调用阻塞操作


mutex_lock() / wait_event() / read() 等


set_current_state(TASK_INTERRUPTIBLE) 或 TASK_UNINTERRUPTIBLE


schedule()


__schedule(SM_NONE) ← sched_mode = 0,不是抢占

├── prev_state = READ_ONCE(prev->__state) // 非 0,进程确实要睡眠
├── switch_count = &prev->nvcsw // 指向主动切换计数器
├── deactivate_task(rq, prev, DEQUEUE_SLEEP)
└── pick_next_task() → context_switch()

被动切换(Involuntary Context Switch)的内核路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
定时器中断触发


scheduler_tick() ← 每 HZ 次/秒调用


curr->sched_class->task_tick() ← CFS: task_tick_fair()


entity_tick() → check_preempt_tick()

▼ 时间片用尽
resched_curr(rq) ← 设置 TIF_NEED_RESCHED 标志

▼ 中断返回时检测
preempt_schedule_irq() ← 从 IRQ 上下文返回时触发


__schedule(SM_PREEMPT) ← sched_mode = SM_PREEMPT

├── prev_state = ... (不关心,因为是抢占)
├── switch_count = &prev->nivcsw // 指向被动切换计数器
└── pick_next_task() → context_switch()

核心代码(kernel/sched/core.c:6593):

1
2
3
4
5
6
7
8
switch_count = &prev->nivcsw;    // 默认:被动切换

prev_state = READ_ONCE(prev->__state);
if (!(sched_mode & SM_MASK_PREEMPT) && prev_state) {
// 进程主动睡眠(非抢占路径且 state != RUNNING)
...
switch_count = &prev->nvcsw; // 改为:主动切换
}

2.2 过多上下文切换的常见原因

原因 表现 主要类型
互斥锁竞争激烈 nvcsw 快速增长 主动切换
大量 IO 操作(磁盘/网络) nvcsw 快速增长 主动切换
时间片过短 nivcsw 快速增长 被动切换
高优先级任务频繁唤醒 nivcsw 快速增长 被动切换
线程池线程数过多 两者都增长 混合
cgroup CPU 配额被限流 nivcsw 增长 + 被动切换 被动切换

2.3 诊断命令

**vmstat 1**:查看系统级切换速率

1
2
3
4
5
6
7
8
$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 3421568 98304 2048000 0 0 0 32 412 856 8 3 89 0 0
3 0 0 3419200 98304 2048128 0 0 0 0 523 1823 18 7 75 0 0
4 0 0 3416832 98304 2048256 0 0 0 0 618 3421 24 9 67 0 0
3 0 0 3418496 98304 2048128 0 0 0 0 589 2987 21 8 71 0 0
2 0 0 3420160 98304 2048000 0 0 0 0 445 1234 15 5 80 0 0
  • r:运行队列长度,持续 > CPU 核数说明 CPU 饱和
  • cs:每秒上下文切换次数。正常 Web 服务器通常在 1000-5000/s,若超过 50000/s 需关注
  • in:每秒中断次数

**pidstat -w 1**:按进程查看切换速率

1
2
3
4
5
6
7
8
9
10
$ pidstat -w -p 12345 1 5
Linux 6.4.0 (prod-host) 04/14/2026 _x86_64_ (8 CPU)

15:52:01 UID PID cswch/s nvcswch/s Command
15:52:02 33 12345 423.00 8.00 nginx
15:52:03 33 12345 456.00 12.00 nginx
15:52:04 33 12345 389.00 6.00 nginx
15:52:05 33 12345 412.00 9.00 nginx
15:52:06 33 12345 401.00 7.00 nginx
Average: 33 12345 416.20 8.40 nginx
  • cswch/s:每秒主动切换次数(对应 nvcsw
  • nvcswch/s:每秒被动切换次数(对应 nivcsw

nginx 的 cswch/s 约 400/s 属于正常范围(每次 epoll 等待唤醒都是一次主动切换)。若 nvcswch/s 突然上升,说明时间片频繁用尽。

**perf stat**:精确统计硬件事件

1
2
3
4
5
6
7
8
9
10
11
$ perf stat -e context-switches,cpu-migrations,instructions,cycles \
-p 12345 -- sleep 10

Performance counter stats for process id '12345':

4,156 context-switches # 415.6 /sec
38 cpu-migrations # 3.8 /sec
8,234,567,890 instructions # 2.45 insn per cycle
3,361,456,123 cycles # 336.1 MHz

10.001234567 seconds time elapsed

cpu-migrations 过高(正常应极低)说明进程在 CPU 间频繁迁移,可能导致 cache miss 增加。


三、CPU 亲和性与 NUMA 调度

3.1 sched_setaffinity 内核路径

taskset 命令通过 sched_setaffinity(2) 系统调用设置进程的 CPU 亲和性掩码,内核路径如下(kernel/sched/core.c:8318):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
long sched_setaffinity(pid_t pid, const struct cpumask *in_mask)
{
struct affinity_context ac;
struct task_struct *p;
int retval;

p = find_process_by_pid(pid);
...
// 权限检查:需要 CAP_SYS_NICE 或同 owner
if (!check_same_owner(p)) {
if (!ns_capable(__task_cred(p)->user_ns, CAP_SYS_NICE))
return -EPERM;
}

ac = (struct affinity_context){
.new_mask = cpus_allowed,
.flags = SCA_USER,
};
retval = __sched_setaffinity(p, &ac);
// 内部调用 __set_cpus_allowed_ptr() 更新 p->cpus_mask
}

taskset 命令实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看进程当前亲和性
$ taskset -cp 12345
pid 12345's current affinity list: 0-7

# 将进程绑定到 CPU 0,1
$ taskset -cp 0,1 12345
pid 12345's current affinity list: 0-7
pid 12345's new affinity list: 0,1

# 启动时绑定
$ taskset -c 2,3 nginx -g "daemon off;"

# 使用掩码格式(十六进制位图)
$ taskset 0x0f nginx # 绑定到 CPU 0-3(低4位)

何时需要 CPU 亲和性

  • 实时任务隔离:将 RT 任务绑定到独立 CPU,避免被普通任务干扰
  • L3 Cache 局部性:将相互通信频繁的线程绑定到同一物理核(SMT 兄弟核)
  • 中断隔离:将 irqbalance 禁用,手动将网卡中断和应用线程绑定到同侧 NUMA 节点

3.2 NUMA 拓扑感知调度

在多路服务器上,NUMA 架构对性能影响巨大。跨 NUMA 节点的内存访问延迟是本地访问的 1.5-3 倍。

查看 NUMA 拓扑

1
2
3
4
5
6
7
8
9
10
11
12
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 64350 MB
node 0 free: 31204 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 64503 MB
node 1 free: 29876 MB
node distances:
node 0 1
0: 10 21
1: 21 10

内核 NUMA 调度task_numa_placement() 周期性分析任务的内存访问模式,将任务迁移到内存所在的 NUMA 节点。可通过 /proc/PID/sched 中的 numa_preferred_nid 查看内核为任务选择的首选节点。

numactl 实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 将进程绑定到 NUMA 节点 0 的 CPU 和内存
$ numactl --cpunodebind=0 --membind=0 ./myapp

# 内存交错分配(适合多线程均匀访问)
$ numactl --interleave=all ./myapp

# 查看进程的 NUMA 内存分布
$ numastat -p 12345
Per-node process memory usage (in MBs) for PID 12345 (nginx)
Node 0 Node 1 Total
--------------- --------------- ---------------
Huge 0.00 0.00 0.00
Heap 12.34 0.45 12.79
Stack 0.06 0.00 0.06
Private 34.56 1.23 35.79
---------------- --------------- --------------- ---------------
Total 46.96 1.68 48.64

如果 Node 1 的内存占用异常高而进程运行在 Node 0 的 CPU 上,这是 NUMA 不均衡的典型症状,需要重新绑定或启用 numa_balancing


四、实时进程与优先级

4.1 调度策略对比

策略 特点 适用场景
SCHED_OTHERSCHED_NORMAL 0 CFS 公平调度,nice -20~19 普通进程
SCHED_FIFO 1 实时,先进先出,无时间片 实时控制、音频
SCHED_RR 2 实时,轮转,有时间片 实时且需公平
SCHED_BATCH 3 批处理,降低调度频率 后台计算任务
SCHED_IDLE 5 极低优先级 后台维护任务
SCHED_DEADLINE 6 EDF 最早截止优先 硬实时任务

chrt 命令设置实时优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看进程调度策略
$ chrt -p 12345
pid 12345's current scheduling policy: SCHED_OTHER
pid 12345's current scheduling priority: 0

# 设置为 SCHED_FIFO,优先级 50(1-99)
$ chrt -f -p 50 12345

# 设置为 SCHED_RR
$ chrt -r -p 30 12345

# 以 SCHED_FIFO 启动新进程
$ chrt -f 50 ./realtime_app

# 恢复为普通调度
$ chrt -o -p 0 12345

优先级数值说明:实时优先级 1-99(越高越优先)映射到内核内部优先级 99-1(RT 进程占用 0-99,普通进程占用 100-139)。SCHED_FIFO 优先级 99 的进程可以完全饿死所有普通进程。

4.2 SCHED_DEADLINE:三参数 EDF 调度

SCHED_DEADLINE 实现了 EDF(Earliest Deadline First)算法,是目前 Linux 中最严格的实时保证:

1
2
3
4
5
6
7
# 设置 SCHED_DEADLINE:runtime=5ms,deadline=10ms,period=10ms
# 含义:每 10ms 周期内,最多运行 5ms,必须在 10ms 内完成
$ chrt --deadline \
--sched-runtime 5000000 \
--sched-deadline 10000000 \
--sched-period 10000000 \
0 ./deadline_app

三个参数含义:

  • runtime:每个周期内允许使用的 CPU 时间(纳秒),类似 CPU 配额
  • deadline:任务必须完成的时间点(相对于激活时刻),不得超出 period
  • period:任务的激活周期

内核通过 CBS(Constant Bandwidth Server)算法保证每个 DEADLINE 任务不超过其 runtime/period 的带宽比率。

4.3 优先级反转与 PI Mutex

优先级反转场景

1
2
3
高优先级任务 H(FIFO-80)  ────────等待锁──────────────────────────运行
中优先级任务 M(FIFO-50) ────────────占用 CPU(抢占了 L)─────────
低优先级任务 L(FIFO-10) ──持有锁────被 M 抢占──────────────释放锁

H 等待 L 持有的锁,但 L 被中优先级的 M 抢占,导致 H 实际上被 M 阻塞——这违反了优先级调度的语义。

PI Mutex 解决方案:使用 pthread_mutexattr_setprotocol(PTHREAD_PRIO_INHERIT) 或内核的 rt_mutex,当高优先级任务等待锁时,临时将锁持有者的优先级提升至等待者级别:

1
2
3
4
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&mutex, &attr);

4.4 IRQ 线程化

将中断处理程序线程化可以让 RT 进程控制中断处理的优先级:

1
2
3
4
5
6
7
8
# 查看线程化 IRQ 线程
$ ps -eLo pid,tid,cls,rtprio,comm | grep irq
123 123 FF 50 irq/24-eth0
124 124 FF 50 irq/25-nvme0
125 125 FF 49 irq/26-xhci_hcd

# 调整 IRQ 线程优先级
$ chrt -f -p 70 $(pgrep "irq/24-eth0")

五、四大实战案例

案例一:CPU 使用率 100% 但响应慢

症状top 显示某进程 CPU 100%,但 API 响应时间 P99 从 50ms 涨到 500ms。

第一步:用 top/htop 定位高 CPU 进程

1
2
3
4
$ top -b -n 1 -o %CPU | head -20
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
23456 app 20 0 4096m 512m 32m R 99.8 0.8 45:23.12 myapp
12345 nginx 20 0 65m 16m 8m S 2.3 0.0 1:02.34 nginx

第二步:用 perf top 找热点函数

1
2
3
4
5
6
7
8
$ perf top -p 23456 --call-graph dwarf
Samples: 45K of event 'cycles', 4000 Hz, Event count (approx.): 12345678901
Overhead Shared Obj Symbol
34.12% myapp [.] process_request
18.56% myapp [.] json_parse
12.34% libc-2.35.so [.] malloc
8.91% myapp [.] hash_lookup
6.23% [kernel] [k] copy_user_enhanced_fast_string

热点集中在 process_requestjson_parse,且 malloc 占比异常高(12%),初步怀疑内存分配频繁。

第三步:perf record 采集火焰图数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ perf record -F 999 -g --call-graph dwarf -p 23456 -- sleep 30
[ perf record: Woken up 45 times to write data ]
[ perf record: Captured and wrote 234.567 MB perf.data (123456 samples) ]

$ perf report --stdio --no-children | head -40
# To display the perf.data header info, please use --header/--header-only options.
# Samples: 123K of event 'cycles:ppp'
# Event count (approx.): 45678901234
#
# Overhead Command Shared Object Symbol
34.12% myapp myapp [.] process_request
|
|--89.23%-- main
| event_loop
| handle_connection
| process_request
| |
| |--67.12%-- json_parse
| | parse_string
| | malloc ← 在 json_parse 内部大量分配
| |
| |--32.88%-- hash_lookup

第四步:bpftrace 用户态 CPU 采样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ bpftrace -e '
profile:hz:99
/pid == 23456/
{
@[ustack(perf)] = count();
}
interval:s:30 { exit(); }' 2>/dev/null | head -40

@[
process_request+0x234
json_parse+0x89
parse_string+0x45
malloc+0x12
]: 4523

@[
process_request+0x312
hash_lookup+0x67
strcmp+0x8
]: 1234

第五步:区分 user 态 vs sys 态 CPU

1
2
3
4
$ pidstat -u -d -p 23456 1 5
15:52:01 UID PID %usr %system %guest %wait %CPU CPU Command
15:52:02 1000 23456 95.00 4.00 0.00 0.00 99.00 2 myapp
15:52:03 1000 23456 96.00 3.00 0.00 0.00 99.00 2 myapp

%usr 高达 95%,%system 仅 4%,确认是用户态计算密集。

根因分析json_parse 每次请求都用 malloc/free 分配小块内存,触发频繁的内存分配器竞争(ptmalloc 的 arena 锁)。

修复建议

  1. 引入对象池或内存竞技场(arena allocator),复用 JSON 解析缓冲区
  2. 使用 jemalloctcmalloc 替换 glibc malloc,减少线程间锁竞争
  3. 考虑 SIMD 加速的 JSON 解析器(如 simdjson

案例二:调度延迟高(任务等待 CPU 时间长)

症状:服务 P99 延迟高,但 CPU 使用率只有 40%,理论上 CPU 资源充足。

第一步:perf sched 采集调度事件

1
2
3
$ perf sched record -a -- sleep 10
[ perf record: Woken up 234 times to write data ]
[ perf record: Captured and wrote 1234.567 MB perf.data ]

第二步:perf sched latency 查看延迟分布

1
2
3
4
5
6
7
8
9
10
$ perf sched latency --sort max

-----------------------------------------------------------------------------------------------------------------
Task | Runtime ms | Switches | Average delay ms | Maximum delay ms | Maximum delay at |
-----------------------------------------------------------------------------------------------------------------
myapp:23456 | 4523.456 | 12345 | 0.234 | 45.678 | 15:52:03.456789012 |
nginx:12345 | 1234.567 | 8901 | 0.089 | 8.901 | 15:52:05.123456789 |
kworker/2:1:456 | 123.456 | 2345 | 0.012 | 1.234 | 15:52:07.234567890 |
-----------------------------------------------------------------------------------------------------------------
TOTAL: | 6123.456 | 23456 |

myapp 的最大调度延迟达到 45.678ms,而平均延迟只有 0.234ms,说明是偶发性的长尾延迟。

第三步:perf sched timehist 详细时间线

1
2
3
4
5
6
7
8
9
$ perf sched timehist -p 23456 | head -30
time cpu task name wait time sch delay run time
[tid/pid] (msec) (msec) (msec)
------------ ------ ------------ --------- --------- ---------
15:52:03.400234567 [002] myapp[23456] 0.012 0.089 2.345
15:52:03.403234567 [002] myapp[23456] 0.023 0.123 3.456
15:52:03.456123456 [002] myapp[23456] 0.456 45.234 2.123
^^^^^^^
这次等了 45ms 才得到 CPU

第四步:bpftrace 追踪高延迟唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ bpftrace -e '
tracepoint:sched:sched_wakeup
/args->pid == 23456/
{
@wakeup_ts[args->pid] = nsecs;
}

tracepoint:sched:sched_switch
/args->next_pid == 23456/
{
$ts = @wakeup_ts[args->next_pid];
if ($ts > 0) {
$delta = nsecs - $ts;
if ($delta > 5000000) {
printf("HIGH SCHED LATENCY: pid=%d comm=%s delay=%dms at %lld\n",
args->next_pid, args->next_comm,
$delta / 1000000, nsecs);
}
delete(@wakeup_ts[args->next_pid]);
}
}'

HIGH SCHED LATENCY: pid=23456 comm=myapp delay=45ms at 4823756123456789
HIGH SCHED LATENCY: pid=23456 comm=myapp delay=23ms at 4823761234567890

第五步:检查 cgroup CPU throttling

1
2
3
4
5
6
7
8
9
10
11
12
$ cat /sys/fs/cgroup/cpu/myapp/cpu.stat
nr_periods 12345
nr_throttled 2345
throttled_time 45678901234 ← 被限流的总时间(ns): 约 45.7 秒
throttled_usec 45678901 ← 等效微秒

# 查看 CPU 配额设置
$ cat /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us
200000 ← 每 period 200ms
$ cat /sys/fs/cgroup/cpu/myapp/cpu.cfs_period_us
100000 ← period 100ms
# 意味着最多使用 2 个 CPU,但机器有 8 核

nr_throttled / nr_periods ≈ 19% 的限流率——接近 20% 的周期被限流,与偶发性长尾延迟完全吻合。

第六步:检查 CPU 频率状态

1
2
3
4
5
6
7
8
9
10
11
12
$ cpupower frequency-info -c 2
analyzing CPU 2:
driver: intel_pstate
CPUs which run at the same hardware frequency: 2
CPUs which need to have their frequency coordinated by software: 2
maximum transition latency: Cannot determine or is not supported.
hardware limits: 1000 MHz - 3800 MHz
available cpufreq governors: performance powersave
current policy: frequency should be within 1000 MHz and 3800 MHz.
The governor "powersave" may decide which speed to use
current CPU frequency: 1400 MHz (asserted by call to hardware)
boost state support: supported - currently enabled

CPU 频率只有 1.4GHz(最高 3.8GHz),powersave 调速器在低负载时降频,导致偶发任务获得 CPU 后实际执行速度很慢。

根因分析:双重问题:(1) cgroup CPU quota 设置偏紧,19% 的周期被限流;(2) CPU 调速器为 powersave,频率未及时提升。

修复建议

  1. 调高 cgroup cpu.cfs_quota_us,或改用 cpu.weight 的相对权重调度
  2. 将调速器改为 performanceschedutilcpupower frequency-set -g schedutil
  3. 开启 CPU boost:echo 1 > /sys/devices/system/cpu/cpufreq/boost

案例三:进程 D 状态(不可中断睡眠)

症状ps 显示某进程长期处于 D 状态,无法被 kill,系统 load average 居高不下。

第一步:找 D 状态进程

1
2
3
4
5
6
7
8
9
$ ps aux | awk '$8 == "D" {print}'
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 5678 0.0 0.0 0 0 ? D 10:23 0:05 [kworker/2:1]
app 34567 0.5 0.2 256000 32000 ? D 10:45 0:12 myapp

# 用 ps 的状态过滤(更可靠)
$ ps -eo pid,stat,wchan,comm | grep "^[0-9]* D"
34567 D nfs_wait_on_req myapp
5678 D blk_mq_get_tag kworker/2:1

第二步:查看等待的内核函数

1
2
3
4
5
6
$ cat /proc/34567/wchan
nfs_wait_on_request

# 或用更详细的格式
$ cat /proc/34567/status | grep State
State: D (disk sleep)

wchan 显示 nfs_wait_on_request,说明进程在等待 NFS 服务器响应。

第三步:查看完整内核调用栈

1
2
3
4
5
6
7
8
9
10
$ cat /proc/34567/stack
[<0>] nfs_wait_on_request+0x2e/0x40 [nfs]
[<0>] nfs_updatepage+0x1e6/0x2a0 [nfs]
[<0>] nfs_write_begin+0x1a4/0x1f0 [nfs]
[<0>] generic_perform_write+0x12a/0x1e0
[<0>] nfs_file_write+0x113/0x1d0 [nfs]
[<0>] vfs_write+0xcb/0x200
[<0>] ksys_write+0x5f/0xe0
[<0>] do_syscall_64+0x3d/0x80
[<0>] entry_SYSCALL_64_after_hwframe+0x46/0xb0

完整调用栈确认:write() 系统调用 → NFS 文件写入 → 等待 NFS 服务器确认。

第四步:bpftrace 统计 D 状态热点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ bpftrace -e '
kprobe:schedule
{
$task = (struct task_struct *)curtask;
if ($task->__state == 2) {
@d_state_stacks[kstack(10)] = count();
}
}
interval:s:10 { print(@d_state_stacks); exit(); }'

@d_state_stacks[
schedule+0x1
nfs_wait_on_request+0x2e
nfs_updatepage+0x1e6
nfs_write_begin+0x1a4
generic_perform_write+0x12a
]: 4523

@d_state_stacks[
schedule+0x1
blk_mq_get_tag+0x89
blk_mq_submit_bio+0x234
__submit_bio+0x67
]: 1234

第五步:验证 NFS 服务器状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看 NFS 挂载点
$ mount | grep nfs
nfs-server:/data on /mnt/nfs type nfs4 (rw,relatime,...)

# 检查 NFS 统计
$ nfsstat -c
Client rpc stats:
calls retrans authrefrsh
1234567 45678 12345 ← retrans 高说明网络不稳定

# 检查 NFS 服务器响应时间
$ nfsiostat 1
op/s rpc bklog kB/s kB/op retrans avg RTT (ms) avg exe (ms)
read: 123.4 0 987.6 8.0 0 2.3 2.5
write: 89.1 0 712.3 8.0 23 345.6 346.8
^^ ^^^^^
重传23次,平均响应346ms(正常应<10ms)

根因分析:NFS 服务器响应超时(346ms 远超正常),write 操作触发大量重传,导致进程长期卡在 nfs_wait_on_request 的 D 状态。

修复建议

  1. 检查 NFS 服务器负载和网络链路质量(pingtraceroute
  2. 挂载时加 softerrsoft,timeo=30 选项,让 NFS 请求超时后返回错误而非无限等待
  3. 考虑迁移到本地存储或更可靠的分布式存储(如 Ceph、GlusterFS)
  4. 短期应急:umount -f -l /mnt/nfs(强制懒卸载)可以解除 D 状态进程的阻塞

案例四:fork 炸弹 / 进程数过多

症状:系统响应极慢,ps aux 命令本身就需要几秒才能返回,内存占用异常。

第一步:确认进程数情况

1
2
3
4
5
6
7
8
9
10
11
12
$ ps aux | wc -l
32769

$ cat /proc/sys/kernel/pid_max
32768

$ cat /proc/sys/kernel/threads-max
63693

# 已用 PID 数
$ ls /proc | grep '^[0-9]' | wc -l
32234

进程数已经逼近 pid_max 上限(32768),新进程无法 fork。

第二步:pstree 找进程树根

1
2
3
4
5
6
7
$ pstree -a -p | grep -A 5 "bash"
bash(1234)
└─python(2345)
├─python(3456)
│ ├─python(4567)
│ │ └─python(5678)
│ ...(递归 fork)

第三步:找 fork 最多的父进程

1
2
3
4
5
6
7
8
9
# 按 PPID 统计子进程数
$ ps -eo ppid | sort | uniq -c | sort -rn | head -10
28000 2345 ← PID 2345 有 28000 个子进程!
123 1234
45 1

$ ps -p 2345 -o pid,ppid,cmd
PID PPID CMD
2345 1234 python /app/worker.py

第四步:用 bpftrace 实时追踪 fork

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ bpftrace -e '
tracepoint:sched:sched_process_fork
{
@forks[comm] = count();
printf("fork: parent=%s(%d) -> child(%d)\n",
comm, args->parent_pid, args->child_pid);
}
interval:s:1 {
print(@forks);
clear(@forks);
}'

fork: parent=python(2345) -> child(32456)
fork: parent=python(2345) -> child(32457)
fork: parent=python(2345) -> child(32458)
...

@forks[python]: 1234 ← 每秒 fork 1234 次!

第五步:cgroup pids.max 限制

1
2
3
4
5
6
7
8
9
10
# 查看当前 cgroup 的进程数限制
$ cat /sys/fs/cgroup/pids/myapp/pids.max
max ← 无限制!

# 设置限制为 1000 个进程
$ echo 1000 > /sys/fs/cgroup/pids/myapp/pids.max

# 查看当前用量
$ cat /sys/fs/cgroup/pids/myapp/pids.current
987

第六步:用户级进程数限制

1
2
3
4
5
6
7
8
9
10
11
12
# 查看当前用户限制
$ ulimit -u
63693

# 为特定用户设置限制(/etc/security/limits.conf)
$ cat /etc/security/limits.conf
app hard nproc 2048
app soft nproc 1024

# 验证生效
$ su - app -c "ulimit -u"
1024

根因分析python worker.py 中存在递归 fork 炸弹 bug(未限制子进程数量),每次任务处理都 fork 新进程而不是复用进程池,导致进程数指数级增长。

修复建议

  1. 立即处置:kill -STOP 2345(暂停父进程)然后 kill -9 -2345(杀死整个进程组)
  2. 设置 cgroup pids.max 作为安全阀(推荐在容器/Pod 中默认配置)
  3. 代码层面:使用进程池(multiprocessing.Pool)而非无限制 fork
  4. 设置系统级 pid_max 适当降低,防止单个用户耗尽所有 PID

六、perf 工具全面使用指南

6.1 perf stat:硬件计数器统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ perf stat -e \
instructions,cycles,\
cache-references,cache-misses,\
branch-instructions,branch-misses,\
context-switches,cpu-migrations,\
page-faults \
-p 23456 -- sleep 10

Performance counter stats for process id '23456':

23,456,789,012 instructions # 2.45 insn per cycle
9,573,363,474 cycles # 957.3 MHz
456,789,012 cache-references # 45.7 M/sec
45,678,901 cache-misses # 10.00% of all cache refs
5,678,901,234 branch-instructions # 567.9 M/sec
56,789,012 branch-misses # 1.00% of all branches
4,156 context-switches # 415.6 /sec
38 cpu-migrations # 3.8 /sec
1,234 page-faults # 123.4 /sec

10.001234567 seconds time elapsed

关键指标解读

  • insn per cycle (IPC):2.45 是好的指标(现代 CPU 理论值 3-4),若低于 1 说明大量内存等待
  • cache-misses %:10% 偏高(正常应 < 1%),说明数据访问模式不友好
  • branch-misses %:1% 正常
  • page-faults:异常高说明内存不足或 mmap 访问新页面频繁

6.2 perf top:实时热点函数

1
2
3
4
5
6
7
8
9
10
11
# 显示所有进程的热点,带调用图
$ perf top -g --call-graph dwarf

# 只看特定进程
$ perf top -p 23456 --call-graph lbr

# 显示内核符号(需要 kernel debuginfo)
$ perf top -K

# 按事件类型(如 cache miss)采样
$ perf top -e cache-misses -p 23456

6.3 perf record + perf report:离线火焰图

1
2
3
4
5
6
7
8
9
10
11
12
13
# 采集带调用栈的 CPU 性能数据(30秒,999Hz)
$ perf record -F 999 -g --call-graph dwarf \
-p 23456 -- sleep 30

# 生成文本报告
$ perf report --stdio --no-children --percent-limit 1

# 生成火焰图(需要 FlameGraph 工具)
$ perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg

# 包含内核态调用栈
$ perf record -F 999 -g -a --call-graph dwarf -- sleep 30
$ perf report --sort comm,dso,symbol

6.4 perf sched:调度分析套件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 采集调度事件(需要 root)
$ perf sched record -a -- sleep 10

# 延迟分布(按最大延迟排序)
$ perf sched latency --sort max

# 详细调度时间线(过滤特定进程)
$ perf sched timehist -p 23456

# 调度地图(可视化各 CPU 的任务分布)
$ perf sched map

# 统计调度摘要
$ perf sched summary

6.5 perf lock:锁竞争分析

1
2
3
4
5
6
7
8
# 采集锁竞争事件
$ perf lock record -p 23456 -- sleep 10

# 报告锁竞争热点
$ perf lock report
Name acquired contended total wait (ms) avg wait (ms) max wait (ms)
pthread_mutex_lock 89234 12345 456.789 0.037 12.345
futex_wait_queue 4567 789 89.012 0.113 5.678

6.6 perf mem:内存访问分析

1
2
3
4
# 采集内存访问延迟(需要硬件支持 PEBS/LBR)
$ perf mem record -p 23456 -- sleep 10
$ perf mem report
# 显示 NUMA 本地/远程访问分布,L1/L2/L3/DRAM 命中率

6.7 perf ftrace:内核函数追踪

1
2
3
4
5
# 追踪特定内核函数(类似 ftrace,但集成在 perf 中)
$ perf ftrace -G sched_setaffinity -p 23456

# 函数调用延迟分析
$ perf ftrace latency -T do_sys_open -- ls /proc

七、进程诊断工具速查表

工具 用途 核心命令示例
top / htop 实时进程资源使用 top -b -n 1 -o %CPU
ps 进程快照与状态过滤 ps -eo pid,stat,wchan,comm,pcpu --sort=-pcpu
pidstat 进程级 CPU/IO/内存统计(历史) pidstat -u -d -w -p PID 1 10
vmstat 系统级 CPU/内存/IO/切换统计 vmstat 1 60
perf stat 硬件性能计数器采样 perf stat -e cycles,instructions -p PID sleep 10
perf top 实时热点函数 perf top -p PID --call-graph dwarf
perf record/report 离线 CPU 性能分析 + 火焰图 perf record -F 999 -g -p PID sleep 30
perf sched 调度延迟与时间线分析 perf sched record -a -- sleep 10 && perf sched latency
perf lock 内核/用户态锁竞争分析 perf lock record -p PID sleep 10
bpftrace 动态内核追踪,自定义脚本 bpftrace -e 'profile:hz:99 /pid==X/ { @[ustack]=count(); }'
strace 系统调用追踪(有性能开销) strace -T -tt -e trace=sched_yield,futex -p PID
taskset CPU 亲和性查看与设置 taskset -cp PID / taskset -c 0,1 ./app
chrt 调度策略与实时优先级设置 chrt -p PID / chrt -f -p 50 PID
numactl NUMA 绑定与内存策略 numactl --cpunodebind=0 --membind=0 ./app
numastat NUMA 内存使用统计 numastat -p PID
cpupower CPU 频率与调速器管理 cpupower frequency-info / cpupower frequency-set -g performance
tuna 综合线程/IRQ 调优工具 tuna --show_threads / tuna -t myapp -p FIFO:50
lscpu CPU 拓扑(核数/NUMA/缓存) lscpu --extended
stress-ng 调度压力测试 stress-ng --cpu 4 --sched fifo --sched-prio 50 -t 60

八、内核调度参数调优

8.1 CFS 核心参数

1
2
3
4
5
6
7
8
# 查看当前值
$ sysctl -a | grep sched_
kernel.sched_latency_ns = 24000000
kernel.sched_min_granularity_ns = 3000000
kernel.sched_migration_cost_ns = 500000
kernel.sched_nr_migrate = 32
kernel.sched_wakeup_granularity_ns = 4000000
kernel.sched_child_runs_first = 0

sched_latency_ns(调度延迟周期,默认 24ms)

CFS 保证在这个时间窗口内,每个可运行进程都能得到至少一次运行机会。窗口内的 CPU 时间按进程权重比例分配。

  • 降低此值:减少调度延迟,提升交互响应性,但增加上下文切换频率
  • 提升此值:适合批处理场景,减少切换开销
1
2
3
4
5
# 交互式场景(如桌面)
$ sysctl -w kernel.sched_latency_ns=6000000 # 6ms

# 批处理/HPC 场景
$ sysctl -w kernel.sched_latency_ns=48000000 # 48ms

sched_min_granularity_ns(最小运行时间粒度,默认 3ms)

单个进程一次调度后,至少运行这么长时间才可被抢占。防止进程刚被调度就立刻被抢走。

  • 当进程数 N 超过 sched_latency_ns / sched_min_granularity_ns 时,实际调度周期延长为 N * sched_min_granularity_ns
1
2
# 高并发(数百线程)服务,避免切换风暴
$ sysctl -w kernel.sched_min_granularity_ns=10000000 # 10ms

sched_migration_cost_ns(任务迁移代价阈值,默认 500μs)

当某 CPU 上的进程最近执行时间 < 此值时,调度器认为它的 Cache 还”热”,避免迁移到其他 CPU。

  • 增大此值:减少任务迁移(减少 Cache 污染),适合 Cache 敏感的延迟任务
  • 减小此值:允许更积极的负载均衡,适合吞吐优先场景
1
$ sysctl -w kernel.sched_migration_cost_ns=5000000    # 5ms,减少迁移

8.2 CONFIG_HZ 对延迟的影响

CONFIG_HZ 决定内核时钟中断频率,直接影响调度粒度:

CONFIG_HZ 时钟间隔 适用场景 调度延迟下限
100 Hz 10ms 服务器,低中断开销 ~10ms
250 Hz 4ms 通用桌面/服务器(默认) ~4ms
1000 Hz 1ms 低延迟桌面,实时应用 ~1ms
无滴答(NO_HZ_FULL 动态 HPC,消除抖动 无定时中断
1
2
3
4
5
6
7
# 查看当前内核的 HZ 配置
$ grep "^CONFIG_HZ=" /boot/config-$(uname -r)
CONFIG_HZ=250

# 运行时查看实际 HZ
$ getconf CLK_TCK
250

无滴答内核(tickless / NO_HZ_FULL):对于需要极低延迟抖动的 HPC 或实时场景,可以隔离 CPU 核心并关闭定时中断:

1
2
3
4
5
6
# 内核启动参数:隔离 CPU 2,3 并开启无滴答
GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3"

# 验证
$ cat /sys/devices/system/cpu/nohz_full
2,3

8.3 综合调优建议

延迟敏感型服务(如交易系统、游戏服务器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 调度参数
sysctl -w kernel.sched_latency_ns=4000000
sysctl -w kernel.sched_min_granularity_ns=500000
sysctl -w kernel.sched_migration_cost_ns=5000000
sysctl -w kernel.sched_wakeup_granularity_ns=1000000

# CPU 频率
cpupower frequency-set -g performance
echo 0 > /sys/devices/system/cpu/cpufreq/boost # 或开启 boost

# 实时优先级(服务进程)
chrt -f -p 50 $(pgrep myapp)

# NUMA 绑定
numactl --cpunodebind=0 --membind=0 ./myapp

高吞吐批处理服务(如 MapReduce、编译集群)

1
2
3
4
5
6
7
8
9
10
# 更大的调度粒度,减少切换开销
sysctl -w kernel.sched_latency_ns=48000000
sysctl -w kernel.sched_min_granularity_ns=6000000
sysctl -w kernel.sched_migration_cost_ns=500000 # 允许积极迁移

# 调速器用 schedutil(根据实际负载动态调频)
cpupower frequency-set -g schedutil

# NUMA 内存交错(均匀分布)
numactl --interleave=all ./batch_job

总结

Linux 进程调度诊断是一个从宏观到微观逐步下钻的过程。本文建立的排查框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
观察症状


宏观指标收集
vmstat cs列 / top CPU / load average


进程级定位
pidstat -w (切换率) / ps stat (D状态) / perf stat

├──── CPU 热点 ──→ perf top / perf record + 火焰图 / bpftrace profile

├──── 调度延迟 ──→ perf sched latency/timehist / bpftrace sched_wakeup

├──── D 状态 ────→ /proc/PID/wchan + stack / bpftrace kprobe:schedule

└──── 进程过多 ──→ pstree / bpftrace sched_process_fork / cgroup pids


参数调优 + 代码修复
sched_latency_ns / cgroup quota / 亲和性 / 调速器

核心原则:先测量,后调优。用 perfbpftrace 拿到数据,确认根因,再有针对性地修改调度参数或应用代码——盲目调参往往适得其反。