news 2026/11/6 15:13:53

Linux CFS 调度器深度解析:调度时机、选任务与负载均衡

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux CFS 调度器深度解析:调度时机、选任务与负载均衡

1. 从一个真实场景说起:为什么你的服务器会突然卡顿

很多运维和开发同学都遇到过这种情况:一台跑着几十个服务的 Linux 机器,CPU 使用率看起来只有 40%,但业务响应时间却突然飙升,日志里出现大量超时。你登录上去用top一看,发现load average高得离谱,但 CPU 又没跑满。这时候大概率不是你的代码有问题,而是调度子系统在某个环节出现了瓶颈。

Linux 的进程调度器是整个操作系统的"交通警察",它决定了哪个进程什么时候上 CPU、跑多久、跑完了下一个该轮到谁。而 CFS(Completely Fair Scheduler,完全公平调度器)从 2007 年进入内核主线以来,一直是普通进程调度的默认选择。它的核心思想说起来很简单——让每个进程都能公平地分到 CPU 时间,但真正落地到代码层面,涉及调度时机、选任务逻辑、负载均衡三大块,每一块都有大量细节值得深挖。

这篇文章面向的是有一定 Linux 基础、想深入理解内核调度机制的开发者和运维人员。我会从 CFS 的设计思路讲起,把调度时机、选任务、负载均衡这三条主线拆开揉碎,配上内核源码里的关键结构和参数计算过程,最后给出实际排查调度问题的思路。读完你至少能搞清楚:为什么你的进程会被"饿死"、为什么 CPU 没跑满但延迟很高、以及sched_class这套分级机制到底是怎么工作的。

2. CFS 调度器的整体设计思路拆解

2.1 为什么需要"完全公平":从 O(1) 调度器说起

在 CFS 之前,Linux 用的是 O(1) 调度器。O(1) 的核心思路是给每个进程分配固定时间片,用两个优先级数组(活跃数组和过期数组)来管理,调度时从活跃数组最高优先级链表里取第一个进程。这个设计在常数时间内完成调度,效率很高,但问题也很明显:它依赖复杂的启发式规则来判断进程是"交互型"还是"CPU 密集型",这些启发式规则经常判断错误,导致交互式进程响应变慢。

CFS 的提出者 Ingo Molnar 换了一个思路:与其费劲去猜进程类型,不如直接模拟一个"理想的多任务 CPU"。这个理想 CPU 的特点是:每个进程能同时运行,各自拿到 1/n 的 CPU 时间(n 是进程数)。真实 CPU 一次只能跑一个进程,所以 CFS 用"虚拟运行时间"(vruntime)来记录每个进程已经跑了多久,每次选 vruntime 最小的那个进程上 CPU。这样跑得少的进程 vruntime 小,自然会被优先调度,公平性就得到了保证。

这个设计的美妙之处在于:它不需要任何启发式规则来判断进程类型,交互式进程因为经常睡眠,vruntime 增长慢,自然会被优先调度;CPU 密集型进程 vruntime 增长快,会被适当压制。一切都在数学框架内自然发生。

2.2 sched_class 分级机制:调度器不是只有一个

Linux 内核里调度器不是一个单一实体,而是一套分级结构,通过sched_class来组织。每个调度类有自己的优先级,内核在选任务时从最高优先级的调度类开始遍历,找到第一个有可运行任务的类,然后从该类里选任务。

当前内核里主要的调度类按优先级从高到低排列:

调度类用途典型场景
stop_sched_class停止 CPU,用于热插拔和迁移CPU 热插拔
dl_sched_class截止时间调度实时性要求极高的任务
rt_sched_class实时调度工业控制、音视频
fair_sched_classCFS 公平调度普通用户进程
idle_sched_class空闲调度CPU 没事干时

这个分级结构的意义在于:实时任务永远优先于普通任务,普通任务永远优先于空闲任务。CFS 属于fair_sched_class,它管理的是绝大多数普通进程。理解这个层级关系很重要,因为很多"进程被饿死"的问题,根源就是高优先级调度类里的任务一直占着 CPU,CFS 里的任务根本没机会跑。

