4. CPU 调度延迟分析:调度器、C-State、频率缩放、中断负载、RT线程抢占、Scheduling延迟

4.1 现象识别:Systrace 中的调度失调迹象

CPU 调度延迟在 Systrace 中的典型“指纹”是:一个任务已经处于 Runnable 状态(在调度跟踪中表现为橙色/红色区块或 Sched 与 wakeup 标记之间的 gap),但迟迟没有获得 CPU 时间片。具体可观察以下迹象:

  • Sched 与 Wakeup 间 gap 大:选中任务后,查看 Scheduling wakeup latency 或 Runnable to Running 时间 > 20ms。
  • Running 段短但间隔长:任务频繁被换出,每次 Running 时长 < 1ms,但 Sched 到下次 Running 间隔 > 50ms。
  • CPU 频率过低:在延迟区间内,CPU 频率柱状图显示工作在最低档(如 300MHz),同时有 cpufreq 事件。
  • 中断风暴集中在一个核:所有 irq / softirq 都扎堆在 CPU0,而目标任务被调度到该核上排队。
  • RT 线程压制:发现 sched_rt_rq 或 RT 优先级任务连续运行 > 20ms,阻塞 CFS 任务。
  • C-State 退出过长:在 idle 后首次调度前,有 cpu_idle: state=4 且 exit_latency > 10ms。
快速识别规则:若选中一个 Runnable 线程后,Waiting for CPU 时间占比超过总延迟的 60%,则属于典型调度延迟,优先排查本模块。

4.2 数据采集:抓取完整调度决策上下文

仅靠默认的 sched_switch 不够,必须开启调度器、频率、idle 和中断的细粒度 tracepoint。使用以下参数录制:

# 抓取典型调度延迟场景(以 Android 为例)
systrace.py --time=10 -o scheduling.html \
  sched freq idle irq power workq

# 或使用 atrace 直接指定
atrace --async_start --buffer_size 16384 \
  -t sched freq idle irq power workq gfx input view

# 确定延迟区间后,用 CLI 导出原始 atrace 文本
atrace --async_dump > /data/local/tmp/trace_raw.txt

在 Trace View 中增加以下的关键列(自定义显示):

跟踪项 意义 默认开启
cpufreq_interactive_target 频率调速器目标频率 否
cpu_idle C-State 进入/退出事件 否
sched_wakeup_new 新建任务的首次唤醒 是
sched_wakeup 任务被唤醒时间点 是
sched_switch 上下文切换 是
irq_handler_entry 中断服务入口 否
采样技巧:针对周期性延迟问题,在 Systrace 中直接搜索 sched_wakeup 后紧接着 sched_switch(目标 PID)之间的时间戳差。若该差 > 10ms 且 CPU 不是 0 号核,优先怀疑中断亲和性和RT 抢占。

4.3 原因定位:调度延迟故障树排查表

使用以下故障树判断路径,依据 Systrace 事件逐层下钻定位根因:

CPU 调度延迟故障排查树 Runnable→Running 延迟大 频率/Governor C-State 深度 RT/中断负载 cpufreq 未升频 频率上升过慢 C-State 4~8 退出 Tickless 超时 RT 任务长时间运行 中断/softirq 集中在 目标核上 定位:检查负载激增 或热电限制导致锁频 定位:cpuidle 驱动 或 BIOS 未设置 C1E 定位:修改中断亲和 或 RT 任务优先级

结合故障树,按以下排查表逐个检验根因:

可疑根因 Systrace 定界事件 判断规则
频率缩放延迟 cpufreq_interactive_target 前后 gap 大,频率档位明明已提高但任务未运行 查看 cpufreq_interactive_target 与 sched_switch 之间的延迟,若 > 5ms 且无其他任务运行,则为频率切换慢。
C-State 退出延迟 cpu_idle: state=4 到 sched_switch 时间 > 8ms 检查实际 exit_latency 是否匹配硬件规格;若 C-State 3+ 退出时没有先降频,则可能是 PMC 或 BIOS 配置。
RT 线程抢占 sched_rt_rq 事件中显示 RT 任务运行时长 > 20ms,且 CFS 任务在等待 计算 RT 任务连续占用 CPU 的时间,若 > 40ms 则必须降低其优先级或将 RT 任务限制到专用小核。
中断亲和性不均 所有 irq_handler_entry 出现在 CPU0,且 CPU0 上 CFS 任务等待时间最长 查看 interrupts 跟踪或 /proc/interrupts 分布;优先将网络/GPU 中断移到其他核。
Scheduling 延迟(调度器自身) sched_switch 调用前无其他任务运行,但 next_prio 却高于当前任务 启用 sched_stat_wait 查看等待队列长度;CFS 的 min_granularity 过大时会延迟任务被选中。

