时间片计算代价:CFS 的时间片 ideal_runtime = sched_latency / nr_running 在进程数增加时减小,当 nr_running 很大时可能降到 min_granularity(默认 0.75ms)以下,此时进程频繁切换导致上下文切换开销上升。

4.2 EEVDF 算法核心

EEVDF(Earliest Eligible Virtual Deadline First)引入三个关键变量解决上述问题:

  • runtime:已执行的 CPU 时间(等同于 CFS 的 sum_exec_runtime)。
  • deadline = arrival_time + (granularity / weight):下次可以调度它的时间点。arrival_time 是上一次被唤醒/入队时的当前时间(不是 vruntime)。
  • lag = runtime - (weight_sum × (now - arrival_time) / total_weight):进程被"亏欠"或"透支"的 CPU 时间。

调度决策简化为:选择 deadline 最小的可运行进程。这天然解决了 CFS 中的唤醒抢占问题——唤醒进程的 deadline 比普通运行进程小一个精度(granularity/weight),它将在第一个 deadline 时刻公平地参与竞争,而不会无限期霸占 CPU。

关键性质:EEVDF 是 work-conservinglag-preserving 的。任何被延迟的进程(lag > 0)会在接下来的调度中获得补偿,直到 lag 恢复为零。整个系统的总 lag 恒为零。

4.3 EEVDF 数据结构与实现

EEVDF 保留了红黑树结构,但键值从 vruntime 变为 deadline:

// kernel/sched/fair.c (EEVDF 后)
struct sched_entity {
    u64         exec_start;      // 本次调度开始时间
    u64         sum_exec_runtime;// 累计执行时间
    u64         vruntime;        // 仍保留用于计算 lag
    u64         deadline;        // 核心调度键值 = arrival + (granularity/weight)
    u64         min_vruntime;    // 兼容性保留
    unsigned long       weight;  // nice 权重
    // ...
};

// 入队时计算 deadline
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    // ...
    se->deadline = calc_delta_fair(se->granularity, se->load) + sched_clock();
    __enqueue_entity(cfs_rq, se);  // 以 deadline 为键插入红黑树
}

实际内核实现中 EEVDF 引入了时间戳截断(latency nice)概念,允许进程牺牲延迟换取吞吐量(类似 CFS 的 sched_nice,但更精细)。默认 latency_nice = 0,CFS 行为接近于 Nice 值映射。

4.4 如何切换回 CFS

Linux 6.6 起 EEVDF 成为默认,但保留了 CFS 作为可选项:

# 切换回 CFS(需要重启)
sudo grubby --update-kernel=ALL --args="sched_tickless=0"
# 或者运行时切换(6.6+)
# EEVDF 和 CFS 都编译在内核中,可通过 kernel parameter sched_policy=cfs 切回
sysctl kernel.sched=0  # 0=EEVDF, 1=CFS

注意:不同内核版本实现可能有变化,生产切换前必须全面测试

5. 上下文切换深度剖析

5.1 切换开销构成

上下文切换(context switch)是调度器最昂贵的操作之一,通常需要 1-10μs,具体构成:

  • 硬件状态保存/恢复:通用寄存器、浮点/SIMD 寄存器(Linux 使用惰性 FP 切换优化)、PC/SP。
  • MMU 切换:更新 CR3 寄存器指向新进程的页表;触发 TLB 刷新(实际上用 PCID 标签避免完整刷新)。
  • 内核栈切换thread_info 指针从 old task 转移到 new task。
  • SPECULATIVE 防护开销:含 IBRS/STIBP/L1D flush 等缓解 Spectre/Meltdown 的指令。
  • Btr FS 切换:保存/恢复 Branch History Buffer 状态。

Linux 的 惰性 FPU 切换 是个重要优化:首次在切换后进程使用 FPU 时触发 #NM 异常,内核才恢复 FPU 状态。对于大量不使用浮点运算的进程,这节省了很多不必要的状态保存。

5.2 进程切换 vs 线程切换 vs 协程切换

切换类型共享资源典型开销地址空间
进程切换无(除文件描述符表)2-10μs需要更换页表(PCID 优化后 TLB 命中率提升)
线程切换(同进程)地址空间、文件映射0.5-3μs同进程无需切换页表
用户态协程切换所有内核状态10-100ns完全不经过内核