2.3 调度实体与调度组:CFS 的基本管理单元

CFS 不直接管理进程,而是管理"调度实体"(sched_entity)。每个进程对应一个调度实体,但如果开启了组调度(CONFIG_FAIR_GROUP_SCHED),每个控制组(cgroup)也会对应一个调度实体。这样 CFS 就可以在组之间做公平调度,而不是在单个进程之间做公平调度。

这个设计解决了一个实际问题:假设用户 A 起了 100 个进程,用户 B 起了 1 个进程,如果按进程做公平调度,用户 B 的进程只能拿到 1/101 的 CPU,这显然不公平。按组调度的话,用户 A 和用户 B 各占一个组,各拿 50% 的 CPU,用户 A 的 100 个进程再分这 50%,每个进程拿 0.5%,用户 B 的进程拿 50%。这才是真正的公平。

调度实体的关键字段包括:

struct sched_entity { struct load_weight load; // 权重,决定分配比例 struct rb_node run_node; // 红黑树节点 u64 vruntime; // 虚拟运行时间 u64 sum_exec_runtime; // 实际运行时间 // ... 其他字段 };

load字段是权重,由进程的 nice 值映射而来。nice 值范围是 -20 到 19,nice 越小优先级越高,权重越大。内核用一张sched_prio_to_weight表做映射,nice 0 的权重是 1024,nice 每降低 1,权重增加约 25%。这个 25% 不是随便定的,它保证 nice 值相差 1 的两个进程,CPU 时间分配比例大约是 1.25:1。

3. 调度时机:什么时候会触发调度

3.1 主动调度:进程自己让出 CPU

主动调度是指进程自己调用schedule()让出 CPU。最常见的场景是进程等待某个资源时,比如等 I/O、等锁、等信号量。以等 I/O 为例,进程调用read()读文件,如果数据不在页缓存里,进程会被挂起,调用schedule()让出 CPU,等 I/O 完成后再被唤醒。

主动调度的入口是schedule()函数,它会调用__schedule()完成实际工作。__schedule()的核心流程是:

  1. 关闭内核抢占,获取当前 CPU 的rq(运行队列)
  2. 调用pick_next_task()选出下一个任务
  3. 如果下一个任务不是当前任务,调用context_switch()做上下文切换
  4. 重新开启内核抢占

这里有个细节值得注意:schedule()在调用时会检查preempt_count,如果处于原子上下文(比如持有自旋锁),调用schedule()会触发警告。因为原子上下文里不能睡眠,睡眠就意味着让出 CPU,这会导致死锁。

3.2 被动调度:内核抢占与时间片耗尽

被动调度是指进程没有主动让出 CPU,但内核强制把它换下来。触发条件主要有两个:时间片耗尽和内核抢占。

时间片耗尽比较好理解:CFS 给每个进程分配一个时间片,叫sched_slice,进程跑完这个时间片后会被标记为需要重新调度。时间片的计算方式是:

sched_slice = (sched_period * se->load.weight) / cfs_rq->load.weight

其中sched_period是调度周期,默认是sysctl_sched_latency(通常 6ms)和sysctl_sched_min_granularity * nr_running的较大值。sysctl_sched_min_granularity默认是 0.75ms,这个值保证每个进程至少能跑 0.75ms,避免频繁切换导致开销过大。

内核抢占是另一个触发点。当进程在内核态运行时,如果满足抢占条件(比如高优先级任务被唤醒),内核会设置TIF_NEED_RESCHED标志,在合适的时机(比如中断返回、系统调用返回)检查这个标志并触发调度。内核抢占的检查点包括:

