前言
适用平台 :通用 x86_64 / ARM64(含 NXP i.MX8MP 嵌入式实时系统)
内核版本 :Linux 5.10 LTS 及以上(含 EEVDF 调度器演进说明)
文档定位 :从 CFS 虚拟运行时原理到 RT/Deadline 调度、从 cgroup CPU 带宽控制到 PREEMPT_RT 实时补丁、从调度器调优到嵌入式低延迟优化的全栈调度器参考
配套代码/命令:所有命令均已在 i.MX8MP(Yocto/Buildroot)与 Ubuntu 22.04 (Kernel 5.15) 上验证
目录
- 调度器全景架构
- 进程状态与调度类
- CFS:完全公平调度器
- [EEVDF:CFS 的演进(6.6+)](#EEVDF:CFS 的演进(6.6+) "#4-eevdfcfs-%E7%9A%84%E6%BC%94%E8%BF%9B66")
- [实时调度:SCHED_FIFO / SCHED_RR](#实时调度:SCHED_FIFO / SCHED_RR "#5-%E5%AE%9E%E6%97%B6%E8%B0%83%E5%BA%A6sched_fifo--sched_rr")
- [Deadline 调度:SCHED_DEADLINE](#Deadline 调度:SCHED_DEADLINE "#6-deadline-%E8%B0%83%E5%BA%A6sched_deadline")
- [NUMA 感知调度](#NUMA 感知调度 "#7-numa-%E6%84%9F%E7%9F%A5%E8%B0%83%E5%BA%A6")
- [cgroup CPU 控制与带宽限制](#cgroup CPU 控制与带宽限制 "#8-cgroup-cpu-%E6%8E%A7%E5%88%B6%E4%B8%8E%E5%B8%A6%E5%AE%BD%E9%99%90%E5%88%B6")
- [CPU 亲和性与负载均衡](#CPU 亲和性与负载均衡 "#9-cpu-%E4%BA%B2%E5%92%8C%E6%80%A7%E4%B8%8E%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1")
- [抢占模型与 PREEMPT_RT](#抢占模型与 PREEMPT_RT "#10-%E6%8A%A2%E5%8D%A0%E6%A8%A1%E5%9E%8B%E4%B8%8E-preempt_rt")
- 调度器观测与调优工具
- 嵌入式实时优化实战(i.MX8MP)
- 经典案例与排查流程
- 总结与最佳实践
1. 调度器全景架构
1.1 调度器要解决的问题
Linux 调度器需要在多个相互竞争的目标间平衡:
| 目标 | 含义 | 典型冲突 |
|---|---|---|
| 公平性 | 所有进程公平分享 CPU | 与低延迟冲突 |
| 响应性 | 交互式任务快速响应 | 与吞吐量冲突 |
| 实时性 | RT 任务确定性时限 | 与公平性冲突 |
| 能效 | 大小核/调频省电 | 与性能冲突 |
| NUMA 亲和 | 减少跨节点访问 | 与负载均衡冲突 |
1.2 调度器架构图
bash
┌─────────────────────────────────────────────────────────┐
│ 用户空间接口 │
│ sched_setscheduler() sched_setattr() sched_setaffinity│
│ nice() sched_yield() cpuset cgroup v2 cpu.* │
└────────────────────────┬────────────────────────────────┘
│ 系统调用
┌────────────────────────▼────────────────────────────────┐
│ 核心调度器 │
│ ┌────────────────────────────────────────────────┐ │
│ │ 调度类(sched_class) │ │
│ │ ┌──────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │Deadline │ │ RT │ │ CFS │ │ Idle │ │ │
│ │ │(dl_sched)│ │(rt_sched)│ │(fair_sched)│ │ │ │ │
│ │ │ 最高优先级│ │ 实时 │ │ 普通 │ │ 空闲 │ │ │
│ │ └──────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼─────────────────────────┐ │
│ │ pick_next_task() │ │
│ │ 按优先级顺序遍历调度类,选中第一个非空的 │ │
│ └───────────────────────┬─────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼─────────────────────────┐ │
│ │ put_prev_task() / set_next_task() │ │
│ │ enqueue_task() / dequeue_task() │ │
│ │ update_curr() 等钩子 │ │
│ └─────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 底层机制 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │调度域 │ │负载均衡 │ │CPU亲和性 │ │抢占控制 │ │
│ │(sd) │ │(lb) │ │(affinity)│ │(preempt) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────┤
│ 时钟中断 │
│ scheduler_tick() → 调度器周期性决策 │
└─────────────────────────────────────────────────────────┘
1.3 调度类(Sched Class)优先级
内核按固定优先级顺序遍历调度类,选中第一个有可运行任务的类:
bash
优先级(高→低):
1. stop_sched_class // 最高:迁移线程(内核内部)
2. dl_sched_class // SCHED_DEADLINE
3. rt_sched_class // SCHED_FIFO / SCHED_RR
4. fair_sched_class // SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE
5. idle_sched_class // 空闲任务(idle thread)
bash
// kernel/sched/core.c
static struct sched_class *highest_class = &stop_sched_class;
#define for_class_range(class, start, end) \
for (class = start; class != end; class = class->next)
// pick_next_task 核心逻辑
static struct task_struct *pick_next_task(struct rq *rq)
{
struct sched_class *class;
struct task_struct *p;
for_class_range(class, &fair_sched_class, &idle_sched_class) {
p = class->pick_next_task(rq);
if (p)
return p;
}
return idle_task(); // 都没有任务 → 跑 idle
}
2. 进程状态与调度类
2.1 进程状态
bash
┌──────────────────┐
fork/clone │ │
──────────→│ TASK_RUNNING │←─────────┐
│ (可运行/运行中) │ │
└────────┬─────────┘ │
│ schedule() │ 唤醒
┌──────────────┼──────────────┐ │
│ │ │ │
▼ ▼ ▼ │
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ TASK_ │ │ TASK_ │ │ TASK_ │
│ INTERRUPTIBLE│ │ UNINTERRUPTIBLE│ │ STOPPED │
│ (可中断睡眠) │ │ (不可中断) │ │ (停止) │
└──────┬──────┘ └──────┬──────┘ └─────────────┘
│ │
│ 信号/事件 │ IO 完成
└───────────────┴──→ TASK_RUNNING
| 状态 | 宏 | 说明 |
|---|---|---|
| 可运行 | TASK_RUNNING (R) |
在运行队列中,等待或正在 CPU 上执行 |
| 可中断睡眠 | TASK_INTERRUPTIBLE (S) |
等待事件,可被信号唤醒 |
| 不可中断睡眠 | TASK_UNINTERRUPTIBLE (D) |
等待 IO,不可被信号唤醒(ps 中 D 状态) |
| 停止 | TASK_STOPPED (T) |
收到 SIGSTOP/SIGTSTP |
| 僵尸 | EXIT_ZOMBIE (Z) |
进程退出但父进程未 wait() |
| 僵尸停止 | EXIT_DEAD (X) |
正在被销毁 |
2.2 调度策略(Scheduling Policy)
| 策略 | 宏 | 类型 | 优先级范围 | 用途 |
|---|---|---|---|---|
SCHED_NORMAL |
SCHED_OTHER |
CFS | nice -20~19 | 普通进程(默认) |
SCHED_BATCH |
CFS | nice -20~19 | 批处理,减少抢占 | |
SCHED_IDLE |
CFS | 最低 | 极低优先级后台任务 | |
SCHED_FIFO |
RT | 1~99 | 实时 FIFO | |
SCHED_RR |
RT | 1~99 | 实时时间片轮转 | |
SCHED_DEADLINE |
DL | 最高 | 硬实时(截止时间) |
查看/设置调度策略:
bash
# 查看进程调度策略
$ chrt -p $PID
pid 1234's current scheduling policy: SCHED_OTHER
pid 1234's current scheduling priority: 0
# 设置为 SCHED_FIFO 优先级 50
$ chrt -f -p 50 $PID
# 设置为 SCHED_DEADLINE(需要特殊权限)
$ chrt -d --sched-runtime 1000000 --sched-deadline 10000000 --sched-period 10000000 -p $PID
# nice 值调整
$ nice -n -10 ./myapp # 提高优先级(需要 root)
$ renice -n 10 -p $PID # 降低优先级
3. CFS:完全公平调度器
3.1 CFS 核心思想:虚拟运行时
CFS(Completely Fair Scheduler)用虚拟运行时(vruntime) 来保证公平:
- 每个进程维护一个
vruntime(虚拟运行时间) - 每次运行,
vruntime增加(实际运行时间 × 权重) - 调度器总是选择
vruntime最小的进程(红黑树最左节点) - nice 值高的进程(低优先级)权重小,
vruntime增长快 → 更少被调度
bash
实际运行时间 × nice权重 → vruntime 增量
nice = 0(权重 1024):运行 10ms → vruntime += 10
nice = -10(权重 9102):运行 10ms → vruntime += 10×(1024/9102) ≈ 1.1
nice = 10(权重 110):运行 10ms → vruntime += 10×(1024/110) ≈ 93.1
效果:nice=-10 的进程每获得 10ms CPU,nice=10 的进程获得约 93ms。两者"公平"地按权重分享 CPU。
3.2 权重与 nice 值
bash
// 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 0 权重 = 1024
// nice -20 权重 = 88761(约 86.7 倍)
// nice 19 权重 = 15(约 1/68)
CPU 时间分配公式(两个进程):
bash
进程A CPU份额 = 权重A / (权重A + 权重B) × 100%
例:nice=0(权重1024)与 nice=10(权重110)
A 获得 1024/(1024+110) ≈ 90.3% CPU
B 获得 110/(1024+110) ≈ 9.7% CPU
3.3 CFS 数据结构
bash
// kernel/sched/sched.h
struct cfs_rq {
struct load_weight load; // 总负载权重
unsigned long nr_running; // 可运行任务数
u64 min_vruntime; // 全局最小 vruntime(基准)
struct rb_root_cached tasks_timeline; // 红黑树(按 vruntime 排序)
struct sched_entity *curr; // 当前运行的任务
// ...
};
struct sched_entity {
struct load_weight load; // 权重(基于 nice)
struct rb_node run_node; // 红黑树节点
u64 exec_start; // 本次运行开始时间
u64 sum_exec_runtime; // 累计实际运行时间
u64 vruntime; // 虚拟运行时间(核心!)
u64 prev_sum_exec_runtime; // 上次统计的运行时间
// ...
};
3.4 红黑树:O(log n) 选择下一个任务
bash
CFS 用红黑树按 vruntime 排序,最左节点(最小 vruntime)就是下一个运行的任务:
[vruntime=100]
/ \
[vruntime=50] [vruntime=150]
/ \
[vruntime=20] [vruntime=70]
↑
最左节点 → 下一个调度运行
插入/删除/查找最小值都是 O(log n)
关键函数:
bash
// kernel/sched/fair.c
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec = now - curr->exec_start;
curr->exec_start = now;
curr->sum_exec_runtime += delta_exec;
// 计算 vruntime 增量:实际时间 × (权重基准/当前权重)
curr->vruntime += calc_delta_fair(delta_exec, curr);
// 更新 cfs_rq 的 min_vruntime
update_min_vruntime(cfs_rq);
}
static u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
return delta;
}
3.5 唤醒抢占(Wakeup Preemption)
当一个高优先级(低 vruntime)的任务被唤醒时,CFS 检查是否应抢占当前任务:
bash
// kernel/sched/fair.c
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
struct sched_entity *curr = rq->curr->se;
struct sched_entity *se = p->se;
// 如果唤醒任务的 vruntime 显著小于当前任务 → 抢占
if (wake_flags & WF_FORK) // fork 时
goto preempt;
if (vruntime_gt(curr->vruntime, se->vruntime))
goto preempt;
preempt:
resched_curr(rq); // 设置当前任务 TIF_NEED_RESCHED,返回用户态时触发调度
}
3.6 CFS 的局限性
- vruntime 溢出:长时间运行 vruntime 会溢出(u64 约 584 年,实际无问题)
- NUMA 不友好:纯按 vruntime 调度,不考虑内存访问延迟
- 延迟抖动:大量任务时红黑树深度大,调度延迟增加
- 组调度开销:cgroup 层级深时,虚拟时间计算复杂
这些局限推动了 EEVDF(6.6+)和 NUMA 调度的演进。
4. EEVDF:CFS 的演进(6.6+)
4.1 EEVDF 简介
Linux 6.6 引入 EEVDF(Earliest Eligible Virtual Deadline First)作为 CFS 的替代。EEVDF 在 vruntime 基础上引入虚拟截止时间(virtual deadline):
bash
传统 CFS:选择 vruntime 最小的任务
EEVDF:选择 eligible(已到 eligible 时间)且 virtual deadline 最早的任务
优点:
1. 更精确的延迟保证
2. 解决 CFS 的"迟到唤醒"问题
3. 更好的交互式响应
4.2 EEVDF 核心概念
bash
// kernel/sched/fair.c (6.6+)
struct sched_entity {
u64 vruntime; // 虚拟运行时间
u64 deadline; // 虚拟截止时间
u64 min_deadline; // 组内最小截止时间(层级)
// ...
};
// deadline 计算
static u64 calc_deadline(struct sched_entity *se, u64 slice)
{
// deadline = vruntime + slice / weight
// 权重高(nice 低)→ deadline 更早 → 更容易被选中
}
EEVDF 选择逻辑:
- 找出所有
eligible(vruntime ≤ 当前时间偏移)的任务 - 从中选
deadline最小的 - 如果当前任务的 deadline 已过,新任务可抢占
4.3 i.MX8MP 内核版本注意
bash
# 查看内核版本
$ uname -r
5.10.198-rt99 # 仍是 CFS,无 EEVDF
# 6.6+ 才有 EEVDF
# i.MX8MP NXP BSP 目前是 5.10/5.15,仍用 CFS
# 若需 EEVDF 特性,需升级到 6.6+ 并验证 BSP 兼容性
5. 实时调度:SCHED_FIFO / SCHED_RR
5.1 RT 调度类特点
RT 调度类优先级严格高于 CFS,只要 RT 任务可运行,CFS 任务就得不到 CPU:
bash
优先级范围:1~99(数字越大优先级越高)
99:最高(通常给硬中断线程化后)
1:最低 RT
同一优先级内:
- SCHED_FIFO:FIFO 顺序,不主动让出就一直运行
- SCHED_RR:时间片轮转(默认 100ms),同优先级公平分享
5.2 RT 调度数据结构
bash
// kernel/sched/rt.c
struct rt_rq {
struct rt_prio_array active; // 每个优先级一个链表 + 位图
unsigned int rt_nr_running; // RT 可运行任务数
// ...
};
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO + 1); // 位图:哪个优先级有任务
struct list_head queue[MAX_RT_PRIO]; // 每优先级一个 FIFO 链表
};
// 选择下一个 RT 任务:找到位图中最高的优先级,取链表第一个
static struct task_struct *pick_next_task_rt(struct rq *rq)
{
struct rt_prio_array *array = &rt_rq->active;
int idx = sched_find_first_bit(array->bitmap); // 找最高优先级
if (idx >= MAX_RT_PRIO)
return NULL;
return list_first_entry(&array->queue[idx], struct task_struct, ...);
}
RT 调度是 O(1):位图找最高优先级 + 链表取第一个,与任务数无关。
5.3 RT 调度编程
bash
#include <sched.h>
#include <sys/resource.h>
int setup_rt_priority(int priority)
{
struct sched_param param;
param.sched_priority = priority; // 1~99
// 设置为 SCHED_FIFO
if (sched_setscheduler(0, SCHED_FIFO, ¶m) < 0) {
perror("sched_setscheduler");
return -1;
}
// 或设置为 SCHED_RR
// sched_setscheduler(0, SCHED_RR, ¶m);
return 0;
}
// 查询当前策略
void print_sched_info(void)
{
int policy = sched_getscheduler(0);
struct sched_param param;
sched_getparam(0, ¶m);
printf("Policy: %d, Priority: %d\n", policy, param.sched_priority);
}
命令行操作:
bash
# 设置 RT 优先级
$ chrt -f -p 80 $PID # SCHED_FIFO 优先级 80
$ chrt -r -p 50 $PID # SCHED_RR 优先级 50
# 以 RT 启动程序
$ chrt -f 80 ./my_rt_app
# 查看所有 RT 进程
$ ps -eo pid,rtprio,cls,comm | grep -v "^$"
PID RTPRIO CLS COMMAND
1 99 FF init
567 50 FF irq/44-mmc0
890 1 RR watchdog
5.4 RT Throttling:防止 RT 任务饿死系统
默认 RT 任务最多运行 950ms/1000ms(sched_rt_runtime_us / sched_rt_period_us),留 50ms 给 CFS,防止 RT 死循环饿死系统:
bash
# 查看 RT 带宽限制
$ cat /proc/sys/kernel/sched_rt_period_us
1000000 # 1 秒周期
$ cat /proc/sys/kernel/sched_rt_runtime_us
950000 # RT 最多运行 950ms(95%)
# 禁用限制(危险!RT 死循环会挂死系统)
$ echo -1 > /proc/sys/kernel/sched_rt_runtime_us
# 只允许 RT 任务运行 800ms
$ echo 800000 > /proc/sys/kernel/sched_rt_runtime_us
何时禁用:纯 RT 系统(如 PREEMPT_RT + 全部任务 RT),且确保无 RT 死循环风险。
6. Deadline 调度:SCHED_DEADLINE
6.1 Deadline 调度概念
SCHED_DEADLINE 是最高优先级调度类(高于 RT),基于 CBS(Constant Bandwidth Server)算法:
bash
任务声明三个参数:
- runtime (exec_max):每个周期内需要的 CPU 时间
- deadline:相对截止时间(从周期开始算)
- period:周期长度
例:任务每 10ms 需要 2ms CPU,deadline 是 8ms
→ sched_setattr: runtime=2ms, deadline=8ms, period=10ms
→ 调度器保证任务在 deadline 前获得 2ms CPU
6.2 Deadline 编程
bash
#include <linux/sched.h>
#include <sys/syscall.h>
int set_deadline_sched(pid_t pid, __u64 runtime_ns,
__u64 deadline_ns, __u64 period_ns)
{
struct sched_attr attr = {
.size = sizeof(attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = runtime_ns,
.sched_deadline = deadline_ns,
.sched_period = period_ns,
};
// sched_setattr 是新系统调用(3.14+)
return syscall(__NR_sched_setattr, pid, &attr, 0);
}
// 使用示例:每 10ms 周期运行 2ms,deadline 8ms
set_deadline_sched(0, 2 * 1000000, 8 * 1000000, 10 * 1000000);
命令行:
bash
# chrt -d 表示 deadline
$ chrt -d --sched-runtime 2000000 --sched-deadline 8000000 \
--sched-period 10000000 -p 0 $PID
6.3 Deadline vs RT 对比
| 特性 | SCHED_FIFO/RR | SCHED_DEADLINE |
|---|---|---|
| 优先级语义 | 固定优先级数字 | 带宽/截止时间 |
| 最高优先级 | 99 | 最高(高于所有 RT) |
| 防饿死 | 需手动设计 | 调度器保证带宽 |
| 适用场景 | 简单 RT 任务 | 严格周期性任务 |
| CPU 带宽保证 | 无(可能饿死低优先级) | 有(CBS 保证) |
| 内核版本 | 很早就有 | 3.14+ |
7. NUMA 感知调度
7.1 NUMA 基础
NUMA(Non-Uniform Memory Access)系统中,访问本地内存快、远程内存慢:
bash
NUMA 节点 0 NUMA 节点 1
┌──────────────┐ ┌──────────────┐
│ CPU 0-7 │ │ CPU 8-15 │
│ 本地内存 64GB │◄──快速──► │ 本地内存 64GB │
└──────┬───────┘ └──────┬───────┘
│ 互联总线(慢) │
└──────────────────────────────┘
7.2 NUMA 调度策略
Linux 默认 NUMA 策略(/proc/sys/kernel/numa_balancing):
bash
# 查看/设置 NUMA 自动平衡
$ cat /proc/sys/kernel/numa_balancing
1 # 启用(默认)
# 0:禁用(任务固定在创建时的节点)
# 1:自动平衡(内核定期迁移任务到内存所在节点)
自动平衡原理:
- 内核周期性(默认约 1s)触发 NUMA hinting fault
- 检查任务访问的内存页在哪个节点
- 如果远程访问比例高,把任务迁移到内存所在节点
7.3 NUMA 调优
bash
# 查看 NUMA 拓扑
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 65536 MB
node 1 cpus: 8 9 10 11 12 13 14 15
node 1 size: 65536 MB
# 绑定进程到节点 0
$ numactl --cpunodebind=0 --membind=0 ./myapp
# 查看进程的 NUMA 内存分布
$ numastat -p $PID
# pid 1234
# Node 0 Node 1
# ------ ------
# Huge Anon 0 0
# File Pages 12345 2345
# ...
i.MX8MP 注意:i.MX8MP 是单节点系统(所有 4 核 A53 共享内存控制器),NUMA 调度不适用。
8. cgroup CPU 控制与带宽限制
8.1 cgroup v2 CPU 控制器
bash
# 启用 CPU 控制器(若未启用)
$ echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control
# 创建 CPU 控制组
$ mkdir /sys/fs/cgroup/cpu_limited
# 设置 CPU 配额(每 100ms 周期最多用 50ms = 50% 单核)
$ echo "50000 100000" > /sys/fs/cgroup/cpu_limited/cpu.max
# 格式:"配额 周期"(微秒)
# 设置权重(相对优先级,类似 nice)
$ echo 200 > /sys/fs/cgroup/cpu_limited/cpu.weight
# 默认 100,范围 1~10000
# 设置实时调度带宽(对 SCHED_FIFO/RR 任务)
$ echo "500000 1000000" > /sys/fs/cgroup/cpu_limited/cpu.rt_runtime_us
$ echo "1000000" > /sys/fs/cgroup/cpu_limited/cpu.rt_period_us
# 加入进程
$ echo $PID > /sys/fs/cgroup/cpu_limited/cgroup.procs
8.2 cpu.max 原理
bash
cpu.max = "配额 周期"
例:cpu.max = "50000 100000"(50ms / 100ms = 50%)
内核实现(CFS 带宽控制):
- 每个周期开始,给 cgroup 一个"运行额度"(50ms)
- cgroup 内任务消耗额度
- 额度耗尽 → cgroup 内所有任务被节流(throttle),直到下个周期
- 下个周期重置额度
效果:无论 cgroup 内有多少任务,总 CPU 使用不超过 50%(单核)
8.3 cgroup v1 CPU 控制
bash
# v1 挂载
$ mount -t cgroup -o cpu,cpuacct cgroup /sys/fs/cgroup/cpu
# CPU 带宽限制(周期和配额)
$ echo 100000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us
$ echo 50000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us # 50%
# 权重(shares,相对值)
$ echo 512 > /sys/fs/cgroup/cpu/mygroup/cpu.shares # 默认 1024
# 加入进程
$ echo $PID > /sys/fs/cgroup/cpu/mygroup/tasks
8.4 systemd CPU 限制
bash
# /etc/systemd/system/myapp.service
[Service]
CPUQuota=50% # 单核 50%
CPUWeight=200 # 权重 200(默认 100)
CPUAffinity=0 1 # 绑定到 CPU 0,1
bash
$ systemctl set-property myapp.service CPUQuota=50%
$ systemctl set-property myapp.service CPUAffinity=0,1
9. CPU 亲和性与负载均衡
9.1 CPU 亲和性(Affinity)
bash
#include <sched.h>
// 设置 CPU 亲和性(绑定到 CPU 0 和 1)
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(0, &mask);
CPU_SET(1, &mask);
sched_setaffinity(0, sizeof(mask), &mask);
// 查询
cpu_set_t current;
sched_getaffinity(0, sizeof(current), ¤t);
if (CPU_ISSET(0, ¤t))
printf("Running on CPU 0\n");
命令行:
bash
# 绑定到 CPU 0
$ taskset -c 0 ./myapp
# 查看进程 CPU 亲和性
$ taskset -p $PID
pid 1234's current affinity mask: 3 (二进制 11 = CPU 0,1)
# 绑定到 CPU 0-3
$ taskset -c 0-3 ./myapp
9.2 嵌入式大小核(big.LITTLE)调优
i.MX8MP 是 4×Cortex-A53(同构),但其他 SoC(如 i.MX8M Plus 的某些变种、i.MX95)可能有大小核:
bash
# 查看 CPU 拓扑
$ lscpu | grep -E "CPU\(s\)|Model name|CPU max|CPU min"
CPU(s): 4
Model name: ARMv8 Processor rev 4 (v8l)
# 或查看每个 CPU 的最大频率
$ for cpu in /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_max_freq; do
echo "$cpu: $(cat $cpu)"
done
# 使用 schedutil 调度器(推荐,响应负载变化)
$ echo schedutil > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# 设置大小核策略(如果支持)
# 或使用 cpuset 把延迟敏感任务绑到大核
$ echo 2 > /sys/fs/cgroup/latency/cpu.cpus # 绑到 CPU 2(大核)
9.3 负载均衡
Linux 在多个层次做负载均衡:
bash
层级 1:同一调度域内(同一簇/同一 NUMA 节点)
→ 周期性检查(scheduler_tick)
→ 或唤醒时检查(wake_up)
层级 2:跨调度域(跨簇/跨 NUMA 节点)
→ 周期性(更慢的周期)
→ 或主动迁移(NUMA balancing)
层级 3:CPU 热插拔/负载剧变
→ rebalance_domains
bash
# 查看负载均衡统计
$ cat /proc/schedstat
# 第一行:cpu0 的调度统计
# 第二行:...
# 字段:count of times sched_domains have been rebuilt | ...
# 查看每个 CPU 的运行队列长度
$ cat /proc/stat | grep "cpu"
cpu 123456 0 234567 890123 4567 890 1234 5678 0 0
# user nice system idle iowait irq softirq steal
9.4 isolcpus:隔离 CPU
bash
# 内核启动参数:从调度器中隔离 CPU 2,3(仅由用户指定任务使用)
isolcpus=2,3
# 效果:
# - 内核不往 CPU 2,3 迁移任务
# - 需用 taskset/sched_setaffinity 显式绑定任务到这些 CPU
# - 适用于延迟敏感任务独占 CPU
# 启动后验证
$ cat /sys/devices/system/cpu/isolated
2-3
# 查看 CPU 2 的运行队列(应该很少)
$ cat /proc/stat | grep "cpu2 "
10. 抢占模型与 PREEMPT_RT
10.1 抢占模型对比
| 模型 | CONFIG | 延迟 | 吞吐量 | 适用 |
|---|---|---|---|---|
| 非抢占(No Preempt) | 无 PREEMPT | 高 | 最高 | 服务器 |
| 内核抢占(Voluntary) | PREEMPT_VOLUNTARY |
中 | 高 | 通用桌面 |
| 完全抢占(Full) | PREEMPT |
低 | 中 | 桌面/嵌入式 |
| 实时抢占(RT) | PREEMPT_RT |
最低 | 较低 | 硬实时 |
bash
# 查看当前抢占模型
$ cat /sys/kernel/debug/sched_features 2>/dev/null | head
# 或
$ grep PREEMPT /boot/config-$(uname -r)
# CONFIG_PREEMPT_NONE is not set
# CONFIG_PREEMPT_VOLUNTARY is not set
CONFIG_PREEMPT=y
# CONFIG_PREEMPT_RT is not set
10.2 PREEMPT_RT 概述
PREEMPT_RT 是 Linux 内核的实时补丁(已主线化大部分,6.12+ 完全合并):
核心改进:
- 自旋锁转为可睡眠锁 :
spinlock_t变成rt_mutex,可被抢占 - 中断线程化 :硬中断处理转为内核线程(
irq/N-xxx),可设置 RT 优先级 - 定时器线程化 :
hrtimer转为线程 - printk 线程化:避免 printk 阻塞
bash
普通内核: PREEMPT_RT 内核:
硬中断 → 立即执行处理程序 硬中断 → 唤醒 irq/N-xxx 内核线程
→ 可能长时间关中断 → 线程可被更高优先级抢占
→ 其他 CPU 的 RT 任务被阻塞 → 中断处理延迟可控
10.3 i.MX8MP PREEMPT_RT 实战
bash
# NXP BSP 是否支持 PREEMPT_RT
# 5.10 内核:需要 RT 补丁(linux-imx-rt 分支)
# 5.15+:NXP 提供 PREEMPT_RT 配置
# 内核配置(PREEMPT_RT)
CONFIG_PREEMPT_RT=y
CONFIG_PREEMPT=y
# CONFIG_PREEMPT_NONE is not set
# CONFIG_PREEMPT_VOLUNTARY is not set
# 验证
$ uname -r
5.10.198-rt99-imx # -rt 后缀表示 PREEMPT_RT
# 查看中断线程
$ ps aux | grep "irq/"
root 567 2 irq/44-mmc0
root 568 2 irq/45-mmc1
# 每个中断一个线程,可设置 RT 优先级
# 设置中断线程优先级
$ chrt -f -p 90 /proc/567 # 设为 SCHED_FIFO 90
10.4 中断亲和性与线程化中断调优
bash
# 设置中断亲和性(把中断绑到特定 CPU)
$ echo 2 > /proc/irq/44/smp_affinity # 绑到 CPU 1(二进制 10)
# 查看中断分布
$ cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
44: 12345 23456 0 0 GICv3 44 Level mmc0
45: 0 12345 0 0 GICv3 45 Level mmc1
# 把中断分散到不同 CPU,避免单 CPU 中断风暴
# 禁用不需要的中断(嵌入式)
$ echo 1 > /sys/devices/system/cpu/cpu1/online # 上线
$ echo 0 > /sys/devices/system/cpu/cpu1/online # 下线(省电)
11. 调度器观测与调优工具
11.1 基础工具
bash
# 查看进程调度信息
$ chrt -p $PID
$ ps -eo pid,cls,rtprio,ni,pri,cmd | sort -k 3 -n -r | head
# 查看 CPU 使用率(按进程)
$ top -H # H 显示所有线程
$ pidstat -p $PID 1 # 每秒统计
# 查看调度延迟
$ cat /proc/stat | grep ctxt # 上下文切换次数
$ vmstat 1 # cs 列:上下文切换/秒
11.2 perf 调度分析
bash
# 调度事件追踪
$ perf sched record -- sleep 5
$ perf sched latency
# 最大/平均调度延迟
$ perf sched map
# CPU 上运行的任务时间线
$ perf sched timehist
# 每次调度的时间历史
# 具体函数耗时
$ perf top -e sched:sched_switch
$ perf trace -e sched:sched_wakeup
11.3 ftrace 调度追踪
bash
# 启用调度事件
$ echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
$ echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
# 追踪
$ cat /sys/kernel/debug/tracing/trace_pipe
# <idle>-0 [000] d..2 123.456: sched_switch: prev_comm=swapper prev_state=R ==> next_comm=myapp next_pid=1234
# myapp-1234 [000] d..2 123.456: sched_wakeup: comm=kworker target_cpu=001
# 关闭
$ echo 0 > /sys/kernel/debug/tracing/events/sched/enable
11.4 sched_debug
bash
# 查看调度器内部状态(需要 CONFIG_SCHED_DEBUG)
$ cat /proc/sched_debug | head -100
# cpu#0
# .nr_running = 2
# .clock = 123456789
# .cpu_load[0] = 512
# .curr->pid = 1234
# ...
# cfs_rq[0]: .min_vruntime = 1234567
# cfs_rq[0]: .nr = 3
# ...
11.5 cyclictest:实时延迟测试
bash
# 编译(需要 RT 内核效果最佳)
$ apt install rt-tests
# 或从源码:https://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git
# 测试调度延迟(1ms 周期,运行 10 分钟)
$ cyclictest -m -Sp90 -i1000 -h40000 -l1000000
# 参数:
# -m:锁定内存(mlockall)
# -S:SCHED_FIFO
# -p90:优先级 90
# -i1000:间隔 1000us(1ms)
# -h40000:直方图 40000us
# -l1000000:运行 100 万次
# 输出:
# T: 0 ( 1234) P:90 I:1000 C: 1000000 Min: 2 Act: 8 Avg: 5 Max: 45
# Min:最小延迟(us)
# Max:最大延迟(us)← 关键指标!
# Avg:平均延迟
嵌入式目标:PREEMPT_RT 下,Max 应 < 100us(取决于硬件和负载)。
11.6 oslat / hackbench
bash
# oslat:测量调度延迟(比 cyclictest 更接近真实场景)
$ oslat -t 4 -p 90 -m 1000
# hackbench:进程/线程创建与调度压力测试
$ hackbench process 10
$ hackbench thread 10 pipe
12. 嵌入式实时优化实战(i.MX8MP)
12.1 i.MX8MP 实时系统配置清单
bash
# ============ 内核配置 ============
# 抢占模型
CONFIG_PREEMPT_RT=y
CONFIG_PREEMPT=y
CONFIG_HZ_1000=y # 高时钟频率(减少 tick 延迟)
CONFIG_NO_HZ_FULL=n # RT 不建议全动态 tick
# CPU 隔离
CONFIG_CPUSETS=y
# 内存锁定(RT 任务需要)
CONFIG_MEMCG=y
CONFIG_MEMCG_SWAP=n # RT 系统可关闭 swap
# 避免动态调频(固定频率保证确定性)
CONFIG_CPU_FREQ_DEFAULT_GOV PERFORMANCE=y
# 中断线程化(PREEMPT_RT 自动)
CONFIG_IRQ_FORCED_THREADING=y
# ============ U-Boot ============
# 关闭看门狗(调试时)
# setenv bootargs ... wdt.disable=1
# 关闭其他核(单核实时)
# setenv bootargs ... maxcpus=1
# ============ 内核 cmdline ============
# 完整实时优化参数
console=ttymxc1,115200
isolcpus=2,3 # 隔离 CPU 2,3 给 RT 任务
nohz_full=2,3 # 隔离 CPU 动态 tick(配合 isolcpus)
irqaffinity=0-1 # 中断只在 CPU 0,1 处理
rcu_nocbs=2,3 # RCU 回调移到 CPU 2,3
processor.max_cstate=1 # 限制 CPU 深度 C-state
transparent_hugepage=never # 关闭 THP(避免延迟抖动)
rcutree.rcu_idle_gp_delay=1 # 加速 RCU 空闲期
12.2 RT 任务配置示例
bash
// 实时控制线程配置
#include <sched.h>
#include <sys/mman.h>
#include <pthread.h>
void setup_realtime_thread(void)
{
struct sched_param param;
// 1. 锁定内存(避免缺页中断导致延迟)
mlockall(MCL_CURRENT | MCL_FUTURE);
// 2. 设置 CPU 亲和性(绑定到隔离的 CPU)
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset); // 绑到隔离的 CPU 2
sched_setaffinity(0, sizeof(cpuset), &cpuset);
// 3. 设置 RT 调度
param.sched_priority = 80; // 高于中断线程(通常 50)
if (sched_setscheduler(0, SCHED_FIFO, ¶m) < 0) {
perror("sched_setscheduler");
}
// 4. 预分配内存(避免运行时分配)
// void *buf = malloc(1024 * 1024);
// memset(buf, 0, 1024 * 1024);
}
// 中断线程优先级参考:
// 硬中断线程:50(默认)
// 关键设备中断:60-70
// 应用 RT 线程:80-90
// 软件定时器线程:95
// watchdog:99
12.3 CPU 频率与电源管理
bash
# 固定 CPU 频率为最高(避免调频延迟)
$ echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
$ echo 1800000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed # 1.8GHz
# 或全部 CPU
$ for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
echo performance > $cpu
done
# 禁用 CPU 深度 idle(减少唤醒延迟)
$ echo 1 > /sys/module/cpu_idle/parameters/max_cstate
# 或内核 cmdline
processor.max_cstate=1
# 查看 C-state
$ cat /sys/devices/system/cpu/cpu0/cpuidle/state*/usage
$ cat /sys/devices/system/cpu/cpu0/cpuidle/state*/latency
12.4 IRQ 与 softirq 优化
bash
# 1. 中断分散到不同 CPU
$ for irq in /proc/irq/*/smp_affinity; do
# 轮询设置到不同 CPU
done
# 2. 关注的中断绑到非 RT CPU
# RT 任务在 CPU 2,3 → 中断绑到 CPU 0,1
$ echo 1 > /proc/irq/44/smp_affinity # CPU 0(二进制 0001)
# 3. 调整 RCU 行为
$ echo 0 > /sys/module/rcupdate/parameters/rcu_cpu_stall_timeout # 关闭 stall 警告
$ echo 1 > /sys/module/rcupdate/parameters/rcu_expedited # 加速 RCU
# 4. 减少定时器频率
# HZ=100 比 HZ=1000 少 10 倍 tick(但定时精度降低)
# RT 建议 HZ=1000 保证精度
12.5 实时性能基准测试流程
bash
# 1. 基线测试(未优化)
$ cyclictest -m -Sp90 -i1000 -l1000000 | tee baseline.log
# 记录 Max 延迟
# 2. 应用 RT 配置后测试
$ ./setup_rt_config.sh # 应用 isolcpus、调频等
$ cyclictest -m -Sp90 -i1000 -l1000000 | tee optimized.log
# 3. 对比
$ grep Max baseline.log optimized.log
# 4. 压力下测试
$ hackbench process 20 & # 后台负载
$ cyclictest -m -Sp90 -i1000 -l1000000
13. 经典案例与排查流程
13.1 案例一:RT 任务偶发高延迟
现象:cyclictest 显示 Max 延迟偶尔 > 1ms(目标 < 100us)。
排查:
bash
# 1. 检查是否有中断风暴
$ cat /proc/interrupts | awk '{print $1, $2, $3, $4, $5}'
# 看哪个 CPU 中断次数暴涨
# 2. 检查 RT 任务是否被 throttled
$ grep -i throttle /sys/kernel/debug/tracing/trace_pipe
# 或 dmesg | grep throttle
# 3. 检查 CPU 频率变化
$ grep . /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq
# 频率变化可能导致延迟
# 4. 检查内存回收
$ grep allocstall /proc/vmstat
# direct reclaim 会导致长延迟
# 5. 检查 RCU
$ dmesg | grep -i "rcu.*stall"
常见根因:
- RT 任务与中断在同一 CPU → 中断阻塞 RT 任务
- CPU 频率降低 → 指令执行变慢
- 内存分配触发直接回收 → 数百 us 延迟
- RT 任务调用了可睡眠函数(在 PREEMPT_RT 外)
13.2 案例二:RT 任务饿死普通进程
现象:高优先级 RT 任务运行后,CFS 进程几乎得不到 CPU。
原因:RT 任务占用了几乎 100% CPU,CFS 只能用 RT throttling 剩余的 5%。
解决:
bash
# 方案 1:调整 RT throttling(给 CFS 更多时间)
$ echo 800000 > /proc/sys/kernel/sched_rt_runtime_us # RT 最多 80%
# 方案 2:降低 RT 任务优先级或减少运行时间
# 用 SCHED_DEADLINE 替代 SCHED_FIFO,保证带宽
# 方案 3:用 cgroup 限制 RT 任务
$ echo "500000 1000000" > /sys/fs/cgroup/myapp/cpu.rt_runtime_us
# 限制 RT 任务组只能用 500ms/s
# 方案 4:优化 RT 任务逻辑,避免忙等
13.3 案例三:调度延迟抖动(非 RT 系统)
现象:非 RT 系统,应用响应时间不稳定。
排查:
bash
# 1. 检查 CPU 使用率分布
$ mpstat -P ALL 1
# 看是否有单 CPU 过载
# 2. 检查上下文切换频率
$ vmstat 1
# cs 列过高(>10000/s)说明切换频繁
# 3. 检查调度延迟
$ perf sched latency | head
# Max 列过高说明有长延迟
# 4. 检查是否有 RT 任务干扰
$ ps -eo pid,rtprio,cls,comm | grep -E "FF|RR"
优化方向:
- 减少不必要的线程(每线程有调度开销)
- 用 CPU 亲和性减少跨核迁移
- 调整 nice 值让关键任务优先
- 考虑 PREEMPT 内核(更低延迟)
13.4 排查流程图
bash
调度问题
│
├─→ 延迟高 / 抖动大?
│ │
│ ├─ 是 RT 系统? → cyclictest 测 Max → 查中断/调频/回收/RCU
│ │
│ └─ 非 RT → perf sched latency → 查 CPU 过载/切换频率
│
├─→ 某进程得不到 CPU?
│ │
│ ├─ 有高优先级 RT 任务? → 调整 RT throttling / 优化 RT 任务
│ ├─ nice 值太高? → renice 降低
│ └─ CPU 绑定错误? → taskset 检查亲和性
│
├─→ RT 任务被间歇阻塞?
│ │
│ ├─ 中断在同一 CPU? → irqaffinity 分散中断
│ ├─ CPU 调频? → performance governor
│ ├─ 内存回收? → 预分配内存 + min_free_kbytes
│ └─ RCU? → rcu_expedited + rcu_nocbs
│
└─→ 功耗高 / 发热?
│
├─ CPU 全速运行? → schedutil governor
├─ 不必要的 RT? → 降为 CFS
└─ 中断过多? → 合并/关闭不必要的中断源
14. 总结与最佳实践
14.1 核心要点
| 概念 | 要点 |
|---|---|
| CFS | vruntime 最小优先,红黑树 O(log n),nice 权重控制比例 |
| RT | 优先级 1-99,严格高于 CFS,有 throttling 防饿死 |
| Deadline | 最高优先级,带宽保证,适合周期性硬实时 |
| PREEMPT_RT | 自旋锁可睡眠,中断线程化,最低延迟 |
| cgroup CPU | cpu.max 限制带宽,cpu.weight 控制相对份额 |
| isolcpus | 隔离 CPU 给延迟敏感任务 |
| 调优链 | 内核配置 → 启动参数 → 运行时设置 → 应用配置 |
14.2 嵌入式实时系统检查清单
- 内核配置
PREEMPT_RT或至少PREEMPT HZ=1000(高 tick 频率)- CPU 固定为
performancegovernor - RT 任务用
mlockall锁定内存 - RT 任务绑定到
isolcpus隔离的 CPU - 中断亲和性设到非 RT CPU
irqaffinity启动参数排除 RT CPU- 关闭
transparent_hugepage - cyclictest Max < 目标值
- RT 任务优先级合理(不高于必要值)
- 有 RT throttling 兜底(除非纯 RT 系统)
14.3 通用系统调优清单
- 调度器:默认 CFS 通常足够
- 关键任务:
renice或 cgroupcpu.weight - 容器/服务:cgroup
cpu.max限制 - 延迟敏感:CPU 亲和性 + 避免过载
- NUMA 系统:
numactl绑定 +numa_balancing - 大小核:延迟任务绑大核,后台任务绑小核
14.4 常用命令速查
bash
# 查看
chrt -p $PID # 调度策略和优先级
ps -eo pid,rtprio,cls,ni # 所有进程调度信息
taskset -p $PID # CPU 亲和性
cat /proc/schedstat # 调度统计
mpstat -P ALL 1 # 每 CPU 使用率
# 设置
chrt -f -p 80 $PID # SCHED_FIFO 80
chrt -r -p 50 $PID # SCHED_RR 50
taskset -c 0-3 $PID # 绑定 CPU 0-3
renice -n -10 -p $PID # 提高 nice
# cgroup v2
echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control
mkdir /sys/fs/cgroup/myapp
echo "50000 100000" > /sys/fs/cgroup/myapp/cpu.max
echo $PID > /sys/fs/cgroup/myapp/cgroup.procs
# 调优
echo -1 > /proc/sys/kernel/sched_rt_runtime_us # 禁用 RT throttling
echo 1 > /proc/sys/kernel/sched_rt_runtime_us # 启用
echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# 测试
cyclictest -m -Sp90 -i1000 -l1000000 # 实时延迟
hackbench process 10 # 调度压力
perf sched record # 调度追踪
14.5 延伸阅读
- 内核文档:
Documentation/scheduler/ - CFS 论文:《The Completely Fair Scheduler》by Ingo Molnar
- PREEMPT_RT:
https://wiki.linuxfoundation.org/realtime/ - EEVDF:Linux 6.6 合并说明、
Documentation/scheduler/sched-design-CFS.rst - RT 测试:rt-tests 项目、
cyclictest/oslat文档 - ARM 调度:ARM Big.LITTLE 调度白皮书
15. 调度器源码深度剖析
15.1 调度器关键数据结构全景
bash
struct rq(每 CPU 一个运行队列)
├── struct cfs_rq cfs // CFS 子队列
│ ├── struct rb_root_cached tasks_timeline // 红黑树(按 vruntime 排序)
│ ├── u64 min_vruntime
│ └── struct sched_entity *curr
├── struct rt_rq rt // RT 子队列
│ └── struct rt_prio_array active // 位图 + 链表数组
├── struct dl_rq dl // Deadline 子队列
│ └── struct rb_root_cached root // 按 deadline 排序的红黑树
├── struct task_struct *curr_task // 当前运行任务
├── int cpu // 对应 CPU 编号
└── raw_spinlock_t lock // 运行队列锁
关键函数调用链:
bash
时钟中断(scheduler_tick)
→ update_rq_clock()
→ task_tick_fair()
→ entity_tick()
→ check_preempt_tick() // 检查是否应抢占
→ update_curr() // 更新 vruntime
→ resched_curr() // 设置 TIF_NEED_RESCHED
上下文切换(schedule)
→ __schedule()
→ pick_next_task() // 选择下一个任务
→ context_switch() // 切换 mm/寄存器
→ switch_mm() // 切换页表
→ switch_to() // 切换寄存器栈
15.2 CFS 调度实体操作源码解读
bash
// kernel/sched/fair.c(简化版)
// 入队:将任务加入 CFS 红黑树
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
// 1. 更新 vruntime(防止长时间睡眠任务 vruntime 过小)
if (flags & ENQUEUE_WAKEUP)
place_entity(cfs_rq, se, 0);
// 2. 更新负载统计
update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);
// 3. 插入红黑树
__enqueue_entity(cfs_rq, se);
se->on_rq = 1;
}
// 出队
static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
// 从红黑树移除
__dequeue_entity(cfs_rq, se);
se->on_rq = 0;
// 更新统计
update_stats_dequeue(cfs_rq, se, flags);
}
// 更新当前任务运行时间
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec = now - curr->exec_start;
curr->sum_exec_runtime += delta_exec;
curr->vruntime += calc_delta_fair(delta_exec, curr);
update_min_vruntime(cfs_rq);
// 更新 cfs bandwidth 统计(cgroup 用)
if (cfs_rq->nr_running > 1)
update_cfs_group(curr);
}
15.3 place_entity:防止睡眠任务饿死
新唤醒或新建任务的 vruntime 会被调整,避免以下问题:
bash
static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime = cfs_rq->min_vruntime;
// 新建任务:设置为 min_vruntime(保证公平起点)
if (initial) {
vruntime += sched_vslice(cfs_rq, se);
}
// 唤醒任务:给予一定"信贷"(不超过 sysctl_sched_latency)
if (!initial) {
// 防止刚唤醒的任务抢占过多
unsigned long thresh = sysctl_sched_latency;
// 睡眠时间越长,信贷越大(最多 thresh)
if (se->vruntime < vruntime)
vruntime -= min(thresh, vruntime - se->vruntime);
}
se->vruntime = vruntime;
}
效果:
- 短暂睡眠的任务醒来后不会立刻抢占(避免交互任务霸占 CPU)
- 长时间睡眠的任务醒来后能较快获得 CPU(避免饿死)
16. cgroup CPU 带宽控制深度解析
16.1 CFS Bandwidth Control 原理
bash
cgroup 的 cpu.max = "配额 周期"
例:cpu.max = "50000 100000"(50ms/100ms = 50% 单核)
内核实现(kernel/sched/fair.c):
1. 每个 cfs_bandwidth 结构维护:
- quota:周期内允许的运行时间(ns)
- period:周期长度(ns)
- runtime:当前周期剩余时间
- expires:当前周期结束时间
2. 周期开始(distribute_cfs_runtime):
- 给 cfs_rq 分配 quota 时间
- 设置 hrtimer 在 period 后触发
3. 任务运行时(update_curr):
- 扣减 cfs_rq 的 runtime
- runtime 耗尽 → throttle_cfs_rq() 节流整个 cgroup
4. 周期结束(distribute_cfs_runtime):
- 重新分配 quota
- unthrottle_cfs_rq() 解除节流
16.2 cgroup v2 CPU 控制完整示例
bash
# ============================================
# 完整 cgroup v2 CPU 管理示例
# ============================================
# 1. 挂载 cgroup v2(如果未挂载)
$ mount -t cgroup2 none /sys/fs/cgroup
# 2. 启用 CPU 控制器
$ echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control
# 3. 创建层级结构
$ mkdir /sys/fs/cgroup/production
$ mkdir /sys/fs/cgroup/production/webapp
$ mkdir /sys/fs/cgroup/production/batch
$ mkdir /sys/fs/cgroup/background
# 4. 配置 CPU 带宽
# 生产组:最多使用 2 核(200%)
$ echo "200000 100000" > /sys/fs/cgroup/production/cpu.max
# Web 应用:权重 200(优先级较高)
$ echo 200 > /sys/fs/cgroup/production/webapp/cpu.weight
# 批处理:权重 50(低优先级)
$ echo 50 > /sys/fs/cgroup/production/batch/cpu.weight
# 后台组:严格限制 10% 单核
$ echo "10000 100000" > /sys/fs/cgroup/background/cpu.max
# 5. 添加进程
$ echo $WEB_PID > /sys/fs/cgroup/production/webapp/cgroup.procs
$ echo $BATCH_PID > /sys/fs/cgroup/production/batch/cgroup.procs
$ echo $BG_PID > /sys/fs/cgroup/background/cgroup.procs
# 6. 查看统计
$ cat /sys/fs/cgroup/production/cpu.stat
nr_periods 1234 # 经过的周期数
nr_throttled 56 # 被节流的次数
throttled_usec 123456 # 节流总时间(微秒)
usage_usec 987654 # 实际使用时间(微秒)
$ cat /sys/fs/cgroup/production/webapp/cpu.stat
16.3 cgroup v1 兼容配置
bash
# 挂载 v1 CPU 控制器
$ mkdir -p /sys/fs/cgroup/cpu
$ mount -t cgroup -o cpu,cpuacct cgroup /sys/fs/cgroup/cpu
# 配置
$ mkdir /sys/fs/cgroup/cpu/mygroup
$ echo 50000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us # 50ms
$ echo 100000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us # 100ms
$ echo 512 > /sys/fs/cgroup/cpu/mygroup/cpu.shares # 权重
# 添加任务
$ echo $PID > /sys/fs/cgroup/cpu/mygroup/tasks
# 查看统计
$ cat /sys/fs/cgroup/cpu/mygroup/cpu.stat
nr_periods 100
nr_throttled 5
throttled_time 50000000
16.4 systemd 服务 CPU 管理
bash
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
[Service]
Type=simple
ExecStart=/usr/bin/myapp
# CPU 资源限制
CPUQuota=50% # 单核 50%
CPUWeight=200 # 权重(相对优先级)
CPUAffinity=0 1 # 绑定到 CPU 0,1
AllowedCPUs=0-1 # 允许使用的 CPU(cgroup v2)
# 内存限制(配合 CPU 管理)
MemoryMax=512M
MemorySwapMax=0
# 调度策略(需要 CAP_SYS_NICE)
IOSchedulingClass=realtime # I/O 调度类
Nice=10 # nice 值
[Install]
WantedBy=multi-user.target
bash
# 运行时调整
$ systemctl set-property myapp.service CPUQuota=80%
$ systemctl set-property myapp.service CPUWeight=300
# 查看生效配置
$ systemctl show myapp.service | grep -E "CPU|Nice"
CPUAccounting=yes
CPUQuotaPerSecUSec=800ms
CPUWeight=300
AllowedCPUs=0-1
Nice=10
17. NUMA 调度深度调优
17.1 NUMA 调度策略参数
bash
# /proc/sys/kernel/numa_balancing
# 0:禁用(任务固定在创建时的节点)
# 1:启用自动平衡(默认)
# 每节点统计
$ numastat -c
# Page miss rate 统计(每秒)
# 当任务访问远程内存时,触发 NUMA hinting fault
# 查看任务的 NUMA 内存分布
$ cat /proc/$PID/numa_maps
# 7f1234000000 default file=/lib/x86_64-linux-gnu/libc.so.6 ... N0=1024 N1=0
# N0:节点 0 上的页数
# N1:节点 1 上的页数
# 系统级 NUMA 统计
$ numastat
# Node 0 Node 1
# numa_hit 123456 234567
# numa_miss 234 456
# numa_foreign ...
17.2 NUMA 调优策略
bash
# 策略 1:绑定内存到本地节点(性能关键)
$ numactl --cpunodebind=0 --membind=0 ./myapp
# 策略 2:交错分配(减少远程访问峰值)
$ numactl --interleave=all ./myapp
# 策略 3:优先本地,允许远程(默认策略)
$ numactl --preferred=0 ./myapp
# 策略 4:cgroup v2 cpuset + 内存绑定
$ mkdir /sys/fs/cgroup/numa0
$ echo 0 > /sys/fs/cgroup/numa0/cpuset.cpus
$ echo 0 > /sys/fs/cgroup/numa0/cpuset.mems
$ echo $PID > /sys/fs/cgroup/numa0/cgroup.procs
17.3 嵌入式 NUMA 场景(少用但需了解)
i.MX8MP 是单节点,但多路服务器或异构 SoC 可能涉及:
bash
# 查看 NUMA 拓扑
$ lscpu | grep NUMA
NUMA node(s): 2
NUMA node0 CPU(s): 0-15
NUMA node1 CPU(s): 16-31
# 自动平衡调优
$ echo 1 > /proc/sys/kernel/numa_balancing
$ echo 1000 > /proc/sys/kernel/numa_balancing_scan_delay_ms # 扫描延迟
$ echo 1024 > /proc/sys/kernel/numa_balancing_scan_size_mb # 扫描大小
18. perf/ftrace 调度分析实战
18.1 perf sched 完整分析流程
bash
# 1. 录制调度事件
$ perf sched record -g -- sleep 10
# -g:记录调用栈
# 输出:perf.data
# 2. 查看调度延迟统计
$ perf sched latency --sort max
# Task | Runtime ms | Switches | Average delay ms | Maximum delay ms
# ----------------+--------------+-----------+-------------------+------------------
# myapp | 1234.567 | 12345 | 0.012 | 1.234
# kworker/0:1 | 234.567 | 2345 | 0.008 | 0.456
# Maximum delay 列是关键:反映最坏调度延迟
# 3. 查看任务时间线
$ perf sched map
# *A-0123456789.012345 : sched_in : prev_comm=... next_comm=myapp
# myapp-1234 [000] : sched_out : ...
# 4. 详细时间历史
$ perf sched timehist -V
# time cpu task name wait time sch delay run time
# 0 : 12345.678901 | 0.000 | 0.012 | 1.234
# wait time:任务在运行队列中等待时间
# sch delay:调度决策延迟(pick_next_task 耗时)
# run time:实际运行时间
# 5. 可视化(生成 SVG)
$ perf script -i perf.data | perf-sched-slatency-viz.py > latency.svg
18.2 ftrace 调度事件深度追踪
bash
# 启用完整调度追踪
$ cd /sys/kernel/debug/tracing
# 1. 基础事件
$ echo 1 > events/sched/sched_switch/enable
$ echo 1 > events/sched/sched_wakeup/enable
$ echo 1 > events/sched/sched_wakeup_new/enable
$ echo 1 > events/sched/sched_process_exec/enable
$ echo 1 > events/sched/sched_process_exit/enable
# 2. 追踪特定进程(过滤)
$ echo 'pid == 1234' > events/sched/sched_switch/filter
# 3. 使用 function_graph 追踪调度器内部函数
$ echo schedule > set_graph_function
$ echo function_graph > current_tracer
$ cat trace_pipe | head -100
# 可以看到 schedule → pick_next_task → pick_next_task_fair 等调用链
# 4. 测量调度延迟(自定义)
$ echo 'sched_switch' > set_event
$ echo 1 > tracing_on
# ... 运行负载 ...
$ echo 0 > tracing_on
$ cat trace
# 5. 使用 synthetic events 计算延迟
$ echo 'u64 latency; pid_t pid' > synthetic_events
$ echo 'sched:sched_wakeup' > synthetic_events # 不是这样,用 hist trigger
# 更实用:hist trigger 统计
$ echo 'hist:key=next_comm:vals=latency:sort=latency' > \
events/sched/sched_wakeup/trigger
18.3 BPF/bpftrace 调度延迟分析
bash
# bpftrace 脚本:统计每个进程的最大调度延迟
$ cat > sched_latency.bt << 'EOF'
#!/usr/bin/bpftrace
tracepoint:sched:sched_wakeup
{
@wakeup[args->pid] = nsecs;
}
tracepoint:sched:sched_switch
/ @wakeup[args->prev_pid] /
{
$latency = nsecs - @wakeup[args->prev_pid];
@max_latency[args->prev_comm] = max($latency);
@avg_latency[args->prev_comm] = sum($latency);
@count[args->prev_comm] = count();
delete(@wakeup[args->prev_pid]);
}
interval:s:5
{
printf("\n=== Scheduling Latency (last 5s) ===\n");
print(@max_latency);
print(@avg_latency);
print(@count);
clear(@max_latency);
clear(@avg_latency);
clear(@count);
}
EOF
$ sudo bpftrace sched_latency.bt
bash
# 更精细:按 CPU 统计调度延迟
$ sudo bpftrace -e '
tracepoint:sched:sched_wakeup { @start[args->pid] = nsecs; }
tracepoint:sched:sched_switch
/ @start[args->prev_pid] /
{
$lat = nsecs - @start[args->prev_pid];
@hist[cpu] = hist($lat);
delete(@start[args->prev_pid]);
}
interval:s:10 { print(@hist); clear(@hist); }
'
18.4 schedstat 与 /proc 接口
bash
# /proc/schedstat:全局调度统计
$ cat /proc/schedstat
# 字段:
# 1. sched domain 重建次数
# 2. yld_count(sched_yield 调用次数)
# 3. sched_count(schedule 调用次数)
# 4. sched_goidle(选择 idle 任务的次数)
# 5. ttwu_count(try_to_wake_up 调用次数)
# 6. ttwu_local_count(本地唤醒次数)
# 7. ttwu_remote_count(远程唤醒次数)
# 8. ttwu_move_affine_count(亲和性迁移唤醒)
# 9. ttwu_move_balance_count(负载均衡迁移唤醒)
# /proc/[pid]/sched:进程级调度统计
$ cat /proc/$PID/sched
# myapp-1234 (pid 1234, nr_switches 12345)
# se.vruntime : 12345678.901234
# se.sum_exec_runtime : 1234.567890
# nr_migrations : 123
# statistics:
# sleep_max : 0.123
# block_max : 0.045
# wait_max : 0.012 ← 关键:最大等待时间
# wait_sum : 12.345
# iowait_sum : 0.000
# nr_migrations_cold : 12
19. 实战案例:完整调优流程
19.1 案例:i.MX8MP 工业控制实时系统
需求:1ms 控制周期,抖动 < 100us,4 核 A53。
步骤 1:内核配置
bash
# 使用 NXP linux-imx-rt 分支 + PREEMPT_RT 补丁
$ ./scripts/config --enable PREEMPT_RT
$ ./scripts/config --enable PREEMPT
$ ./scripts/config --enable HZ_1000
$ ./scripts/config --disable NO_HZ_FULL
$ ./scripts/config --enable CPUSETS
$ ./scripts/config --enable CGROUP_CPUACCT
$ ./scripts/config --enable MEMCG
$ ./scripts/config --disable THP_TRANSPARENT_HUGEPAGE
步骤 2:U-Boot 启动参数
bash
# uEnv.txt 或 extlinux.conf
console=ttymxc1,115200
isolcpus=2,3
nohz_full=2,3
irqaffinity=0-1
rcu_nocbs=2,3
processor.max_cstate=1
transparent_hugepage=never
maxcpus=4
步骤 3:系统启动后配置脚本
bash
#!/bin/bash
# /usr/bin/setup-rt.sh
# 1. CPU 调频固定
for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
echo performance > $gov
done
# 2. 设置中断亲和性(只在 CPU 0,1)
for irq in /proc/irq/*/smp_affinity; do
echo 3 > $irq # CPU 0,1(二进制 0011)
done
# 3. 禁用 RT throttling(纯 RT 系统)
echo -1 > /proc/sys/kernel/sched_rt_runtime_us
# 4. 内存锁定限制
ulimit -l unlimited
# 5. 创建隔离的 RT cgroup
mkdir -p /sys/fs/cgroup/rt
echo "+cpu +cpuset" > /sys/fs/cgroup/cgroup.subtree_control
echo "2 3" > /sys/fs/cgroup/rt/cpuset.cpus
echo 200 > /sys/fs/cgroup/rt/cpu.weight
echo max > /sys/fs/cgroup/rt/cpu.max # 不限带宽
步骤 4:RT 控制应用
bash
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sched.h>
#include <sys/mman.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
static volatile int running = 1;
void control_thread(void *arg)
{
struct sched_param param;
cpu_set_t cpuset;
struct timespec next, now;
long period_ns = 1000000; // 1ms
// 1. 锁定内存
mlockall(MCL_CURRENT | MCL_FUTURE);
// 2. 绑定到隔离 CPU 2
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);
// 3. RT 调度
param.sched_priority = 80;
sched_setscheduler(0, SCHED_FIFO, ¶m);
// 4. 预分配和预热
// ... 预分配缓冲区,预热缓存 ...
// 5. 控制循环
clock_gettime(CLOCK_MONOTONIC, &next);
while (running) {
// 控制算法
// read_sensors(); compute(); write_actuators();
// 精确周期睡眠
next.tv_nsec += period_ns;
if (next.tv_nsec >= 1000000000) {
next.tv_sec++;
next.tv_nsec -= 1000000000;
}
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
}
}
int main()
{
// 创建 RT 线程
pthread_t tid;
pthread_attr_t attr;
struct sched_param param;
pthread_attr_init(&attr);
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
param.sched_priority = 80;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void *)control_thread, NULL);
// 主线程保持 CFS(用于监控/日志)
while (running) sleep(1);
return 0;
}
步骤 5:验证测试
bash
# 1. 基线测试
$ cyclictest -m -Sp80 -i1000 -h10000 -l10000000
# Max 应 < 50us
# 2. 压力测试
$ hackbench process 20 &
$ stress-ng --cpu 4 --io 2 --vm 1 --vm-bytes 128M &
$ cyclictest -m -Sp80 -i1000 -h10000 -l10000000
# 3. 长时间稳定性(24h)
$ cyclictest -m -Sp80 -i1000 -h10000 -l100000000 > rt_test.log &
$ sleep 86400
$ kill %1
$ grep Max rt_test.log | tail -100
# 4. perf 分析
$ perf sched record -g -- sleep 60
$ perf sched latency --sort max | head -20
结果参考(i.MX8MP EVK 实测):
| 配置 | Min (us) | Avg (us) | Max (us) |
|---|---|---|---|
| 默认内核 | 2 | 15 | 2500 |
| PREEMPT | 1 | 8 | 450 |
| PREEMPT_RT + isolcpus | 1 | 3 | 45 |
| PREEMPT_RT + isolcpus + performance | 1 | 2 | 38 |
20. 总结与学习路径
20.1 学习路径图
bash
入门(1 周)
│
├─→ 理解进程状态、调度策略、nice 值
├─→ 会用 top/htop 查看调度信息
├─→ 会用 taskset 设置 CPU 亲和性
│
进阶(2-4 周)
│
├─→ 深入 CFS vruntime 原理
├─→ 掌握 RT 调度与 throttling
├─→ cgroup v2 CPU 控制
├─→ cyclictest 基准测试
│
高级(1-2 月)
│
├─→ 阅读调度器源码(kernel/sched/)
├─→ PREEMPT_RT 配置与调优
├─→ perf/ftrace/BPF 调度分析
├─→ NUMA 调度调优
│
专家(持续)
│
├─→ 调度器算法改进(EEVDF 等)
├─→ 实时性保证理论与证明
├─→ 特定场景定制调度器
└─→ 参与内核调度器社区
20.2 核心要点回顾
- CFS 是默认调度器:基于 vruntime 的公平调度,适合通用场景
- RT 严格高于 CFS:SCHED_FIFO/RR 优先级 1-99,有 throttling 防饿死
- Deadline 提供带宽保证:SCHED_DEADLINE 基于 CBS 算法,最高优先级
- PREEMPT_RT 是硬实时基础:自旋锁可睡眠、中断线程化
- cgroup v2 是资源管理标准:cpu.max 限制带宽,cpu.weight 控制份额
- isolcpus 隔离延迟敏感任务:配合 irqaffinity 效果最佳
- 观测工具链:cyclictest(延迟)、perf(调度)、ftrace(事件)、BPF(自定义)
20.3 推荐工具与资源
| 工具 | 用途 | 获取方式 |
|---|---|---|
| cyclictest | 实时延迟测试 | apt install rt-tests |
| oslat | 调度延迟测试 | rt-tests 包 |
| hackbench | 调度压力测试 | rt-tests 包 |
| perf sched | 调度分析 | 内核自带 |
| bpftrace | 自定义 BPF 追踪 | apt install bpftrace |
| trace-cmd | ftrace 前端 | apt install trace-cmd |
| likwid | 性能监控 | GitHub likwid |
| turbostat | CPU 频率/C-state | Linux tools |
文档版本 :v1.0
最后更新 :2026-10-28
适用内核 :Linux 5.10 LTS(兼容 5.15 / 6.1 / 6.6 EEVDF 标注)
适用平台 :ARM64(i.MX8MP 为主)、x86_64
作者注:所有实时性优化参数已在 i.MX8MP + PREEMPT_RT 实测验证。EEVDF 部分基于 6.6+ 内核源码,i.MX8MP NXP BSP 若为 5.10 则仍用 CFS。扩展章节补充了调度器源码剖析、cgroup 深度、NUMA 调优、perf/ftrace/BPF 实战、完整调优案例。