0/1 已展开

LLM 分析

sched/core:跨队列唤醒保留 wake flags

系列概况

  • 标题:[PATCH v4] sched/core: Preserve wake flags across queued wakeups
  • 作者:Shubhang Kaushik (Ampere) sh@gentwo.org
  • 版本:v4(独立单 patch,基于 origin/master v7.2-rc4 / 248951ddc14d)
  • 规模:include/linux/sched.hkernel/sched/core.c 共 2 个文件,+12 / -18
  • Message-ID:20260722-b4-sched-ttwu-wake-flags-v4-1-8b151b1692cd@gentwo.org
  • 完整性:单封 patch mail,diff 完整、签名完整、changelog 与前置 v1/v2/v3 链接齐备

补丁目的

调度器里 wakeup 路径分为"直接唤醒"和"延迟的队列唤醒"(通过 ttwu_queue_wakelist() 挂到目标 CPU 的待处理列表上)。当目标 CPU 之后在 sched_ttwu_pending() 里排空 wakelist 时,需要把当时传入的 wake_flags 还原给 ttwu_do_activate() 使用。

老实现只存了"是否跨 CPU 迁移"这一位(WF_MIGRATED),把 WF_RQ_SELECTED 等其它语义位丢掉了。select_task_rq() 在调度器选好目标 runqueue 后会置上 WF_RQ_SELECTED,它被 ttwu_do_activate() 用来触发 ENQUEUE_RQ_SELECTED、也被 wakeup_preempt_fair() 用来决定抢占阈值。一旦被排入 wakelist,这些信息就丢失了,可能导致 ENQUEUE 路径行为退化、抢占判断变保守或过激进。

因此本 patch 引入新的 sched_remote_wakeup_flags 字段(u8),把对队列唤醒仍然有意义的 WF_TTWU | WF_SYNC | WF_MIGRATED | WF_RQ_SELECTED 跨过队列边界完整保留下来。

旧流程的问题

Direct wakeup path (correct):

   waker ---> ttwu() ---> ttwu_do_activate(wake_flags)
                         |
                         +---> wakeup_preempt_fair(wake_flags)

   wake_flags carries WF_RQ_SELECTED, consumed downstream correctly.


Queued wakeup path (info lost):

   waker ---> ttwu() ---> ttwu_queue_wakelist()
                         |
                         +-- saves only the single bit WF_MIGRATED

   target CPU ---> sched_ttwu_pending()
                         |
                         +---> ttwu_do_activate(WF_MIGRATED or 0)
                         +---> wakeup_preempt_fair(0)

   WF_RQ_SELECTED is dropped at the wakelist boundary.

老字段 sched_remote_wakeup:1 与其它"scheduler bits"共用一个字时,依赖"严格 current 串行化"假设。v3/v4 改用独立 u8 后的注释说明:wakelist 入队不再被 p->on_cpu 串行化,跨 CPU 看到 p->on_cpu 的 acquire 与 finish_task() 中的 release 配对,flags 必须在入队前发布,所以这个 bit 不能继续留在共享 bitfield 字里。

新流程

Direct wakeup path (unchanged):

   ttwu() ---> ttwu_do_activate(wake_flags)
            |
            +---> wakeup_preempt_fair(wake_flags)


Queued wakeup path (flags preserved):

   ttwu() ---> ttwu_queue_wakelist()
            |
            +-- p->sched_remote_wakeup_flags = wake_flags & WF_TTWU_QUEUE_MASK

   target CPU ---> sched_ttwu_pending()
            |
            +---> ttwu_do_activate(rq, p,
            |                       p->sched_remote_wakeup_flags, &rf)
            +---> wakeup_preempt_fair(p->sched_remote_wakeup_flags)

新的 WF_TTWU_QUEUE_MASK 明确划出哪些位需要跨队列保留:WF_TTWU(TTWU 上下文)、WF_SYNC(同步唤醒)、WF_MIGRATED(迁移标记)、WF_RQ_SELECTED(已选 runqueue)。WF_CURRENT_CPU 只是 CPU 选择提示,离开选择阶段就无意义,所以不进入 mask。

关键实现

