进程调度算法(CFS)

Linux 使用 CFS(Completely Fair Scheduler)完全公平调度器作为默认的进程调度算法。本章深入探讨 CFS 的设计原理和实现机制。

调度器演进历史

Linux 2.4
├─ O(n) 调度器
└─ 遍历所有进程,复杂度 O(n)

Linux 2.6.0 - 2.6.22
├─ O(1) 调度器
├─ 固定时间片
└─ 140 个优先级队列

Linux 2.6.23+
├─ CFS (Completely Fair Scheduler)
├─ 红黑树结构
├─ 虚拟运行时间
└─ 完全公平

CFS 核心概念

虚拟运行时间(vruntime)

公式

vruntime = 实际运行时间 × (NICE_0_LOAD / 进程权重)

其中:
- NICE_0_LOAD = 1024 (nice 值为 0 的权重)
- 进程权重根据 nice 值确定
- nice 值越低,权重越大,vruntime 增长越慢

nice 值与权重对应表

// kernel/sched/core.c
const int sched_prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};

// nice 值每增加 1,权重约减少 10%
// nice 值每减少 1,权重约增加 10%

红黑树调度队列

// kernel/sched/sched.h
struct cfs_rq {
    struct load_weight load;              // 队列总权重
    unsigned int nr_running;              // 运行进程数
    u64 min_vruntime;                     // 最小 vruntime
    
    struct rb_root_cached tasks_timeline; // 红黑树根节点
    struct rb_node *rb_leftmost;          // 最左节点(最小 vruntime)
    
    struct sched_entity *curr;            // 当前运行的调度实体
    struct sched_entity *next;            // 下一个调度实体
    struct sched_entity *last;            // 上一个调度实体
    struct sched_entity *skip;            // 跳过的调度实体
};

红黑树特性

  • 查找最小 vruntime:O(1) - 直接取 leftmost
  • 插入/删除:O(log n)
  • 自平衡二叉搜索树

调度实体(sched_entity)

// include/linux/sched.h
struct sched_entity {
    struct load_weight      load;           // 权重
    unsigned int            on_rq;          // 是否在运行队列
    
    u64                     exec_start;     // 开始执行时间
    u64                     sum_exec_runtime; // 累计运行时间
    u64                     vruntime;       // 虚拟运行时间
    u64                     prev_sum_exec_runtime;
    
    u64                     nr_migrations;
    
    struct sched_statistics statistics;
    
#ifdef CONFIG_FAIR_GROUP_SCHED
    int                     depth;
    struct sched_entity     *parent;
    struct cfs_rq           *cfs_rq;        // 所在 CFS 队列
    struct cfs_rq           *my_q;          // 组调度队列
#endif
};

CFS 调度流程

选择下一个进程

// kernel/sched/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;
    
    // 如果队列为空,返回 NULL
    if (!cfs_rq->nr_running)
        return NULL;
    
    // 将当前进程重新放回红黑树
    put_prev_task(rq, prev);
    
    // 选择 vruntime 最小的进程(红黑树最左节点)
    se = pick_next_entity(cfs_rq, NULL);
    
    return task_of(se);
}

static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
    struct sched_entity *left = __pick_first_entity(cfs_rq);
    
    // 返回最左节点(vruntime 最小)
    return left;
}

进程唤醒

// kernel/sched/fair.c
static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
    struct cfs_rq *cfs_rq;
    struct sched_entity *se = &p->se;
    
    // 将调度实体加入 CFS 队列
    for_each_sched_entity(se) {
        if (se->on_rq)
            break;
        cfs_rq = cfs_rq_of(se);
        enqueue_entity(cfs_rq, se, flags);
        cfs_rq->h_nr_running++;
    }
    
    // 更新 CPU 负载
    add_nr_running(rq, 1);
}

