0/2 已展开

LLM 分析

sched:用 per-CPU 计数器替换 nr_pinned 偏移访问

系列概况

  • 标题:[PATCH RFC 0/1] sched: replace the nr_pinned offset access with a per-CPU counter
  • 作者:Qiurong Fang fangqiurong@kylinos.cn
  • 版本:RFC v1(1/1,共 1 个补丁)
  • 规模:1 个 patch,5 个文件,+6/-53,删除整个 kernel/sched/rq-offsets.c
  • 修改文件Kbuildinclude/linux/sched.hkernel/sched/core.ckernel/sched/rq-offsets.c(删除)、kernel/sched/sched.h
  • 代码统计:6 行新增,53 行删除;净减 47 行
  • Message-ID20260825041320.2486529-1-fangqiurong@kylinos.cn(cover),20260825041320.2486529-2-fangqiurong@kylinos.cn(patch)
  • 完整性:thread 仅含 cover letter 与 1 个 patch,无回复;作者自评是 cleanup 而非 bugfix

补丁目的

include/linux/sched.h 里被 inline 展开的 migrate_enable() / migrate_disable() 不再需要绕道访问 rq->nr_pinned

migrate_{en,dis}able() 被 inline 进 <linux/sched.h> 之后,函数体里看不到完整的 struct rq(因为这个头文件被海量代码包含,不能拖入调度器大结构体)。为了让编译器能找到 rq.nr_pinned 的偏移量,先前选择了一条曲线救国的路:

  • 新增 kernel/sched/rq-offsets.c,编译期生成 include/generated/rq-offsets.h
  • sched.h 顶端 #include <generated/rq-offsets.h>,并以 RQ_nr_pinned 常量寻址
  • inline 函数里用 arch_raw_cpu_ptr() / PERCPU_PTR() 绕过 this_cpu_ptr() 的类型检查,再用 (unsigned int *) 强转回读

这条路径虽然能用,但拉进了:一个独立 Kbuild rule、一个手工 filechk 调用、一个被全局头文件 include 的 generated header、以及若干先 cast 再 deref 的黑魔法。本 RFC 把它彻底替换成一颗干净的 per-CPU 计数器 rq_nr_pinned

旧流程的问题

  • 类型安全被破坏:表达式 (*(unsigned int *)((void *)this_rq_raw() + RQ_nr_pinned)) 把 void 强转后再 deref,丢失了 struct rq 的类型信息
  • 构建系统被拖累:多一个 rq-offsets.s 目标、一次 filechk、一次额外 FORCE 依赖
  • 全局 generated header 污染include/linux/sched.h 这样一个被普遍包含的头文件被迫 include 一次 generated header,耦合很重
  • 可读性差:后续读者要跳三个文件(sched.hrq-offsets.cKbuild)才能理解 RQ_nr_pinned 是什么
  • 不可移植的 workaroundarch_raw_cpu_ptr() vs PERCPU_PTR() 的两段 #ifdef CONFIG_SMP 完全是绕开 __verify_pcpu_ptr() 校验的临时变通

新流程

+--------------------------------+
| include/linux/sched.h          |
| DECLARE_PER_CPU(unsigned int,  |
|                rq_nr_pinned);  |
+--------------------------------+
              |
              v
+--------------------------------+
| migrate_disable()              |
|   (preempt-disabled already)   |
| __this_cpu_inc(rq_nr_pinned);  |
+--------------------------------+
              |
              v
+--------------------------------+
| rq_nr_pinned[cpu]  per-CPU var |
+--------------------------------+
              ^
              |
+--------------------------------+
| rq_has_pinned_tasks(rq)        |
| READ_ONCE(*per_cpu_ptr(        |
|     &rq_nr_pinned,             |
|     cpu_of(rq)));              |
+--------------------------------+

数据流变成单变量、单访问语义:写端是已关抢占的 per-CPU RMW,读端是带 READ_ONCE 的远程 CPU 读取。

关键实现

1) 删掉偏移生成器

// kernel/sched/rq-offsets.c(删除)
#define COMPILE_OFFSETS
#include <linux/kbuild.h>
#include <linux/types.h>
#include "sched.h"

int main(void)
{
    DEFINE(RQ_nr_pinned, offsetof(struct rq, nr_pinned));

    return 0;
}

Kbuild 里与之相关的 rq-offsets.s target、filechk 调用、$(rq-offsets-file): FORCE 全部移除;missing-syscalls 重新挂回标准的 $(offsets-file)

2) 头文件清空 inline 黑魔法

// include/linux/sched.h
-DECLARE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
-#ifdef CONFIG_SMP
-#define this_rq_raw() arch_raw_cpu_ptr(&runqueues)
-#else
-#define this_rq_raw() PERCPU_PTR(&runqueues)
-#endif
-#define this_rq_pinned() \
-        (*(unsigned int *)((void *)this_rq_raw() + RQ_nr_pinned))
+DECLARE_PER_CPU(unsigned int, rq_nr_pinned);

