1. 引言:为什么进程调度如此重要

在操作系统的世界中,CPU 是最为稀缺的计算资源之一。无论是一台运行着数百个容器的云服务器,还是一部需要同时处理触摸响应、音频播放和网络通信的智能手机,操作系统都必须回答同一个关键问题:在任意时刻,究竟让哪个任务占用 CPU?回答这个问题的机制,就是进程调度(Process Scheduling)

进程调度的设计质量直接影响着整个系统的性能表现。调度器需要在多个彼此矛盾的目标之间寻找平衡:

  • 吞吐量(Throughput):单位时间内完成的任务数量尽可能多;
  • 响应时间(Response Time):交互任务从触发到获得 CPU 的延迟尽可能短;
  • 公平性(Fairness):所有任务都能获得与其优先级相称的 CPU 时间;
  • 可扩展性(Scalability):在数百核的服务器上调度开销不会成为瓶颈;
  • 实时性(Real-time):对有时间限制的任务提供足够确定的延迟保证。

Linux 的进程调度器经历了从朴素到精密的演进历程:早期 O(n) 调度器的线性扫描、O(1) 调度器的位图优化、统治了十六年之久的 CFS(Completely Fair Scheduler,完全公平调度器),再到 Linux 6.6 内核引入的 EEVDF(Earliest Eligible Virtual Deadline First) 调度器。每一次变革都回应着当时硬件体系和负载形态的深刻变化。

本文将从进程与线程的基本概念出发,系统性地构建 Linux 进程调度的完整知识框架。内容涵盖:调度器演进历史、CFS 公平性数学原理、核心数据结构红黑树、调度类与策略体系、优先级与 nice 值、抢占机制、多核负载均衡、EEVDF 新调度器、实时调度机制、系统调用编程接口、cgroup 资源控制,以及一线工程师最需要的观测工具和调优方法论。文章兼顾理论深度和工程实践,适合系统开发者、运维工程师、性能优化工程师以及所有希望深入理解 Linux 内核调度机制的读者。

2. 进程与线程:调度的基本对象

在深入调度算法之前,必须先厘清一个根本性问题:调度器调度的到底是什么?答案是——Linux 内核中的任务(task),由数据结构 struct task_struct 描述的执行实体。

2.1 进程与线程:内核视角的异同

从传统操作系统教科书的视角看,进程和线程具有清晰的区分:

  • 进程(Process):拥有独立的虚拟地址空间、独立打开的文件描述符表、独立的信号处理设置,是操作系统资源分配的基本单位。
  • 线程(Thread):共享所属进程的地址空间和大部分资源,拥有独立的寄存器和栈,是 CPU 调度的基本单位。

然而,Linux 内核对这两者的区分远不如教科书那么泾渭分明。Linux 使用统一的 clone() 系统调用来创建所有类型的任务,通过不同的标志位(flags)组合来决定父子任务共享哪些资源:

  • CLONE_VM:共享地址空间;
  • CLONE_FILES:共享文件描述符表;
  • CLONE_SIGHAND:共享信号处理函数表;
  • CLONE_THREAD:将新任务放入与父任务相同的线程组。

clone() 设置了 CLONE_VM | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD 时,创建出来的就是传统意义上的线程;否则就是独立的进程。但无论哪种情况,内核中都将其表示为一个 task_struct 实例。调度器对它们一视同仁——它只关心任务的调度属性(优先级、权重、状态、运行时间),而不关心这个任务在用户层面是"进程"还是"线程"。

2.2 task_struct:内核调度的核心数据结构

struct task_struct 是 Linux 内核中最庞大、最复杂的数据结构之一,包含了一个任务所需的全部信息:内存管理、文件系统、信号处理、安全凭证、调度信息等。其中与调度直接相关的关键字段包括:

  • state:进程当前状态(可运行、睡眠、停止、僵尸等);
  • prio:动态优先级,调度器实际用于决策的优先级值;
  • static_prio:静态优先级,由 nice 值换算而来,对普通任务生效;
  • normal_prio:普通优先级,综合调度策略和静态优先级计算得出;
  • rt_priority:实时优先级,范围为 0 到 99;
  • policy:调度策略标识(SCHED_NORMALSCHED_FIFOSCHED_DEADLINE 等);
  • sched_class:指向所属调度类结构体的指针;
  • se:公平调度实体(struct sched_entity),CFS/EEVDF 操作的对象;
  • rt:实时调度实体(struct sched_rt_entity);
  • dl:Deadline 调度实体(struct sched_dl_entity)。

2.3 调度实体:抽象出来的"可调度单元"

为了支持组调度(Group Scheduling),Linux 内核引入了一个关键抽象:调度实体(sched_entity)。一个调度实体可以对应两种情况:

  • 单个任务:直接代表一个 task_struct
  • 任务组(task group):代表一组任务的集合,组内可以继续嵌套子组。

这种树状抽象使得 cgroup 可以在调度器层面实现层级化的 CPU 资源分配:先在大的 cgroup 组之间按照权重分配 CPU,再在组内各个进程之间进行二级分配。抽象出调度实体的设计,让 CFS 调度算法的适用范围从"任务级"扩展到了"层级资源分组级",这是 Linux 调度器能够支撑现代容器化基础设施的关键设计之一。

struct sched_entity {
    struct load_weight      load;          /* 负载权重 */
    unsigned long           runnable_weight;  /* 可运行权重 */
    struct rb_node          run_node;      /* 红黑树节点 */
    u64                     exec_start;    /* 本次开始执行的时间戳 */
    u64                     sum_exec_runtime; /* 累计执行时间 */
    u64                     vruntime;      /* 虚拟运行时间 */
    u64                     prev_sum_exec_runtime;
    struct sched_entity     *parent;       /* 父调度实体 */
    struct cfs_rq           *cfs_rq;       /* 所属的 CFS 就绪队列 */
    struct cfs_rq           *my_q;         /* 组调度时的子就绪队列 */
    /* ... 更多字段省略 ... */
};

3. Linux 调度器演进史

理解 Linux 调度器的演进历史,能够帮助我们理解当前设计的每一个细节背后的动机。Linux 调度器大致经历了四个重要阶段,每个阶段都解决了一组特定时代面临的挑战。

3.1 O(n) 调度器:朴素的开端

Linux 2.4 及更早版本使用的是一个极为简单直接的调度器。每次需要选择下一个运行的任务时,它会遍历整个就绪队列,逐一计算每个任务的"调度权重"(goodness),最终选出"最好"的任务投入运行。这个算法的时间复杂度为 O(n),其中 n 是系统中可运行任务的总数。

