欢迎光临
我们一直在努力

Linux 调度核心概念拆解:运行队列、vruntime 与调度实体的底层逻辑

一、简介:为什么必须深入调度器底层?

Linux 内核调度器是操作系统最核心的组件之一,直接决定系统响应速度、吞吐量和实时性。随着云计算、边缘计算、自动驾驶等场景对延迟要求的不断提高,"能用"调度器已无法满足需求,"精通"调度器成为高级开发者的必备技能。

实际应用场景:

  • 云原生场景:Kubernetes 集群中 Pod 的 CPU 限制(limits/requests)最终转化为 CFS 的 quota 和 period,理解 vruntime 才能准确预测容器性能边界

  • 实时系统:工业控制、机器人操作系统(ROS2)要求任务抖动 < 100μs,必须掌握 SCHED_FIFO/SCHED_RR 与 CFS 的交互机制

  • 性能优化:数据库、缓存系统的高并发场景,需要调整调度器参数以降低上下文切换开销

掌握运行队列、vruntime、sched_entity 的底层逻辑,是阅读内核源码、编写调度器模块、进行系统级性能调优的必经之路。


二、核心概念:三大基石的完整解析

2.1 运行队列(runqueue, rq)

运行队列是调度器的核心数据结构,每个 CPU 拥有一个独立的 rq,管理该 CPU 上所有可运行任务。

// kernel/sched/sched.h
struct rq {
/* 运行队列锁 */
raw_spinlock_t lock;

/* CFS 运行队列 */
struct cfs_rq cfs;

/* 实时运行队列 */
struct rt_rq rt;

/* 当前运行任务 */
struct task_struct *curr;

/* 时钟相关 */
u64 clock;
u64 clock_task;

/* CPU 编号 */
int cpu;

/* 负载统计 */
unsigned long nr_running;
struct load_weight load;

/* … 更多字段 */
};

关键设计:

  • 每 CPU 一个 rq:避免全局锁竞争,提升多核扩展性

  • 分层结构:CFS、RT、DL(Deadline)各自维护子队列,调度类(sched_class)决定遍历顺序

2.2 虚拟运行时间(vruntime)

vruntime 是 CFS 实现"公平调度"的核心机制,将实际执行时间归一化为可比较的虚拟时间。

// kernel/sched/fair.c
/*
* 计算 vruntime 的增量
* delta_exec: 实际执行时间(纳秒)
* weight: 任务权重(由 nice 值决定)
* lw: 负载权重(load_weight)
*/
static u64 calc_delta_fair(u64 delta_exec, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD))
delta_exec = __calc_delta(delta_exec, NICE_0_LOAD, &se->load);

return delta_exec;
}

/*
* 更新 vruntime
*/
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;

if (unlikely(!curr))
return;

delta_exec = now – curr->exec_start;
if (unlikely((s64)delta_exec <= 0))
return;

curr->exec_start = now;

/* 计算并累加 vruntime */
curr->vruntime += calc_delta_fair(delta_exec, curr);

/* 更新统计信息 */
update_min_vruntime(cfs_rq);
}

vruntime 计算公式:

vruntimenew​=vruntimeold​+weightdelta_exec×NICE_0_LOAD​

其中 NICE_0_LOAD = 1024,weight 由 nice 值查表获得:

niceweight与 nice 0 的比例
-20 88761 86.7x
-10 9548 9.3x
0 1024 1.0x
10 110 0.11x
19 15 0.015x

设计意义:nice 值低的任务(高优先级)weight 大,相同执行时间产生的 vruntime 增量小,从而获得更多 CPU 时间。

2.3 调度实体(sched_entity)

sched_entity 是任务在 CFS 中的抽象表示,支持普通任务与任务组(cgroup)的统一调度。

// include/linux/sched.h
struct sched_entity {
/* 用于红黑树的键值 */
struct load_weight load;
struct rb_node run_node;

/* 虚拟运行时间 */
u64 vruntime;

/* 执行统计 */
u64 exec_start;
u64 sum_exec_runtime;
u64 prev_sum_exec_runtime;

/* 任务组层级 */
struct sched_entity *parent;
struct cfs_rq *cfs_rq;
struct cfs_rq *my_q; /* 如果是任务组,指向子队列 */

/* 迁移相关 */
unsigned long runnable_weight;

/* 唤醒预测 */
u64 avg_vruntime;
u64 avg_load_sum;
u64 avg_util_sum;

/* … 更多字段 */
};