同时把 __migrate_disable() / __migrate_enable()#else /* !COMPILE_OFFSETS */ 空函数分支也一起删了,因为那条分支是为了让 rq-offsets.c 自身能编译通过而存在的,offset 机制退役后它没有存在意义。

3) 写端替换为 RMW

// include/linux/sched.h(inline 体内)
 static inline void __migrate_enable(void)
 {
     barrier();
     p->migration_disabled = 0;
-    this_rq_pinned()--;
+    __this_cpu_dec(rq_nr_pinned);
 }

 static inline void __migrate_disable(void)
 {
     guard(preempt)();
     p->migration_disabled = 1;
-    this_rq_pinned()++;
+    __this_cpu_inc(rq_nr_pinned);
 }

调用点已经在 preempt-disabled 区,per-CPU 写入安全;编译产物与原先的开地址加 RMW 汇编等价。

4) 读端换成远程 per-CPU 读

// kernel/sched/core.c
+DEFINE_PER_CPU(unsigned int, rq_nr_pinned);

 int migrate_enable(void)
 {
-    return rq->nr_pinned;
+    return READ_ONCE(*per_cpu_ptr(&rq_nr_pinned, cpu_of(rq)));
 }

DEFINE 必须落在 core.c 这种非模块独立编译单元里(<linux/sched.h> 仍然只看到声明)。

5) 把字段从 struct rq 抠走

// kernel/sched/sched.h
 struct rq {
     ...
-    unsigned int nr_pinned;
     ...
 };

注意 struct rq 自己的 nr_pinned 字段整体被去掉,这个字段本来就是仅为 inline 的 migrate_* 设立的中转站,现在中转站也可以拆了。

类比

把它想象成公司门禁统计。原来统计当前在岗人数是这样做的:

  • 门卫要在每个职员的工牌上记一笔我进大楼了
  • 但是工牌样本(struct rq)放在公司主楼大堂的档案柜里,门卫办公室(include/linux/sched.h)只能拿到工牌样本的照片(type-incomplete view),看不到原件
  • 没办法,只好请档案室预先量好姓名栏到工号栏的距离(offset 常量),门卫按照片位置加量好的距离硬读
  • 每张工牌的位置还要写个特殊 Kbuild 规则生成(rq-offsets.c 加 filechk),并且每天要刷新一次

新方案相当于在门卫办公室自己装了一块白板,名字叫大楼内在岗人数(rq_nr_pinned),每人刷卡进门卫就加一,出门减一。档案柜里那张表(rq->nr_pinned)就再也不需要存在了。

Patch 概览

整个 series 只有 1 个 patch(1/1),整体就是删除偏移生成器加引入 per-CPU 计数器的一对一替换。

+-----+------------------+---------+-----------------+
| #   | File             | + / -   | Note            |
+-----+------------------+---------+-----------------+
| 1/1 | Kbuild           |  - 10   | drop offset gen |
| 1/1 | include/sched.h  |  - 30   | drop macro hack |
| 1/1 | kernel/core.c    | + 2 - 1 | define + reader |
| 1/1 | kernel/rq-off.c  |  - 12   | file removed    |
| 1/1 | kernel/sched.h   |    - 1  | drop field      |
+-----+------------------+---------+-----------------+
| tot | 5 files          | + 6 - 53| net -47 lines   |
+-----+------------------+---------+-----------------+

Highlight:风险与注意点

  • 关抢占边界必须保持:写端 __this_cpu_* 之所以安全,是因为 migrate_disable()/migrate_enable() 内部已 preempt-disabled。一旦未来有人拆掉这层 guard 还想保持正确的 per-CPU 语义,需要重新审计
  • per-CPU 变量的分配位置DEFINE_PER_CPU 只能放在非 COMPILE_OFFSETS、能看到完整 struct rq 类型的 .c 里,所以放在了 kernel/sched/core.c,这是惯例
  • 读端的 READ_ONCE 不能省:跨 CPU 读需要至少一屏障;写端不需要 READ_ONCE,因为 RMW 自身就是原子的
  • 不要回退到把 nr_pinned 字段加回 struct rq:那是上一版被刻意拒绝的重排字段方案,会破坏 cache line 排布
  • missing-syscalls 重新挂到 $(offsets-file) 而不是 $(rq-offsets-file):升级工具链或迁移到非 x86 架构时不要漏改
  • 后续观察:是否需要把 rq_nr_pinnedstruct rq 上的其它为了头文件可见性而被加进来的小字段做一轮同样的清理

版本变化

本帖只有 v1 / RFC 一版,无 v2 可比。

一句话总结

<linux/sched.h> 里 inline 的 migrate_* 不能再访问 struct rq 时的用 generated offset 寻址折中方案替换成一颗简单的 per-CPU 计数器 rq_nr_pinned,砍掉 rq-offsets.c 及其 Kbuild 规则,让类型安全和头文件清洁度双双回血。