在那个桌面电脑只有几十个并发任务的年代,O(n) 调度器足够使用。但随着服务器负载的持续上升,任务数量达到数百甚至上千时,每次调度的线性扫描就成为了不可忽视的性能开销。更严重的是,O(n) 调度器在 SMP(对称多处理器)系统上的设计存在先天不足:所有 CPU 共享同一个就绪队列,由一个全局自旋锁保护。CPU 核数越多,锁竞争就越激烈,调度器本身反而成为系统的性能瓶颈。

O(n) 调度器的另一个问题是:它依赖复杂的好感度计算和交互性启发式规则来猜测哪些任务是"交互式"的。这些规则在负载简单时运行良好,但在负载复杂、任务行为多变时表现极不稳定。

3.2 O(1) 调度器:为可扩展性而生

Linux 2.6 早期引入了由 Ingo Molnar 重新设计的 O(1) 调度器,其核心设计思想包括:

  • 每 CPU 独立就绪队列:彻底消除了全局队列锁的竞争问题;
  • 位图加速查找:使用 bitmap 记录哪些优先级存在就绪任务,通过 find_first_set_bit() 可以在常数时间内确定最高非空优先级的任务;
  • 活跃队列与过期队列:维护 active 和 expired 两个优先级数组,避免低优先级任务被长期饿死——当活跃队列中的任务全部用完了自己的时间片后,队列整体翻转为过期队列,重新开始新一轮分配。

O(1) 调度器极大改善了大规模 SMP 系统的调度性能。但它也有明显的代价:为了维持交互性,O(1) 调度器引入了大量复杂的启发式算法来识别"交互式任务"并动态调整它们的优先级。这些启发式算法代码复杂、参数繁多、行为难以预测,在负载动态变化时会出现"交互判断错误"的情况,导致桌面体验出现卡顿。

3.3 CFS:公平性的哲学转折

2007 年,Ingo Molnar 推出了 CFS(Completely Fair Scheduler),随 Linux 2.6.23 合入主线。CFS 的设计哲学与 O(1) 截然不同:抛弃复杂的交互性猜测启发式算法,把"公平分配 CPU 时间"作为唯一的核心目标。事实证明,当公平性得到严格保证时,交互性自然会得到改善——因为交互式任务通常运行短暂、频繁睡眠,它们在 CFS 模型下自然会被优先调度。

CFS 使用红黑树维护就绪任务,选择下一个任务的时间复杂度为 O(log n)。虽然没有达到 O(1) 的理论下界,但 CFS 换来了极其简洁、可预测和稳定的调度行为。在长达 16 年的时间里,CFS 一直是 Linux 的默认调度器,支撑了从嵌入式设备到超大规模数据中心的各种负载。

CFS 的核心创新是引入虚拟运行时间(vruntime)概念——用一个经过权重归一化处理的"虚拟时间"来度量每个任务消耗的 CPU,确保在任意时间尺度上都能维护公平性。后文将深入剖析这一机制。

3.4 EEVDF:迈向更精准的延迟控制

Linux 6.6(2024 年发布)引入了 EEVDF(Earliest Eligible Virtual Deadline First)调度器,由 Peter Zijlstra 主导开发,取代 CFS 成为新的默认调度器。EEVDF 的理论基础来自 1995 年 Stoica 和 Abdel-Wahab 发表的学术论文,核心思想是:为每个任务计算一个虚拟截止时间(virtual deadline),调度器每次选择虚拟截止时间最早的任务运行。

CFS 虽然实现了长期公平,但其公平性保证是"渐近"的——在极短的时间窗口内,一个任务可能经历无法预期的调度延迟。EEVDF 在保持长期公平性的同时,还能提供短时间尺度上的延迟上界,这对音频处理、网络数据面、云原生微服务等延迟敏感型负载具有重要意义。后文第 10 章将专门介绍 EEVDF 的原理。

4. CFS 完全公平调度器核心原理

CFS 的设计目标可以用一句话概括:在理想情况下,每个任务都能获得与其权重成正比的 CPU 时间,而且这种公平性在任意时间尺度上都成立。这句话看似简单,但要在一个真实系统上实现,需要精妙的数学建模和工程实现。

4.1 理想公平模型

假设存在一个"理想 CPU",它可以无限细分时间片、以零开销无限频繁地切换任务。在这个理想世界中,如果在 [t0, t1] 时间段内有 N 个权重相同的任务处于就绪状态,那么每个任务应恰好获得 (t1 - t0) / N 的 CPU 时间。如果任务权重不同,则按比例分配。

遗憾的是,真实的 CPU 一次只能运行一个任务,且每次任务切换都伴随上下文切换开销。CFS 的思路不是追求微观上的绝对轮转,而是引入虚拟运行时间(vruntime)作为统一的"公平性度量尺":让 vruntime 最小的任务优先运行,长期来看所有任务的 vruntime 趋于相等,宏观公平性自然成立。

4.2 虚拟运行时间 vruntime:CFS 的灵魂

vruntime 的计算方式大致为:

vruntime += delta_exec * (NICE_0_LOAD / se->load.weight);

其中各符号的含义:

  • delta_exec:任务本次实际运行的物理时间(以纳秒为单位);
  • NICE_0_LOAD:nice 值为 0 时的基准权重,通常为 1024;
  • se->load.weight:该任务的负载权重,权重越高表示应获得越多的 CPU。

这个公式的优雅之处在于:权重越高的任务,其 vruntime 增长速度越慢。具体来说,当两个任务 A(权重 2048)和 B(权重 1024)分别运行了相同长度的物理时间 T 时:

  • A 的 vruntime 增长量 = T * (1024 / 2048) = T / 2
  • B 的 vruntime 增长量 = T * (1024 / 1024) = T

也就是说,同样运行 10 毫秒,A 的 vruntime 只增长 5 毫秒,而 B 增长了 10 毫秒。在红黑树中,vruntime 更小的任务排得更靠前,因此 A 会获得更多被选中的机会。从宏观效果看,A 和 B 在平衡状态下会以 2:1 的比例分享 CPU 时间,恰好等于它们的权重比。

4.3 最小调度粒度与调度周期

CFS 在追求公平的同时,也必须面对两个现实约束:

  • 调度周期(sched_period / sched_latency_ns):在一段时间内,每个就绪任务至少应该获得一次运行机会。当就绪任务增多时,调度周期会动态拉长。默认值约为 6 毫秒。
  • 最小调度粒度(sched_min_granularity_ns):单个任务一次连续运行的最短时间,防止任务切换过于频繁。典型默认值为 0.75 毫秒。

