0/4 已展开

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-ID20260722115238.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 路径留下的三种耦合问题:

  1. frozen CPU-hotplug 回调经过 cpuset → rebuild_sched_domains() → partition_sched_domains(1, NULL, NULL) 仍然要 1 个 domain,而 fallback span 为空,触发空 span WARNING 和 build_perf_domains() GPF;
  2. 受限于那些 frozen CPU 的用户任务在 select_fallback_rq() 中无任何 active 兜底 CPU 可用,可能耗尽 fallback 状态;
  3. 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==0ndoms_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:风险与注意点

  1. task_cpu_possible_mask() 作为临时 fallback 引入新的兜底路径,需要确认它不会绕过 CPU_ISOLATION_MASK 之外其它隐式的"任务不可上 CPU"约束(某些架构的热/拓扑约束)。
  2. restore_cpu_fallback_tasks() 在 tasklist read_lock 下遍历,再用 kvcalloc + put_task_struct,与 thaw 过程中 cpuset/亲和性 sysfs 写者之间的并发需要回归覆盖。
  3. WARN_ON_ONCE(cpu_fallback_active) 只看一次,如果同一次 suspend 周期内嵌套调用或失败重试,会漏报;可考虑改成 WARN_ON 或计数器。
  4. ac.flags = 0 在 fallback 路径上绕过 SCA_USER,本质上在 select_fallback_rq() 里开了"保留 user_cpus_ptr"的例外,与 commit 851a723e45d1 的"总是清掉"约定相反,注释里需要明确点出这是 freeze-only 行为。
  5. partition_sched_domains_locked()if (ndoms_new && !doms_new) 改动改变了公共函数的语义边界,所有调用方(不仅是 cpuset)都需要重新审视 ndoms_new==0 的语义。
  6. 三 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