红黑树组织:CFS 使用红黑树(rbtree)管理所有 runnable 的 sched_entity,key 为 vruntime,最左节点即为下一个应执行的任务。

// kernel/sched/fair.c
/*
* 选择下一个要运行的任务
* 返回最左节点(最小 vruntime)
*/
static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);

if (!left)
return NULL;

return rb_entry(left, struct sched_entity, run_node);
}


三、环境准备:搭建调度器研究平台

3.1 硬件需求

组件规格说明
CPU x86_64, ≥4 核 观察多核负载均衡
内存 ≥8 GB 编译内核需要
存储 SSD, ≥50 GB 空闲 内核源码 + 多版本构建

3.2 软件环境

组件版本用途
Ubuntu Server 22.04 LTS 基础系统
Linux 内核源码 5.15.y / 6.1.y 主线 + PREEMPT_RT
GCC 11.3+ 内核编译
perf 5.15+ 性能分析
bpftrace 0.14+ 动态追踪
KernelShark 2.0+ 可视化调度事件

3.3 一键搭建脚本

#!/bin/bash
# setup_sched_lab.sh
set -e

WORK_DIR="$HOME/sched-lab"
mkdir -p $WORK_DIR && cd $WORK_DIR

# 1. 安装依赖
sudo apt update
sudo apt install -y build-essential libncurses-dev bison flex \\
libssl-dev libelf-dev bc git dwarves python3-pip \\
linux-tools-common linux-tools-generic bpftrace trace-cmd

# 2. 下载内核源码
KERNEL_VER="5.15.120"
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-${KERNEL_VER}.tar.xz
tar -xf linux-${KERNEL_VER}.tar.xz
cd linux-${KERNEL_VER}

# 3. 下载 PREEMPT_RT 补丁
RT_PATCH="patch-5.15.120-rt65.patch.xz"
wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/5.15/${RT_PATCH}
xzcat ${RT_PATCH} | patch -p1

# 4. 配置内核(调度器调试选项)
make defconfig
./scripts/config –enable CONFIG_SCHED_DEBUG
./scripts/config –enable CONFIG_SCHEDSTATS
./scripts/config –enable CONFIG_PREEMPT_RT
./scripts/config –enable CONFIG_FTRACE
./scripts/config –enable CONFIG_FUNCTION_TRACER
./scripts/config –enable CONFIG_SCHED_TRACER

# 5. 编译
make -j$(nproc) 2>&1 | tee build.log

echo "内核编译完成,请执行: sudo make modules_install install"


四、应用场景:云原生数据库的调度优化

在 Kubernetes 部署的分布式数据库(如 TiDB、CockroachDB)场景中,调度器优化直接影响查询延迟与吞吐量。典型配置:数据库 Pod 绑定独占 CPU(CPU Manager 的 static 策略),但 CFS 的 vruntime 累积仍可能导致"伪抢占"——当数据库线程因 IO 阻塞后重新入队,其 vruntime 已落后大量,CFS 会优先调度其他任务,造成数据库响应延迟抖动。

通过理解 sched_entity 的 avg_vruntime 预测机制,可以调整 sched_wakeup_granularity_ns 参数,或改用 SCHED_FIFO 绑定关键线程到隔离 CPU,将 P99 延迟从 5ms 降至 500μs 以下。


五、实际案例与步骤:源码级实验验证

5.1 实验一:观察 vruntime 变化

// vruntime_monitor.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <sys/syscall.h>
#include <linux/sched.h>

#define gettid() syscall(SYS_gettid)

/*
* 读取 /proc/[pid]/schedstat 获取 vruntime
* 格式: cpu_time run_delay pcount vruntime
*/
unsigned long long get_vruntime(pid_t tid) {
char path[256];
unsigned long long cpu_time, run_delay, vruntime;
unsigned int pcount;

snprintf(path, sizeof(path), "/proc/%d/schedstat", tid);
FILE *fp = fopen(path, "r");
if (!fp) {
perror("fopen");
return 0;
}

fscanf(fp, "%llu %llu %u %llu",
&cpu_time, &run_delay, &pcount, &vruntime);
fclose(fp);
return vruntime;
}

int main(int argc, char *argv[]) {
pid_t tid = gettid();
cpu_set_t cpuset;

/* 绑定到 CPU 0,避免迁移干扰 */
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);

/* 设置不同 nice 值对比 */
int nice_val = (argc > 1) ? atoi(argv[1]) : 0;
nice(nice_val);