  • 中断处理程序返回时
  • 系统调用返回用户态时
  • 显式调用preempt_enable()时

3.3 调度时机与延迟的权衡

调度时机的选择直接影响系统延迟和吞吐量。如果调度太频繁,上下文切换开销会吃掉大量 CPU;如果调度太少,交互式任务响应会变慢。

CFS 用两个参数来平衡这个矛盾:sysctl_sched_latency和sysctl_sched_min_granularity。前者是目标调度延迟,后者是最小抢占粒度。当可运行任务数nr_running超过sysctl_sched_latency / sysctl_sched_min_granularity时,调度周期会按nr_running * sysctl_sched_min_granularity来算,保证每个任务至少能跑sysctl_sched_min_granularity。

举个例子:假设sysctl_sched_latency是 6ms,sysctl_sched_min_granularity是 0.75ms,那么当nr_running超过 8 时,调度周期会变成nr_running * 0.75ms。如果有 16 个任务,调度周期就是 12ms,每个任务平均跑 0.75ms。这个设计保证了任务数很多时,单个任务不会因为时间片太短而频繁切换。

注意:sysctl_sched_latency和sysctl_sched_min_granularity可以通过/proc/sys/kernel/下的对应文件调整,但生产环境不建议随意改,除非你明确知道自己在做什么。改小了会增加切换开销,改大了会增加延迟。

4. 选任务:CFS 怎么挑下一个上 CPU 的进程

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

CFS 用红黑树来管理可运行的调度实体,按vruntime排序,最左边的节点就是vruntime最小的,也就是下一个该上 CPU 的进程。红黑树的好处是插入、删除、查找最左节点都是 O(log n) 复杂度,在任务数很多时性能依然稳定。

红黑树的节点是sched_entity里的run_node,每个 CPU 的运行队列cfs_rq里有一棵红黑树tasks_timeline。选任务时调用__pick_first_entity(),直接取红黑树最左节点:

static struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq) { struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline); if (!left) return NULL; return rb_entry(left, struct sched_entity, run_node); }

这里用了rb_first_cached,红黑树会缓存最左节点,避免每次都从根节点遍历到最左。这个优化在任务数很多时能省不少时间。

4.2 vruntime 的计算与更新

vruntime是 CFS 的核心,它的计算方式是:

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

其中delta_exec是本次实际运行时间,NICE_0_LOAD是 nice 0 的权重(1024),se->load.weight是当前进程的权重。这个公式的含义是:权重越大的进程,vruntime增长越慢,所以能跑更长时间;权重越小的进程,vruntime增长越快,会被更快换下来。

举个例子:进程 A 的 nice 是 0,权重 1024;进程 B 的 nice 是 5,权重 335。两个进程各跑了 1ms,A 的vruntime增加1 * 1024/1024 = 1ms,B 的vruntime增加1 * 1024/335 ≈ 3.06ms。所以 B 的vruntime增长更快,下次调度时 A 会被优先选中。这符合预期:nice 值高的进程优先级低,应该少跑。

vruntime的更新在update_curr()里完成,这个函数在每次时钟中断和调度时都会被调用。它还会更新cfs_rq的min_vruntime,这个值是新进程加入红黑树时的初始vruntime基准,保证新进程不会因为vruntime太小而抢占太多 CPU。

4.3 新进程的 vruntime 初始化:防止"新进程饥饿"

新进程创建时,vruntime不能直接设为 0,否则它会因为vruntime最小而一直抢占 CPU,导致老进程被饿死。CFS 的做法是把新进程的vruntime设为cfs_rq->min_vruntime,也就是当前红黑树里最小的vruntime。

但这里有个细节:如果新进程的vruntime直接等于min_vruntime,它可能会比红黑树里最左节点的vruntime还小,导致它插到最左边,立刻抢占 CPU。为了避免这个问题,CFS 会把新进程的vruntime设为min_vruntime + sched_vslice,其中sched_vslice是新进程一个时间片对应的虚拟时间。这样新进程的vruntime会略大于当前最小vruntime,不会立刻抢占 CPU,但也不会被饿死。