生产环境中的黄金法则:当协程切换开销成为瓶颈时(如百万级频切换),使用异步 I/O(io_uring)+ 单线程事件循环模型能获得最佳性能。Nginx/Redis 的成功正是基于此原理。

6. 内核抢占模型

Linux 提供四种内核抢占模型,控制内核代码执行何时可被抢占:

6.1 PREEMPT_NONE(服务器/批处理)

CONFIG_PREEMPT_NONE:只有当系统调用返回、中断处理完成或显式调用 cond_resched() 时才切换。最大化吞吐量,但响应延迟不可控(可达数十至数百毫秒)。适合 HPC 和批处理。

6.2 PREEMPT_VOLUNTARY(桌面默认)

CONFIG_PREEMPT_VOLUNTARY:在显式的抢占点(might_sleepmutex_lock 等)检查是否需要抢占。平衡吞吐量与响应性。

6.3 PREEMPT(低延迟桌面/基础 RT)

CONFIG_PREEMPT:内核代码大部分位置(除持有 spinlock 的关键区)可被抢占。将默认调度延迟从数十毫秒降到几毫秒。SCHED_FIFO/RR 配合使用可实现数十至数百微秒级响应。代价是吞吐量下降约 1-3%。

6.4 PREEMPT_RT(硬实时)

CONFIG_PREEMPT_RT:实时抢占补丁集(已于 Linux 6.12 前逐步合入主线),将大多数 spinlock 转换为可睡眠的 rtmutex,将硬 IRQ 线程化。目标是将最坏情况延迟降低到

检查当前系统抢占模型:

$ cat /sys/kernel/debug/sched/preempt
# 或查看内核配置
zcat /proc/config.gz | grep PREEMPT

7. CPU 拓扑感知调度与 NUMA 优化

7.1 调度域(Sched Domain)层次

Linux 调度器通过 sched_domain 树形结构感知硬件拓扑,从低到高:

DIE/MC domainsPKG domainNUMA domain

  • MC(Multi-core)domain:同一 CPU die 内的核,共享 L1/L2 cache。
  • DIE domain:某些架构(如 Zen3+)将多个 CCX 组成一个 die。
  • PKG domain:同一物理插槽内的核,共享 L3 cache。
  • NUMA domain:多插槽系统中按 NUMA 节点划分。

负载均衡从最底层向上逐层搜索空闲 CPU 以平衡负载,优先在共享 cache 的核间迁移(这样减少 cache miss)。

7.2 NUMA 调度策略

调度器通过 task_numa_placement() 将进程尽量放在靠近其内存的 NUMA 节点上。跨节点访问内存的延迟是同节点的 1.5-3 倍。关键机制:

  • NUMA Balancing:内核周期性扫描进程地址空间,统计每个页面被哪个节点上的 CPU 访问最多(通过缺页异常采样)。将频繁页面迁移到访问它的 CPU 所在节点。
  • AutoNUMA:通过 numactl --interleave 与内核自适应策略更早地放置进程。
  • fault_around:当采样到某段内存被远程访问时,将整个 folio(2MB 大页)迁移。

NUMA Balancing 的开关与参数:

sysctl kernel.numa_balancing=1          # 默认开启
sysctl kernel.numa_balancing_scan_delay_ms=1000  # 首次扫描延迟
sysctl kernel.numa_balancing_scan_period_min_ms=2000
sysctl kernel.numa_balancing_scan_period_max_ms=60000
sysctl kernel.numa_balancing_scan_size_mb=256     # 每次扫描大小

7.3 跨节点访问性能陷阱

在双路 EPYC 7763(64 核 × 2)服务器上,跨 NUMA 节点访问 DDR4-3200 内存的延迟从 ~80ns 升至 ~140ns,带宽下降 40%。对于内存密集型应用(如 NVMe-oF 目标端、大规模 Redis),错误的 NUMA 绑定会导致吞吐量断崖式下降。

检查 NUMA 拓扑:

$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 ... 63
node 0 size: 257698 MB
node 0 free: 245632 MB
node 1 cpus: 64 65 66 ... 127
node 1 size: 257698 MB
node 1 free: 253104 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

距离 10 表示同节点,21 表示跨节点(具体数值取决于架构,距离越大性能越差)。

8. cgroups v2 CPU 资源控制

cgroups v2 重建了 CPU 资源管理机制,使用

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部