sched discussion
[PATCH RFC 0/1] sched: replace the nr_pinned offset access with a per-CPU counter
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 - 修改文件:
Kbuild、include/linux/sched.h、kernel/sched/core.c、kernel/sched/rq-offsets.c(删除)、kernel/sched/sched.h - 代码统计:6 行新增,53 行删除;净减 47 行
- Message-ID:
20260825041320.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.h、rq-offsets.c、Kbuild)才能理解RQ_nr_pinned是什么 - 不可移植的 workaround:
arch_raw_cpu_ptr()vsPERCPU_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_pinned和struct rq上的其它为了头文件可见性而被加进来的小字段做一轮同样的清理
版本变化
本帖只有 v1 / RFC 一版,无 v2 可比。
一句话总结
把 <linux/sched.h> 里 inline 的 migrate_* 不能再访问 struct rq 时的用 generated offset 寻址折中方案替换成一颗简单的 per-CPU 计数器 rq_nr_pinned,砍掉 rq-offsets.c 及其 Kbuild 规则,让类型安全和头文件清洁度双双回血。