调度器-基础篇:CPU 资源分配的核心问题与设计框架
"调度器不仅是任务的分配者,更是 CPU 核心的指挥官。
本文将从零设计一个完整的调度框架,
解决任务选择、CPU 选择、负载均衡三大核心问题,
为多核时代的高效调度奠定基础。"
引言:现代调度器的双重职责
在单核时代,调度器只需解决一个问题:选择哪个任务运行?
但在多核时代,调度器面临双重挑战:
- 任务选择(Task Selection):从可运行任务中选最优者
- CPU 选择(CPU Selection):决定任务在哪个核心上运行
更复杂的是,负载均衡(Load Balancing)成为必须:
- 核间负载不均:某些核 100% 利用,某些核空闲
- 缓存亲和性:任务迁移导致缓存失效,性能下降
本文将为自制 OS 设计一个完整的多核调度框架,
系统性地解决这三大核心问题。
第一章:调度器的核心问题域
1.1 三大核心问题
任务选择(Task Selection)
- 目标:在单个 CPU 上选择最优任务
- 策略:时间片轮转、优先级调度、公平调度
- 挑战:平衡公平性、响应性、吞吐量
CPU 选择(CPU Selection)
- 目标:决定新任务在哪个 CPU 上运行
- 策略:亲和性绑定、负载感知分配
- 挑战:减少跨核迁移,保持缓存亲和性
负载均衡(Load Balancing)
- 目标:确保所有 CPU 负载均衡
- 策略:定期检查、忙核迁移、空闲核唤醒
- 挑战:迁移开销 vs 负载收益
1.2 调度时机与触发条件
| 触发条件 | 任务选择 | CPU 选择 | 负载均衡 |
|---|---|---|---|
| 新任务创建 | - | ✓ | - |
| 任务唤醒 | ✓ | ✓ | - |
| 时钟中断 | ✓ | - | ✓ |
| CPU 空闲 | ✓ | - | ✓ |
| 系统调用 | ✓ | - | - |
✅ 完整的调度器必须同时处理这三个维度!
第二章:多核调度框架设计
2.1 调度域(Scheduling Domain)概念
调度域层次:
- **MC **(Multi-Core):同一物理 CPU 的核心
- **SMT **(Simultaneous Multi-Threading):超线程(逻辑核心)
自制 OS 简化:两级调度域
System
├── Package 0 (物理 CPU 0)
│ ├── CPU 0
│ └── CPU 1
└── Package 1 (物理 CPU 1)
├── CPU 2
└── CPU 3
2.2 数据结构设计
CPU 运行队列:
// sched.h
struct rq {
raw_spinlock_t lock;
struct list_head queue; // 可运行任务队列
unsigned long nr_running; // 可运行任务数
struct task_struct *curr; // 当前运行任务
unsigned long cpu; // CPU 编号
unsigned long idle_stamp; // 空闲时间戳
unsigned long avg_load; // 平均负载
};
调度域:
struct sched_domain {
struct sched_domain *parent; // 父域
struct sched_domain *child; // 子域
struct cpumask span; // 覆盖的 CPU 位图
unsigned int level; // 域层级
unsigned long min_interval; // 负载均衡最小间隔
unsigned long max_interval; // 负载均衡最大间隔
};
全局调度状态:
// sched.c
static DEFINE_PER_CPU(struct rq, cpu_rq);
static cpumask_t sched_rq_pending; // 有任务等待的 CPU
// 每个 CPU 的调度域
static DEFINE_PER_CPU(struct sched_domain *, cpu_sched_domains);
2.3 任务结构扩展
struct task_struct {
// ... 基础字段
int on_rq; // 0=不在队列, 1=在队列
int cpu; // 当前分配的 CPU
int wake_cpu; // 唤醒时的目标 CPU
cpumask_t cpus_allowed; // 允许运行的 CPU 位图
// 调度统计
u64 sum_exec_runtime; // 累计执行时间
u64 prev_sum_exec_runtime; // 上次调度时的执行时间
u64 last_ran; // 上次运行时间戳
// 负载均衡相关
struct sched_entity se; // 调度实体(CFS 用)
struct list_head rt_run_list; // 实时任务队列
};
第三章:任务选择机制
3.1 运行队列管理
入队操作:
// sched.c
static void enqueue_task(struct task_struct *p, struct rq *rq) {
if (p->on_rq) return;
// 1. 标记任务在队列中
p->on_rq = 1;
p->cpu = rq->cpu;
// 2. 加入运行队列
list_add_tail(&p->run_list, &rq->queue);
rq->nr_running++;
// 3. 更新负载统计
update_rq_load(rq);
// 4. 标记 CPU 有可运行任务
cpumask_set_cpu(rq->cpu, &sched_rq_pending);
}
出队操作:
static void dequeue_task(struct task_struct *p, struct rq *rq) {
if (!p->on_rq) return;
// 1. 从队列移除
list_del(&p->run_list);
rq->nr_running--;
p->on_rq = 0;
// 2. 更新负载统计
update_rq_load(rq);
// 3. 清除 CPU 标记(如果队列为空)
if (rq->nr_running == 0) {
cpumask_clear_cpu(rq->cpu, &sched_rq_pending);
}
}
3.2 调度主循环
核心函数:__schedule()
// sched.c
static void __schedule(bool preempt) {
struct task_struct *prev, *next;
struct rq *rq;
int cpu;
cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr;
// 1. 获取运行队列锁
raw_spin_lock(&rq->lock);
// 2. 选择下一个任务
next = pick_next_task(rq);
// 3. 清除当前任务
clear_preempt_need_resched();
// 4. 如果任务切换
if (likely(prev != next)) {
rq->curr = next;
prev->on_rq = 0;
// 5. 执行上下文切换
context_switch(rq, prev, next);
}
raw_spin_unlock(&rq->lock);
}
任务选择策略:
// sched/rt.c
static struct task_struct *pick_next_task_rt(struct rq *rq) {
// 实时任务:选择最高优先级
if (!list_empty(&rq->rt_queue)) {
return list_first_entry(&rq->rt_queue, struct task_struct, rt_run_list);
}
return NULL;
}
// sched/fair.c
static struct task_struct *pick_next_task_fair(struct rq *rq) {
// 公平调度:选择 vruntime 最小的任务
struct sched_entity *se = __pick_first_entity(&rq->cfs);
return se ? task_of(se) : NULL;
}
// 调度器入口
static struct task_struct *pick_next_task(struct rq *rq) {
// 1. 优先实时任务
struct task_struct *p = pick_next_task_rt(rq);
if (p) return p;
// 2. 回退到公平调度
return pick_next_task_fair(rq);
}
第四章:CPU 选择与亲和性
4.1 CPU 选择策略
新任务 CPU 分配:
// sched/cpupri.c
static int select_task_rq(struct task_struct *p, int prev_cpu, int sd_flags) {
// 1. 检查亲和性
if (!cpumask_test_cpu(prev_cpu, &p->cpus_allowed)) {
prev_cpu = cpumask_any(&p->cpus_allowed);
}
// 2. 检查缓存亲和性
if (cpu_rq(prev_cpu)->nr_running < 2) {
return prev_cpu; // 原 CPU 负载低,保持亲和性
}
// 3. 选择最空闲的 CPU
return find_least_loaded_cpu(&p->cpus_allowed);
}
任务唤醒 CPU 选择:
// sched/wake_q.c
static int wake_wakee_cpu(struct task_struct *p, struct task_struct *curr) {
int target = p->wake_cpu;
// 1. 如果唤醒 CPU 空闲,直接选择
if (idle_cpu(target)) {
return target;
}
// 2. 检查当前 CPU 是否更优
if (cpu_rq(smp_processor_id())->nr_running <
cpu_rq(target)->nr_running) {
return smp_processor_id();
}
return target;
}
4.2 CPU 亲和性管理
设置亲和性:
// sys_sched.c
long sys_sched_setaffinity(pid_t pid, unsigned int len, unsigned long *user_mask_ptr) {
struct task_struct *p;
cpumask_var_t new_mask;
// 1. 复制用户态 CPU 位图
if (copy_from_user(new_mask, user_mask_ptr, len)) {
return -EFAULT;
}
// 2. 找到目标任务
p = find_task_by_vpid(pid);
if (!p) return -ESRCH;
// 3. 更新亲和性
p->cpus_allowed = *new_mask;
// 4. 如果当前 CPU 不在允许列表中,迁移任务
if (!cpumask_test_cpu(task_cpu(p), &p->cpus_allowed)) {
migrate_task_to(p, cpumask_any(&p->cpus_allowed));
}
return 0;
}
第五章:负载均衡机制
5.1 负载均衡触发
定时均衡:
// sched/fair.c
static void run_rebalance_domains(struct irq_work *work) {
int this_cpu = smp_processor_id();
struct rq *rq = cpu_rq(this_cpu);
struct sched_domain *sd;
// 遍历所有调度域
for_each_domain(this_cpu, sd) {
if (time_after(jiffies, sd->last_balance + sd->balance_interval)) {
load_balance(this_cpu, rq, sd, CPU_IDLE);
sd->last_balance = jiffies;
}
}
}
// 时钟中断中触发
void scheduler_tick(void) {
// ... 其他逻辑
// 检查是否需要负载均衡
if (unlikely(rebalance_domains_need()))
irq_work_queue(&rebalance_irq_work);
}
空闲均衡:
// sched/idle.c
void sched_idle_balance(void) {
struct rq *rq = this_rq();
// 如果有可运行任务,无需均衡
if (rq->nr_running) return;
// 从其他 CPU 拉取任务
load_balance(this_cpu, rq, sd, CPU_IDLE);
}
5.2 负载均衡算法
负载计算:
// sched/loadavg.c
static unsigned long source_load(int cpu, int type) {
struct rq *rq = cpu_rq(cpu);
unsigned long total = rq->nr_running;
// 加权负载:运行任务数 + 睡眠任务影响
return total;
}
static unsigned long target_load(int cpu, int type) {
return source_load(cpu, type);
}
任务迁移:
// sched/migrate.c
static int move_one_task(struct rq *this_rq, int this_cpu,
struct rq *busiest, struct sched_domain *sd) {
struct list_head *tasks = &busiest->queue;
struct task_struct *p, *n;
// 遍历最忙 CPU 的任务队列
list_for_each_entry_safe(p, n, tasks, run_list) {
// 检查亲和性
if (!cpumask_test_cpu(this_cpu, &p->cpus_allowed))
continue;
// 迁移任务
deactivate_task(busiest, p, 0);
set_task_cpu(p, this_cpu);
activate_task(this_rq, p, 0);
// 更新负载统计
update_rq_load(this_rq);
update_rq_load(busiest);
return 1;
}
return 0;
}
5.3 负载均衡策略
域级均衡策略:
| 调度域 | 均衡策略 | 间隔 | |——–|———-|——| | SMT **(超线程) | 轻量均衡(仅迁移休眠任务) | 1ms | | **MC **(多核) | 中等均衡(迁移可运行任务) | 10ms | | **NUMA | 重量均衡(跨节点迁移) | 100ms |
迁移开销评估:
- 缓存失效成本:迁移任务导致 L1/L2 缓存失效
- TLB 刷新成本:跨核迁移需刷新 TLB
- 同步开销:获取多个运行队列锁
✅ 只有当负载收益 > 迁移开销时,才执行迁移!
第六章:调度器初始化与启动
6.1 多核调度器初始化
// sched.c
void __init sched_init(void) {
int cpu;
// 1. 初始化每个 CPU 的运行队列
for_each_possible_cpu(cpu) {
struct rq *rq = cpu_rq(cpu);
raw_spin_lock_init(&rq->lock);
INIT_LIST_HEAD(&rq->queue);
rq->nr_running = 0;
rq->curr = &idle_task;
rq->cpu = cpu;
rq->idle_stamp = jiffies;
rq->avg_load = 0;
}
// 2. 初始化调度域
sched_init_domains();
// 3. 初始化负载均衡工作队列
init_irq_work(&rebalance_irq_work, run_rebalance_domains);
}
6.2 调度域构建
// sched/topology.c
static void sched_init_domains(void) {
int cpu;
for_each_possible_cpu(cpu) {
struct sched_domain *sd, *p;
// 1. 构建 MC 域(多核)
sd = build_sched_domain(&cpu_coregroup_mask, SD_INIT_MC);
cpu_rq(cpu)->sd = sd;
// 2. 构建 NUMA 域(如果存在)
if (numa_aware()) {
p = build_sched_domain(&numa_nodes_mask, SD_INIT_NUMA);
sd->parent = p;
p->child = sd;
}
}
}
6.3 Idle 任务初始化
// sched/idle.c
void __cpuinit init_idle(struct task_struct *idle, int cpu) {
struct rq *rq = cpu_rq(cpu);
// 1. 设置 idle 任务
idle->on_rq = 0;
idle->cpu = cpu;
rq->curr = idle;
// 2. 设置亲和性(仅允许在本 CPU 运行)
cpumask_clear(&idle->cpus_allowed);
cpumask_set_cpu(cpu, &idle->cpus_allowed);
}
第七章:性能优化与调试
7.1 关键性能指标
| 指标 | 说明 | 目标 |
|---|---|---|
| 上下文切换次数 | 每秒切换次数 | < 1000 |
| 负载均衡频率 | 每秒均衡次数 | < 10 |
| 任务迁移次数 | 每秒迁移次数 | < 100 |
| CPU 利用率方差 | 各核利用率差异 | < 10% |
7.2 调试接口
/proc/sched_debug:
// sched/debug.c
static int sched_debug_show(struct seq_file *m, void *v) {
int cpu;
for_each_online_cpu(cpu) {
struct rq *rq = cpu_rq(cpu);
seq_printf(m, "CPU%d:\n", cpu);
seq_printf(m, " nr_running: %lu\n", rq->nr_running);
seq_printf(m, " avg_load: %lu\n", rq->avg_load);
seq_printf(m, " curr: %s\n", rq->curr->comm);
}
return 0;
}
调度统计:
// sched/stats.c
void update_sched_stats(struct task_struct *p) {
p->sched_info.run_delay +=
(jiffies - p->last_ran) * jiffies_to_ns(1);
p->sched_info.pcount++;
}
7.3 优化方向
锁优化:
- 无锁运行队列:使用 RCU 保护只读操作
- 分层锁:调度域锁 + CPU 锁
亲和性优化:
- NUMA 亲和性:优先分配本地内存
- 缓存行对齐:运行队列结构体对齐
负载均衡优化:
- 预测性均衡:基于历史负载预测
- 增量均衡:每次只迁移必要任务
结论:构建完整的多核调度框架
现代调度器是操作系统最复杂的子系统之一。
通过系统性的设计,
我们解决了:
- 任务选择:通过可扩展的调度类
- CPU 选择:通过亲和性与负载感知
- 负载均衡:通过分层调度域
此框架为后续实现 CFS、实时调度、节能调度 奠定了坚实基础。
真正的调度器,始于对多核并行本质的深刻理解。
附录:关键数据结构与接口速查
核心数据结构
| 结构 | 作用 | |——|——| | struct rq | CPU 运行队列 | | struct sched_domain | 调度域 | | struct task_struct | 任务描述符 |
调度接口
void schedule(void); // 主调度函数
void enqueue_task(struct task_struct *p, struct rq *rq);
void dequeue_task(struct task_struct *p, struct rq *rq);
int select_task_rq(struct task_struct *p, int prev_cpu, int sd_flags);
void load_balance(int this_cpu, struct rq *this_rq,
struct sched_domain *sd, enum cpu_idle_type idle);
调试接口
/proc/sched_debug # 调度器状态
/proc/schedstat # 调度统计
注:本文所有代码均为简化实现,实际使用需添加错误处理、边界检查、安全审计等。