这两个参数通过 /proc/sys/kernel/sched_min_granularity_ns/proc/sys/kernel/sched_latency_ns 可调。当就绪任务非常多时,调度周期会被拉长,以保证每个任务都能获得不少于最小粒度的时间片。这两个参数直接决定了"公平性"和"上下文切换开销"之间的权衡点。默认值经过了大量测试和调优,除非有明确的性能验证依据,不建议随意修改。

4.4 CFS 如何选择下一个任务

CFS 每次调度选择的过程极其简洁:

  1. 在红黑树中找到最左侧的节点,即 vruntime 最小的调度实体;
  2. 让该实体对应的任务投入运行;
  3. 任务运行一段时间后,它的 vruntime 持续增长,可能不再是树中最小的节点;
  4. 当时钟中断触发调度检查时,调度器比较当前任务与最左节点的 vruntime,如果差距超过阈值则触发抢占;
  5. 被抢占的任务重新插入红黑树,新的最左节点获得 CPU。

由于红黑树的最左节点被缓存(cfs_rq->rb_leftmost),查找下一个任务的操作是 O(1)。插入和删除操作的复杂度为 O(log n)。这种设计在"查找极快"和"维护成本可控"之间取得了很好的平衡。

static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
    struct rb_node *left = cfs_rq->rb_leftmost;
if (!left)
    return NULL;
return rb_entry(left, struct sched_entity, run_node);
}

5. 权重与 nice 值:优先级如何影响 CPU 分配

5.1 nice 值的含义:越小越高

在 Linux 中,普通进程(非实时)的优先级通过 nice 值表示,范围为 -20+19,共 40 个级别。这里有一个非常容易混淆的点:nice 值越小,优先级越高,获得的 CPU 时间越多。默认 nice 值为 0。

nice 这个名称的由来很有意思:可以把 nice 理解为"这个任务愿意对其他任务有多友好(nice)"。nice 值越大,说明该任务越"谦让",主动降低自己的优先级,把 CPU 让给其他任务。所以 nice 19 的任务对别人"极其友好",而 nice -20 的任务则"相当不友好",会抢占尽可能多的 CPU 时间。

5.2 从 nice 到权重:非线性的映射

CFS 并不直接使用 nice 值进行调度计算,而是将 nice 值映射为一个权重(weight)。这个映射不是线性的,而是近似按照 1.25 的等比数列设计:nice 值每减少 1,权重大约增加 25%。这种指数式的映射使得 nice 值对 CPU 分配比例的影响是乘法式的,更符合人们对"优先级"的感知。

内核中的映射表如下(节选自 kernel/sched/core.c):

static const int 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,nice +19 只有 15。两者相差约 5900 倍。这种巨大的动态范围正是 Linux 优先级体系强大表达力的体现,也让"高优先级任务"在竞争激烈时能够真正"碾压"低优先级任务。

5.3 权重如何转化为时间片

在一个完整的调度周期内,任务 i 应获得的时间片为:

time_slice_i = sched_period * (weight_i / total_weight);

其中 total_weight 是所有就绪任务权重之和。举一个具体例子:假设调度周期为 6 毫秒,三个就绪任务 A(权重 1024)、B(权重 1024)、C(权重 2048),则三者的时间片分别为 1.5ms1.5ms3ms。这就是 CFS 公平性的直观表达:CPU 时间按照权重严格按比例分配。

5.4 三层优先级体系

内核中涉及任务优先级的概念有三个,需要明确区分:

  • 静态优先级(static_prio):由 nice 值换算而来(static_prio = MAX_RT_PRIO + nice + 20),是普通任务的基础优先级。除非用户通过 nice()setpriority() 系统调用主动修改,否则保持不变。
  • 普通优先级(normal_prio):综合调度策略和静态优先级计算出的优先级。对于普通任务,它等于 static_prio;对于实时任务,它取实时优先级。
  • 动态优先级(prio):调度器在实际决策中使用的优先级。对普通任务,它通常等于 normal_prio;但在优先级继承(priority inheritance)等特殊情况下可能临时改变。

需要特别指出的是,CFS 在选择任务时并不直接比较 prio 的数字,而是通过权重vruntime 来表达优先级差异。prio 字段更多用于调度类之间的选择(如实时类优先于公平类)以及通用的调度框架层。

6. 红黑树:CFS 的核心数据结构

6.1 为什么选择红黑树

CFS 需要一种数据结构来高效维护就绪任务集合,需要频繁执行的操作包括:

  • 查找最小 vruntime 任务(每次调度都要执行);
  • 插入新就绪任务(任务唤醒、新任务创建时);
  • 删除正在运行或离开就绪队列的任务(任务被选中运行、睡眠时)。

红黑树是一种自平衡二叉搜索树,保证在最坏情况下插入、删除、查找的时间复杂度都是 O(log n)。CFS 使用 vruntime 作为红黑树的排序键值,并将最左节点缓存到 cfs_rq->rb_leftmost,使得"找到下一个任务"这一最频繁的操作降为 O(1)

6.2 就绪队列 cfs_rq 的结构

每个 CPU(以及组调度的每个层级)都有一个 struct cfs_rq 数据结构,它持有:

  • 红黑树的根节点和最左节点缓存
  • 就绪任务总数 nr_running
  • 队列总权重 load.weight
  • 当前正在运行的任务 curr
  • 队列最小虚拟运行时间 min_vruntime

任务的入队操作围绕 cfs_rq 的红黑树展开。下面是简化的入队逻辑:

static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node;
    struct rb_node *parent = NULL;
    struct sched_entity *entry;
    s64 key = entity_key(cfs_rq, se);
    int leftmost = 1;
/* 在红黑树中查找合适的插入位置 */
while (*link) {
    parent = *link;
    entry = rb_entry(parent, struct sched_entity, run_node);
    if (key < entity_key(cfs_rq, entry)) {
        link = &parent->rb_left;
    } else {
        link = &parent->rb_right;
        leftmost = 0;
    }
}
if (leftmost)
cfs_rq->rb_leftmost = &se->run_node;  /* 更新最左缓存 */
rb_link_node(&se->run_node, parent, link);
rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
}

6.3 min_vruntime:公平性基准线

