第1章:Linux调度器架构基础
大家好,我是你们的老朋友。今天咱们聊聊Linux调度器的架构基础。说实话,调度器这东西,我刚开始接触内核时也觉得挺玄乎的。不就是选个进程跑吗?能有多复杂?后来在项目里踩过坑,才明白这里面的门道有多深。
嗯,咱们先从最核心的CFS调度器说起。
1.1 CFS调度器核心原理
CFS,全称Completely Fair Scheduler,完全公平调度器。名字里带"公平",那它到底怎么个公平法?
说白了,CFS追求的是让每个进程都能获得"公平"的CPU时间。但它不是简单的时间片轮转,而是基于一个叫虚拟运行时间的概念。
核心思想:每个进程都有一个vruntime(虚拟运行时间)。调度器总是选择vruntime最小的进程来运行。这样,所有进程的vruntime会逐渐趋近,实现"公平"。
我刚开始看CFS代码时,有个问题一直想不通:为什么叫"虚拟"运行时间?后来在调试一个实时性要求很高的项目时,我才真正理解了。CFS引入了nice值的概念,不同nice值的进程,vruntime的增长速度不一样。nice值越低(优先级越高),vruntime增长越慢,所以能获得更多的CPU时间。
来看个简单的例子:
// 假设两个进程,nice值分别为0和10
// nice=0的进程,vruntime增长速度为1
// nice=10的进程,vruntime增长速度为0.1
// 运行10ms后:
// nice=0的进程:vruntime = 10ms
// nice=10的进程:vruntime = 1ms
// 调度器会选择vruntime更小的进程,也就是nice=10的进程继续运行
为什么会这样?因为CFS认为,nice值高的进程已经"吃亏"了,所以要让它的vruntime增长慢一点,从而获得更多补偿。嗯,这里要注意,这个补偿机制在实际项目中很容易被忽略。
我曾经在一个服务器项目里,把所有进程的nice值都设成了默认值0,结果发现某些后台任务总是抢不到CPU。后来排查才发现,这些后台任务虽然优先级不高,但CFS的公平性保证它们至少能获得一定的CPU时间。问题出在别的地方,但那次经历让我对vruntime有了更深的理解。
1.2 调度类与调度策略
Linux内核里,调度器不是只有一个。它用调度类的概念,把不同的调度策略组织起来。每个调度类负责管理一类进程。
目前内核里主要有这么几个调度类:
| 调度类 | 优先级 | 适用场景 |
|---|---|---|
| stop_sched_class | 最高 | 停止调度,用于CPU热插拔等特殊场景 |
| dl_sched_class | 高 | Deadline调度,实时性要求极高的任务 |
| rt_sched_class | 中 | 实时调度,如音频、视频处理 |
| fair_sched_class | 低 | 普通进程,CFS调度器所在 |
| idle_sched_class | 最低 | 空闲进程,CPU没事干时运行 |
每个调度类对应不同的调度策略。比如:
- SCHED_NORMAL:普通进程,用CFS调度
- SCHED_FIFO:先入先出实时进程
- SCHED_RR:时间片轮转实时进程
- SCHED_DEADLINE:截止时间调度
我个人习惯在调试功耗问题时,先确认进程用的是哪个调度策略。因为不同策略对CPU频率的影响差别很大。比如实时进程(SCHED_FIFO/SCHED_RR)如果长时间占用CPU,会导致CPU频率降不下来,功耗飙升。
避坑指南:我曾经在一个嵌入式项目里,把某个后台服务设成了SCHED_FIFO策略,结果设备发热严重。排查了半天才发现,这个服务里有个死循环,因为优先级太高,其他进程根本抢不到CPU。后来改成SCHED_NORMAL,问题就解决了。
1.3 调度实体与运行队列
调度实体(sched_entity)是调度器管理的基本单位。每个进程都有一个调度实体,里面记录了vruntime、权重等信息。
运行队列(runqueue)则是每个CPU上维护的一个数据结构,里面放着所有可运行的调度实体。CFS调度器用红黑树来组织这些实体,按vruntime排序。
你想想看,为什么用红黑树?因为CFS需要频繁地找到vruntime最小的进程,红黑树的查找、插入、删除都是O(log n)复杂度,性能很好。
来看个简化版的调度实体结构:
struct sched_entity {
struct load_weight load; // 权重,由nice值决定
struct rb_node run_node; // 红黑树节点
unsigned int on_rq; // 是否在运行队列中
u64 vruntime; // 虚拟运行时间
u64 prev_sum_exec_runtime; // 上次运行的总时间
// ... 还有很多其他字段
};
运行队列的结构大致是这样的:
struct cfs_rq {
struct load_weight load; // 队列总权重
unsigned int nr_running; // 可运行进程数
u64 min_vruntime; // 队列中最小的vruntime
struct rb_root_cached tasks_timeline; // 红黑树根节点
struct sched_entity *curr; // 当前正在运行的进程
// ... 还有很多其他字段
};
这里有个关键点:min_vruntime。它是队列中所有进程vruntime的最小值。新进程加入时,它的vruntime会被设置为min_vruntime,这样新进程不会因为vruntime太小而抢占太多CPU时间。
我记得有一次,我在优化一个多线程应用的功耗时,发现某个线程的vruntime异常大。排查后发现,这个线程被频繁地迁移到不同CPU上,每次迁移都会导致vruntime重置,最终影响了调度公平性。嗯,这个问题在NUMA架构下尤其常见。
注意事项:进程迁移时,调度实体的vruntime需要做归一化处理。否则,不同CPU上的vruntime没有可比性,会导致调度不公平。内核里通过calc_delta_fair()函数来处理这个问题。
好了,这一章的内容就到这里。咱们把CFS的核心原理、调度类与调度策略、调度实体与运行队列都过了一遍。下一章,我会结合实际案例,讲讲怎么利用这些知识来做功耗优化。
记住一句话:理解调度器,是做好功耗优化的第一步。很多功耗问题,追根溯源,都是调度策略没选对,或者调度参数没调好。
咱们下章见。