当前位置:首页>Linux>linux内核——调度系统——代理执行 (SCHED_PROXY_EXEC)

linux内核——调度系统——代理执行 (SCHED_PROXY_EXEC)

  • 2026-10-11 06:03:46
linux内核——调度系统——代理执行 (SCHED_PROXY_EXEC)

代理执行 (SCHED_PROXY_EXEC) 解析

Android17-kernel-6.18 已经默认打开SCHED_PROXY_EXEC特性,且应用于mutex和rwsem中,借此机会梳理一下SCHED_PROXY_EXEC特性


一、一句话概述

代理执行(Proxy Execution) 是 Linux 调度器的一项高级特性,它让持有互斥锁(mutex/rwsem)的低优先级任务,在被高优先级任务等待时,「借用」高优先级任务的调度身份去抢占 CPU 运行——从而从根本上解决优先级反转问题。


二、核心问题:什么是优先级反转?

2.1 直观场景

假设系统中有三个任务:

任务
优先级
状态
任务 H(高优先级)
高
等待 mutex(被 L 持有)
任务 M(中优先级)
中
就绪,可以运行
任务 L(低优先级)
低
持有 mutex,但被 M 抢占了

传统调度器的困境:

  • H 被阻塞在 mutex 上,无法运行
  • L 持有锁却因为优先级低无法获得 CPU(被 M 抢占),无法释放锁
  • M 这个「中间人」一直跑,让 H 永远等待

这就是经典的 优先级反转 问题。

2.2 传统方案:

2.2.1 优先级继承(PI)

传统的 RT mutex 采用 优先级继承:

  • 当 H 等待 L 持有的锁时,L 临时获得 H 的高优先级
  • 这样 L 可以抢占 M,快速完成临界区并释放锁

PI 的局限:

  • PI 只影响优先级,不改变「谁在跑」这个根本问题

  • 对于普通 mutex(非 RT),内核默认不做 PI

  • PI 只能往上提,不能解决复杂的多级依赖链

2.2.2 其他优先级传递

各个厂商都有针对cfs任务、各类同步机制客制化的优先级传递方案来解决优先级逆置的问题,这里不多赘述。

2.3 代理执行的思路

既然 H 跑不了,那我就让 H 去替 L 跑!让 L 的身体执行 H 想要做的事情。

代理执行的核心洞察:调度上下文和运行上下文可以分离。

  • 调度上下文(donor/捐献者)
    :H 任务告诉调度器它应该抢占谁、该运行在哪个 CPU
  • 执行上下文(curr/当前执行者)
    :L 任务实际在 CPU 上运行的代码

三、核心概念架构

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
执行上下文——实际在 CPU 上跑的任务
谁踩了油门(实际开车的人)
无代理执行
二者是同一个任务
自己开车自己导航
有代理执行
二者可以是不同任务
「代驾」模式: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)
  • 但实际在 CPU 上执行的 curr 是 L
  • 当调度器发现 donor(H)是阻塞的,就追踪 blocked_on → mutex → owner(L),让 L 作为执行上下文去跑
  • L 跑完之后释放 mutex,H 被唤醒

四、核心算法: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
暂时让给 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的)核心逻辑:

  1. 先将当前 rq 的 donor 切为 idle(proxy_resched_idle)
  2. 沿着 blocked_donor 链,把所有阻塞任务从当前 rq 上 deactivate
  3. 释放当前 rq 锁
  4. 获取目标 CPU 的 rq 锁
  5. 在目标 rq 上重新 activate 这些任务
  6. 重新获取原 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 不是合法指针),所以可以安全地用作特殊标记。

流程:

  1. 释放原 rq 锁
  2. 重新获取 task_rq_lock(带 pi_lock)
  3. 检查中间是否有变化(任务可能已经被其他人处理了)
  4. 在正确的 rq 上 deactivate 任务
  5. 调用 select_task_rq() 选择正确的目标 CPU
  6. 在目标 rq 上 activate 任务
  7. 重新获取原 rq 锁返回

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阻塞等待在其他不支持代理执行的同步机制。

核心做法:

  1. 不着急叫醒 sleeping 的 owner
  2. 把等待者 p 挂到 owner->blocked_head 链表上
  3. 让 p 也进入 blocked 状态
  4. 当 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() 中调用)。

核心逻辑(「公平份额」机制):

  1. 把 owner->blocked_head 上挂的所有等待任务都摘下来
  2. 依次激活这些任务
  3. 关键:
     被激活的任务自己也可能是某个锁的持有者,它们自己的 blocked_head 上也挂着任务
  4. 所以需要继续检查 p->blocked_head,如果非空,把 p 也加入处理队列
  5. 这是一个 BFS 广度优先 的级联激活过程
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
 (0x01)
有任务在等待队列上
MUTEX_FLAG_HANDOFF
 (0x02)
需要将锁转交给等待者
MUTEX_FLAG_PICKUP
 (0x04)
HANDOFF 已做,等待等待者来取锁
// 文件: 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(&current->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(&current->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(&current->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 简单,核心差异在于:

方面
Mutex
RWSEM
阻塞条件
总有明确的 task_struct owner
仅当 writer 持有时
有明确 owner
读者阻塞
-
读者间可并发,不需代理执行
blocked_on 设置时机
获取锁失败即设置
仅当 rwsem 被 writer 持有时设置
unlock 端
优先唤醒 blocked_donor
不优先唤醒 blocked_donor
HANDOFF 机制
有 PICKUP/HANDOFF 标志位
有独立 HANDOFF 标志位但无 PICKUP

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(&current->blocked_lock);        __set_task_blocked_on(current, sem, BO_T_RWSEM);        raw_spin_unlock_irq(&current->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(&current->blocked_lock);    __set_task_blocked_on(current, sem, BO_T_RWSEM);    blocked_on_set = true;    raw_spin_unlock_irq(&current->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;}

三个拒绝条件:

  1. RWSEM_WRITER_MASK
     未设置 → 没有 writer,返回 NULL
  2. RWSEM_READER_OWNED
     已设置 → 是读者持有,返回 NULL
  3. RWSEM_NONSPINNABLE
     已设置 → 不可自旋的读者持有,返回 NULL

当 __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 拿回自己的调度上下文,正常调度

最新文章

随机文章