cfs_rq->min_vruntime 记录该就绪队列中最小的 vruntime 基准值,在以下三个场景中发挥关键作用:

  • 新任务入队:新创建或被唤醒的任务,其 vruntime 会被设置为队列 min_vruntime 附近的值。这样新任务既不会因为 vruntime 从 0 开始而"碾压"所有老任务(否则新任务会霸占 CPU),也不会过于落后而迟迟得不到运行机会。
  • 防止 vruntime 数值无限增长:当队列中所有任务都睡眠、队列为空时,min_vruntime 会继续推进,确保后续新任务有一个合理的起点。
  • 唤醒补偿的参照:任务从睡眠中被唤醒时,其 vruntime 的调整以 min_vruntime 为参照。

6.4 唤醒与睡眠任务的处理策略

任务从睡眠中被唤醒时,CFS 面临一个关键决策:它的 vruntime 采用什么值?如果直接沿用睡眠前的 vruntime,由于它在睡眠期间没有消耗 CPU,其 vruntime 会远小于一直在运行的其他任务,唤醒后会长期占据 CPU,破坏公平性。

为了平衡,内核在唤醒时将任务的 vruntime 调整为:

vruntime = max(原 vruntime, cfs_rq->min_vruntime - thresh);

其中 thresh 是一个可配置的偏移量。这种设计让唤醒任务在合理范围内享受一定的"优待"(对交互性有利),但又不会过度抢占其他任务。内核的 GENTLE_FAIR_SLEEPERS 特性及相关参数进一步细化了这一策略,共同塑造了 CFS 对交互式负载的良好响应性。

7. 调度类与调度策略全景

Linux 调度框架采用调度类(scheduling class)的分层设计。每个调度类实现一组标准接口(如 enqueue_taskdequeue_taskpick_next_task),调度器按优先级从高到低依次询问各调度类"你有没有任务需要运行"。

7.1 五类调度器的层次结构

从高到低(优先级依次降低)依次是:

  • stop_sched_class:优先级最高,用于停机任务(stop task),负责 CPU 热插拔、任务强制迁移等特殊操作。普通用户无法使用。
  • dl_sched_class:Deadline 调度类,对应 SCHED_DEADLINE 策略,面向有严格时限要求的实时任务。
  • rt_sched_class:实时调度类,对应 SCHED_FIFOSCHED_RR 策略。
  • fair_sched_class:公平调度类(CFS/EEVDF),对应 SCHED_NORMALSCHED_BATCHSCHED_IDLE 策略,是绝大多数任务所在的位置。
  • idle_sched_class:最低优先级。每个 CPU 都有一个 idle 任务,当没有其他任务可运行时运行它,负责让 CPU 进入低功耗状态。

7.2 Linux 调度策略一览

Linux 定义了以下主要调度策略,可以通过 sched_setscheduler() 系统调用设置:

策略常量调度类适用场景核心行为
SCHED_NORMALfair普通交互/服务进程使用 CFS/EEVDF 公平调度,按权重比例分配 CPU
SCHED_BATCHfairCPU 密集型批处理减少唤醒抢占,让任务一次运行更长时间
SCHED_IDLEfair非常低优先级的后台任务比 nice 19 的优先级还低,仅系统空闲时运行
SCHED_FIFOrt高优先级实时任务先进先出,任务运行直到阻塞或主动让出
SCHED_RRrt需要轮转的实时任务在 FIFO 基础上增加同优先级时间片轮转
SCHED_DEADLINEdl有明确时限要求的周期性任务基于最早截止时间优先(EDF)算法

7.3 调度类选择流程:从快速路径到通用路径

主调度函数 __schedule() 在需要重新调度时,会调用 pick_next_task() 选择一个任务。对于最常见的场景(所有就绪任务都属于 fair 类),内核做了快速路径优化:

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev)
{
    const struct sched_class *class;
    struct task_struct *p;
if (likely(prev->sched_class == &fair_sched_class &&
           rq->nr_running == rq->cfs.h_nr_running)) {
    /* 快速路径:只有 fair 类任务 */
    p = pick_next_task_fair(rq, prev);
    if (unlikely(p == RETRY_TASK))
        goto restart;
    return p;
}
/* 通用路径:按优先级从高到低遍历各调度类 /
for_each_class(class) {
p = class->pick_next_task(rq);
if (p)
return p;
}
/ 理论上不会到达这里,因为 idle 类总是有任务 */
BUG();
}

快速路径避免了逐级询问高优先级调度类的开销。当任何实时任务就绪时,它会立即抢占普通任务——这是通过"先检查高优先级调度类"的分层设计保证的。

8. 进程状态与上下文切换

8.1 进程的七种状态

Linux 中进程状态由 task_struct->state 表示,常见状态包括:

  • TASK_RUNNING:可运行状态,正在运行或在就绪队列中等待运行。
  • TASK_INTERRUPTIBLE:可中断睡眠,等待某个条件满足,可被信号唤醒(ps 中显示为 S)。
  • TASK_UNINTERRUPTIBLE:不可中断睡眠,通常用于等待 IO 完成等不可被打断的场景(ps 中显示为 D)。
  • TASK_STOPPED:停止状态,进程收到 SIGSTOP 等信号后进入(ps 中显示为 T)。
  • TASK_TRACED:被调试器跟踪的状态(ps 中显示为 t)。
  • EXIT_ZOMBIE:僵尸状态,进程已结束但父进程尚未回收退出状态(ps 中显示为 Z)。
  • EXIT_DEAD:彻底退出的最终状态。

对调度器而言,真正关心的是"是否可运行":只有 TASK_RUNNING 状态的任务会进入就绪队列;睡眠的任务被移出就绪队列,直到被唤醒。

8.2 上下文切换的完整过程

上下文切换是调度的执行环节,也是调度器产生开销的主要来源。当调度器决定从任务 A 切换到任务 B 时,发生的步骤包括:

上下文切换的代价来自多个层面:寄存器保存恢复的直接开销、页表切换导致的 TLB 失效、以及缓存(L1/L2/LLC)中的内容可能失效带来的间接性能损失。CFS 通过合理设置最小调度粒度来限制切换频率,在响应性和切换开销之间取得平衡。

8.3 抢占机制:何时触发调度

调度器并不会"主动跳出来"打断正在运行的任务,必须有触发点。Linux 的抢占机制分为两类:

此外,周期性时钟中断(tick)是调度器检查时间片和 vruntime 的重要驱动。每次 tick 到来时,调度器更新当前任务的运行时间,判断是否需要设置抢占标志。对于启用 CONFIG_NO_HZ_FULL 的系统,还可以为 CPU 密集型任务禁用周期性 tick,减少不必要的调度中断。

9. 多核调度与负载均衡

