在Linux中,task_struct 用于描述一个任务(进程或线程)的完整状态,而 rq(就绪队列)则是调度器用来管理“可运行任务”的核心数据结构。两者紧密配合:task_struct 描述“谁”,rq 决定“何时运行谁”。既然 rq 决定了哪个任务运行,那么它从哪里获取候选任务?当多个任务就绪时,谁先谁后又是如何确定的呢?
本系列文章将围绕这两个核心问题展开探讨。磨刀不误砍柴工,我们先了解一下就绪队列。
什么是就绪队列?
在kernel\sched\core.c中,rq 被定义为Per-CPU变量。
DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);这意味着,在多核 CPU 系统中,会为每个 CPU 分配一个独立的就绪队列(rq)实例。从而实现调度本地化:避免多核竞争,支持无锁或低开销调度,显著提升并发性能与可扩展性。
可以用NanCode的dt命令查看rq结构,结果如下:
dt lk!rq
//调度核心状态
+0x018 nr_running : Uint4B // 可运行任务总数
+0x070 nr_switches : Uint8B // 发生的上下文切换总次数
+0xb00 nr_uninterruptible : Uint4B // 处于 uninterruptible 睡眠状态的任务数
//当前运行任务与特殊任务
+0xb08 curr : Ptr64 task_struct // 当前正在该 CPU 上执行的任务
+0xb10 idle : Ptr64 task_struct // 该 CPU 对应的 idle 任务(swapper/X)
+0xb18 stop : Ptr64 task_struct // stop 任务(用于 CPU 紧急操作,如热插拔)
//调度时钟
+0xb38 clock : Uint8B // 本 CPU 的调度时钟(纳秒)
+0xb40 clock_task : Uint8B // 仅在非 idle 任务运行时推进的时钟
//调度子队列
+0x200 cfs : cfs_rq //CFS(完全公平调度器)队列,管理普通进程/线程
+0x3c0 rt : rt_rq //实时任务队列
+0xa88 dl : dl_rq //截止时间调度队列
//一些链表结构
+0xae8 leaf_cfs_rq_list : list_head // 叶子 CFS 运行队列列表
+0xaf8 tmp_alone_branch : Ptr64 list_head // 临时孤立分支(用于负载均衡)
+0xbf8 cfs_tasks : list_head // 当前 CPU 上所有可运行的 CFS 任务链表其中curr这个指针指向了在当前cpu上真正运行着的task_struct,调度的时候会改变这个指针的指向,指向谁呢?下面探讨第一个关键问题:调度器从哪里获取候选任务?
候选任务从哪来?
要弄清这个问题,不妨先看一下调度器的核心函数__schedule(),下面是这个函数的简化版代码:
static void __sched notrace __schedule(unsigned int sched_mode)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
unsigned long prev_state;
struct rq_flags rf;
struct rq *rq;
int cpu;
cpu = smp_processor_id();
rq = cpu_rq(cpu); //获取当前 CPU 的 rq
prev = rq->curr; // 当前正在运行的任务
/* 关键步骤 1:选择下一个要运行的任务 */
next = pick_next_task(rq, prev, &rf);
/* 关键步骤 2:如果需要切换 */
if (likely(prev != next)) {
rq->nr_switches++;
switch_count = &prev->nivcsw; // 非自愿上下文切换计数
/* 执行架构相关的上下文切换 */
context_switch(rq, prev, next, &rf);
} else {
rq->clock_update_flags &= ~(RQCF_ACT_SKIP | RQCF_REQ_SKIP);
rq_unlock_irq(rq, &rf);
}简单解释一下这个过程:先把当前任务用prev存起来,然后调用pick_next_task()来选出下一个任务,最后在context_switch()执行上下文切换的时候,会修改curr,指向选出的下一个任务。
pick_next_task()里能会通过各个子队列来获取task_struct。
各个子队列中并不直接存放 task_struct,而是通过调度实体与 task_struct 建立关联。调度实体是与具体调度策略绑定的数据结构,不同调度策略可能在数据结构设计、排序依据以及关键调度参数等方面存在差异,自然各个调度队列会有独特的调度实体。
我用一张图来表示它们的关系:

