Linux 进程调度器深入解析与实时性优化指南(基于 Linux 5.10+)

前言

适用平台 :通用 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) 上验证


目录

  1. 调度器全景架构
  2. 进程状态与调度类
  3. CFS:完全公平调度器
  4. [EEVDF:CFS 的演进(6.6+)](#EEVDF:CFS 的演进(6.6+) "#4-eevdfcfs-%E7%9A%84%E6%BC%94%E8%BF%9B66")
  5. [实时调度:SCHED_FIFO / SCHED_RR](#实时调度:SCHED_FIFO / SCHED_RR "#5-%E5%AE%9E%E6%97%B6%E8%B0%83%E5%BA%A6sched_fifo--sched_rr")
  6. [Deadline 调度:SCHED_DEADLINE](#Deadline 调度:SCHED_DEADLINE "#6-deadline-%E8%B0%83%E5%BA%A6sched_deadline")
  7. [NUMA 感知调度](#NUMA 感知调度 "#7-numa-%E6%84%9F%E7%9F%A5%E8%B0%83%E5%BA%A6")
  8. [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")
  9. [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")
  10. [抢占模型与 PREEMPT_RT](#抢占模型与 PREEMPT_RT "#10-%E6%8A%A2%E5%8D%A0%E6%A8%A1%E5%9E%8B%E4%B8%8E-preempt_rt")
  11. 调度器观测与调优工具
  12. 嵌入式实时优化实战(i.MX8MP)
  13. 经典案例与排查流程
  14. 总结与最佳实践

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 的局限性

  1. vruntime 溢出:长时间运行 vruntime 会溢出(u64 约 584 年,实际无问题)
  2. NUMA 不友好:纯按 vruntime 调度,不考虑内存访问延迟
  3. 延迟抖动:大量任务时红黑树深度大,调度延迟增加
  4. 组调度开销: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 选择逻辑:

  1. 找出所有 eligible(vruntime ≤ 当前时间偏移)的任务
  2. 从中选 deadline 最小的
  3. 如果当前任务的 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, &param) < 0) {
        perror("sched_setscheduler");
        return -1;
    }
    
    // 或设置为 SCHED_RR
    // sched_setscheduler(0, SCHED_RR, &param);
    
    return 0;
}

// 查询当前策略
void print_sched_info(void)
{
    int policy = sched_getscheduler(0);
    struct sched_param param;
    sched_getparam(0, &param);
    
    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:自动平衡(内核定期迁移任务到内存所在节点)

自动平衡原理:

  1. 内核周期性(默认约 1s)触发 NUMA hinting fault
  2. 检查任务访问的内存页在哪个节点
  3. 如果远程访问比例高,把任务迁移到内存所在节点

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), &current);
if (CPU_ISSET(0, &current))
    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+ 完全合并):

核心改进:

  1. 自旋锁转为可睡眠锁 :spinlock_t 变成 rt_mutex,可被抢占
  2. 中断线程化 :硬中断处理转为内核线程(irq/N-xxx),可设置 RT 优先级
  3. 定时器线程化 :hrtimer 转为线程
  4. 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, &param) < 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"

常见根因:

  1. RT 任务与中断在同一 CPU → 中断阻塞 RT 任务
  2. CPU 频率降低 → 指令执行变慢
  3. 内存分配触发直接回收 → 数百 us 延迟
  4. 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"

优化方向:

  1. 减少不必要的线程(每线程有调度开销)
  2. 用 CPU 亲和性减少跨核迁移
  3. 调整 nice 值让关键任务优先
  4. 考虑 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 固定为 performance governor
  • RT 任务用 mlockall 锁定内存
  • RT 任务绑定到 isolcpus 隔离的 CPU
  • 中断亲和性设到非 RT CPU
  • irqaffinity 启动参数排除 RT CPU
  • 关闭 transparent_hugepage
  • cyclictest Max < 目标值
  • RT 任务优先级合理(不高于必要值)
  • 有 RT throttling 兜底(除非纯 RT 系统)

14.3 通用系统调优清单

  • 调度器:默认 CFS 通常足够
  • 关键任务:renice 或 cgroup cpu.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, &param);
    
    // 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, &param);
    
    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 核心要点回顾

  1. CFS 是默认调度器:基于 vruntime 的公平调度,适合通用场景
  2. RT 严格高于 CFS:SCHED_FIFO/RR 优先级 1-99,有 throttling 防饿死
  3. Deadline 提供带宽保证:SCHED_DEADLINE 基于 CBS 算法,最高优先级
  4. PREEMPT_RT 是硬实时基础:自旋锁可睡眠、中断线程化
  5. cgroup v2 是资源管理标准:cpu.max 限制带宽,cpu.weight 控制份额
  6. isolcpus 隔离延迟敏感任务:配合 irqaffinity 效果最佳
  7. 观测工具链: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 实战、完整调优案例。

相关推荐
Tokenfree1 小时前
给大模型调用加一个熔断器:60 行代码,三个阈值
后端
136096757231 小时前
三个 Agent 干同一个活,职责边界到底该怎么划
后端
房西杭1 小时前
GM 指令:修改玩家属性
后端
胡写代码1 小时前
删掉的数据还占着唯一键:逻辑删除 + 唯一索引的四次取舍
spring boot·后端
IT_陈寒1 小时前
React的useEffect依赖数组把我坑惨了
前端·人工智能·后端
用户8356290780511 小时前
Python 调整 Excel 工作表显示设置
后端·python
ServBay2 小时前
API Key 泄露如何在 10 分钟内止血?AI 密钥管理必备
后端·ai编程
代码丰2 小时前
大 JSON 放进 Redis 之后,如何用 Stream 解耦耗时任务?
redis·后端·系统架构
武子康3 小时前
手机加语音配件,断开连接后谁来保住这段话?
人工智能·后端·agent