现代服务器通常拥有几十甚至上百个 CPU 核心。如何在这些核心之间均衡分配任务,是调度器面临的另一个核心挑战。Linux 采用"每 CPU 就绪队列 + 负载均衡"的架构解决多核调度问题。

9.1 每 CPU 就绪队列:消除锁竞争

每个 CPU 都有独立的数据结构 struct rq(runqueue),其中包含了该 CPU 上的 CFS 队列、实时队列和 Deadline 队列。任务平时固定在某一个 CPU 的队列中,这种设计避免了多核共享队列的锁竞争问题,同时天然利用 CPU 本地缓存亲和性——任务在其上次运行的 CPU 上继续运行,可以复用仍驻留在缓存中的数据。

但每 CPU 队列也带来了新问题:如果某个 CPU 上堆积了大量任务而另一个 CPU 处于空闲状态,就需要把任务从繁忙 CPU 迁移到空闲 CPU。这就是负载均衡(load balancing)的工作。

9.2 调度域与调度组:按硬件拓扑分层均衡

负载均衡并不是在所有 CPU 之间扁平地进行,而是按照硬件拓扑分层组织为调度域(sched domain)

内核在每一层调度域内分别进行负载均衡,优先在成本更低的层级(如同一个物理核的超线程之间)迁移任务,只有在必要时才跨 NUMA 节点迁移。这种分层策略最大限度地减少了任务迁移带来的缓存失效和内存访问延迟。

9.3 负载计算:从简单权重到 PELT

CFS 用任务权重作为其负载的基本度量。一个 CPU 的负载就是其就绪队列中所有任务权重之和。负载均衡器周期性地比较各 CPU 的负载,当某个 CPU 的负载显著高于同一调度域内的平均水平时,触发任务迁移。

对于多核环境,内核引入了 PELT(Per-Entity Load Tracking,逐实体负载跟踪)机制。PELT 使用带时间衰减的几何级数来跟踪每个调度实体(包括任务和任务组)的历史负载,使得负载估计既能反映当前压力,又不会因瞬时波动而剧烈跳变。PELT 的计算结果用于负载均衡决策、频率调节(EAS,Energy Aware Scheduling)等多个子系统。

9.4 任务迁移的代价与策略

任务迁移不是免费的——迁移到另一个 CPU 会导致该任务失去原 CPU 的 L1/L2 缓存内容;跨 NUMA 节点迁移还会使后续内存访问变慢。因此负载均衡器会尽量:

9.5 NUMA 感知调度

在 NUMA 架构下,CPU 访问本地节点内存快,访问远端节点内存慢。Linux 调度器通过 sched_domain 的 NUMA 层级和 numa_balancing 特性,尽量让任务运行在离其内存所在节点更近的 CPU 上。对于内存占用大的进程,这种局部性优化能带来显著的性能提升。内核还会通过 task_numa_fault() 跟踪任务的内存访问分布,在必要时将任务及其内存页面迁移到同一 NUMA 节点。

10. EEVDF:CFS 的继任者

10.1 EEVDF 的背景与动机

CFS 运行了十几年,表现非常优秀。但它存在一个结构性局限:CFS 只保证长期的公平性,无法给出短时间尺度上的延迟上界。在 CFS 下,一个任务在最坏情况下可能等待多久才能获得 CPU,并没有严格的数学保证。对于普通的服务器负载,这通常不是问题;但随着延迟敏感应用(音频处理、网络数据面、云原生微服务、云游戏等)的普及,人们希望调度器能够提供更可预期的延迟行为。

EEVDF 应运而生,理论源自 1995 年 Stoica 和 Abdel-Wahab 发表的论文。其核心思想是:为每个任务计算一个虚拟截止时间(virtual deadline),调度器总是选择虚拟截止时间最早的任务运行。这样既能保证长期公平(权重比例),又能给出延迟上界。

10.2 核心概念:Eligible(合格)与 Deadline(截止)

EEVDF 为每个任务维护两个关键时间量:

调度器的选择过程分两步:

直观理解:Eligible 保证任务不会"超前消费" CPU,Deadline 则让"最紧急"的任务优先运行。

10.3 EEVDF 与 CFS 的对比

维度CFSEEVDF
选择依据vruntime 最小者eligible 任务中 deadline 最早者
核心数据结构红黑树(按 vruntime 排序)红黑树(按 deadline 排序)
公平性保证长期公平(渐近式)长期公平 + 短时间延迟上界
延迟敏感负载表现依赖唤醒优待等启发式天然提供可预期的延迟
实现复杂度较低略高,但概念更清晰
上游状态Linux 2.6.23 至 6.5 默认Linux 6.6 起为新默认

10.4 EEVDF 的关键参数

EEVDF 保留了 CFS 的 nice 值、权重体系以及 sched_periodmin_granularity 等核心概念,同时引入了新的可调参数:

这些参数位于 /sys/kernel/debug/sched/ 或 procfs 下,普通用户一般无需调整。

10.5 对用户的影响

对绝大多数用户来说,从 CFS 切换到 EEVDF 对上层接口没有任何影响:nicetasksetchrt、cgroup 等工具照常工作。唯一的变化是内核内部的调度决策更精确了,延迟敏感型负载在混合负载下的表现通常更稳定。

从技术实现角度看,EEVDF 并未完全废弃 CFS 的代码。当前 fair 调度类的实现中,红黑树结构、调度实体、就绪队列等基础设施被大量复用。EEVDF 可以理解为在 CFS 的框架上重写了"裁决算法"的核心部分。

11. 实时调度与 SCHED_DEADLINE

11.1 Linux 实时调度的含义

Linux 的"实时"指的是调度层面的确定性:实时任务能够在可预期的时间内获得 CPU,而不是普通任务的尽力而为。Linux 提供三类实时机制:

需要说明的是,Linux 默认配置下的实时性属于软实时(soft real-time):能够提供很好的统计意义上的低延迟,但不能像专用 RTOS 那样给出硬件级严格上界。如果需要硬实时,应选择 CONFIG_PREEMPT_RT 补丁或专用的实时内核发行版。

11.2 SCHED_FIFO 与 SCHED_RR

实时任务的优先级范围是 1-99(0 保留给普通任务),数字越大优先级越高。任何实时任务的优先级都高于所有普通任务,因此不当使用实时优先级可能"饿死"整个系统的普通进程。

设置 SCHED_FIFO 的典型代码:

