第22天:CFS 调度:完全公平调度的核心原理
从"抢跑"到"公平":Linux 进程调度的进化之路
想象一下这样的场景:在一个繁忙的餐厅里,厨师需要为几十桌客人准备饭菜。如果厨师只专注于一桌客人的菜,其他桌的客人可能会等得不耐烦;但如果厨师频繁切换,又会影响做菜的效率。Linux 内核中的进程调度器就像这位厨师,它需要在多个进程之间分配 CPU 时间,既要保证每个进程都能得到执行机会,又要尽可能提高系统整体性能。
在 Linux 2.6.23 版本之前,内核使用的是基于优先级的 O(1) 调度器。这种调度器虽然效率很高,但存在一个严重的问题:高优先级进程可能会"饿死"低优先级进程。为了解决这个问题,Ingo Molnar 引入了 CFS(Completely Fair Scheduler,完全公平调度器),它的核心思想是:让每个进程都能公平地获得 CPU 时间。
CFS 的核心原理
1. 公平的本质:虚拟运行时间
CFS 的核心创新在于引入了虚拟运行时间(Virtual Runtime)的概念。虚拟运行时间不是进程实际占用的 CPU 时间,而是经过"权重"加权后的时间。
/* 虚拟运行时间的计算逻辑 */ static u64 sched_vruntime(struct task_struct *p) { struct sched_entity *se = &p->se; /* 虚拟时间 = 实际运行时间 * NICE_0_LOAD / 进程权重 */ return se->vruntime; }这里的关键公式是:
虚拟运行时间 = 实际运行时间 × (1024 / 进程权重)其中NICE_0_LOAD = 1024是默认进程(nice=0)的权重。
2. 红黑树:高效的调度队列
CFS 使用红黑树来管理所有可运行的进程。树的键值是进程的虚拟运行时间vruntime,树的最左节点就是虚拟运行时间最小的进程,也就是下一个应该被调度的进程。
struct cfs_rq { struct rb_root tasks_timeline; /* 红黑树的根节点 */ struct rb_node *rb_leftmost; /* 最左节点(下一个要调度的进程)*/ unsigned int nr_running; /* 队列中的进程数量 */ unsigned int h_nr_running; /* 高优先级进程数量 */ u64 min_vruntime; /* 最小虚拟运行时间 */ /* ... */ };3. 调度时机:何时选择新进程
CFS 会在以下几种情况下重新选择下一个要运行的进程:
- 进程主动放弃 CPU(如调用
sleep()) - 时间片耗尽
- 更高优先级进程唤醒
- 周期性时钟中断(通常每毫秒)
static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr) { unsigned long ideal_runtime, delta_exec; /* 计算理想运行时间 = 调度周期 / 进程数量 */ ideal_runtime = sched_slice(cfs_rq, curr); /* 计算当前进程自上次调度以来的运行时间 */ delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime; /* 如果超过理想时间片,触发抢占 */ if (delta_exec > ideal_runtime) resched_curr(rq_of(cfs_rq)); }关键数据结构解析
task_struct 中的调度相关字段
每个进程都有一个task_struct结构,其中与调度相关的关键字段如下:
struct task_struct { /* 调度实体,包含虚拟运行时间等信息 */ struct sched_entity se; /* 进程优先级相关 */ int prio; /* 动态优先级(0-139)*/ int static_prio; /* 静态优先级(100-139)*/ int normal_prio; /* 常规优先级 */ unsigned int rt_priority; /* 实时优先级 */ /* 调度策略 */ unsigned int policy; /* SCHED_NORMAL/SCHED_FIFO/SCHED_RR */ /* 进程状态 */ volatile long state; /* TASK_RUNNING/TASK_INTERRUPTIBLE 等 */ /* ... */ };sched_entity:调度实体
struct sched_entity { struct load_weight load; /* 进程权重,影响虚拟时间计算 */ struct rb_node run_node; /* 红黑树节点 */ struct list_head group_node; /* 组调度链表节点 */ u64 vruntime; /* 虚拟运行时间 */ u64 prev_vruntime; /* 上一次调度时的虚拟时间 */ u64 sum_exec_runtime; /* 累计实际运行时间 */ u64 prev_sum_exec_runtime; /* 上一次调度时的累计运行时间 */ /* ... */ };实战:观察 CFS 调度行为
查看进程调度信息
使用ps命令查看进程的调度策略和优先级:
# 查看所有进程的调度策略 ps -eo pid,tid,class,rtprio,pri,nice,cmd # 输出示例: # PID TID CLS RTPRIO PRI NI CMD # 1234 1234 TS - 19 0 bash # 5678 5678 FF 99 1 -20 realtime_app使用 chrt 命令修改调度策略
# 将进程设置为实时 FIFO 调度策略,优先级为 50 chrt -f -p 50 1234 # 将进程恢复为普通 CFS 调度 chrt -o -p 0 1234查看内核调度统计
# 查看调度器统计信息 cat /proc/schedstat # 查看特定进程的调度信息 cat /proc/[pid]/schedCFS 的优势与局限性
优势
- 公平性:每个进程都能获得公平的 CPU 时间片
- 低延迟:交互进程响应迅速
- 自适应:能够根据系统负载自动调整
- 简单优雅:相比 O(1) 调度器,代码更简洁
局限性
- 红黑树操作开销:虽然是 O(log n),但在进程数极多时仍有开销
- cache 友好性:频繁的进程切换会影响 CPU cache 效率
- 实时性:CFS 是通用调度器,实时性能不如 RT 调度器
互动讨论
思考题:如果系统中有一个 CPU 密集型进程和一个 I/O 密集型进程,CFS 会如何分配 CPU 时间?为什么 I/O 密集型进程通常能获得更好的响应性?
实践题:尝试使用
chrt命令修改一个进程的调度优先级,然后使用top或htop观察它的 CPU 占用率变化。你能观察到什么现象?
请帮忙点赞收藏+关注,内容持续更新,感谢大家~~~