static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    // 更新 vruntime
    update_curr(cfs_rq);
    
    // 如果是新进程或睡眠唤醒,调整 vruntime
    if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED))
        se->vruntime += cfs_rq->min_vruntime;
    
    // 插入红黑树
    __enqueue_entity(cfs_rq, se);
    se->on_rq = 1;
    
    // 更新最小 vruntime
    if (cfs_rq->nr_running == 1) {
        list_add_leaf_cfs_rq(cfs_rq);
    }
}

时间片和调度周期

// kernel/sched/fair.c

// 调度周期(默认 6ms)
unsigned int sysctl_sched_latency = 6000000ULL;

// 最小粒度(默认 0.75ms)
unsigned int sysctl_sched_min_granularity = 750000ULL;

// 计算时间片
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    u64 slice = __sched_period(cfs_rq->nr_running);
    
    // 根据权重分配时间片
    slice *= se->load.weight;
    do_div(slice, cfs_rq->load.weight);
    
    return slice;
}

// 调度周期 = max(nr_running * min_granularity, latency)
static u64 __sched_period(unsigned long nr_running)
{
    if (unlikely(nr_running > sched_nr_latency))
        return nr_running * sysctl_sched_min_granularity;
    else
        return sysctl_sched_latency;
}

优先级和 nice 值

设置进程优先级

# 查看进程优先级
ps -eo pid,ni,pri,comm

# 启动时设置 nice 值
nice -n 10 ./program

# 调整运行中进程的 nice 值
renice -n 5 -p <PID>

# 只有 root 可以降低 nice 值(提高优先级)
sudo renice -n -5 -p <PID>

实时优先级

// 实时调度策略
#define SCHED_FIFO      1  // 先进先出
#define SCHED_RR        2  // 时间片轮转
#define SCHED_DEADLINE  6  // 截止时间调度

// 设置实时优先级(需要 root)
#include <sched.h>

struct sched_param param;
param.sched_priority = 50;  // 1-99

// 设置为 FIFO 调度
sched_setscheduler(getpid(), SCHED_FIFO, &param);

// 设置为 RR 调度
sched_setscheduler(getpid(), SCHED_RR, &param);

查看实时进程

# 显示实时优先级
ps -eo pid,rtprio,cls,comm

# chrt 工具
chrt -p <PID>           # 查看
chrt -f -p 50 <PID>     # 设置 FIFO,优先级 50
chrt -r -p 50 <PID>     # 设置 RR,优先级 50

CPU 亲和性

设置 CPU 亲和性

// 使用 sched_setaffinity
#define _GNU_SOURCE
#include <sched.h>

cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);  // 绑定到 CPU 0
CPU_SET(1, &cpuset);  // 绑定到 CPU 1

sched_setaffinity(getpid(), sizeof(cpuset), &cpuset);

// 查看当前亲和性
cpu_set_t current;
sched_getaffinity(getpid(), sizeof(current), &current);

使用 taskset

# 查看进程的 CPU 亲和性
taskset -p <PID>

# 设置 CPU 亲和性
taskset -p 0x3 <PID>     # 绑定到 CPU 0 和 1 (二进制 11)
taskset -cp 0,1 <PID>    # 绑定到 CPU 0 和 1

# 启动时指定
taskset -c 0,1 ./program

组调度(Control Groups)

cgroup CPU 控制

# 创建 cgroup
sudo mkdir /sys/fs/cgroup/cpu/mygroup

# 设置 CPU 份额(相对权重)
echo 512 > /sys/fs/cgroup/cpu/mygroup/cpu.shares

# 设置 CPU 配额(绝对限制)
echo 50000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us    # 50ms
echo 100000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us  # 100ms
# 限制为 50% CPU

# 将进程加入 cgroup
echo <PID> > /sys/fs/cgroup/cpu/mygroup/cgroup.procs

# 查看统计
cat /sys/fs/cgroup/cpu/mygroup/cpuacct.usage

systemd cgroup 管理