#include <sched.h>
#include <stdio.h>
int main(void)
{
struct sched_param param;
param.sched_priority = 50;  /* 实时优先级 1-99 */
if (sched_setscheduler(0, SCHED_FIFO, &amp;param) == -1) {
    perror("sched_setscheduler");
    return 1;
}
printf("当前进程已切换到 SCHED_FIFO,优先级 50\n");
return 0;
}

SCHED_FIFO 任务一旦获得 CPU 就会持续运行,直到以下情况之一发生:

SCHED_RR 则额外为相同优先级的任务提供轮转:每个任务运行一个时间片后,让给同优先级队列中的下一个任务。RR 适合需要多任务轮转但又要求实时优先级的场景。

11.3 SCHED_DEADLINE:基于时限的现代实时调度

SCHED_DEADLINE 让任务自己声明三个参数:

内核会执行准入控制(admission control):如果新任务的参数会导致 CPU 总带宽超限(默认实时带宽上限为 95%),则拒绝接受该任务。Deadline 调度器采用 EDF 策略:截止时间越近的任务越先运行。

#include <sched.h>
#include <stdio.h>
int main(void)
{
/* runtime=10ms, deadline=30ms, period=30ms /
struct sched_attr attr = {
.size = sizeof(struct sched_attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 10 * 1000 * 1000,   / 纳秒 */
.sched_deadline = 30 * 1000 * 1000,
.sched_period = 30 * 1000 * 1000,
};
if (sched_setattr(0, &amp;attr, 0) == -1) {
    perror("sched_setattr");
    return 1;
}
/* 任务在 30ms 周期内最多运行 10ms */
return 0;
}

SCHED_DEADLINE 适合周期性、有明确时限要求的任务,例如音视频采集处理、传感器数据采集、机器人控制循环等。

11.4 实时带宽限制

为了防止实时任务恶意或意外耗尽 CPU,内核提供实时带宽限制机制:每个实时调度域默认最多只能把 95% 的 CPU 时间用于实时任务(SCHED_FIFO/RR),预留 5% 给普通任务,确保系统不会被完全卡死。相关参数通过 procfs 调整:

12. 调度相关的系统调用与编程接口

12.1 nice 与 setpriority:调整普通任务优先级

#include <unistd.h>
#include <sys/resource.h>
/* 将当前进程的 nice 值增加 5(优先级降低) */
int nice(int inc);
/* 更通用的设置方式 */
int ret = setpriority(PRIO_PROCESS, getpid(), 5);
if (ret == -1) {
perror("setpriority");
}

权限控制:普通用户只能提高自己进程的 nice 值(降低优先级);只有 root 或具有 CAP_SYS_NICE 能力的进程才能降低 nice 值(提高优先级)。

12.2 sched_setscheduler 与 sched_setattr

设置调度策略和实时优先级:

#include <sched.h>
struct sched_param param;
param.sched_priority = 80;
/* 把进程设置为 SCHED_RR,实时优先级 80 */
if (sched_setscheduler(pid, SCHED_RR, &param) == -1) {
perror("sched_setscheduler");
}

sched_setattr() 是更现代、更通用的接口,支持设置 SCHED_DEADLINE 参数以及 SCHED_FLAG_RESET_ON_FORK(子进程 fork 后恢复默认策略)等标志。建议新代码优先使用 sched_setattr()

12.3 sched_setaffinity:CPU 亲和性控制

#define _GNU_SOURCE
#include <sched.h>
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(2, &set);  /* 绑定到 CPU 2 /
CPU_SET(3, &set);  / 以及 CPU 3 */
if (sched_setaffinity(0, sizeof(set), &set) == -1) {
perror("sched_setaffinity");
}

CPU 亲和性在多 NUMA 节点系统上非常有用:把频繁通信的一组线程绑定到同一节点的 CPU 上,可以显著降低跨节点内存访问延迟。但过度绑定也会限制调度器的负载均衡空间,需要谨慎权衡。

12.4 sched_yield:主动让出 CPU

#include <sched.h>
/* 主动放弃 CPU,回到就绪队列 */
sched_yield();

需要特别提醒:在现代调度器下,sched_yield() 往往不是解决性能问题的好办法。它会导致任务被放回队列,可能引发额外调度开销,甚至在某些负载下适得其反。遇到并发性能问题时,应优先考虑锁设计和数据结构优化,而不是滥用 yield。

12.5 getrusage:CPU 时间与切换次数统计

#include <sys/resource.h>
#include <stdio.h>
struct rusage usage;
getrusage(RUSAGE_SELF, &usage);
printf("用户态时间: %ld.%06ld 秒\n",
usage.ru_utime.tv_sec, usage.ru_utime.tv_usec);
printf("系统态时间: %ld.%06ld 秒\n",
usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);
printf("主动上下文切换: %ld\n", usage.ru_nvcsw);
printf("被动上下文切换: %ld\n", usage.ru_nivcsw);

13. 工具与命令:观察和调优调度行为

13.1 top / htop:日常观察首选

top 是最常用的性能观察工具。在 top 输出中与调度相关的关键列:

# 按 CPU 使用率排序显示前 20 个进程
top -o %CPU -n 1 | head -20
查看特定进程
top -p 1234
htop 提供更直观的 CPU 条形图
htop

13.2 ps:查看调度策略与优先级

# 查看所有进程的 pid、优先级、nice 值和调度类
ps -eo pid,pri,ni,cls,pcpu,stat,comm --sort=-pcpu | head -20
CLS 列说明:
TS  = SCHED_NORMAL(普通)
FF  = SCHED_FIFO
RR  = SCHED_RR
DLN = SCHED_DEADLINE
IDL = SCHED_IDLE
B   = SCHED_BATCH

13.3 chrt:设置实时调度策略

# 查看进程 1234 的调度策略
chrt -p 1234
以 SCHED_FIFO、优先级 80 启动命令
sudo chrt -f 80 ./my_realtime_app
以 SCHED_RR 启动
sudo chrt -r 90 ./my_realtime_app
以 SCHED_DEADLINE 启动(runtime=10ms, deadline=30ms, period=30ms)
sudo chrt -d --sched-runtime 10000000 
--sched-deadline 30000000
--sched-period 30000000
0 ./my_dl_app

13.4 taskset:设置 CPU 亲和性

# 查看进程 1234 的 CPU 亲和性掩码
taskset -p 1234
将进程绑定到 CPU 0 和 CPU 1
taskset -cp 0,1 1234
启动新进程并绑定到 CPU 0
taskset -c 0 ./my_app

13.5 /proc/pid/sched:调度统计信息

