进程调度是 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, ----------------------------------------------------------- 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
三个数字含义:
运行时间 (nanoseconds):进程实际在 CPU 上执行的累计时间,对应 se.sum_exec_runtime
等待时间 (nanoseconds):进程在运行队列中就绪但等待 CPU 的累计时间(即调度延迟之和)
切换次数 :总上下文切换次数
等待时间与运行时间的比值 是调度延迟的重要指标。如果等待时间远大于运行时间,说明系统 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) { ... 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 38 cpu-migrations 8,234,567,890 instructions 3,361,456,123 cycles 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); ... 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); }
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-7pid 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 $ numactl --cpunodebind=0 --membind=0 ./myapp $ numactl --interleave=all ./myapp $ 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_OTHER(SCHED_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$ chrt -f -p 50 12345 $ chrt -r -p 30 12345 $ 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 $ 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 $ 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 $ 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_request 和 json_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 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 锁)。
修复建议 :
引入对象池或内存竞技场(arena allocator),复用 JSON 解析缓冲区
使用 jemalloc 或 tcmalloc 替换 glibc malloc,减少线程间锁竞争
考虑 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 ← 等效微秒 $ 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
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,频率未及时提升。
修复建议 :
调高 cgroup cpu.cfs_quota_us,或改用 cpu.weight 的相对权重调度
将调速器改为 performance 或 schedutil:cpupower frequency-set -g schedutil
开启 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 -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 $ mount | grep nfs nfs-server:/data on /mnt/nfs type nfs4 (rw,relatime,...) $ nfsstat -c Client rpc stats: calls retrans authrefrsh 1234567 45678 12345 ← retrans 高说明网络不稳定 $ 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.5write: 89.1 0 712.3 8.0 23 345.6 346.8 ^^ ^^^^^ 重传23次,平均响应346ms(正常应<10ms)
根因分析 :NFS 服务器响应超时(346ms 远超正常),write 操作触发大量重传,导致进程长期卡在 nfs_wait_on_request 的 D 状态。
修复建议 :
检查 NFS 服务器负载和网络链路质量(ping、traceroute)
挂载时加 softerr 或 soft,timeo=30 选项,让 NFS 请求超时后返回错误而非无限等待
考虑迁移到本地存储或更可靠的分布式存储(如 Ceph、GlusterFS)
短期应急: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 $ 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 $ 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 $ cat /sys/fs/cgroup/pids/myapp/pids.max max ← 无限制! $ 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 $ 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 新进程而不是复用进程池,导致进程数指数级增长。
修复建议 :
立即处置:kill -STOP 2345(暂停父进程)然后 kill -9 -2345(杀死整个进程组)
设置 cgroup pids.max 作为安全阀(推荐在容器/Pod 中默认配置)
代码层面:使用进程池(multiprocessing.Pool)而非无限制 fork
设置系统级 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 9,573,363,474 cycles 456,789,012 cache-references 45,678,901 cache-misses 5,678,901,234 branch-instructions 56,789,012 branch-misses 4,156 context-switches 38 cpu-migrations 1,234 page-faults 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 $ perf top -K $ 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 $ perf record -F 999 -g --call-graph dwarf \ -p 23456 -- sleep 30 $ perf report --stdio --no-children --percent-limit 1 $ 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 $ perf sched record -a -- sleep 10 $ perf sched latency --sort max $ perf sched timehist -p 23456 $ 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 $ perf mem record -p 23456 -- sleep 10 $ perf mem report
6.7 perf ftrace:内核函数追踪 1 2 3 4 5 $ 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 $ sysctl -w kernel.sched_latency_ns=48000000
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
sched_migration_cost_ns(任务迁移代价阈值,默认 500μs)
当某 CPU 上的进程最近执行时间 < 此值时,调度器认为它的 Cache 还”热”,避免迁移到其他 CPU。
增大此值:减少任务迁移(减少 Cache 污染),适合 Cache 敏感的延迟任务
减小此值:允许更积极的负载均衡,适合吞吐优先场景
1 $ sysctl -w kernel.sched_migration_cost_ns=5000000
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 $ grep "^CONFIG_HZ=" /boot/config-$(uname -r) CONFIG_HZ=250 $ getconf CLK_TCK 250
无滴答内核(tickless / NO_HZ_FULL) :对于需要极低延迟抖动的 HPC 或实时场景,可以隔离 CPU 核心并关闭定时中断:
1 2 3 4 5 6 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 cpupower frequency-set -g performance echo 0 > /sys/devices/system/cpu/cpufreq/boost chrt -f -p 50 $(pgrep myapp) 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 cpupower frequency-set -g schedutil 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 / 亲和性 / 调速器
核心原则:先测量,后调优 。用 perf 和 bpftrace 拿到数据,确认根因,再有针对性地修改调度参数或应用代码——盲目调参往往适得其反。