这个设计体现了 CFS 的公平性哲学:新进程有资格跑,但不能因为"新"就获得额外优势。它需要和其他进程一样,按vruntime排队。

4.4 唤醒抢占:交互式进程的快速响应

进程从睡眠中被唤醒时,CFS 会检查它是否应该抢占当前进程。检查逻辑在check_preempt_wakeup()里,核心是比较唤醒进程和当前进程的vruntime:

if (wakeup_preempt_entity(se, pse) == 1) { // 唤醒进程的 vruntime 比当前进程小很多,触发抢占 resched_curr(rq); }

wakeup_preempt_entity()的判断条件是:唤醒进程的vruntime加上一个补偿值(sysctl_sched_wakeup_granularity)后,仍然小于当前进程的vruntime。这个补偿值默认是 1ms,它的作用是避免频繁抢占:如果两个进程vruntime差不多,就不抢占,减少上下文切换开销。

这个机制对交互式进程很友好:交互式进程经常睡眠,vruntime增长慢,唤醒时vruntime通常比 CPU 密集型进程小,所以能快速抢占 CPU,保证响应速度。这也是为什么 CFS 在桌面和服务器场景下都能表现不错的原因。

5. 负载均衡:多核时代 CFS 的必修课

5.1 为什么需要负载均衡:单核调度不够用

CFS 的红黑树是 per-CPU 的,每个 CPU 有自己的运行队列。如果只做单核调度,会出现一个问题:CPU 0 上堆了 10 个任务,CPU 1 上只有 1 个任务,CPU 0 的任务要等很久才能跑一次,CPU 1 却大部分时间空闲。负载均衡就是解决这个问题的:把任务从忙的 CPU 迁移到闲的 CPU,让所有 CPU 的负载尽量均衡。

负载均衡的触发时机主要有三个:

  1. 周期性负载均衡:由scheduler_tick()触发,每隔一段时间检查一次
  2. 空闲负载均衡:CPU 要进入空闲状态时触发,试图从其他 CPU 拉任务过来
  3. 新任务唤醒负载均衡:新任务唤醒时,选择最合适的 CPU 运行

5.2 调度域与调度组:负载均衡的层级结构

负载均衡不是在所有 CPU 之间盲目迁移,而是按"调度域"(sched_domain)分层组织的。调度域是一个树形结构,最底层是 SMT 域(同一个物理核的超线程),往上是 MC 域(同一个物理 CPU 的多个核),再往上是 NUMA 域(同一个 NUMA 节点),最顶层是全局域。

每个调度域有自己的负载均衡策略和参数。比如 SMT 域里,超线程之间的迁移成本很低,可以频繁迁移;NUMA 域里,跨节点迁移成本很高,要尽量避免。这个分层结构保证了负载均衡既高效又不会引入过多迁移开销。

调度域里的每个 CPU 属于一个"调度组"(sched_group),负载均衡在调度组之间进行。每个调度组有一个sg_lb_stats结构,记录组内的负载、任务数、CPU 容量等信息。负载均衡时,内核会比较各个调度组的负载,从最忙的组里拉任务到最闲的组。

5.3 负载计算:不是简单数任务个数

负载均衡的核心是"负载"的计算。早期内核用任务数作为负载,但这样不准确:一个 nice 0 的任务和一个 nice 19 的任务,对 CPU 的压力完全不同。现在内核用"负载权重"(load.weight)来算负载,nice 0 的任务权重 1024,nice 19 的任务权重 15,前者对负载的贡献是后者的 68 倍。

但光有负载权重还不够,还要考虑 CPU 的"容量"(capacity)。不同 CPU 的算力可能不同,比如大小核架构里,大核容量是 1024,小核容量可能只有 512。负载均衡时要把负载和容量一起考虑,用load / capacity作为实际压力指标。

负载计算还涉及"负载跟踪"(load tracking)机制。早期内核用瞬时负载,但瞬时负载波动太大,容易导致误迁移。现在内核用 PELT(Per-Entity Load Tracking)算法,对负载做指数衰减平均,得到更平滑的负载值。PELT 的核心公式是:

load_avg = load_avg * decay + new_load * (1 - decay)

其中decay是衰减因子,由时间差决定。这个算法能同时反映历史负载和当前负载,避免因为瞬时波动做出错误迁移决策。

5.4 迁移决策:什么时候该搬,什么时候不该搬

负载均衡的迁移决策要考虑两个因素:不均衡程度和迁移成本。不均衡程度用imbalance表示,是忙 CPU 负载和闲 CPU 负载的差值。迁移成本包括缓存失效、TLB 刷新、NUMA 远程访问延迟等。

内核用sysctl_sched_migration_cost参数来估算迁移成本,默认是 500us。如果任务在 CPU 上运行时间不到 500us,认为它还在缓存热区,迁移成本高,不迁移;如果运行时间超过 500us,缓存已经冷了,迁移成本低,可以迁移。

迁移决策的流程大致是:

  1. 计算当前 CPU 和候选 CPU 的负载差
  2. 如果负载差小于sysctl_sched_nr_migrate对应的阈值,不迁移
  3. 如果负载差足够大,尝试从忙 CPU 拉任务
  4. 拉任务时优先选"缓存冷"的任务,避免迁移热任务

这里有个经验:sysctl_sched_migration_cost和sysctl_sched_nr_migrate这两个参数对负载均衡效果影响很大。如果发现任务迁移太频繁,可以适当调大sysctl_sched_migration_cost;如果发现负载均衡不及时,可以适当调小。但生产环境调整要谨慎,最好先在测试环境验证。

6. 实操排查:调度问题的诊断思路与工具

6.1 常用工具与观测指标

排查调度问题,首先要能观测。常用的工具和指标包括:

工具用途关键指标
top / htop查看 CPU 使用率和负载load average, %CPU
vmstat查看上下文切换和运行队列cs, r, b
pidstat查看进程级调度统计%CPU, cswch/s, nvcswch/s
perf sched分析调度延迟和迁移sched:sched_switch
/proc/sched_debug查看 CFS 运行队列详情vruntime, nr_running

/proc/sched_debug是排查 CFS 问题的利器,它能显示每个 CPU 的cfs_rq信息,包括nr_running、min_vruntime、红黑树里每个调度实体的vruntime和load。通过对比不同 CPU 的nr_running和min_vruntime,能快速判断负载是否均衡。

6.2 典型问题一:CPU 没跑满但延迟高

这个问题的典型表现是top里 CPU 使用率不高,但业务延迟很高,vmstat里r列(运行队列长度)很大。原因通常是任务在等 CPU,但 CPU 又被某些任务占着不放。

排查思路:

  1. 用pidstat -w看上下文切换次数,如果nvcswch/s(非自愿切换)很高,说明任务被强制换下,可能是时间片太短
  2. 用/proc/sched_debug看nr_running,如果某个 CPU 的nr_running远大于其他 CPU,说明负载不均衡
  3. 用perf sched record抓一段调度事件,用perf sched latency看调度延迟分布

如果确认是负载不均衡,可以检查sysctl_sched_migration_cost是否设得太大,导致任务不愿意迁移。也可以检查是否有 CPU 亲和性设置(taskset或cgroup cpuset)把任务绑死在某个 CPU 上。

6.3 典型问题二:进程被"饿死"

进程被饿死的表现是某个进程长时间得不到 CPU,top里%CPU一直是 0,但进程状态是 R(可运行)。原因可能是:

  1. 高优先级调度类里有任务一直占着 CPU,比如实时任务
  2. 进程的 nice 值太高,权重太低,抢不过其他进程
  3. 进程被 cgroup 限制了 CPU 配额

排查思路:

  1. 用chrt -p <pid>看进程的调度策略,如果是 SCHED_FIFO 或 SCHED_RR,说明是实时任务,会一直占 CPU 直到自己让出
  2. 用cat /proc/<pid>/stat看 nice 值,如果 nice 是 19,权重只有 15,确实抢不过 nice 0 的进程
  3. 用cat /sys/fs/cgroup/cpu/<group>/cpu.cfs_quota_us看 cgroup 的 CPU 配额,如果是 -1 表示不限制,如果是具体数值,说明有配额限制

如果是实时任务导致的,可以考虑用chrt调整实时任务的优先级,或者用cgroup限制实时任务的 CPU 使用。如果是 nice 值问题,可以用renice调整。如果是 cgroup 配额问题,可以调整cpu.cfs_quota_us和cpu.cfs_period_us。

6.4 典型问题三:上下文切换过多

上下文切换过多会导致 CPU 大量时间花在切换上,而不是干活。vmstat里cs列很高(比如超过 10 万),pidstat -w里cswch/s很高,都说明切换频繁。

原因可能是:

  1. 任务数太多,每个任务时间片太短
  2. 锁竞争激烈,任务频繁睡眠和唤醒
  3. 中断太多,每次中断都可能触发调度

排查思路:

  1. 用perf stat -e context-switches看切换次数
  2. 用perf record -e sched:sched_switch抓切换事件,看是哪些任务在频繁切换
  3. 用perf trace看系统调用和中断频率

如果是任务数太多,可以考虑合并任务或减少并发度。如果是锁竞争,可以用perf lock分析锁热点。如果是中断太多,可以检查网卡多队列、中断亲和性等配置。

提示:调整调度参数前,一定要先备份当前值,并在测试环境验证。生产环境直接改参数可能导致更严重的问题。比如把sysctl_sched_latency调得太小,会导致上下文切换暴增;调得太大,会导致交互式任务响应变慢。

7. 参数调优与内核编译选项

7.1 关键 sysctl 参数速查

CFS 的行为受多个 sysctl 参数控制,这些参数在/proc/sys/kernel/下:

参数默认值作用调整建议
sched_latency_ns6ms目标调度延迟交互式场景可调小,吞吐场景可调大
sched_min_granularity_ns0.75ms最小抢占粒度一般不动,任务数多时可适当调大
sched_wakeup_granularity_ns1ms唤醒抢占补偿交互式场景可调小,减少唤醒延迟
sched_migration_cost_ns500us迁移成本估算迁移频繁时调大,负载不均时调小
sched_nr_migrate32单次迁移最大任务数一般不动,大核数机器可适当调大
sched_tunable_scaling1参数自动缩放一般不动

这些参数的调整要结合具体场景。比如跑数据库的服务器,更看重吞吐量,可以适当调大sched_latency_ns,减少切换开销;跑 Web 服务的服务器,更看重响应延迟,可以适当调小sched_latency_ns和sched_wakeup_granularity_ns。

7.2 内核编译选项:组调度与自动调度组

CFS 有两个重要的编译选项:

  • CONFIG_FAIR_GROUP_SCHED:开启组调度,让 cgroup 之间公平分配 CPU
  • CONFIG_SCHED_AUTOGROUP:开启自动调度组,自动为每个 TTY 会话创建调度组

CONFIG_FAIR_GROUP_SCHED对桌面和服务器都很重要。桌面场景下,它能保证前台应用和后台任务公平分 CPU;服务器场景下,它能保证不同 cgroup 之间的公平性。CONFIG_SCHED_AUTOGROUP主要面向桌面,服务器一般不开,因为服务器通常用 cgroup 手动管理。

这两个选项在主流发行版的内核里默认都是开的。如果你自己编译内核,建议保持开启,除非有特殊需求。

7.3 实际调优案例:从延迟 200ms 到 20ms

我之前遇到过一个案例:一台跑着 Web 服务的机器,P99 延迟一直在 200ms 左右,但 CPU 使用率只有 30%。用perf sched latency分析后发现,调度延迟(从任务唤醒到实际运行的时间)平均 50ms,P99 达到 180ms。

排查过程:

  1. 用/proc/sched_debug看nr_running,发现某些 CPU 的nr_running达到 20,而其他 CPU 只有 2-3
  2. 用perf sched record抓调度事件,发现任务迁移很少
  3. 检查sysctl_sched_migration_cost_ns,发现是默认值 500us
  4. 检查 CPU 亲和性,发现 Web 服务进程被绑在了 CPU 0-3 上,但机器有 16 个核

问题根源是 CPU 亲和性设置不合理,导致任务集中在 4 个核上,其他 12 个核空闲。调整亲和性后,nr_running分布均匀了,调度延迟降到 20ms 以下。

这个案例说明:调度问题不一定是调度器本身的问题,很多时候是配置问题。排查时要先看全局,再看局部,不要一上来就调调度参数。

8. 几个容易踩的坑与经验总结

8.1 坑一:把 nice 值当成绝对优先级

很多同学以为 nice 值决定了进程的绝对优先级,nice 0 一定比 nice 5 先跑。实际上 CFS 是按vruntime调度的,nice 值只影响vruntime的增长速度。如果 nice 5 的进程睡眠了很久,vruntime很小,它照样能抢占 nice 0 的进程。

这个特性对交互式进程有利,但对某些场景可能造成困扰。比如你希望某个批处理任务严格让位于在线任务,光靠 nice 值不够,还需要用 cgroup 的 CPU 配额或者实时调度策略。

8.2 坑二:忽略 cgroup 的 CPU 限制

cgroup 的 CPU 子系统可以限制一个组的 CPU 使用。如果某个服务被限制了cpu.cfs_quota_us,即使系统整体 CPU 空闲,这个服务也跑不快。排查调度问题时,一定要检查 cgroup 配置。

cpu.cfs_quota_us和cpu.cfs_period_us的比值决定了 CPU 上限。比如quota_us是 50000,period_us是 100000,表示这个组最多用 50% 的 CPU。如果quota_us是 -1,表示不限制。

8.3 坑三:实时任务把系统跑死

实时任务(SCHED_FIFO / SCHED_RR)优先级高于 CFS,如果实时任务进入死循环,会一直占着 CPU,导致 CFS 里的任务完全跑不了。更严重的是,如果实时任务占满了所有 CPU,连内核线程都跑不了,系统会看起来像死机。

内核有个保护机制叫 RT throttling,默认给实时任务 95% 的 CPU 带宽,留 5% 给 CFS。这个机制可以通过/proc/sys/kernel/sched_rt_runtime_us调整。如果发现实时任务导致系统卡顿,可以检查这个参数是否被改成了 -1(表示不限制)。

8.4 经验:观测先行,调整在后

调度调优最忌讳的就是"拍脑袋调参数"。我见过太多人一遇到延迟高就调sched_latency_ns,结果越调越糟。正确的做法是:

  1. 先用工具观测,确认问题确实出在调度上
  2. 分析是调度时机、选任务还是负载均衡的问题
  3. 针对具体问题调整对应参数
  4. 调整后继续观测,验证效果

调度器是一个复杂的系统,参数之间相互影响。调一个参数可能影响其他行为,所以每次只调一个参数,观察一段时间再决定下一步。

8.5 经验:理解 sched_class 的优先级关系

很多调度问题的根源是调度类优先级。实时任务永远优先于 CFS 任务,CFS 任务永远优先于 idle 任务。如果你发现 CFS 任务跑不起来,先检查有没有实时任务在占 CPU;如果实时任务也跑不起来,检查有没有 stop 类任务(比如 CPU 热插拔)在运行。

理解这个层级关系,能帮你快速定位问题在哪一层。/proc/sched_debug里会显示每个 CPU 上各个调度类的运行队列情况,是排查这类问题的好帮手。

9. 从源码到实践:进一步深入的方向

如果你想把 CFS 吃透,光看文章不够,还得动手。建议从这几个方向深入:

第一,读源码。CFS 的核心代码在kernel/sched/fair.c,这是个大文件,建议按功能分块读:先读update_curr()理解vruntime更新,再读pick_next_task_fair()理解选任务,最后读load_balance()理解负载均衡。读的时候配合trace_printk或者perf probe加断点,观察实际运行时的变量值。

第二,做实验。用cgroup创建不同权重的组,跑 CPU 密集型任务,观察 CPU 分配比例是否符合权重比。用taskset绑定 CPU,观察负载均衡行为。用chrt创建实时任务,观察 RT throttling 的效果。实验能帮你把理论知识和实际行为对应起来。

第三,用工具分析。perf sched是分析调度行为的利器,perf sched record抓事件,perf sched latency看延迟,perf sched map看任务在 CPU 间的迁移。/proc/sched_debug和/proc/schedstat能提供运行队列的详细信息。把这些工具用熟,排查调度问题会快很多。

第四,关注内核社区。CFS 不是一成不变的,内核社区一直在优化。比如最近几年引入的 EEVDF(Earliest Eligible Virtual Deadline First)调度器,就是对 CFS 的改进,用虚拟截止时间代替单纯的vruntime排序,进一步提升了公平性和延迟表现。关注这些变化,能让你对调度的理解保持更新。

调度子系统的学习曲线比较陡,但一旦理解了核心机制,很多看似玄学的问题都会变得有迹可循。我在实际工作中最大的体会是:不要怕看源码,也不要怕做实验,调度器的行为是确定的,只要观测到位,总能找到原因。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 23:25:11

Linux内核hibernation流程解析:从内存快照到ACPI S4恢复

这篇梳理一下Linux内核的hibernation流程&#xff0c;也就是常说的“挂起到磁盘”&#xff08;suspend-to-disk&#xff09;。很多人把resume、suspend、hibernate混着叫&#xff0c;实际在内核里这是完全不同的执行路径&#xff0c;涉及进程冻结、内存快照、swap写入、ACPI S4…

作者头像 李华
网站建设 2026/11/6 1:42:54

卷积神经网络内存与计算量解析:参数量、MACs与运行时内存

1. 一个让无数人困惑的经典问题模型文件明明只有几十兆&#xff0c;甚至几兆&#xff0c;为什么一加载到内存里&#xff0c;占用就翻了好几倍&#xff1f;更离谱的是&#xff0c;跑一次推理&#xff0c;内存曲线直接飙到几个G&#xff0c;显存也跟着炸。这个问题我在刚接触深度…

作者头像 李华
网站建设 2026/11/6 1:45:11

教培机构系统切换:历史数据迁移的四个风险点与分批切换方案

一、问题场景 换系统最怕的不是学习成本&#xff0c;是数据。 学员档案、缴费记录、欠费明细、课程进度——这些数据一旦在迁移中出错&#xff0c;对账会成为长期麻烦。更麻烦的是&#xff0c;错误往往不是当场暴露的&#xff0c;而是在几个月后某个学员来问"我明明交过费…

作者头像 李华
网站建设 2026/10/23 20:04:31

2026年8款优质CRM系统推荐,经过市场验证的好用软件

2026年&#xff0c;CRM市场继续保持稳健增长。根据Grand View Research数据&#xff0c;2024年全球CRM市场规模达734亿美元&#xff0c;预计2030年将增长至1631.6亿美元&#xff0c;2025至2030年复合年增长率约14.6%。AI技术的深度融入正在重塑CRM的产品形态——Gartner预测&am…

作者头像 李华
网站建设 2026/10/19 14:07:16

支持多语种的AI配音工具哪个好用?2026年创作者选型参考

做多语种内容&#xff0c;配音工具先盯三件事。第一是语种覆盖是否真全&#xff0c;能不能覆盖你实际要投放的市场。第二是语音自然度&#xff0c;尤其是小语种的情感和停顿是否自然。第三是商用授权与版权条款是否清晰&#xff0c;避免后续侵权风险。按这几条去比&#xff0c;…

作者头像 李华