2. runqueue 数据结构:struct rq 核心字段解析

runqueue(就绪队列)是 Linux 内核调度器的核心数据结构,每个 CPU 都拥有一个独立的 runqueue 实例。struct rq 定义在 kernel/sched/sched.h 中,它聚合了该 CPU 上所有调度类(CFS、RT、DL)的队列、统计信息、负载跟踪以及调度控制所需的全部状态。理解 struct rq 的布局是掌握调度器运行机制的第一步。

2.1 struct rq 核心字段一览

下表列出了 struct rq 中最关键的字段及其作用:

字段名 类型 作用描述
nr_running unsigned int 当前 CPU 上所有调度类中处于 TASK_RUNNING 状态的进程总数,是调度器判断是否空闲的首要指标。
load struct load_weight 当前 CPU 的瞬时负载权重,由所有可运行进程的权重累加得到,用于负载均衡决策。
cfs struct cfs_rq CFS 调度类的就绪队列,管理普通进程(SCHED_NORMAL/SCHED_BATCH)的红黑树和虚拟运行时间。
rt struct rt_rq 实时调度类的就绪队列,管理实时进程(SCHED_FIFO/SCHED_RR)的优先级数组。
dl struct dl_rq Deadline 调度类的就绪队列,管理截止时间调度进程的红黑树和带宽控制。
curr struct task_struct * 指向当前正在 CPU 上执行的进程描述符。
idle struct task_struct * 指向该 CPU 的空闲进程(idle task),当 nr_running 为 0 时运行。
clock u64 该 CPU 的调度时钟,用于计算进程运行时间和调度延迟。
nr_switches unsigned long 该 CPU 上发生的上下文切换次数统计。
pushable_tasks struct list_head 可被推送到其他 CPU 的实时/Deadline 任务链表,用于负载均衡。
关键理解: nr_running 和 load 是 runqueue 的"晴雨表"。调度器在每次 tick 中断和进程唤醒时都会更新这两个字段,负载均衡器则根据它们判断是否需要迁移任务。

2.2 per-CPU runqueue 布局

Linux 内核使用 per-CPU 变量机制为每个 CPU 分配独立的 struct rq 实例。这种设计避免了多核并发访问同一队列时的锁竞争,是调度器可扩展性的基石。

per-CPU runqueue 的布局特点:

  • 独立存储: 每个 CPU 的 runqueue 存放在该 CPU 的本地内存(L1/L2 cache)附近,减少缓存一致性开销。
  • 直接访问: 通过 this_rq() 宏获取当前 CPU 的 runqueue,通过 cpu_rq(cpu) 获取指定 CPU 的 runqueue。
  • 锁机制: 每个 runqueue 拥有自己的自旋锁(rq->lock),操作本 CPU 队列时只需锁定本地锁。
  • 负载均衡例外: 当需要跨 CPU 迁移任务时,必须同时锁定源和目标 CPU 的 runqueue 锁,避免死锁。
per-CPU 声明方式(代码片段):
// 定义 per-CPU runqueue 变量
DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);

// 获取当前 CPU 的 runqueue
#define this_rq()       this_cpu_ptr(&runqueues)

// 获取指定 CPU 的 runqueue
#define cpu_rq(cpu)     per_cpu_ptr(&runqueues, cpu)

下图展示了 per-CPU runqueue 在双核系统中的布局以及各调度子队列的关系:

per-CPU runqueue 布局与调度子队列关系 CPU 0 - runqueue nr_running = 3 load.weight = 2048 cfs_rq (CFS 调度类) 红黑树根节点: rb_root min_vruntime = 1024000 进程数: 2 (bash, nginx) rt_rq (实时调度类) 优先级位图: bitmap 进程数: 1 (实时音频进程) dl_rq (Deadline 调度类) 红黑树: dl_rb_root 进程数: 0 (空闲) CPU 1 - runqueue nr_running = 1 load.weight = 1024 cfs_rq (CFS 调度类) 红黑树根节点: rb_root min_vruntime = 2048000 进程数: 1 (sshd) rt_rq (实时调度类) 优先级位图: 全 0 进程数: 0 dl_rq (Deadline 调度类) 红黑树: 空 进程数: 0 每个 CPU 独立拥有 runqueue,包含 cfs/rt/dl 三个子队列,通过 nr_running 和 load 反映整体负载