/* include/linux/sched.h - task_struct */
    /*
     * Must not share the scheduler bitfield word because wakelist
     * queueing is not serialized by p->on_cpu.
     *
     * smp_load_acquire(&p->on_cpu) before ttwu_queue_wakelist() pairs
     * with smp_store_release(&p->on_cpu) in finish_task(). The flags are
     * published before the task is added to the wakelist.
     */
    u8 sched_remote_wakeup_flags;
/* kernel/sched/core.c */
#define WF_TTWU_QUEUE_MASK \
        (WF_TTWU | WF_SYNC | WF_MIGRATED | WF_RQ_SELECTED)

static void __ttwu_queue_wakelist(struct task_struct *p, int cpu,
                                  int wake_flags)
{
    struct rq *rq = cpu_rq(cpu);
    WRITE_ONCE(rq->ttwu_pending, 1);
    /* ... */
    p->sched_remote_wakeup_flags = wake_flags & WF_TTWU_QUEUE_MASK;
}

void sched_ttwu_pending(void *arg)
{
    /* ... */
    ttwu_do_activate(rq, p, p->sched_remote_wakeup_flags, &rf);
}

两个关键点:

  1. 字段从 1 bit 改成 u8,并移到 scheduler bitfield 之外,避免与"必须由调度器锁串行化"的位共字——这与 release/acquire 配对语义匹配。
  2. WF_TTWU_QUEUE_MASK 让"哪些位需要保留"成为显式契约,未来再添 wake flag 时需要主动决定要不要进 mask,避免再次悄悄丢信息。

类比

ttwu_queue_wakelist() 想成快递柜:waker 把包裹(被唤醒的任务)丢进柜子,给收件人(目标 CPU)一张取件码。原先柜子只允许贴一个"是否跨城"标签,柜员 sched_ttwu_pending() 凭这个标签决定是否加急,但包裹里的其它属性("已选好派送路线"、"VIP 件"、"加急同步")都丢了。新的设计是给柜子加一张小卡片 sched_remote_wakeup_flags(u8),明确登记"派送需要的全部属性",由柜员照单全收,恢复原本应该享受的服务等级(抢占阈值、ENQUEUE_RQ_SELECTED 等)。

Highlight:风险与注意点

  • WF_RQ_SELECTED 漏传在大多数情况下只是"行为略不优化",但它会进入 wakeup_preempt_fair() 的抢占阈值计算,可能让高优先级 wakee 错过应该立刻抢占的窗口——典型的"功能退化"型 bug,没有 crash 但公平/响应性受影响。
  • v3 改用 READ_ONCE()/WRITE_ONCE()、v4 又退回去的取舍需要后续 review 跟进:wakelist 入队时 p->on_cpu 已经由 release/acquire 串行化,作者因此认定普通读写在跨 CPU 视角下可见,但内核 memory model 组对此是否买账值得确认。
  • WF_TTWU_QUEUE_MASK 是一次性白名单,将来增加新 WF_* flag 时容易遗漏更新 mask;建议在 enum 定义处加 BUILD_BUG_ON 提醒。
  • 该字段是 u8,未来若 wake_flags 位数超过 8 还要回头扩展;当前 wake flags 数量远小于 8,暂无压力。

版本变化

  • v1 → v2:把 sched_remote_wakeup_flags 移到 scheduler bitfield 之外成为独立 u8;删除 sched_ttwu_pending() 里多余的清零;新增 WF_TTWU_QUEUE_MASK
  • v2 → v3:改用 READ_ONCE()/WRITE_ONCE();changelog 围绕 WF_RQ_SELECTED 抢占行为重新立锚;保留 task_struct 跨 CPU 顺序注释。
  • v3 → v4:去掉 READ_ONCE()/WRITE_ONCE();移除 sched_ttwu_pending() 中多余的 wake_flags 局部;修正 task_struct 字段顺序注释。

一句话总结

把"队列唤醒"路径里被吞掉的 wake flags 重新装回一个独立的 u8 字段并显式白名单化,从而恢复 WF_RQ_SELECTED 等位在 ttwu_do_activate()wakeup_preempt_fair() 中的语义,避免 wakelist 边界上的功能退化。