# 查看服务的 CPU 使用
systemctl show nginx.service -p CPUQuotaPerSecUSec

# 设置 CPU 配额(50%)
systemctl set-property nginx.service CPUQuota=50%

# 设置 CPU 权重
systemctl set-property nginx.service CPUWeight=500

# 查看 cgroup 树
systemd-cgls

调度器性能分析

查看调度统计

# 查看进程调度统计
cat /proc/<PID>/sched

# 关键指标:
# nr_switches: 上下文切换次数
# nr_voluntary_switches: 自愿切换
# nr_involuntary_switches: 非自愿切换
# se.sum_exec_runtime: 总运行时间
# se.vruntime: 虚拟运行时间

调度延迟分析

# 使用 perf 分析调度延迟
perf sched record -- sleep 10
perf sched latency

# 输出示例:
# Task             | Runtime ms | Switches | Average delay | Maximum delay |
# :26656:26656     |   1000.00  |    100   |    0.50 ms    |    5.00 ms    |

实时性测试

#!/bin/bash
# test-scheduling-latency.sh

# 编译测试程序
cat > latency_test.c << 'EOF'
#include <stdio.h>
#include <time.h>
#include <unistd.h>

int main() {
    struct timespec start, end;
    long long latency;
    
    for (int i = 0; i < 1000; i++) {
        clock_gettime(CLOCK_MONOTONIC, &start);
        usleep(1000);  // 睡眠 1ms
        clock_gettime(CLOCK_MONOTONIC, &end);
        
        latency = (end.tv_sec - start.tv_sec) * 1000000000LL +
                 (end.tv_nsec - start.tv_nsec);
        
        printf("Iteration %d: %lld ns\n", i, latency);
    }
    
    return 0;
}
EOF

gcc -o latency_test latency_test.c -lrt
./latency_test | awk '{sum+=$3; count++} END {print "Average:", sum/count, "ns"}'

调度器调优

调整调度参数

# 查看当前调度参数
sysctl -a | grep sched

# 调度延迟(默认 6ms)
sysctl -w kernel.sched_latency_ns=10000000

# 最小粒度(默认 0.75ms)
sysctl -w kernel.sched_min_granularity_ns=1000000

# 唤醒粒度(默认 1ms)
sysctl -w kernel.sched_wakeup_granularity_ns=2000000

# 持久化
echo "kernel.sched_latency_ns=10000000" >> /etc/sysctl.conf
sysctl -p

负载均衡

# 查看负载均衡域
cat /proc/sys/kernel/sched_domain/cpu*/domain*/name

# 调整负载均衡间隔
# /proc/sys/kernel/sched_migration_cost_ns
sysctl -w kernel.sched_migration_cost_ns=500000

实践案例

高性能计算场景

#!/bin/bash
# optimize-for-hpc.sh

# 1. 设置实时优先级
chrt -f -p 99 <HPC_PID>

# 2. 绑定 CPU
taskset -cp 0-7 <HPC_PID>

# 3. 禁用 CPU 频率调节
for cpu in /sys/devices/system/cpu/cpu[0-7]; do
    echo performance > $cpu/cpufreq/scaling_governor
done

# 4. 禁用中断均衡
systemctl stop irqbalance

# 5. 调整调度参数
sysctl -w kernel.sched_min_granularity_ns=10000000
sysctl -w kernel.sched_wakeup_granularity_ns=15000000

Web 服务器场景

#!/bin/bash
# optimize-for-web.sh

# 1. 使用默认 CFS 调度
# 2. 适度调整 nice 值
renice -n -5 -p $(pgrep nginx)

# 3. 使用 cgroup 限制资源
systemctl set-property nginx.service CPUQuota=200%
systemctl set-property nginx.service CPUWeight=1000

# 4. 启用自动 NUMA 平衡
sysctl -w kernel.numa_balancing=1

下一步:学习 进程间通信(IPC) 章节。