# 每个进程的详细调度统计
cat /proc/1234/sched
关键字段:
nr_switches            : 上下文切换总次数
nr_voluntary_switches  : 主动切换次数
nr_involuntary_switches: 被动切换次数
se.nr_migrations       : CPU 迁移次数
se.sum_exec_runtime    : 累计运行时间

13.6 perf sched:深入分析调度行为

# 记录调度事件(10 秒)
sudo perf sched record -- sleep 10
打印延迟分析报告
sudo perf sched latency
导出调度时间线脚本
sudo perf sched script > sched_trace.txt
查看每个任务的调度延迟统计
sudo perf sched latency -s max

perf sched latency 报告中的关键指标包括最大调度延迟(从就绪到真正运行的等待时间)、平均延迟和总运行时间。这些数据是评估调度器公平性和实时性的最直接依据。

13.7 其他实用工具

# 查看上下文切换率(cs 列)
vmstat 1
查看各 CPU 使用率
mpstat -P ALL 1
查看 NUMA 节点拓扑和内存分布
numactl --hardware
numastat
查看调度域拓扑
cat /proc/sys/kernel/sched_domain/cpu0/domain0/name

14. Cgroup 与 CPU 资源控制

14.1 为什么需要 cgroup

在容器化和云原生时代,一台物理机上可能运行着成百上千个容器。仅靠 nice 值已经不足以精细控制 CPU 资源分配——nice 只能表达"相对优先级",不能表达"绝对配额上限",也无法有效地对不同服务进行隔离。cgroup(control group)提供了按"组"分配 CPU 资源的能力,能够对一组进程统一设置权重配额、硬性上限和亲和性策略,是 Kubernetes、Docker 等平台的底层资源隔离基础。

14.2 cpu.weight:权重式分配

cgroup v2 中,cpu.weight 控制组内进程相对其他组的 CPU 权重,概念上类似 nice 值,但作用于整个组。默认权重为 100,范围为 1-10000。

# 创建 cgroup
sudo mkdir /sys/fs/cgroup/myapp
设置权重为 200(相对默认 100 获得更多 CPU)
echo 200 | sudo tee /sys/fs/cgroup/myapp/cpu.weight
将进程放入 cgroup
echo 1234 | sudo tee /sys/fs/cgroup/myapp/cgroup.procs

14.3 cpu.max:硬性 CPU 上限

cpu.max 可以硬性限制组的 CPU 使用上限,确保即使组内任务繁忙,也不会占用超过指定份额:

# 限制组最多使用 150% CPU(1.5 个核)
# 格式:允许时间(微秒) 周期(微秒)
echo "150000 100000" | sudo tee /sys/fs/cgroup/myapp/cpu.max

底层由 CFS 带宽控制(CFS Bandwidth Control)机制实现。当组超过配额时会被节流(throttled),其任务被暂停调度直到下一个周期。可以通过 cpu.stat 观察节流统计:

cat /sys/fs/cgroup/myapp/cpu.stat
# nr_throttled: 被节流的次数
# throttled_time: 总节流时间

14.4 cpuset:CPU 与 NUMA 绑定

sudo mkdir /sys/fs/cgroup/myapp
绑定到 CPU 2-3
echo "2-3" | sudo tee /sys/fs/cgroup/myapp/cpuset.cpus
绑定到 NUMA 节点 0
echo 0 | sudo tee /sys/fs/cgroup/myapp/cpuset.mems
放入进程
echo 1234 | sudo tee /sys/fs/cgroup/myapp/cgroup.procs

cpuset 适用于延迟敏感的服务:将服务绑定到专用 CPU 和本地 NUMA 节点,避免与其他负载竞争和跨节点访问。

15. 性能调优清单与常见误区

15.1 常见调度性能问题诊断

15.2 面向不同负载的调优策略

15.3 值得了解的内核调度参数

以下参数位于 /proc/sys/kernel/,调整前务必理解含义并做好验证:

15.4 常见误区提醒

16. 源码导读:从入口到调度核心

对于希望深入内核源码的读者,建议按以下路径阅读调度器实现。

16.1 核心源文件

16.2 关键函数调用链

以一次典型调度为例梳理核心路径:

理解这条主线后,再横向展开研究 enqueue_task_fair()(入队)、dequeue_task_fair()(出队)、load_balance()(负载均衡)、select_task_rq_fair()(任务放置)等分支,就能建立起完整的调度器认知框架。

17. 总结

Linux 进程调度是一个横跨数据结构、算法、硬件拓扑和工程权衡的宏大主题。本文从进程与调度实体的基本概念出发,梳理了 Linux 调度器从 O(n) 到 O(1)、从 CFS 到 EEVDF 的演进逻辑,深入解析了 vruntime 与权重的公平性原理、红黑树的高效实现、调度类与调度策略的分层设计、优先级体系、抢占机制、多核负载均衡,以及 EEVDF 为延迟敏感负载带来的改进。同时,我们还覆盖了实时调度、系统调用编程接口、cgroup 资源控制、实用观测工具和性能调优方法论。

需要特别强调的是:现代 Linux 调度器的默认配置在绝大多数场景下已经足够优秀。日常工作中,与其过早地手动调整调度参数,不如先用 toppsmpstatperf sched 等工具建立清晰的观测,先理解瓶颈究竟在 CPU 调度、IO 等待、锁竞争还是内存访问,再有针对性地实施调优。对调度器而言,观察先行、理解后改永远是更稳妥的工程实践。