printf("PID=%d, nice=%d\\n", tid, nice_val);

unsigned long long vruntime_before, vruntime_after;
struct timespec ts;

for (int i = 0; i < 5; i++) {
vruntime_before = get_vruntime(tid);

/* 消耗 CPU 100ms */
clock_gettime(CLOCK_MONOTONIC, &ts);
unsigned long long start = ts.tv_sec * 1000000000ULL + ts.tv_nsec;
while (1) {
clock_gettime(CLOCK_MONOTONIC, &ts);
unsigned long long now = ts.tv_sec * 1000000000ULL + ts.tv_nsec;
if (now – start >= 100000000) break; // 100ms
}

vruntime_after = get_vruntime(tid);

printf("Iteration %d: vruntime_delta = %llu ns\\n",
i, vruntime_after – vruntime_before);
}

return 0;
}

编译运行:

gcc -O2 -o vruntime_monitor vruntime_monitor.c

# 终端 1: nice 0(默认)
sudo ./vruntime_monitor 0

# 终端 2: nice -10(高优先级)
sudo nice -n -10 ./vruntime_monitor -10

预期结果:nice -10 的 vruntime_delta 约为 nice 0 的 1/10,验证权重反比关系。

5.2 实验二:红黑树遍历与任务选择

// rbtree_inspect.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/sched.h>
#include <linux/cpumask.h>

static int __init rbtree_inspect_init(void) {
int cpu;
struct rq *rq;
struct cfs_rq *cfs_rq;
struct rb_node *left;
struct sched_entity *se;
struct task_struct *p;

printk(KERN_INFO "=== CFS Red-Black Tree Inspection ===\\n");

for_each_online_cpu(cpu) {
rq = cpu_rq(cpu);
cfs_rq = &rq->cfs;

printk(KERN_INFO "CPU %d:\\n", cpu);
printk(KERN_INFO " nr_running: %d\\n", cfs_rq->nr_running);
printk(KERN_INFO " min_vruntime: %llu\\n", cfs_rq->min_vruntime);
printk(KERN_INFO " load_avg: %lu\\n", cfs_rq->avg.load_avg);

/* 读取最左节点(下一个执行任务)*/
rcu_read_lock();
left = rb_first_cached(&cfs_rq->tasks_timeline);
if (left) {
se = rb_entry(left, struct sched_entity, run_node);
if (se->my_q) {
printk(KERN_INFO " leftmost: task_group (has children)\\n");
} else {
p = container_of(se, struct task_struct, se);
printk(KERN_INFO " leftmost: pid=%d, comm=%s, vruntime=%llu\\n",
p->pid, p->comm, se->vruntime);
}
}
rcu_read_unlock();
}

return 0;
}

static void __exit rbtree_inspect_exit(void) {
printk(KERN_INFO "rbtree_inspect unloaded\\n");
}

module_init(rbtree_inspect_init);
module_exit(rbtree_inspect_exit);
MODULE_LICENSE("GPL");

Makefile:

obj-m += rbtree_inspect.o

KDIR ?= /lib/modules/$(shell uname -r)/build

all:
make -C $(KDIR) M=$(PWD) modules

clean:
make -C $(KDIR) M=$(PWD) clean

加载与查看:

make
sudo insmod rbtree_inspect.ko
sudo dmesg | tail -20
sudo rmmod rbtree_inspect

5.3 实验三:perf 分析调度延迟

#!/bin/bash
# sched_latency_analyze.sh

# 1. 记录调度事件 10 秒
sudo perf sched record -a — sleep 10

# 2. 生成延迟报告
sudo perf sched latency –sort max

# 3. 可视化调度时间线
sudo perf sched map > sched_map.txt

# 4. 分析特定进程的调度延迟
sudo perf sched timehist -p $(pgrep mysqld) > mysql_sched.txt

关键指标解读:

指标含义健康阈值
avg sched latency 平均调度延迟 < 10μs
max sched latency 最大调度延迟 < 100μs(实时系统 < 50μs)
run time 实际运行时间 与 vruntime 增长比例一致

5.4 实验四:bpftrace 动态追踪 vruntime

#!/usr/bin/bpftrace
// trace_vruntime.bt

#include <linux/sched.h>

