sched discussion
[PATCH] sched/cpupri: Remove count field from struct cpupri_vec
LLM 分析
sched/cpupri:移除 struct cpupri_vec 的 count 字段
系列概况
- 标题:
[PATCH v2] sched/cpupri: Remove count field from struct cpupri_vec - 作者:Luigi Rizzo lrizzo@google.com
- 版本:v2(共两封邮件,v1 -> v2 单版修订,无新增 patch)
- 规模:2 个文件改动,净删除 60 行(v1: +2/-61,v2: +14/-74,差异主要是新增注释与测试说明)
- 修改文件:
kernel/sched/cpupri.ckernel/sched/cpupri.h
- 代码统计:v2 净 14 insertions, 74 deletions
- Message-ID:
- v1:
20260819095129.4056035-1-lrizzo@google.com - v2:
20260820103553.1099094-1-lrizzo@google.com
- v1:
- 完整性:两封邮件均完整,commit message、Signed-off-by 与 diff 齐全;v2 在 v1 基础上补全 benchmark 与注释。
补丁目的
struct cpupri_vec 中的 count 字段原本是 __cpupri_find() 的 early-exit 启发式:count 为 0 时跳过该优先级的 cpumask 检查。
但在大型多核 ARM 系统上,维护 count 需要原子操作和 memory barrier(dmb ish)。重 I/O 工作负载(如 fio + threaded IRQ)下,irq_thread 以 SCHED_FIFO 50 频繁入队/出队,每次出队都调用 cpupri_set(),导致:
- cache line 抖动
- 流水线停顿
- softirq CPU 25% 的时间花在
cpupri_set()上
补丁移除 count 字段后,cpupri_set() 与 __cpupri_find() 都不再触碰 atomic / barrier。代价仅仅是 cpupri_find() 在 count==0 时多做一次 cpumask_any_and(),而这个开销远低于原本的 barrier 开销。
旧流程的问题
__cpupri_find(vec):
skip = 0
if !atomic_read(&vec->count):
skip = 1
smp_rmb() # read barrier, every iteration
if skip:
return 0 # skip mask check
cpupri_set():
set mask (new priority)
smp_mb__before_atomic()
atomic_inc(&vec->count) # write barrier + atomic add
do_mb = 1
if do_mb:
smp_mb__after_atomic() # another barrier
atomic_dec(&vec->count) # atomic sub on old prio
smp_mb__after_atomic()
clear mask
旧流程中 count 充当"快速判断空向量"的角色,但每个 cpupri_set() 调用都要:
- 2 次
smp_mb__before/after_atomic()(ARM 上编译为dmb ish) - 2 次 atomic 操作
220 核 ARM 实测:softirq CPU 25% 的 perf top 时间都在 cpupri_set()。
新流程
__cpupri_find(vec):
if cpumask_any_and(&p->cpus_mask, vec->mask) >= nr_cpu_ids:
return 0
cpupri_set():
if newpri != CPUPRI_INVALID:
cpumask_set_cpu(cpu, vec_new->mask)
if oldpri != CPUPRI_INVALID:
cpumask_clear_cpu(cpu, vec_old->mask)
没有 atomic,没有 smp_mb。唯一多出来的代价是 cpumask_any_and() 在空 mask 上多跑一次,而这个比 dmb ish 便宜得多。
Patch 概览
kernel/sched/cpupri.h:删除#include <linux/atomic.h>和atomic_t count字段kernel/sched/cpupri.c:__cpupri_find:删除 count 预读与 smp_rmbcpupri_set:删除 do_mb、atomic_inc/dec、smp_mb__*cpupri_init:删除atomic_set(&vec->count, 0)
关键实现
cpupri_set() 不再有 memory barrier 后,正确性靠两点保证:
- cpupri 是 best-effort 路由提示:
- 若
cpupri_find()漏掉一个降优先级的 CPU,那个 CPU 之后会自己通过balance_rt()/pull_rt_task()拉任务 - 若漏掉一个升优先级的 CPU,最多错过一次 push,并不会破坏正确性
- 若
- stale match 的二次校验:最终选中的 lowest_rq 仍会在
find_lock_lowest_rq()里以rq->lock重新校验,不依赖 cpupri 的无锁读
v2 在新代码上加了一段注释,把这套推理写进源码,避免下一个维护者再把 count 加回来。
关键代码(v2 cpupri_set 核心逻辑)
void cpupri_set(struct cpupri *cp, int cpu, int newpri)
{
int *currpri = &cp->cpu_to_pri[cpu];
int oldpri = *currpri;
newpri = convert_prio(newpri);
*currpri = newpri;
if (likely(newpri != CPUPRI_INVALID))
cpumask_set_cpu(cpu, cp->pri_to_cpu[newpri].mask);
if (likely(oldpri != CPUPRI_INVALID))
cpumask_clear_cpu(cpu, cp->pri_to_cpu[oldpri].mask);
}
find_lock_lowest_rq() 在 rq->lock 下做最终二次校验,是这套无 barrier 设计的兜底。
类比
把 count 想成图书馆门口的"今日入馆人数"计数器:
- 馆员(
__cpupri_find)路过时常瞥一眼这个数字,零就跳过、不进去看 - 但每进一个读者(
cpupri_set)都要触发"登记、拍照、广播、写日志"四件套(atomic + barrier),否则数字会和"是否真的有人"对不上 - 重 I/O 场景下读者流量极大,四件套堆到馆员时间的 25%
- 直接把计数器拆掉,馆员最多在"确实没人"的阅览室多走两步确认,而这两步远比四件套便宜
关键对象关系
+----------------------+ +----------------------+
| struct cpupri | | struct cpupri_vec |
| - cpu_to_pri[] | ---> | - mask: cpumask |
| - pri_to_cpu[] | holds | (count removed) |
+----------------------+ N +----------------------+
| ^
| set/clear | early-exit was
v | via atomic count
+----------------------+ |
| cpupri_set() | --------------+ now: just scan
| - no atomic | | mask directly
| - no smp_mb | |
+----------------------+ |
| |
v |
+----------------------+ |
| cpupri_find() | --------------+ fallback:
| - cpumask_any_and() | | find_lock_lowest_rq
| - no smp_rmb | | under rq->lock
+----------------------+ |
|
(best-effort hint, double-checked under rq->lock)
Highlight:风险与注意点
- 正确性论证靠注释与历史 commit message,没有引入新的运行时校验。建议 review 重点放在
find_lock_lowest_rq()是否仍然是 stale match 的最后一道防线 - ARM
dmb ish影响范围比 x86 mfence 更大,跨 cluster 同步代价显著;x86 上此 patch 收益较小,但也没有副作用 - 没有跑 RT 压力测试的公开数据(
hackbench -l 10000或stress-ng --sched);v2 给了 fio + chrt 组合,可继续补 sched_debug 跟踪 CPUPRI_INVALID分支保持不变,新流程里*currpri = newpri仍是无保护写;这原本就不是 count 的职责范围,patch 没有引入回归- 风险点是 v1 缺注释,v2 才把"无 barrier 也安全"的论证写进源码。reviewer 可以质问为什么早期设计要这么重,现在能不能也清理掉其它类似"防御性 atomic"
版本变化
v1 -> v2:
+ add cpupri_find() benchmark under stress workload
+ add inline comment in cpupri_set() explaining
"no barrier still safe" reasoning
+ simplify cpumask_set_cpu / cpumask_clear_cpu calls
- net deletion grows 59 -> 60 lines (due to new comment)
与其他相关 patch 系列的关联
- 历史上 cpupri count 的引入是 RT 调度器在多核扩展性上的折中,本 patch 是把那个折中回收
- 同期可能存在对
cpumask_var_t选型或cpupri_find路径上其它 atomic 的清理工作;本 thread 未涉及
一句话总结
把 struct cpupri_vec 当作"无 barrier 也能用"的近似结构看待后,移除多余的 atomic count 字段,让重 I/O 路径上 softirq CPU 的 cpupri_set 时间从 25% 降到 2% 以内,RT 调度的可扩展性显著提升。