sched discussion
[RFC PATCH 0/3] sched: Handle CPU freeze without active domain housekeeping CPUs
LLM 分析
Linux Scheduler:CPU 冻结路径下没有 active housekeeping CPU 的处理
系列概况
- 标题:[RFC PATCH 0/3] sched: Handle CPU freeze without active domain housekeeping CPUs
- 作者:Guopeng Zhang zhangguopeng@kylinos.cn
- 版本:v1 (RFC),共 3 个 patch
- 规模:7 个文件,+207/-14 行
- 修改文件:include/linux/cpuset.h、include/linux/sched/hotplug.h、kernel/cgroup/cpuset.c、kernel/cpu.c、kernel/sched/core.c、kernel/sched/sched.h、kernel/sched/topology.c
- 代码统计:patch 1 +185/-7、patch 2 +9/-2、patch 3 +15/-7(合计 +207/-14 与 cover letter 一致)
- Message-ID:20260722115238.351821-1-guopeng.zhang@linux.dev
- 完整性:作者给出 reproducer(isolcpus=domain,0,3-31 + processor-stage suspend)、warning + GPF 调用栈、taskset 限亲和性下的回归测试、真实 S3 rtcwake 测试,逐 patch 与最终全 series 都做了 x86_64_defconfig 构建,并补做 CONFIG_CPUSETS=n、CONFIG_CPU_ISOLATION=y 的对象级构建,证据链完整
- base-commit:0718283ab28bc3907e10b61a6b4be6fefa1cbb2f
补丁目的
解决一个具体配置组合(isolcpus=domain,... 把 suspend primary 排除出 HK_TYPE_DOMAIN)下,suspend 路径留下的三种耦合问题:
- frozen CPU-hotplug 回调经过 cpuset → rebuild_sched_domains() → partition_sched_domains(1, NULL, NULL) 仍然要 1 个 domain,而 fallback span 为空,触发空 span WARNING 和 build_perf_domains() GPF;
- 受限于那些 frozen CPU 的用户任务在 select_fallback_rq() 中无任何 active 兜底 CPU 可用,可能耗尽 fallback 状态;
- set_cpus_allowed_force() 在 fallback 路径上清掉 user_cpus_ptr,使临时亲和性在 thaw 之后变成永久。
旧流程的问题
旧实现把 suspend primary 当作"必然在 HK_TYPE_DOMAIN 中"的隐式假设:
- partition_sched_domains_locked() 的
!doms_new分支会用cpu_active_mask & HK_TYPE_DOMAIN构造 fallback_doms,当 mask 为空时该 span 就是空的,下游 build_sched_domains() 把它当作有效 span 继续走完,最终撞到 build_perf_domains() 的空指针引用。 - select_fallback_rq() → set_cpus_allowed_force() 走 SCA_USER(commit 851a723e45d1),把 user_cpus_ptr swap 出去当作永久亲和性。
- task_cpu_fallback_mask() 仅返回 HK_TYPE_DOMAIN,domain-isolated CPU 永远不参与兜底(commit bf5b0c27fad2)。
新流程
三个 patch 分工明确:
- patch 1 引入 freeze transaction:在
freeze_secondary_cpus()入口调sched_cpu_fallback_begin()、在thaw_secondary_cpus()出口调sched_cpu_fallback_end()。事务期内对非 kthread 标记MDF_CPUHP_FALLBACK,并绕过 SCA_USER,让 user_cpus_ptr 保留;thaw 时遍历所有受影响任务,调用__sched_setaffinity()把 effective mask 还原回 user_cpus_ptr。 - patch 2 在 patch 1 的
mark_cpu_fallback()中增加 fallback_mask 参数:当cpu_fallback_active为真、且 HK_TYPE_DOMAIN 与 cpu_active_mask 无交集时,把 fallback 切到task_cpu_possible_mask(),仍要求至少有一个 active CPU 才使用。 - patch 3 改写 cpuset_reset_sched_domains() 与 rebuild_sched_domains():当 active 与 HK_TYPE_DOMAIN 无交集时,传
partition_sched_domains(0, NULL, NULL);并把partition_sched_domains_locked()内的两个if (!doms_new)改成if (ndoms_new && !doms_new),让ndoms_new==0与ndoms_new>0+NULL显式区分。
Patch 概览
- patch 1/3
sched: Preserve user affinity across frozen CPU fallback:freeze transaction 状态机 + task 追踪 + thaw 恢复 - patch 2/3
sched: Allow isolated CPUs as a last resort during CPU freeze:mark_cpu_fallback 切换 fallback mask 到 task_cpu_possible_mask() - patch 3/3
sched/topology: Tear down domains without active domain housekeeping CPUs:cpuset_reset_sched_domains() 显式 zero-domain 请求 + 语义边界区分
关键实现
freeze transaction 状态机(patch 1):
static DEFINE_RAW_SPINLOCK(cpu_fallback_lock);
static bool cpu_fallback_active;
void sched_cpu_fallback_begin(void)
{
raw_spin_lock_irqsave(&cpu_fallback_lock, flags);
WARN_ON_ONCE(cpu_fallback_active);
WRITE_ONCE(cpu_fallback_active, true);
raw_spin_unlock_irqrestore(&cpu_fallback_lock, flags);
}
void sched_cpu_fallback_end(void)
{
raw_spin_lock_irqsave(&cpu_fallback_lock, flags);
WRITE_ONCE(cpu_fallback_active, false);
raw_spin_unlock_irqrestore(&cpu_fallback_lock, flags);
restore_cpu_fallback_tasks();
}
select_fallback_rq() 入口新增 fallback 标记与 mask 切换(patch 1+2):
scoped_guard (__task_rq_lock, p) {
if (mark_cpu_fallback(p, &ac.new_mask))
ac.flags = 0; /* 绕过 SCA_USER,保留 user_cpus_ptr */
}
do_set_cpus_allowed(p, &ac);
mark_cpu_fallback() 在 freeze 期内、且 HK_TYPE_DOMAIN fallback mask 与 cpu_active_mask 无交集时,把 *fallback_mask 替换为 task_cpu_possible_mask(p)(前提是它仍与 cpu_active_mask 有交集)。
restore_cpu_fallback_tasks() 在 thaw 时做两遍扫描:第一遍用 kvcalloc 预分配 task_struct 指针数组,避免在持 tasklist_lock 时做可能睡眠的分配;若中途用户更新 affinity 触发 MDF_CPUHP_AFFINITY_CHANGED,就重试恢复以让最新请求生效;扫描结束后再做一次线性 scan 兜底遗漏的子进程。
partition_sched_domains_locked() 语义边界(patch 3):
/* ndoms_new==0 && !doms_new -> 显式清空 */
/* ndoms_new>0 && !doms_new -> 沿用 fallback_doms 旧行为 */
if (ndoms_new && !doms_new) {
doms_new = &fallback_doms;
cpumask_and(doms_new[0], cpu_active_mask, ...);
}
类比
把 CPU 想象成一栋办公楼:HK_TYPE_DOMAIN 是值夜班的保安楼层,primary 是负责巡逻但不在固定岗位的"机动保安"。每天 freeze 就是"下班清楼":所有 secondary 楼层断电下班。如果机动保安恰好今天被隔离在保安楼层之外,freeze 一旦完成,整个保安楼层都没有"在场"的保安——
- patch 3 相当于发现今夜没有保安后,把"巡逻路线图"(scheduler domain)整张收起,避免发空指令;
- patch 2 是在所有正常出口都关掉后,允许保安把客人临时带到"还能走动的电梯间"(task_cpu_possible_mask 里的 active CPU)而不是困在大厅;
- patch 1 是给每位移到临时电梯间的客人留一张"原楼层通行证",等保安回来值夜后再凭这张证回到原楼层——而不是把客人的房间号直接改写成电梯间,让它再也回不来。
Highlight:风险与注意点
task_cpu_possible_mask()作为临时 fallback 引入新的兜底路径,需要确认它不会绕过CPU_ISOLATION_MASK之外其它隐式的"任务不可上 CPU"约束(某些架构的热/拓扑约束)。restore_cpu_fallback_tasks()在 tasklist read_lock 下遍历,再用kvcalloc+put_task_struct,与 thaw 过程中 cpuset/亲和性 sysfs 写者之间的并发需要回归覆盖。WARN_ON_ONCE(cpu_fallback_active)只看一次,如果同一次 suspend 周期内嵌套调用或失败重试,会漏报;可考虑改成WARN_ON或计数器。- 把
ac.flags = 0在 fallback 路径上绕过 SCA_USER,本质上在select_fallback_rq()里开了"保留 user_cpus_ptr"的例外,与 commit 851a723e45d1 的"总是清掉"约定相反,注释里需要明确点出这是 freeze-only 行为。 partition_sched_domains_locked()的if (ndoms_new && !doms_new)改动改变了公共函数的语义边界,所有调用方(不仅是 cpuset)都需要重新审视ndoms_new==0的语义。- 三 patch 互相依赖:patch 1 的 transaction 状态是 patch 2 的前提,patch 2/3 又各自独立依赖于 housekeeping mask 的判空,需要确保 merge 顺序与 backport 顺序一致。
版本变化
RFC v1 首版,三 patch 同时发送,没有 v1→v2 演进可对照;作者明确以 RFC 形式征集对 task 追踪/恢复方案、task_cpu_possible_mask() 用法、以及显式 zero-domain 请求这三方面的反馈。
一句话总结
通过 freeze transaction 状态、临时 fallback mask 切换到 task_cpu_possible_mask()、以及 cpuset 路径显式 zero-domain 请求三件事,让 suspend 在 primary CPU 被 domain 隔离之后仍能干净地冻结和恢复 CPU,避免空 span 警告与 build_perf_domains() GPF,并保证受限任务的 affinity 在 thaw 后回到原 mask。
+---------------------------+
| freeze_secondary_cpus() |
+-------------+-------------+
|
v
sched_cpu_fallback_begin()
|
v
+-----------+ select_fallback_rq() +-----------+
| user task |------------------------->| task_rq ? |
+-----------+ normal mask miss +-----+-----+
|
v
cpu_active & HK_DOMAIN == empty ?
yes / \ no
/ \
fallback_mask = task_cpu_possible use HK_DOMAIN
/
v
mark MDF_CPUHP_FALLBACK
keep user_cpus_ptr (no SCA_USER)
|
... CPUs frozen, suspend runs ...
|
v
thaw_secondary_cpus()
|
v
sched_cpu_fallback_end()
|
v
restore_cpu_fallback_tasks()
__sched_setaffinity(task, user_mask)
|
v
affinity restored to original mask
partition_sched_domains_locked() semantics:
ndoms_new = 0, doms_new = NULL --> explicit zero-domain teardown
ndoms_new > 0, doms_new = NULL --> fallback to cpu_active & HK_DOMAIN
ndoms_new > 0, doms_new != NULL --> use caller's domain array