背景

migrate_pages_batch() 批量迁移 folio 时逐个 unmap 再搬移,每次 unmap 都要跑一遍 mmu_notifier 的 invalidate 回调。在 KVM 宿主机上这条回调链不便宜:try_to_migrate() 一路走进 kvm_mmu_notifier_invalidate_range_start() 再到 tdp_mmu_zap_leafs()。大批量迁移时 CPU 被这个循环长时间占住。

RCU-tasks 没有读侧加锁原语,grace period 的判定靠任务自己走到静止状态:自愿上下文切换、执行用户态代码、进 idle 或主动上报都算,被抢占不算。循环里本来安排了 cond_resched() 让出 CPU,但在 CONFIG_PREEMPTION 内核上任何点都可被抢占,它被实现成空操作。于是迁移任务(如 kcompactd)既不做自愿切换也不上报,被挂上 holdout 链表,grace period 被拖到 stall。

大批量迁移把 CPU 钉在 unmap 循环里
┌──────────────────────────────────────┐
│ migrate_pages_batch() 逐 folio unmap │
│ 再搬移                               │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 每个 unmap 跑 mmu_notifier,KVM 宿主 │
│ 走 tdp_mmu_zap_leafs() 很贵          │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ cond_resched() 空转不上报静止状态,  │
│ 任务成 holdout                       │
└──────────────────────────────────────┘

问题

  • migrate_pages_batch() 的 unmap 循环里 cond_resched() 在 PREEMPTION 内核是空操作
  • KVM 宿主上每次 unmap 的 mmu_notifier 回调昂贵,大批量迁移长时间占住 CPU
  • 非自愿抢占不算 RCU-tasks 静止状态,迁移任务一直挂在 holdout 链表
  • Meta 舰队实测 kcompactd 触发 rcu_tasks stall,grace period 被拖至分钟级

方案

把循环里的 cond_resched() 换成 cond_resched_tasks_rcu_qs(),每一轮 unmap 间隙都向 RCU-tasks 上报一次静止状态,不再以发生调度为前提。

每个 unmap 批次间隙主动上报
                     ┌──────────────────────────────────┐
                     │ 换成 cond_resched_tasks_rcu_qs() │
                     └────────────────┬─────────────────┘
                 ┌────────────────────┴────────────────────┐
                 ▼                                         ▼
┌──────────────────────────────────┐     ┌───────────────────────────────────┐
│ rcu_tasks_qs() 清 holdout 标记, │     │ cond_resched() 保留原语义,有需求 │
│ 不等调度发生                     │     │ 才让出 CPU                        │
└──────────────────────────────────┘     └───────────────────────────────────┘

前半段 rcu_tasks_qs() 检查当前任务的 holdout 标记,若已被 RCU-tasks 的 GP kthread 置位就清掉,效果等同声明 grace period 不必再等自己;后半段 cond_resched() 保持原语义,有调度需求时仍然让出 CPU。上报与调度就此解耦,cond_resched() 是不是空操作不再影响判定。

上报点在 unmap 循环的间隙,本来就是 cond_resched() 的合法调用点,未持有锁,安全性与原有调度点一致。

收益

作者未提供性能数据,从代码逻辑推断的预期收益:

  • 大批量迁移不再拖延 RCU-tasks grace period,消除分钟级 stall 告警
  • trampoline 等依赖 RCU-tasks 的回调得以及时执行
  • 无 grace period 时每轮循环仅多一次标记读取,开销可忽略