4.4 修复动作

根据定位到的具体分支执行修复:

  • 频率缩放优化:修改 cpufreq 调速器从 interactive 切换为 schedutil;若系统不支持则调小 hispeed_freq 和 target_loads。示例:
# 切换到 schedutil(基于调度器提示的调频)
echo schedutil > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# 调高通调频的响应性(interactive 调速器下)
echo "80 1000000:90 1600000:95" > /sys/devices/system/cpu/cpufreq/interactive/target_loads
  • C-State 控制:限制深度 C-State 的可用性,减少退出延迟:
# 禁止进入 C-State 3 或更深(调试用)
echo 2 > /sys/module/intel_idle/parameters/max_cstate  # Intel
echo 1 > /sys/devices/system/cpu/cpu0/cpuidle/state3/disable  # 通用
  • 中断迁移:使用 smp_affinity 将高频率中断从目标核移走:
# 查询中断号(以 eth0 为例)
cat /proc/interrupts | grep eth0  # 获取 IRQ 号,如 48
# 将 IRQ 48 的亲和掩码设为 0x00000002(仅 CPU1)
echo 2 > /proc/irq/48/smp_affinity
  • RT 任务隔离:移除非关键 RT 任务到特定小核,或使用 chrt 降低其优先级:
# 将 RT 任务的优先级降为 1(最弱)
chrt -r -p 1 $(pidof rt_task_name)
# 使用 cgroup/cpuset 将 RT 任务绑定到 CPU4-7
echo 0-3 > /dev/cpuset/rt/cpus
避坑提醒:大量设备默认关闭了 tickless 或 dynticks,导致 CPU 即使空闲也每秒多次被时钟中断唤醒,破坏 C-State。若 C-State 退出延迟高,先确认 CONFIG_NO_HZ_FULL 是否启用。

4.5 复测与验证

修改后必须重新抓取相同场景的 Systrace,并验证以下指标:

  • Runnable to Running gap:目标任务的 Waiting for CPU 时间从原来的 > 20ms 下降到 < 3ms。
  • 频率响应曲线:在负载启动后的 2ms 内频率从最低档升到最高档,且 schedutil 事件没有明显滞后。
  • 中断分布:确认 irq_handler_entry 不再全部集中在 CPU0,而是均匀分散(如 CPU0 20%、CPU1 25%、CPU2 25%、CPU3 30%)。
  • C-State 退出次数:在负载区间内不应看到深 C-State(state > 2)退出;若仍然出现,继续调高 pm_qos_resume_latency_us 限制。
# 验证中断分散情况
cat /proc/interrupts | head -n 10  # 检查第二列起 CPU 计数是否均衡

# 验证频率切换延迟(使用 trace 中的 time delta)
grep "sched_wakeup" /data/local/tmp/trace_raw.txt | head -n 20

4.6 预防与监控

在工程层面建立长期检查机制,避免同类延迟反复出现:

  • 内核配置硬门控:在 defconfig 中强制开启 CONFIG_SCHEDUTIL、CONFIG_NO_HZ_FULL、CONFIG_IRQ_TIME_ACCOUNTING。
  • 自动化回归脚本:每次 CI 跑性能测试时附带 trace-cmd record -e sched_switch -e cpu_frequency,并用脚本解析 sched_wakeup 延迟的 P95 百分位。
  • 中断负载预警:在 dashborad 上监控 /proc/interrupts 的方差,如果某个核的中断次数超过其他核 2 倍则告警。
  • RT 任务白名单:建立 RT 任务列表,所有人新增 RT 任务时需 Code Review 确认其优先级和 cpuset,防止误用 RT 破坏 CFS 响应性。
长期监控点:推荐在性能看板上添加 调度延迟 P99 指标(Runnable→Running 时间),超过 5ms 则自动触发深度 Systrace 抓取,保留现场用于根因分析。