kprobe:update_curr
{
$cfs_rq = (struct cfs_rq *)arg0;
$curr = $cfs_rq->curr;

if ($curr != 0) {
$pid = $curr->my_q ? 0 :
((struct task_struct *)(
(void *)$curr –
((size_t)&((struct task_struct *)0)->se)
))->pid;

printf("cpu=%d pid=%d vruntime=%llu delta_exec=%llu\\n",
cpu, $pid, $curr->vruntime,
nsecs – $curr->exec_start);
}
}

运行:

sudo bpftrace trace_vruntime.bt


六、常见问题与解答

Q1: vruntime 会溢出吗?

A: 会,但内核已处理。vruntime 是 u64(约 584 年才会溢出),且使用 min_vruntime 相对化,实际比较的是差值。溢出时利用无符号数回绕特性,比较逻辑仍正确。

// 安全的 vruntime 比较
static inline int entity_before(struct sched_entity *a,
struct sched_entity *b)
{
return (s64)(a->vruntime – b->vruntime) < 0;
}

Q2: 为什么实时任务(SCHED_FIFO)没有 vruntime?

A: SCHED_FIFO/SCHED_RR 使用独立的 rt_rq,基于优先级队列(bitmap + queue),而非 CFS 的红黑树。实时调度器追求确定性延迟,不需要"公平"概念。

Q3: 如何查看任务的调度实体信息?

A: 通过 /proc/[pid]/sched:

cat /proc/self/sched | grep -E "(se\\.|nr_cpus_allowed)"

Q4: 任务组(cgroup)的 vruntime 如何计算?

A: 任务组作为 sched_entity 加入父 cfs_rq,其 vruntime 是组内所有任务 vruntime 的加权平均。组内再维护子 cfs_rq,形成层级结构。

Q5: 调度器参数调优的边界在哪里?

A: 关键参数及范围:

参数默认值调优范围影响
sched_latency_ns 6ms 1-10ms 调度周期,影响吞吐
sched_min_granularity_ns 0.75ms 0.1-2ms 最小时间片
sched_wakeup_granularity_ns 1ms 0.5-5ms 唤醒抢占阈值
sched_migration_cost_ns 0.5ms 0.1-2ms 迁移成本估计

七、实践建议与最佳实践

7.1 内核源码阅读路径

kernel/sched/
├── core.c # 调度器主入口:schedule(), pick_next_task()
├── fair.c # CFS 实现:vruntime, 红黑树, 负载均衡
├── rt.c # 实时调度器
├── deadline.c # EDF 调度器
├── stop_task.c # 迁移线程(最高优先级)
├── sched.h # 核心数据结构
└── debug.c # 调试接口

7.2 性能优化 checklist

  • [ ] 使用 isolcpus 隔离关键 CPU

  • [ ] 关键线程绑定 CPU(sched_setaffinity)

  • [ ] 实时线程使用 SCHED_FIFO + 优先级继承

  • [ ] 调整 sched_wakeup_granularity_ns 减少唤醒延迟

  • [ ] 启用 CONFIG_SCHEDSTATS 监控调度事件

7.3 学术研究建议

  • 对比实验:在相同负载下,对比 CFS、BFS、MuQSS 的调度延迟分布

  • 形式化验证:使用 Promela/Spin 验证 vruntime 比较算法的正确性

  • 能耗优化:研究 ARM big.LITTLE 架构下的 vruntime 权重调整策略


  • 八、总结与应用场景

    本文系统拆解了 Linux 调度器的三大核心概念:

    概念核心作用调优接口
    运行队列(rq) 每 CPU 任务管理,分层调度 sched_domain 拓扑调整
    vruntime 公平调度的量化基础 nice 值、cgroup 权重
    sched_entity 统一抽象,支持任务组层级 cpu.shares, cpu.cfs_quota_us

    掌握这些底层机制,开发者可以:

    • 精准定位 生产环境的调度延迟问题

    • 定制优化 特定场景的调度策略

    • 贡献代码 到 Linux 内核社区

    在实时 Linux、云原生、边缘计算等前沿领域,调度器优化将持续创造显著价值。建议读者从本文实验出发,逐步深入到 kernel/sched/fair.c 的 8000+ 行源码,最终形成自己的调度器知识体系。


    参考文献:

  • Linux Kernel Documentation: scheduler/

  • Robert Love, "Linux Kernel Development", 3rd Edition

  • Peter Zijlstra, "The Linux Scheduler: a Decade of Wasted Cores" (Linux Plumbers 2016)

  • 赞(0)
    未经允许不得转载:171主机测评 » Linux 调度核心概念拆解:运行队列、vruntime 与调度实体的底层逻辑
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址