代理执行 (SCHED_PROXY_EXEC) 解析
Android17-kernel-6.18 已经默认打开SCHED_PROXY_EXEC特性,且应用于mutex和rwsem中,借此机会梳理一下SCHED_PROXY_EXEC特性
一、一句话概述
代理执行(Proxy Execution) 是 Linux 调度器的一项高级特性,它让持有互斥锁(mutex/rwsem)的低优先级任务,在被高优先级任务等待时,「借用」高优先级任务的调度身份去抢占 CPU 运行——从而从根本上解决优先级反转问题。
二、核心问题:什么是优先级反转?
2.1 直观场景
假设系统中有三个任务:
传统调度器的困境:
- L 持有锁却因为优先级低无法获得 CPU(被 M 抢占),无法释放锁
这就是经典的 优先级反转 问题。
2.2 传统方案:
2.2.1 优先级继承(PI)
传统的 RT mutex 采用 优先级继承:
- 当 H 等待 L 持有的锁时,L 临时获得 H 的高优先级
PI 的局限:
2.2.2 其他优先级传递
各个厂商都有针对cfs任务、各类同步机制客制化的优先级传递方案来解决优先级逆置的问题,这里不多赘述。
2.3 代理执行的思路
既然 H 跑不了,那我就让 H 去替 L 跑!让 L 的身体执行 H 想要做的事情。
代理执行的核心洞察:调度上下文和运行上下文可以分离。
- 调度上下文(donor/捐献者):H 任务告诉调度器它应该抢占谁、该运行在哪个 CPU
- 执行上下文(curr/当前执行者)
三、核心概念架构
3.1 rq (runqueue) 结构的分离
代理执行最关键的改变在 struct rq 中:
// 文件: kernel/sched/sched.h#ifdef CONFIG_SCHED_PROXY_EXEC struct task_struct __rcu *donor; /* 调度上下文 - 谁在选择 */ struct task_struct __rcu *curr; /* 执行上下文 - 谁在跑 */#else union { struct task_struct __rcu *donor; /* 没有代理执行时,二者合一 */ struct task_struct __rcu *curr; };#endif
| | |
|---|
donor | | |
curr | | |
| | |
| | 「代驾」模式:donor 是车主选路线,curr 是代驾司机在开 |
3.2 blocked_on 链
代理执行的数据结构定义了一组「阻塞关系链」:
// 文件: include/linux/sched.h:enum blocked_on_type { BO_T_NONE, // 没有阻塞在任何锁上 BO_T_MUTEX, // 阻塞在 mutex 上 BO_T_RWSEM, // 阻塞在 rwsem 上};struct blocked_on_lock { void *lock; // 指向阻塞的锁对象 enum blocked_on_type type; // 锁的类型};
3.3 task_struct 中的代理执行字段
// 文件: include/linux/sched.h:1276-1286struct task_struct { // ===== 所有配置下都有 ===== struct task_struct *blocked_donor; // 正在给我们捐赠调度上下文的那个任务 struct blocked_on_lock blocked_on; // 我们阻塞在哪个锁上 raw_spinlock_t blocked_lock; // 保护上述字段的自旋锁#ifdef CONFIG_SCHED_PROXY_EXEC // ===== 仅代理执行开启时 ===== struct list_head migration_node; // 跨 CPU 迁移时使用的链表节点 struct list_head blocked_head; // 阻塞在我们身上的任务链表(我们是锁持有者) struct list_head blocked_node; // 我们在别人 blocked_head 上的链表节点 struct list_head blocked_activation_node; // 用于级联激活的链表节点 struct task_struct *sleeping_owner; // 当我们 sleep 时,持有我们 blocked_node 的 owner#endif};
3.4 关系的可视化
blocked_donor 指向 ┌─────────────────────────────────┐ │ │ ▼ │┌──────────┐ blocked_on ┌──────────┐│ Task H │ ────────────────► │ mutex ││ (高优先) │ 等待这个锁 │ │└──────────┘ │ owner │ │ │ blocked_head ▼ │ ┌──────────┐ ◄───────── ┌──────────┐ │ │ Task H │ │ Task L │──┘ │ │ H 挂在 L 的 │ (低优先) │ 持有锁 │(blocked │ blocked_head│ │ │ _node) │ 链表上 └──────────┘ └──────────┘ ▲ │ blocked_donor = H ┌────────┴──────┐ │ 调度器视角: │ │ donor = H │ ← 调度器按 H 的优先级排 │ curr = L │ ← 但实际跑的是 L └───────────────┘
关键理解:
- 在 runqueue 中,
donor 指向 H(因为 H 的优先级高,调度器选择运行 H) - 当调度器发现
donor(H)是阻塞的,就追踪 blocked_on → mutex → owner(L),让 L 作为执行上下文去跑
四、核心算法:find_proxy_task()
文件: `kernel/sched/core.c
这是代理执行的灵魂函数。每当 __schedule() 选出了下一个要运行的任务 next,如果 next 是阻塞的(task_is_blocked(next)),就调用 find_proxy_task() 来找到真正应该跑的任务。
4.1 算法流程图
find_proxy_task() 是一个沿着 blocked_on 链向前追踪的循环算法。每轮循环处理链上的一个节点,根据节点状态分支不同的处理路径。
4.1.0 总览:从 __schedule() 到最终执行上下文
__schedule() ┌─ 代理执行关闭: 到此为止 │ │ 直接运行 donor ├► pick_next_task(rq) ─► donor │ ├► rq_set_donor(rq, donor) │ │ │ ├► task_is_blocked(donor) ? │ │ ├─ NO ──────────────────────┘ │ └─ YES ──► find_proxy_task(rq, donor) │ │ │ ├── [入口] for(p=donor; task_is_blocked(p); p=owner) │ │ ↓ │ │ ① 原子快照 blocked_on (copy + locked read) │ │ ↓ │ ╔═══════ [判断分叉: 7种场景] ═══════╗ │ ║ │ ║ A: blocked_on.lock == NULL ──► return NULL (链变了, retry) │ ║ B: PROXY_WAKING ──► proxy_force_return 回迁 │ ║ C: 取锁后链又变了 ──► return NULL (重试) │ ║ D: owner 不存在(NULL) ──► 清除 blocked_on 或 回迁 │ ║ E: owner 不在 rq 上(sleep) ──► 挂到 owner 身上等一起醒 │ ║ F: owner 在另一个 CPU ──► proxy_migrate_task 跨核迁移 │ ║ G: owner 正在迁移中 ──► proxy_resched_idle 避让 │ ║ H: owner == p (并发竞态) ──► proxy_resched_idle 避让 │ ╚═════════════════════════════════╝ │ ↓ [正常路径] │ owner->blocked_donor = p ← 建立代理关系 │ p = owner 继续下一轮循环 │ ↓ │ [循环出口] switch(action): │ MIGRATE → proxy_migrate_task → return NULL (重试) │ NEEDS_RETURN → proxy_force_return → return NULL (重试) │ FOUND → return owner (执行上下文!) ★ 终局 │ └► rq->curr = 返回的执行上下文
三种返回值含义:
| | __schedule() |
|---|
| 合法的 task_struct* | | |
| NULL | | goto pick_again |
| rq->idle | | goto keep_resched |
4.2 子算法详解
4.2.1 proxy_migrate_task() - 跨CPU迁移
// 文件: kernel/sched/core.c// 当 blocked_on 链跨越了 CPU 时使用staticvoidproxy_migrate_task(struct rq *rq, struct rq_flags *rf, struct task_struct *p, int target_cpu)
触发条件: blocked_on 链的 owner 在另一个 CPU 上:pick到的blocked的任务的owner已经在其他cpu上了,迁走blocked_on 链的所有任务到owner cpu上,防止再pick到。 (题外话:这里的设计对于SCHED_EXT有点不友善? SCHED_EXT基于全局DSQ分发,而proxy exec 默认是从percpu的视角考虑pick_task的)核心逻辑:
- 先将当前 rq 的 donor 切为 idle(
proxy_resched_idle) - 沿着
blocked_donor 链,把所有阻塞任务从当前 rq 上 deactivate - 重新获取原 rq 锁,回到
__schedule() 重新选择
设计理念: 调度上下文可以忽略 CPU 亲和性,但执行上下文必须尊重亲和性。所以要把调度上下文「搬」到执行上下文所在的 CPU。
4.2.2 proxy_force_return() - 强制返回迁移
// 文件: kernel/sched/core.c// 当任务被标记为 PROXY_WAKING 时使用staticvoidproxy_force_return(struct rq *rq, struct rq_flags *rf, struct task_struct *p)
触发条件: 在放锁路径会给current->blocked_donor设置 PROXY_WAKING,blocked_donor可能被跨cpu迁移至current所在的cpu,因此现在需要被「回迁」到它原本应该运行的 CPU。
PROXY_WAKING 的含义:
#define PROXY_WAKING ((struct mutex *)(-1L))
这是一个特殊的「哨兵值」,放在 blocked_on.lock 中,表示「不要直接运行我,先把我送回老家」。它不等于任何真实 mutex 的地址(-1 不是合法指针),所以可以安全地用作特殊标记。
流程:
- 重新获取 task_rq_lock(带 pi_lock)
- 调用
select_task_rq() 选择正确的目标 CPU
4.2.3 Sleeping Owner 机制
// 文件: kernel/sched/core.cstaticvoidproxy_enqueue_on_owner(struct rq *rq, struct task_struct *owner, struct task_struct *p)
触发条件: blocked_on 链的锁持有者(owner)当前不在 runqueue 上(sleeping), owner 可能sleep阻塞等待在其他不支持代理执行的同步机制。
核心做法:
- 把等待者 p 挂到
owner->blocked_head 链表上 - 当 owner 未来被唤醒时,
activate_blocked_waiters() 会顺便把 p 也一起激活
这是一个巧妙的「延迟批量激活」策略——避免为每个等待者单独处理,而是跟着 owner 一起唤醒。
4.2.4 activate_blocked_waiters() - 级联激活
// 文件: kernel/sched/core.cstaticvoidactivate_blocked_waiters(struct rq *target_rq, struct task_struct *owner, int wake_flags)
触发时机: 当一个锁持有者被唤醒时(在 ttwu_do_activate() 中调用)。
核心逻辑(「公平份额」机制):
- 把
owner->blocked_head 上挂的所有等待任务都摘下来 - 关键: 被激活的任务自己也可能是某个锁的持有者,它们自己的
blocked_head 上也挂着任务 - 所以需要继续检查
p->blocked_head,如果非空,把 p 也加入处理队列
owner 被唤醒 │ ├► 激活 owner->blocked_head 上的 T1 │ │ │ └► T1 自己也有 blocked_head → 加入处理队列 │ │ │ ├► 激活 T1->blocked_head 上的 T2 │ └► 激活 T1->blocked_head 上的 T3 │ ├► 激活 owner->blocked_head 上的 T4 │ └► ...
4.2.5 proxy_resched_idle() - 切回 idle 重试
// 文件: kernel/sched/core.c:7223-7229static inline struct task_struct *proxy_resched_idle(struct rq *rq){ put_prev_set_next_task(rq, rq->donor, rq->idle); rq_set_donor(rq, rq->idle); set_tsk_need_resched(rq->idle); return rq->idle;}
用途: 当 find_proxy_task() 遇到暂时无法处理的情况(如 owner 正在迁移、owner == p 竞态),不强行处理,而是切到 idle 然后重新调度,给系统一个「喘息」的机会。
五、锁子系统的集成
5.0 核心问题:锁和调度器怎么联动?
回顾代理执行的核心设计:调度器通过 blocked_on 链来追踪「谁在等谁」,然后让锁持有者「替」等待者执行。但这个 blocked_on 信息是谁设置的?答案是:锁子系统自己。
每次一个任务尝试获取锁失败时,它主动告诉调度器:「嘿,我阻塞在我这把 mutex 上了,owner 是张三,帮我找张三去跑!」
5.1 Mutex 与代理执行
5.1.1 数据结构回顾
Mutex 内部用 atomic_long_t owner 存储锁持有者,低 3 位是标志位:
| |
|---|
MUTEX_FLAG_WAITERS | |
MUTEX_FLAG_HANDOFF | |
MUTEX_FLAG_PICKUP | |
// 文件: kernel/locking/mutex.h// 通过掩码 ~MUTEX_FLAGS 从 atomic_long 中提取出 task_struct 指针static inline struct task_struct *__mutex_owner(struct mutex *lock){ if (!lock) return NULL; return (struct task_struct *)(atomic_long_read(&lock->owner) & ~MUTEX_FLAGS);}
5.1.2 Mutex Lock 路径:如何通知调度器
当高优先级任务 H 尝试获取 mutex 而失败时,__mutex_lock_common() 慢路径被调用。以下是关键代码流程(kernel/locking/mutex.c):
mutex_lock(lock) │ ├► 快速路径: __mutex_trylock_fast(lock) → 成功则返回 │ └► 慢速路径: __mutex_lock_slowpath(lock) → __mutex_lock_common(lock, ...) │ ├► 乐观自旋: mutex_optimistic_spin() │ 在释放 CPU 前先自旋等一会(万一 owner 很快释放呢) │ ├► 获取 wait_lock 自旋锁 │ ├► 将自己加入 wait_list 等待队列 │ ├► ★★★ 关键步骤 ★★★ │ raw_spin_lock(¤t->blocked_lock); │ __set_task_blocked_on(current, lock, BO_T_MUTEX); │ set_current_state(TASK_UNINTERRUPTIBLE); │ ↑ │ |-- 告诉调度器: "我现在阻塞在 lock 这个 mutex 上" │ | 类型是 BO_T_MUTEX │ | blocked_lock 保护这个信息不被并发访问破坏 │ ├► 等待循环: │ for (;;) { │ if (__mutex_trylock(lock)) // 尝试抢锁 │ break; │ │ 释放 blocked_lock │ 释放 wait_lock │ │ schedule_preempt_disabled(); // ★ 让出CPU给调度器 │ // 调度器此时通过 find_proxy_task() │ // 发现 H 的 blocked_on → 该mutex → owner L │ // 然后让 L 去跑! │ │ 重新获取锁: │ raw_spin_lock(&wait_lock); │ raw_spin_lock(¤t->blocked_lock); │ __set_task_blocked_on(current, lock, BO_T_MUTEX); │ // ↑ 重新设置 blocked_on (可能被清过) │ │ if (__mutex_trylock_or_handoff(lock, first)) │ break; │ │ // 如果是第一个等待者,可以乐观自旋 │ if (first) { │ __clear_task_blocked_on(current, lock); │ // ↑ 暂时清除 blocked_on,不让调度器误判我们不可选 │ opt = mutex_optimistic_spin(lock, &waiter); │ __set_task_blocked_on(current, lock, BO_T_MUTEX); │ // ↑ 自旋完了重新设置 │ } │ } │ └► 获取到锁: __clear_task_blocked_on(current, lock); // ★ 清除 blocked_on __set_current_state(TASK_RUNNING);
5.1.3 Mutex Unlock 路径:「谁来接手这把锁?」
这是代理执行最精妙的部分。释放锁时,mutex 不再简单地唤醒等待队列的第一个人,而是先看 「调度器选了谁作为最高优先级的等待者」(kernel/locking/mutex.c):
mutex_unlock(lock) │ ├► 快速路径: __mutex_unlock_fast(lock) │ 没有等待者 → 直接清零 owner → 返回 │ └► 慢速路径: __mutex_unlock_slowpath(lock) │ ├► ★ 步骤1: 判断是否需要 HANDOFF │ for (;;) { │ if (sched_proxy_exec() && current->blocked_donor) { │ // ★ 关键:如果当前任务有 blocked_donor │ // (即: 有一个高优先任务正在替我们「导航」) │ // 强制进入 HANDOFF 流程 │ owner = MUTEX_FLAG_HANDOFF; │ break; │ } │ if (owner & MUTEX_FLAG_HANDOFF) break; │ // 尝试原子释放... │ } │ ├► 获取 wait_lock 和 blocked_lock │ ├► ★ 步骤2: 优先唤醒 blocked_donor (代理执行核心!) │ if (sched_proxy_exec()) { │ raw_spin_lock(¤t->blocked_lock); │ │ donor = current->blocked_donor; │ // donor 就是那个「替我们导航」的高优先级任务 │ │ if (donor) { │ // 检查 donor 确实是在等这把锁 │ next_lock = __get_task_blocked_on(donor); │ if (next_lock == lock) { │ // ★ 好!donor 确实等了这把锁,让它接手 │ next = donor; │ __set_task_blocked_on_waking(donor, next_lock); │ // ↑ 设置 PROXY_WAKING 标记 │ // donor 醒来时会先被送回它该去的CPU │ wake_q_add(&wake_q, donor); │ current->blocked_donor = NULL; │ // ↑ 断开代理关系 │ } │ } │ } │ │ // 如果没有 blocked_donor,才走传统等待队列 │ if (!next && !list_empty(&lock->wait_list)) { │ waiter = list_first_entry(&lock->wait_list, ...); │ next = waiter->task; │ __set_task_blocked_on_waking(next, lock); │ wake_q_add(&wake_q, next); │ } │ ├► 如果需要 HANDOFF,使用 __mutex_handoff() 原子转移 owner │ └► raw_spin_unlock_irqrestore_wake(&lock->wait_lock, &wake_q); // 批量唤醒 wake_q 中的任务
5.2 RWSEM 与代理执行
5.2.1 与 Mutex 的关键差异
RWSEM (读写信号量) 的代理执行集成比 mutex 简单,核心差异在于:
| | |
|---|
| | 仅当 writer 持有时 |
| | |
| | 仅当 rwsem 被 writer 持有时设置 |
| | 不优先唤醒 blocked_donor |
| | |
RWSEM 只支持 writer owner 代理执行。 读者持有时没有明确的单一 owner,所以不触发代理执行。
5.2.2 Reader Lock 路径的代理执行
// 文件: kernel/locking/rwsem.c (read lock 等待循环)for (;;) { // ... 检查 wakeup ... if (atomic_long_read(&sem->count) & RWSEM_WRITER_MASK) { // ★ 只有当 rwsem 被 writer 持有时才设置 blocked_on raw_spin_lock_irq(¤t->blocked_lock); __set_task_blocked_on(current, sem, BO_T_RWSEM); raw_spin_unlock_irq(¤t->blocked_lock); blocked_on_set = true; } schedule_preempt_disabled(); // 让调度器介入 // ... if (blocked_on_set) { clear_task_blocked_on(current, sem); blocked_on_set = false; }}
关键设计: 每次 schedule() 前检查 RWSEM_WRITER_MASK。如果 writer 已经释放了(count 中没有 writer bit),就不设 blocked_on。这保证了只有实际的 writer-owner 阻塞场景才触发代理执行,读者间的正常并发不受影响。
5.2.3 Writer Lock 路径的代理执行
// 文件: kernel/locking/rwsem.c (write lock 等待循环)if (atomic_long_read(&sem->count) & RWSEM_WRITER_MASK) { raw_spin_lock_irq(¤t->blocked_lock); __set_task_blocked_on(current, sem, BO_T_RWSEM); blocked_on_set = true; raw_spin_unlock_irq(¤t->blocked_lock);}schedule_preempt_disabled();set_current_state(state);if (blocked_on_set) { clear_task_blocked_on(current, sem); blocked_on_set = false;}
Writer 和 reader 的逻辑一致——schedule() 前设置 blocked_on,醒来后清除。
5.2.4 Owner 解析:rwsem_writer_owner()
// 文件: kernel/locking/rwsem.cstruct task_struct *rwsem_writer_owner(struct rw_semaphore *sem){ struct task_struct *owner; long count, flags; count = atomic_long_read(&sem->count); owner = rwsem_owner_flags(sem, &flags); if (!(count & RWSEM_WRITER_MASK) || (flags & (RWSEM_READER_OWNED | RWSEM_NONSPINNABLE))) return NULL; return owner;}
三个拒绝条件:
RWSEM_WRITER_MASKRWSEM_READER_OWNEDRWSEM_NONSPINNABLE
当 __blocked_on_owner() 被 find_proxy_task() 调用时,如果返回 NULL,说明 owner 不存在/不是 writer,则触发 action = NEEDS_RETURN——把等待者送回它自己的 CPU。
六、总结
代理执行的本质
代理执行把「调度器选谁」和「CPU 上谁在跑」这两个概念解耦。当一个高优先级任务因锁而阻塞时,调度器让锁持有者「代它跑」,从而消除优先级反转的根本原因——不是提升持有者的优先级,而是直接让持有者以等待者的调度身份去执行。
关键数据流
高优先级任务 H 阻塞在 mutex 上 ↓ H->blocked_on = { mutex, BO_T_MUTEX } ↓__schedule() → pick_next_task() → 选中 H(因为它优先级高) ↓task_is_blocked(H) == true ↓find_proxy_task(H): H->blocked_on → mutex → owner = L(锁持有者) ↓ L 就是执行上下文(curr) ↓ H = donor(调度上下文),L = curr(执行上下文) ↓ L 运行、释放锁 → wake H ↓ H 拿回自己的调度上下文,正常调度