2.3 nr_running 与 load 的更新机制

nr_running 和 load 是调度器最频繁更新的字段,它们的更新路径如下:

事件 nr_running 变化 load 变化 触发函数
进程被唤醒(wake_up) +1 增加进程权重 activate_task() → enqueue_task()
进程主动睡眠(sleep) -1 减少进程权重 deactivate_task() → dequeue_task()
进程被抢占(preempt) 不变 不变 调度器 tick 检查
进程退出(exit) -1 减少进程权重 sched_exit()
跨 CPU 迁移(migrate) 源 -1,目标 +1 源减,目标加 migrate_task()
注意: nr_running 只统计 TASK_RUNNING 状态的进程,不包括 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 状态的进程。当 nr_running 为 0 时,调度器会切换到 idle 进程运行,CPU 进入低功耗状态。

2.4 调度子队列的协作关系

struct rq 中的 cfs、rt、dl 三个子队列并非独立运行,它们通过调度器核心的 pick_next_task() 函数按优先级顺序选择下一个要运行的进程:

  1. Deadline 优先: 首先检查 dl_rq 是否有可运行的 deadline 进程,如果有则优先选择。
  2. 实时进程次之: 如果 dl_rq 为空,则检查 rt_rq 中优先级最高的实时进程。
  3. CFS 兜底: 如果 dl_rq 和 rt_rq 都为空,则从 cfs_rq 的红黑树中选择虚拟运行时间最小的进程。
  4. 空闲进程: 如果所有子队列都为空,则运行 idle 进程。

下图展示了 pick_next_task 的决策流程:

pick_next_task() 调度优先级决策流程 pick_next_task() dl_rq 是否有可运行进程? 是 选择 dl 进程 否 rt_rq 有进程? 是 选择 rt 进程 否 cfs_rq 有进程? 是 选择 cfs 进程 否 运行 idle 进程 优先级顺序:Deadline > 实时 > CFS > idle

2.5 runqueue 的锁保护与并发访问

在多核系统中,runqueue 的并发访问必须通过锁来保护。struct rq 包含一个自旋锁 rq->lock,其使用规则如下:

  • 本地访问: 操作当前 CPU 的 runqueue 时,只需获取本地 rq->lock,开销较小。
  • 跨 CPU 访问: 负载均衡或任务迁移时,需要同时锁定源和目标 CPU 的 runqueue,且必须按照 CPU 编号顺序加锁以避免死锁。
  • 中断上下文: 在中断处理函数中访问 runqueue 时,需要关闭本地中断(local_irq_disable)并获取锁,防止中断嵌套导致死锁。
  • 双锁协议: 使用 double_lock_balance() 函数安全地获取两个 runqueue 的锁。
双锁加锁示例(代码片段):
// 负载均衡时锁定两个 runqueue
static int double_lock_balance(struct rq *this_rq, struct rq *busiest)
{
    if (this_rq < busiest) {
        raw_spin_lock_nested(&this_rq->lock, SINGLE_DEPTH_NESTING);
        raw_spin_lock_nested(&busiest->lock, SINGLE_DEPTH_NESTING);
    } else {
        raw_spin_lock_nested(&busiest->lock, SINGLE_DEPTH_NESTING);
        raw_spin_lock_nested(&this_rq->lock, SINGLE_DEPTH_NESTING);
    }
    return 0;
}

2.6 本章小结

struct rq 是调度器在 per-CPU 粒度的核心数据结构,它通过 nr_running 和 load 提供瞬时负载快照,通过 cfs/rt/dl 三个子队列实现不同调度类的进程管理,并通过 rq->lock 保证并发安全。理解 runqueue 的布局和字段含义,是后续分析调度器核心流程(如进程入队、出队、负载均衡)的基础。