随着 EEVDF 的落地和未来内核的持续演进,进程调度仍会围绕公平性延迟保证可扩展性这三条主线不断优化。掌握本文所介绍的核心概念与分析方法,将帮助你无论面对哪个内核版本,都能快速建立起对调度行为的基本判断,并在复杂的性能问题面前做出更明智的决策。

  1. 保存 A 的上下文:将 A 的 CPU 寄存器状态(程序计数器、栈指针、通用寄存器等)保存到 A 的内核栈中;
  2. 切换内核栈指针:将当前 CPU 的内核栈指针从 A 切换到 B;
  3. 切换地址空间:如果 A 和 B 属于不同进程,需要切换页表基址寄存器(如 x86 的 CR3),并可能需要刷新 TLB;
  4. 恢复 B 的上下文:从 B 的内核栈恢复其寄存器状态,从 B 上次被切换出去的位置继续执行。
    • 用户态抢占:当任务从内核态返回用户态的路径上检查抢占标志。这是最常见的抢占时机。
    • 内核态抢占:Linux 2.6 引入内核抢占支持后,在内核代码的特定安全点(如释放自旋锁后)也会检查抢占标志。内核通过 preempt_count 计数器标记临界区,只有计数为 0 时才允许抢占。
    • SMT 域:同一物理核心的超线程之间;
    • MC 域:同一封装内多个物理核心之间(共享 LLC 缓存);
    • DIE 域:同一物理封装上的多 die 之间;
    • NUMA 域:跨 NUMA 节点之间。
    • 优先迁移缓存热度低、可运行时间短的任务;
    • 遵循调度域层级,由近及远寻找空闲 CPU;
    • 避免频繁来回迁移(引入滞后阈值和统计窗口)。
    • Eligible(合格时间):任务何时变得"有资格"运行。如果任务还没有到达合格时间就运行,意味着它对其他任务"超前消费"。合格时间的推进速度与任务权重相关:权重越大的任务,合格时间推进越快,能够更频繁地获得运行资格。
    • Virtual Deadline(虚拟截止时间):在合格的前提下,根据任务权重计算出的"理想情况下应当在何时完成当前时间片"的时刻。
    1. 首先筛选出已合格(eligible)的任务集合;
    2. 在合格任务中,选择虚拟截止时间最早的那个任务运行。
    • sched_base_slice:基础时间片长度,默认约 3 毫秒。这是 EEVDF 计算虚拟截止时间的基准单位。
    • sched_eevdf_fast_placement:控制唤醒任务在红黑树中的放置策略,影响交互任务的响应速度。
    • SCHED_FIFO:静态优先级最高的任务独占 CPU,直到阻塞或主动让出;
    • SCHED_RR:在相同优先级的实时任务之间进行时间片轮转;
    • SCHED_DEADLINE:基于 EDF(Earliest Deadline First,最早截止时间优先)的现代实时调度。
    1. 主动调用 sched_yield() 让出 CPU;
    2. 进入睡眠(如等待 IO 完成);
    3. 被更高优先级的实时任务抢占。
    • Runtime(运行时间):每个周期内任务实际需要占用 CPU 的时间;
    • Period(周期):任务执行的时间周期;
    • Deadline(截止时间):每个周期内任务必须完成的时间点(通常等于或早于 period)。
    • /proc/sys/kernel/sched_rt_period_us:实时带宽周期,默认 1 秒(1000000 微秒);
    • /proc/sys/kernel/sched_rt_runtime_us:周期内允许实时任务运行的总时间,默认 0.95 秒(950000 微秒)。
    • PR:进程优先级。普通进程显示为 nice 值加 20;实时进程显示为负数形式的实时优先级(如 rt)。
    • NI:nice 值,范围为 -20 到 19。
    • %CPU:CPU 占用率,超过 100% 表示多核并行使用。
    • S:进程状态,R 运行、S 睡眠、D 不可中断睡眠、Z 僵尸。
    • 频繁上下文切换:大量线程反复被唤醒和睡眠,切换开销上升。用 vmstatcs 列观察,单核每秒超过 10000 次通常值得关注。
    • 负载不均衡:某些 CPU 长期满载而另一些空闲。用 mpstat -P ALLhtop 的 CPU 条形图快速定位。
    • 优先级反转:低优先级任务持锁阻塞高优先级任务,中间优先级任务不断抢占 CPU。内核通过优先级继承机制缓解,但用户态锁设计仍应注意。
    • 睡醒风暴(thundering herd):一个事件同时唤醒大量线程,引发短时间内的调度竞争。使用 epoll 的 EPOLLEXCLUSIVE 标志或合理的连接管理策略缓解。
    • 缓存抖动:任务被频繁迁移到不同 CPU,缓存内容不断失效。观察 /proc/pid/sched 中的迁移次数。
    • IO 密集型服务:适当降低 nice 值(提高优先级)、绑定合理数量的 CPU,避免与 CPU 密集型任务竞争。
    • CPU 密集型批处理:提高 nice 值(降低优先级)或使用 SCHED_BATCH/SCHED_IDLE,把 CPU 让给交互任务。
    • 延迟敏感的在线服务:通过 cpuset 绑定减少迁移,必要时使用 cgroup 预留 CPU 带宽,使用 CONFIG_PREEMPT 内核。
    • 多线程应用:根据 NUMA 拓扑规划线程布局,使用 numactl 观察内存节点亲和性。
    • sched_min_granularity_ns:最小调度粒度,影响上下文切换频率;
    • sched_latency_ns:调度周期,影响就绪任务轮转速度;
    • sched_migration_cost_ns:任务迁移代价估计,影响负载均衡激进程度;
    • sched_rt_runtime_us:实时任务带宽上限,默认 95%;
    • sched_autogroup_enabled:是否按会话自动分组,影响桌面系统交互体验。
    • 把 nice -20 当万能药:过度提高优先级不会突破物理 CPU 限制,反而可能让其他关键任务挨饿。
    • 滥用实时优先级:SCHED_FIFO 任务若写错循环会直接卡死整个系统,必须十分谨慎。
    • 随意关闭 NUMA balancing:某些场景虽然降低了页面迁移开销,但可能让内存远端访问变慢,需用真实负载验证。
    • 迷信 CPU 绑定:绑定可以改善局部性,但过强绑定会阻止调度器利用空闲核心进行负载均衡,可能适得其反。
    • 盲目调优调度参数:默认参数经过了上游开发者长时间测试和社区验证,在没有清晰瓶颈证据前保持默认是最安全的策略。
    • kernel/sched/core.c:调度器核心框架,包含 __schedule()、调度类注册和通用逻辑;
    • kernel/sched/fair.c:CFS/EEVDF 公平调度类实现,是代码量最大的部分;
    • kernel/sched/rt.c:SCHED_FIFO/RR 实时调度类实现;
    • kernel/sched/deadline.c:SCHED_DEADLINE 实现;
    • kernel/sched/topology.c:调度域与负载均衡拓扑构建;
    • kernel/sched/cpufreq.c:调度器与 CPU 频率调节的交互;
    • kernel/sched/loadavg.c:系统负载平均值计算。
    1. schedule():任务主动让出 CPU 或抢占触发时调用,最终进入 __schedule()
    2. __schedule():执行核心调度逻辑,调用 pick_next_task() 选择下一个任务;
    3. pick_next_task_fair():fair 类选择函数,内部调用 __pick_next_entity() 获取最左红黑树节点;
    4. context_switch():完成实际的状态保存、地址空间切换和寄存器恢复;
    5. task_tick_fair():时钟中断处理,更新 vruntime 并判断是否需要抢占。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