0/2 已展开

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.c
    • kernel/sched/cpupri.h
  • 代码统计:v2 净 14 insertions, 74 deletions
  • Message-ID
    • v1: 20260819095129.4056035-1-lrizzo@google.com
    • v2: 20260820103553.1099094-1-lrizzo@google.com
  • 完整性:两封邮件均完整,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_rmb
    • cpupri_set:删除 do_mb、atomic_inc/dec、smp_mb__*
    • cpupri_init:删除 atomic_set(&vec->count, 0)

关键实现

cpupri_set() 不再有 memory barrier 后,正确性靠两点保证:

  1. cpupri 是 best-effort 路由提示
    • cpupri_find() 漏掉一个降优先级的 CPU,那个 CPU 之后会自己通过 balance_rt() / pull_rt_task() 拉任务
    • 若漏掉一个升优先级的 CPU,最多错过一次 push,并不会破坏正确性
  2. 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 10000stress-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 调度的可扩展性显著提升。