• cfs_rq (调度实体结构:struct sched_entity)
cfs_rq ←→ struct rb_node (in sched_entity) ←→ struct sched_entity ←[内嵌有]— task_struct• rt_rq(调度实体结构:struct sched_rt_entity)
rt_rq ←→ rt_rq->active.queue[prio](struct list_head) ←→ struct sched_rt_entity ←[内嵌有]— task_struct• dl_rq(调度实体结构:struct sched_dl_entity)
dl_rq ←→ struct rb_node (in sched_dl_entity) ←→ struct sched_dl_entity ←[内嵌有]— task_struct调度器通过 container_of() 宏,从调度实体反向定位到所属的 task_struct,从而完成“从队列节点 → 实体 → 任务”的完整链路。
此外,我们注意到:在 rq 结构体中还包含两个特殊的 task_struct 指针:
+0xb10 idle : Ptr64 task_struct
+0xb18 stop : Ptr64 task_struct它们会参与调度吗?答案是:会的。
• Idle 任务
当前 CPU 的 CFS、RT、DL 队列全为空,即系统空闲时运行,该任务负责使 CPU 进入低功耗状态• Stop 任务
当有紧急的 per-CPU 操作(例如 CPU 热插拔、任务强制迁移或 TLB 批量刷新)被提交到该 CPU 的 stopper 队列时,rq->stop 任务会被激活。
现在,回到我们的第一个问题:就绪队列从何处获取候选任务?你应该也有了答案。候选任务来源于当前 CPU 的 rq中多个调度子队列以及两个特殊任务。
调度类的优先级顺序
既然就绪队列能从多处获取候选任务,那么又诞生了新的问题:这几处的优先级是如何的?
pick_next_task()中藏着答案(它是调度函数__schedule()中,用于选取下一个任务的函数,你不会忘了吧)。
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
//【关键!】按调度类优先级从高到低依次尝试:
// 一旦某个调度类返回非空任务,立即返回,不再检查更低优先级类。
for_each_class(class) {
/* 调用当前调度类注册的具体选任务函数 */
p = class->pick_next_task(rq, prev, rf);
if (p)
return p;
}
BUG();
}本段的主角——调度类正式登场。首先,让我们认识一下这位核心角色:以下是内核中定义的调度类(sched_class)基类的结构体:
struct sched_class {
// === 任务队列管理 ===
void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags); // 入队
void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags); // 出队
// === 抢占与调度核心 ===
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags); // 检查是否抢占当前任务
struct task_struct *(*pick_next_task)(struct rq *rq); // 【核心】选出下一个运行的任务
// === 任务切换回调 ===
void (*put_prev_task)(struct rq *rq, struct task_struct *p); // 切出前调用
void (*set_next_task)(struct rq *rq, struct task_struct *p, bool first); // 切入后调用
// === 主动让出 CPU ===
void (*yield_task)(struct rq *rq); // 当前任务主动让出
bool (*yield_to_task)(struct rq *rq, struct task_struct *p); // 尝试让给指定任务
// === 时间片与统计 ===
void (*task_tick)(struct rq *rq, struct task_struct *p, int queued); // 时钟滴答处理
void (*update_curr)(struct rq *rq); // 更新当前任务运行统计
// === 生命周期 ===
void (*task_fork)(struct task_struct *p); // 任务 fork 后初始化
void (*task_dead)(struct task_struct *p); // 任务退出前清理
// === 调度类切换通知 ===
void (*switched_from)(struct rq *this_rq, struct task_struct *task); // 任务从此类切出
void (*switched_to)(struct rq *this_rq, struct task_struct *task); // 任务切入此类
void (*prio_changed)(struct rq *this_rq, struct task_struct *task, int oldprio); // 优先级变更在 Linux 调度器中,每个调度策略(CFS、RT、DL 等)都必须实现一个 sched_class 实例,注册一组回调函数。通用调度框架通过这些函数与具体策略交互,而无需知道内部细节。
以下是各调度类的具体实例说明:
调度类(sched_class) | 用途 | 特点 | 典型场景 | 核心数据结构 |
stop_sched_class | 内核高优先级同步操作 | 优先级最高,立即抢占;每 CPU 一个 stop 任务 | CPU 热插拔、任务迁移、TLB 刷新 | task_struct( |
dl_sched_class | 强实时任务 | 基于 EDF,严格保证截止时间 | 自动驾驶、飞行控制、高清视频流 | struct sched_dl_entity |
rt_sched_class | 实时任务 | 固定优先级(1–99),FIFO/RR 调度 | 工业控制、音视频采集 | struct sched_rt_entity |
fair_sched_class | 普通进程 | CFS 公平调度,基于 vruntime | 桌面应用、Web 服务 | struct sched_entity |
idle_sched_class | 极低优先级任务 | 仅在无其他可运行任务时执行 | 日志归档、后台维护 | task_struct( |
调度类也是linux中“面向对象思想”的典型体现。尽管 C 语言本身不支持 class、继承、多态等语法,但内核通过结构体 + 函数指针 + 接口约定,巧妙地实现了面向对象的核心特性。
比如核心函数 pick_next_task():
遍历调度类调用class->pick_next_task(rq),实际行为由具体调度类(如 CFS、RT、DL)的实现决定——这正是运行时多态。新增调度策略只需实现一套回调并注册,无需改动调度核心,实现了高内聚、低耦合与良好扩展性。
调度类中 pick_next_task()用于选择下一个任务,那么遍历调度类的顺序则决定了优先级,顺序在前的调度类,它的pick_next_task()会被优先调用,优先级就更高。
• 链接器脚本的SCHED_DATA宏定义了调度类顺序:
/*
* The order of the sched class addresses are important, as they are
* used to determine the order of the priority of each sched class in
* relation to each other.
*/
#define SCHED_DATA \
STRUCT_ALIGN(); \
__sched_class_highest = .; \
*(__stop_sched_class) \
*(__dl_sched_class) \
*(__rt_sched_class) \
*(__fair_sched_class) \
*(__idle_sched_class) \
__sched_class_lowest = .;这段代码揭示了Linux调度类优先级机制的秘密:优先级不是靠数据结构,而是由链接时的内存布局硬编码实现。在内核链接阶段,各调度类被强制按优先级从高到低(stop → dl → rt → fair → idle)连续排列在内存中。
下面是是一些方便编程的遍历宏:
/* 链接器定义的调度类地址边界(在 vmlinux.lds.h 中) */
extern struct sched_class __sched_class_highest[];
extern struct sched_class __sched_class_lowest[];
/* 从 _from 到 _to遍历调度类 */
#define for_class_range(class, _from, _to) \
for (class = (_from); class < (_to); class++)
/* 遍历所有调度类:按 stop → dl → rt → fair → idle 顺序 */
#define for_each_class(class) \
for_class_range(class, __sched_class_highest, __sched_class_lowest)比如,通过指针遍历(for_each_class)宏,就可以实现“先问高优,有就运行”的调度逻辑。
至此,我们不仅认识了调度类,也清楚了调度优先级的实现原理。
NanoCode插件开发推进
在梳理 Linux 内核调度机制的同时,我也持续推进 NanCode 插件的开发。本次新增的扩展命令为 !ndx.ready6,看到名字大家也能够知道它是为适配内核6而设计的。使用!ndx.ready6 -h可以输出帮助信息。

该命令可直观展示运行队列(rq)的关键统计信息,包括以下内容:
rq 层面的全局状态

字段 | 说明 |
nr_running | 当前在该 CPU 上处于 |
nr_switches | 该 CPU 上发生的 |
nr_uninterruptible | 处于不可中断睡眠状态的进程数 |
next_balance | 下一次负载均衡检查的时间点(单位:jiffies) |
curr->pid | 当前正在运行的进程的 PID |
clock | 该 CPU 的 |
clock_task | 该 CPU 上所有任务的 |
avg_idle | 平均空闲时间(jiffies),用于负载均衡判断 |
max_idle_balance_cost | 触发负载均衡的最大代价阈值(jiffies) |
各调度类子队列(cfs_rq、rt_rq、dl_rq)的详细运行数据


以下是 cfs_rq、rt_rq 和 dl_rq 三个调度队列的关键字段及其说明的简洁表格:
调度队列 | 关键字段 | 说明 |
| cfs_rq | .nr_running | 当前可运行的 |
load_avg | 队列的 | |
util_avg | CPU 利用率平均值(0–1024,≈百分比) | |
min_vruntime | 最小虚拟运行时间,决定下一个被调度的任务 | |
| rt_rq | rt_nr_running | 当前 |
rt_throttled | 是否因超过配额被节流(1=是) | |
rt_runtime | 实时任务可用的时间配额(默认 950ms/秒) | |
| dl_rq | dl_nr_running | 当前可运行的 |
dl_bw->bw | 已分配的截止时间任务带宽(ns) | |
dl_bw->total_bw | 系统为截止时间任务预留的总带宽(ns) |
各个 CPU 上正在运行和排队的任务及其基本信息

上图中,4号CPU很忙,有四个线程已经准备好,排队等待运行。就像要蹦极的人,做好准备排好队,一旦轮到自己就开始跳。

因地制宜的优化方案
在做这部分功能时,我们采用了和内核不同的方法,先看内核中print_rq的实现:

内核出于安全性、完整性和实现简洁性的考虑,在 print_rq() 中选择遍历所有任务,并通过判断任务是否属于当前 CPU 来决定是否打印,而非直接遍历各调度类的子队列。这种做法虽然牺牲了一定性能,但能确保打印信息的全面与可靠,而且不干扰调度器工作。
但是在ndb环境下,系统处于中断状态,无需考虑并发安全问题。此外!process命令已能够输出完整的任务列表,保证了信息的完整性。因此,我们改为直接遍历各调度类的子队列(如 cfs_rq、rt_rq、dl_rq),避免全量扫描任务链表。
这一优化将命令执行时间从约 16 秒显著缩短至 3 秒左右。
优化前:

优化后:

实践演练
我在幽兰代码本上指定 CPU 4 运行了 4 个矩阵乘法程序(每个都是单线程),随后通过 NanCode 中断系统并执行命令:
!ndx.ready6 -f 1结果如下:

如图中cpu[4]中的内容,最左侧的on_cpu为1代表正在运行的进程,此时为PID6023进程。on_cpu为0则为在排队等待运行的进程,此时有三个进程正在排队。
字段 | 说明 |
on_cpu | 是否正在此 CPU 上执行( |
state | 进程状态: |
task | 进程名(例如 |
PID | 进程 ID |
tree-key | CFS 调度树中的虚拟运行时间( |
switches | 此任务在该 CPU 上被调度的次数(即上下文切换次数) |
prio | 调度优先级( |
wait-time | 自进程创建以来,在 runnable 队列中等待 CPU 的累计时间(秒) |
sum-exec | 实际执行时间总和 (秒),即该任务真正占用 CPU 的时间 |
sum-sleep | 睡眠总时间 (秒),处于 |
sum-block | 阻塞总时间 (秒),因等待 I/O、锁、信号量等而被阻塞的时间 |
命令结果中打印了每个CPU的rq地址,可以配合NanoCode中的dt命令查看各个结构体。

例如sched_avg是一个描述运行负载的重要结构体,它相对于rq地址偏移为+0x280(偏移可以通过dt一步步查看),可以使用下面的命令观察它:
dt lK!sched_avg 0xffff0004fd61f200+0x280
内核中 CPU 利用率的最大值是 1024,当前util_avg = 1023。CPU 利用率= 1023/1024≈99.9%,这表示该 CFS 运行队列所占用的 CPU 接近100%,非常忙碌,处于满载状态。
小结
最后,回到开头提出的两个问题。有人可能要问了:无涯,无涯,你说的两个问题,不是只解决了第一个吗?第二个问题:当多个任务就绪时,谁先谁后又是如何确定的?你只说了调度类的优先级,那调度类处理任务的优先级呢?
是的,各个调度类具体如何处理任务,我打算在另一篇文章中详细探讨。各位可以先去查看各调度类的 pick_next_task() 函数,理解不同调度策略的实现逻辑。待我下一篇文章发布时,期待与大家的想法不谋而合。若发现文中存在错误或疏漏,恳请不吝批评指正。
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

也欢迎关注格友公众号

3230

被折叠的 条评论
为什么被折叠?



