一、进程调度器概述
Linux内核的进程调度器(Scheduler)是操作系统的核心组件之一,负责决定哪个进程获得CPU时间片、何时获得、获得多久。自Linux 2.6.23版本起,CFS(Completely Fair Scheduler,完全公平调度器)取代了之前的O(1)调度器,成为默认的普通进程调度算法。
调度器的核心挑战在于:如何在众多竞争CPU的进程中实现公平性,同时保证低延迟和高吞吐。CFS的创新之处在于它不使用传统的时间片概念,而是通过虚拟运行时间(vruntime)来追踪每个进程的CPU使用量,并总是选择vruntime最小的进程调度。
二、CFS完全公平调度算法原理
2.1 核心思想
CFS的核心目标是让所有可运行进程的vruntime尽可能相等。为了实现这一点,CFS为每个CPU维护一棵红黑树(rbtree),以vruntime为键值排序。调度时总是选择树最左侧(vruntime最小)的进程执行。
// 内核 CFS 核心结构 (简化)
struct cfs_rq {
struct rb_root_cached tasks_timeline; // 红黑树根
struct rb_node *rb_leftmost; // 缓存最左侧节点
u64 min_vruntime; // 最小 vruntime
unsigned long weight; // 队列总权重
};
struct sched_entity {
u64 vruntime; // 虚拟运行时间
u64 exec_start; // 开始执行时间
u64 sum_exec_runtime; // 总实际运行时间
int weight; // 调度权重(与nice值对应)
};
2.2 虚拟运行时间计算
vruntime的计算公式为:
vruntime += delta_exec * (NICE_0_LOAD / weight)
其中delta_exec是实际运行时间,weight由进程的nice值决定。优先级越高(nice值越小)的进程,vruntime增长越慢,因此获得更多的实际CPU时间。
nice值到权重的对应关系(部分):
| Nice值 | 权重 | CPU占比 |
|---|---|---|
| -20 | 88761 | ~115% |
| -10 | 11058 | ~102% |
| 0 | 1024 | 基准100% |
| 10 | 110 | ~15% |
| 19 | 15 | ~1% |
2.3 调度延迟与最小粒度
通过/proc/sys/kernel/sched_latency_ns和sched_min_granularity_ns两个参数控制。CFS保证每个可运行进程至少获得一次调度机会的时间不超过目标延迟,同时单次执行不少于最小粒度以避免过频繁的上下文切换。
# 默认值查看
sysctl kernel.sched_latency_ns # 24,000,000 (24ms)
sysctl kernel.sched_min_granularity_ns # 3,000,000 (3ms)
sysctl kernel.sched_wakeup_granularity_ns # 4,000,000 (4ms)
三、Linux调度策略详解
3.1 实时调度策略
Linux 定义了三种实时调度策略:
- SCHED_FIFO (1):先进先出实时调度。进入运行态后一直运行直到阻塞、退出或被更高优先级抢占
- SCHED_RR (2):轮转实时调度。与SCHED_FIFO类似,但同优先级进程按时间片轮转
- SCHED_DEADLINE (6):最后期限调度(Linux 3.14+)。基于EDF(最早截止时间优先),适用于周期性实时任务
3.2 普通调度策略
- SCHED_OTHER/SCHED_NORMAL (0):默认策略,使用CFS
- SCHED_BATCH (3):批处理策略,减少唤醒抢占,适合CPU密集型批处理任务
- SCHED_IDLE (5):极低优先级,仅当系统空闲时才运行
四、核心调度API实战
4.1 设置调度策略和优先级
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
int main() {
struct sched_param param;
int policy;
// 获取当前进程的调度策略和优先级
policy = sched_getscheduler(0);
printf("Current policy: %d\n", policy);
// 设置实时FIFO策略,优先级50
param.sched_priority = 50;
if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) {
perror("sched_setscheduler");
return 1;
}
printf("Set to SCHED_FIFO with priority %d\n", param.sched_priority);
return 0;
}
编译运行需要root权限:
gcc -o set_rt sched_rt.c
sudo ./set_rt
4.2 设置CPU亲和性
#include <sched.h>
void pin_to_cpu(int cpu_id) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);
if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) == -1) {
perror("sched_setaffinity");
}
printf("Pinned to CPU %d\n", cpu_id);
}
// 使用工具命令行
taskset -c 0,2 ./my_program // 绑定到CPU 0和2
taskset -p -c 1,3 <pid> // 修改运行中进程的亲和性
4.3 修改nice值和设置SCHED_DEADLINE
// 修改nice值(需要特权)
setpriority(PRIO_PROCESS, 0, -10);
// SCHED_DEADLINE 设置 (Linux 3.14+)
struct sched_attr attr = {
.size = sizeof(attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 10 * 1000 * 1000, // 10ms
.sched_deadline = 30 * 1000 * 1000, // 30ms
.sched_period = 30 * 1000 * 1000 // 30ms
};
sched_setpid(0, &attr, 0);
五、进程优先级体系
Linux进程优先级从高到低排列:
- 实时优先级 (0-99):SCHED_FIFO/SCHED_RR/SCHED_DEADLINE使用,数值越大优先级越高
- 普通优先级 (100-139):对应nice值-20~19,SCHED_OTHER使用
- SCHED_IDLE (140):最低优先级
// 查看进程调度信息
chrt -p <pid> // 查看实时优先级和策略
ps -eo pid,class,rtprio,pri,ni,comm | head
六、CFS组调度与带宽控制
6.1 CFS组调度 (Group Scheduling)
CFS支持将进程分组,在组间分配CPU资源。配合cgroups v1的cpu子系统使用:
# 创建cgroup
mkdir /sys/fs/cgroup/cpu/group1
echo 512 > /sys/fs/cgroup/cpu/group1/cpu.shares // 相对份额(默认1024)
// 添加进程到组
echo <pid> > /sys/fs/cgroup/cpu/group1/cgroup.procs
6.2 CFS带宽控制 (CPU bandwidth)
通过cfs_quota_us和cfs_period_us限制组内进程的CPU使用上限:
# 限制组内进程每100ms最多使用50ms CPU时间(即最多50% CPU)
echo 100000 > /sys/fs/cgroup/cpu/group1/cpu.cfs_period_us
echo 50000 > /sys/fs/cgroup/cpu/group1/cpu.cfs_quota_us
6.3 cgroups v2 CPU控制
# cgroups v2
echo "50000 100000" > /sys/fs/cgroup/foo/cpu.max // 50% CPU上限
echo "max 100000" > /sys/fs/cgroup/foo/cpu.max // 无限制
七、NUMA感知调度
在NUMA(非统一内存访问)系统中,进程访问本地节点的内存延迟远低于远程节点。Linux调度器提供以下NUMA优化:
- 自动NUMA平衡:内核自动将进程迁移到靠近其内存的CPU节点
- numactl工具:手动控制NUMA策略
- sched_setaffinity:手动绑定CPU
# 查看NUMA拓扑
numactl --hardware
# 在节点0上运行程序,使用节点0内存
numactl --cpunodebind=0 --membind=0 ./program
# 禁止自动NUMA balancing
echo 0 > /proc/sys/kernel/numa_balancing
八、调度器性能调优实践
8.1 低延迟场景优化
# 减少调度延迟(适用于低延迟场景)
sysctl -w kernel.sched_latency_ns=6000000
sysctl -w kernel.sched_min_granularity_ns=750000
sysctl -w kernel.sched_wakeup_granularity_ns=1000000
# 关闭NUMA balancing(固定部署场景)
sysctl -w kernel.numa_balancing=0
8.2 CPU隔离与实时场景
# 内核启动参数隔离CPU(将CPU 2-7隔离)
isolcpus=2,3,4,5,6,7 nohz_full=2,3,4,5,6,7 rcu_nocbs=2,3,4,5,6,7
# 将实时任务绑定到隔离CPU
taskset -c 4 chrt -f 99 ./realtime_task
8.3 容器场景调度优化
# Docker CPU限制(对应cfs quota)
docker run --cpus=2.0 --cpu-shares=512 --cpuset-cpus=0-3 app
# Kubernetes resources 配置
resources:
requests:
cpu: "500m" // 0.5 CPU guarantee
limits:
cpu: "2000m" // 2 CPU quota limit
九、调度器性能分析工具
| 工具 | 用途 |
|---|---|
| perf sched | 调度事件追踪与分析 |
| trace-cmd | function tracer追踪调度函数 |
| runqlat | BPF工具,显示调度队列延迟 |
| runqlen | BPF工具,显示运行队列长度 |
| top/htop | 实时查看进程CPU使用 |
| chrt | 查看/设置实时调度策略和优先级 |
| taskset | 设置CPU亲和性 |
| tuned | 系统级性能调优配置文件 |
# perf sched 记录并分析调度延迟
perf sched record -- sleep 10
perf sched latency // 显示每个进程的调度延迟分布
perf sched map // 显示CPU上的调度事件图
# BPF/BCC runqlat
runqlat 1 10 // 每秒采样,共10秒
十、总结
Linux内核的进程调度器经过二十多年的演进,已经从简单的轮转调度发展到今天高度优化的CFS加多策略并行体系。深入理解调度器的工作原理,对于系统调优、性能瓶颈分析、实时系统设计都至关重要。
关键要点回顾:
- CFS通过vruntime和红黑树实现O(log N)复杂度的公平调度
- 实时策略(SCHED_FIFO/RR/DEADLINE)优先级始终高于普通策略
- CPU亲和性加NUMA拓扑感知是高性能计算的关键优化手段
- CFS带宽控制(cgroups)是容器隔离CPU资源的核心机制
- 实时场景需要内核参数隔离配合启动参数配置

发表评论 取消回复