## 引言:调度器——操作系统的命运裁判官
在Linux内核的宏伟殿堂中,调度器(Scheduler)无疑是最核心的组件之一。它决定了哪个进程获得CPU时间、获得多长时间,直接影响系统的吞吐量、响应时间和公平性。自Linux 2.6.23内核(2007年)引入CFS(Completely Fair Scheduler,完全公平调度器)以来,它彻底取代了之前的O(1)调度器,成为Linux默认的进程调度器,至今仍是内核调度子系统的基石。
CFS的设计哲学可以用四个字概括:"完全公平"。与传统的基于时间片和优先级的调度器不同,CFS引入了一个革命性的概念——虚拟运行时间(virtual runtime,简称vruntime),通过红黑树数据结构实现O(log n)时间复杂度的调度决策,在保证公平性的同时兼顾了高效性。
本文将从CFS的核心数据结构、调度算法原理、组调度机制、NUMA感知、实时进程交互,到生产环境的调优实践,全方位深入剖析CFS调度器。无论你是内核开发者、系统架构师还是性能调优工程师,都能从中获得系统性的知识和实战技巧。
## 第一章:CFS核心数据结构——红黑树与虚拟运行时间
### 1.1 核心数据结构关系
CFS的实现围绕几个关键数据结构展开,理解它们是深入CFS的基础:
```
struct task_struct // 进程描述符
└── struct sched_entity // 调度实体(se)
├── u64 vruntime // 虚拟运行时间
├── u64 exec_start // 开始执行时间
├── u64 sum_exec_runtime // 总实际运行时间
└── struct rb_node run_node // 红黑树节点
struct cfs_rq // CFS运行队列
├── struct rb_leftmost // 最左节点缓存
├── struct rb_root_cached // 红黑树根
├── u64 nr_running // 运行中任务数
└── struct sched_entity *curr // 当前执行实体
```
这里有一个关键设计点:调度实体(`sched_entity`)而非进程描述符(`task_struct`)是红黑树的节点。这意味着CFS可以调度多种"实体"——不仅是单个进程,还可以是进程组(通过`task_group`实现组调度)。
### 1.2 虚拟运行时间(vruntime)的精妙设计
vruntime是CFS的核心度量单位,其计算公式为:
```
vruntime += delta_exec * (NICE_0_LOAD / se->load.weight)
```
其中:
- `delta_exec`:进程实际执行的物理时间(纳秒)
- `NICE_0_LOAD`:nice值为0的进程权重基准(1024)
- `se->load.weight`:该调度实体的权重值
这意味着:
- **nice值为0**的进程:vruntime = 物理运行时间(等比例增长)
- **nice值为-10**的进程:vruntime增长更慢,更容易被选中(获得更多CPU)
- **nice值为+10**的进程:vruntime增长更快,更难被选中(获得更少CPU)
CFS始终选择vruntime最小的进程执行,这确保了"最亏欠CPU的进程优先运行"的公平原则。
### 1.3 红黑树:O(log n)的高效调度
CFS使用红黑树(Red-Black Tree)组织所有可运行进程,以vruntime为键值排序。每次调度决策只需取最左节点(vruntime最小者),时间复杂度仅为O(log n)。
Linux内核在`kernel/sched/fair.c`中的`__pick_next_entity()`函数实现了这一逻辑:
```c
static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
struct rb_node *left = cfs_rq->rb_leftmost;
return rb_entry(left, struct sched_entity, run_node);
}
```
实际实现中,内核通过`rb_leftmost`缓存了最左节点,使得取最小值操作优化到O(1)。
## 第二章:CFS调度算法的完整生命周期
### 2.1 进程创建:调度实体的初始化
当通过`fork()`或`clone()`创建新进程时,内核会调用`fork_init()` → `sched_fork()` → `entity_tick()`路径来初始化调度实体:
```c
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
unsigned long flags;
int cpu = get_cpu();
__sched_fork(clone_flags, p);
p->state = TASK_RUNNING;
p->prio = current->normal_prio;
// 核心:确定子进程的初始vruntime
if (unlikely(p->sched_reset_on_fork)) {
p->se.vruntime = 0; // 重置
} else {
// 将子进程的vruntime设置为当前cfs_rq的最小值
// 防止父进程fork出的子进程"饿死"其他进程
p->se.vruntime = curr_cfs_rq->min_vruntime;
}
// 将实体插入红黑树
enqueue_entity(cfs_rq, se, ENQUEUE_WAKEUP);
}
```
**关键设计决策**:子进程的初始vruntime不设置为0,而是设置为当前队列的`min_vruntime`。这避免了恶意进程通过不断fork子进程来获取不公平的CPU份额。
### 2.2 进程唤醒:入队与补偿
进程从睡眠状态唤醒时,`try_wake_up()` → `check_preempt_curr()` → `enqueue_entity()`路径会执行以下关键操作:
1. **vruntime下限保护**:如果被唤醒进程的vruntime远小于当前最小值,则会补偿到`min_vruntime - `threshold,避免"沉睡暴富"导致的长时间霸占CPU
2. **红黑树插入**:将进程按vruntime插入红黑树
3. **抢占检查**:如果新唤醒进程的vruntime小于当前进程,触发` resched_curr()`标记需要重新调度
```c
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED);
bool curr = cfs_rq->curr == se;
// 标准化vruntime
if (renorm && curr)
se->vruntime += cfs_rq->min_vruntime;
update_curr(cfs_rq);
if (renorm && !curr)
se->vruntime += cfs_rq->min_vruntime;
// 入队前更新负载统计
account_entity_enqueue(cfs_rq, se);
if (flags & ENQUEUE_WAKEUP)
place_entity(cfs_rq, se, 0);
// 插入红黑树
__enqueue_entity(cfs_rq, se);
se->on_rq = 1;
}
```
### 2.3 时钟滴答:周期性调度
每个时钟中断(tick),内核会调用`tick_handle_periodic()` → `scheduler_tick()` → `task_tick_fair()` → `entity_tick()`:
```c
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
// 更新当前进程的vruntime
update_curr(cfs_rq);
// 检查是否应该抢占当前进程
if (cfs_rq->nr_running > 1)
check_preempt_tick(cfs_rq, curr);
}
```
`check_preempt_tick()`是时间片计算的入口:
```c
static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
unsigned long ideal_runtime, delta_exec;
struct sched_entity *se;
s64 delta;
// 计算当前进程已获得的理想运行时间
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));
return;
}
// 最小粒度检查:如果不到最小粒度(1ms),不抢占
if (delta_exec < sysctl_sched_min_granularity)
return;
// 与最左节点比较vruntime差值
se = __pick_first_entity(cfs_rq);
delta = curr->vruntime - se->vruntime;
if (delta > 0)
resched_curr(rq_of(cfs_rq));
}
```
### 2.4 进程切换:上下文切换的完整流程
当需要切换进程时,`schedule()` → `pick_next_task_fair()`路径负责选取下一个进程:
```c
static struct task_struct *pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
struct cfs_rq *cfs_rq = &rq->cfs;
struct sched_entity *se;
struct task_struct *p;
// 处理组调度
do {
se = pick_next_entity(cfs_rq, NULL);
set_next_entity(cfs_rq, se);
cfs_rq = group_cfs_rq(se);
} while (cfs_rq);
p = task_of(se);
// 如果prev是CFS管理的进程,更新其状态
if (prev != p) {
struct sched_entity *pse = &prev->se;
// 将prev重新入队
put_prev_entity(cfs_rq, pse);
}
return p;
}
```
## 第三章:CFS组调度——从进程到容器的资源隔离
### 3.1 组调度(CGroup)与CFS的融合
Linux内核的`cpu`控制组(CGroup)通过CFS的组调度机制实现容器级别(Docker/Kubernetes)的CPU资源限制。其核心数据结构:
```c
struct task_group {
struct cgroup_subsys_state css;
// 组内每个CPU的CFS运行队列
struct cfs_bandwidth *cfs_bwidth; // CPU带宽控制
struct cfs_rq **cfs_rq; // per-cpu cfs_rq数组
struct sched_entity **se; // per-cpu sched_entity数组
// 权重与配额
unsigned long shares; // 相对权重
u64 quota; // 周期内配额(微秒)
u64 period; // 控制周期(默认100ms)
};
```
### 3.2 CFS Bandwidth Control:硬限制机制
CFS带宽控制(`CONFIG_CFS_BANDWIDTH`)提供了一种硬性CPU限制机制,当进程在某个周期内用完配额后,会被"限流"(throttled)直到下一个周期开始:
```c
// 带宽控制核心逻辑
static int do_sched_cfs_period_timer(struct cfs_bandwidth *cfs_b)
{
int overrun;
int idle = 0;
raw_spin_lock(&cfs_b->lock);
overrun = hrtimer_forward_now(&cfs_b->period_timer, cfs_b->period);
if (!overrun) {
idle = 1;
goto out_unlock;
}
// 检查是否超限
if (!cfs_b->timer_active) {
idle = 1;
goto out_unlock;
}
// 补充配额
cfs_b->quota = cfs_b->quota_base;
// 唤醒被限流的运行队列
if (cfs_b->nr_throttled)
distribute_cfs_runtime(cfs_b);
out_unlock:
raw_spin_unlock(&cfs_b->lock);
return idle ? HRTIMER_NORESTART : HRTIMER_RESTART;
}
```
### 3.3 Kubernetes CPU Limit的底层实现
在Kubernetes中,`resources.limits.cpu`翻译为CFS bandwidth control的`quota/period`。例如:
- `cpu: "500m"`(0.5核)→ `quota=50000, period=100000`(50ms/100ms)
- `cpu: "1"`(1核)→ `quota=100000, period=100000`(100ms/100ms)
**关键陷阱分析**:当容器内只有一个进程但设置了严格的CPU limit时,如果该进程在某个周期内用完配额,即使系统有其他空闲CPU核心,该进程也会被强制限流。这是Kubernetes CPU Throttling问题的根源。
### 3.4 生产级CGroup调优
对于Kubernetes集群中的关键Pod,以下调优策略值得参考:
```bash
# 1. 对于低延迟服务,考虑设置较大的period以减少限流频率
# cpu.cfs_period_us=50000 cpu.cfs_quota_us=25000(相当于50%核)
# 2. 对于批处理任务,保持default 100ms period
# cpu.cfs_period_us=100000 cpu.cfs_quota_us=50000
# 3. 使用CPU Manager Static Policy保证独占核心
# kubelet --cpu-manager-policy=static
```
## 第四章:NUMA感知调度——打破内存墙
### 4.1 NUMA拓扑与调度挑战
在现代多路服务器中,NUMA(Non-Uniform Memory Access)架构意味着:
- 访问本地内存延迟低(~100ns)
- 访问远程内存延迟高(~300ns+,是本地3倍)
CFS通过NUMA感知调度(`CONFIG_NUMA_BALANCING`)来优化页面放置和进程迁移。
### 4.2 AutoNUMA:自动负载均衡
Linux 3.13+内核引入的AutoNUMA调度机制结构如下:
```
AutoNUMA Balancing
├── numa_fault() // 页面缺页异常处理
├── task_numa_work() // 周期性扫描进程地址空间
├── migrate_misplaced() // 迁移"错位"页面
└── numa_group // NUMA本地组管理
```
工作流程:
1. 内核周期性地扫描进程的内存页(通过`task_numa_work()`)
2. 采样哪些页面被远程CPU访问(记录在`numa_faults`中)
3. 如果远程访问频率超过阈值,触发页面迁移或进程迁移
### 4.3 查看与调优NUMA调度
```bash
# 查看进程的NUMA访问统计
cat /proc/

发表评论 取消回复