3、Linux内核数据结构概览:list_head、rb_root、rb_node结构体定义
Linux内核进程调度器(CFS)的核心数据结构是红黑树,而红黑树的实现又依赖于内核中两个基础的数据结构:双向循环链表(list_head)和红黑树节点/根节点(rb_node / rb_root)。理解这三个结构体的定义,是掌握调度器运行机制的基石。
本章将逐一剖析这三个结构体的内核源码定义、内存布局以及它们在调度器中的角色。
3.1 双向循环链表:struct list_head
在Linux内核中,list_head 是最基础、使用最广泛的数据结构。它不存储业务数据,仅包含两个指针,用于将任意结构体串联成双向循环链表。
3.1.1 结构体定义
// 定义于 include/linux/types.h
struct list_head {
struct list_head *next, *prev;
};
- next:指向链表中的下一个节点。
- prev:指向链表中的上一个节点。
当链表为空时,next 和 prev 均指向自身(即 head->next = head->prev = head)。
3.1.2 在调度器中的角色
调度器使用 list_head 管理两类关键对象:
| 用途 | 宿主结构体 | 说明 |
|---|---|---|
| 运行队列链表 | struct rq | 管理所有就绪进程的调度实体(CFS、RT、DL 各有一个链表) |
| 等待队列 | struct wait_queue_head | 管理因等待事件而阻塞的进程 |
| 进程组链表 | struct task_struct | 将同一线程组或进程组的 task_struct 串联起来 |
设计哲学:
list_head 采用“侵入式”设计,即链表节点直接嵌入到宿主结构体中,而不是由宿主结构体指向链表节点。这种方式减少了内存分配次数,提高了缓存局部性。
3.1.3 内存布局示意图
下图展示了一个包含两个节点的双向循环链表的内存结构:
3.2 红黑树根节点:struct rb_root
rb_root 是红黑树的入口,结构极其精简,仅包含一个指向根节点的指针。
3.2.1 结构体定义
// 定义于 include/linux/rbtree.h
struct rb_root {
struct rb_node *rb_node;
};
- rb_node:指向红黑树的根节点。如果树为空,该指针为
NULL。
3.2.2 在调度器中的角色
每个 CPU 的运行队列(struct rq)中,CFS 调度类维护一个 rb_root 类型的成员 cfs_rq->tasks_timeline,用于组织所有 CFS 就绪进程的调度实体(struct sched_entity)。
| 字段 | 所属结构体 | 说明 |
|---|---|---|
| tasks_timeline | struct cfs_rq | 红黑树根,按虚拟运行时间(vruntime)排序 |
| rb_leftmost | struct cfs_rq | 缓存树中最左侧节点(即 vruntime 最小的进程),用于快速获取下一个调度实体 |
关键优化: 调度器通过
rb_leftmost 缓存最左节点,避免了每次调度时都从根节点遍历到最左叶子节点,将“查找最小 vruntime 进程”的时间复杂度从 O(log n) 降为 O(1)。
3.2.3 根节点与运行队列的关系图
3.3 红黑树节点:struct rb_node
rb_node 是红黑树的节点结构体,包含指向父子节点的指针以及表示节点颜色的标志位。
3.3.1 结构体定义
// 定义于 include/linux/rbtree.h
struct rb_node {
unsigned long __rb_parent_color; // 父节点指针 + 颜色标志位
struct rb_node *rb_right; // 右子节点
struct rb_node *rb_left; // 左子节点
} __attribute__((aligned(sizeof(long))));
- __rb_parent_color:这是一个“技巧性”字段,在同一个 unsigned long 变量中同时存储了父节点指针和节点颜色(红/黑)。由于指针通常按 4 或 8 字节对齐,最低 1~2 位始终为 0,内核利用这些空闲位来存储颜色信息。
- rb_right:指向右子节点。
- rb_left:指向左子节点。
3.3.2 颜色编码技巧
| 位域 | 含义 | 说明 |
|---|---|---|
| 低 1 位 | 颜色标志 | 0 = 红色,1 = 黑色 |
| 高 (BITS_PER_LONG-1) 位 | 父节点指针 | 存储父节点的内存地址(已对齐,低 1 位为 0) |
注意: 由于颜色位与父节点指针共享存储空间,在访问父节点时必须使用辅助宏
rb_parent() 来屏蔽颜色位;在设置颜色时必须使用 rb_set_parent_color() 等内联函数。直接操作 __rb_parent_color 可能导致指针错误。
3.3.3 辅助宏与内联函数
// 获取父节点指针(屏蔽低 1 位)
#define rb_parent(r) ((struct rb_node *)((r)->__rb_parent_color & ~3))
// 获取节点颜色
#define rb_color(r) ((r)->__rb_parent_color & 1)
// 判断是否为红色
#define rb_is_red(r) (!rb_color(r))
// 判断是否为黑色
#define rb_is_black(r) rb_color(r)
// 设置父节点和颜色
static inline void rb_set_parent_color(struct rb_node *rb,
struct rb_node *p, int color)
{
rb->__rb_parent_color = (unsigned long)p | color;
}
3.3.4 节点在调度实体中的嵌入
调度实体 struct sched_entity 将 rb_node 作为成员嵌入其中:
// 定义于 include/linux/sched.h
struct sched_entity {
struct load_weight load; // 调度实体权重
struct rb_node run_node; // 红黑树节点(嵌入)
struct list_head group_node; // 组调度链表节点
unsigned int on_rq; // 是否在运行队列上
u64 vruntime; // 虚拟运行时间
// ... 其他字段
};
调度器通过 container_of 宏从 rb_node 获取宿主 sched_entity:
struct sched_entity *se = container_of(rb_node, struct sched_entity, run_node);
3.4 三种数据结构对比总结
| 结构体 | 核心字段 | 组织方式 | 在调度器中的用途 | 时间复杂度 |
|---|---|---|---|---|
| list_head | next, prev | 双向循环链表 | 管理等待队列、进程组、运行队列链表 | O(1) 插入/删除 |
| rb_root | rb_node | 红黑树根 | CFS 就绪队列的入口 | — |
| rb_node | __rb_parent_color, rb_left, rb_right | 红黑树节点 | 按 vruntime 排序的调度实体节点 | O(log n) 插入/查找 |
3.5 数据结构关系总览图
3.6 小结
- list_head:双向循环链表,O(1) 插入/删除,用于管理等待队列、进程组等线性结构。
- rb_root:红黑树根节点,仅包含一个指向根节点的指针,是 CFS 就绪队列的入口。
- rb_node:红黑树节点,通过
__rb_parent_color技巧将父节点指针与颜色标志位合并存储,节省内存。 - 调度实体
struct sched_entity将rb_node嵌入自身,通过container_of实现从树节点到调度实体的反向查找。
理解这三个基础结构体的定义和内存布局,为后续学习红黑树的插入、查找、左旋、右旋以及 CFS 调度器的具体实现奠定了坚实的数据结构基础。