新闻详情

新闻详情

首页 / 资讯中心 / 详情

Linux CFS调度器深度解析:vruntime、红黑树与CPU延迟调优实战

发布时间:2026/9/28 8:38:25来源:尧图网络
Linux CFS调度器深度解析:vruntime、红黑树与CPU延迟调优实战
最近帮一个客户排查多任务服务器上的 CPU 延迟抖动问题那台机器同时跑着流媒体转码和 Web API 网关CPU 明明没有超卖资源看起来也够可网关的 p99 延迟偶尔还是会冲到几百毫秒。折腾到最后问题既不在网络也不在应用层 GC而在 Linux 内核的 CPU 调度器这一层——CFS 默认的公平时间分配机制在后台批处理任务没有合理控制调度粒度的场景下照样能把延迟敏感进程的时间挤没。CFSCompletely Fair Scheduler完全公平调度器自 2.6.23 起就作为 Linux 内核默认的 CPU 调度机制一直稳定服役到今天。这篇文章我想从一个内核开发者的视角把 CFS 的来龙去脉、关键数据结构、数学原理和实战调优讲透。无论你是正在读内核源码、排查性能抖动还是面试前想系统地理解调度器这篇都能给你一条完整且清晰的路径。1. 从 O(1) 调度器到 CFS一次调度思路的彻底转向1.1 O(1) 调度器的时间片分层逻辑要说清楚 CFS 解决了什么就得先看它在 2.6.23 之前替代的 O(1) 调度器干了什么。O(1) 这个名字是相对于 2.4 时代的 O(n) 调度器而言的因为它在调度决策时无论系统里有多少进程耗时都是常数级。它的设计核心是active / expired 双优先级数组每个 CPU 维护两个大数组每个优先级一个队列调度时直接从 active 数组里找到最高优先级的非空队列取第一个任务运行时间片耗尽后就把它挪到 expired 数组里active 数组空了就把两个数组交换。这个机制的问题不少。最明显的是时间片的分配方式普通进程按 nice 值-20 到 19映射出一套时间片优先级高的进程分得多优先级低的分得少而且是固定的“长度”概念。交互式进程优先级往往不高但它的特点是“短时间醒来、快速消费、然后继续睡”O(1) 给它的时间片哪怕不小它也只有醒来那一下能跑等它再想跑的时候可能已经排在长队列后面了。所以 O(1) 时代桌面系统里视频解码和后台编译抢 CPU 会非常明显鼠标拖影、窗口卡顿是家常便饭。另一个问题是用“优先级修正”来处理交互性调度器会统计进程睡眠时长如果经常睡眠就认为它是交互进程偷偷抬高它的优先级。这在复杂负载下很容易误判后台数据库的 I/O 等待线程也会被当成交互进程最后把前端交互任务的 CPU 时间抢走大半。1.2 CFS 用“虚拟时钟”解决公平性问题的思路CFS 的思路和上述完全不沾边它不画二维优先级表也不区分“交互进程”和“批处理进程”。它只回答一个问题每个进程应该获得 CPU 时间中的多大比例。这个比例由权重决定交互性的好坏通过“睡眠补补偿 唤醒抢占”这套机制去体现而不是靠猜进程类型。CFS 最核心的抽象是虚拟运行时间也就是 vruntime。每个调度实体sched_entity都维护一个 vruntime调度器用红黑树把所有可运行实体按 vruntime 排序每次总是挑 vruntime 最小的那个进程去运行。为什么这样是“公平”可以类比分蛋糕——假设两个人胃口不同一个食量大一个食量小食量大的人每次吃得多、vruntime 涨得快食量小的人每单位物理时间只涨一点 vruntime那么系统经过几轮分配后两人的 vruntime 会逐渐对齐到同一水平线。谁落后了谁就有资格多获得一些 CPU 时间。这个“公平”的单位不是物理时间而是按权重归一化后的虚拟时间权重越大单位时间内 vruntime 涨得越慢实际获得的 CPU 比例就越高。这套思路可以说把调度器从“管理者分配固定配额”变成了“按需收敛的闭环反馈系统”不需要任何外部干预系统天生就向公平点收敛。O(1) 那种靠预分时间片然后强制剥夺的方式在交互场景下天然吃亏而 CFS 在大多数轻载和中载场景下交互延迟都控制得更好。2. vruntime 的数学底细调度周期与权重的拆解2.1 最核心的公式vruntime 增量如何计算CFS 的 vruntime 不是凭空加的它的更新逻辑是vruntime delta_exec * NICE_0_LOAD / weight其中delta_exec是本次实际运行的物理时间NICE_0_LOAD是 nice 0 对应的权重默认 1024weight是当前进程自己的权重。所以权重为 1024 的 nice 0 进程跑多少物理时间vruntime 就加多少权重小于 1024 的 nice 正值进程跑同样的物理时间vruntime 加得快权重高于 1024 的 nice 负值进程vruntime 加得慢。举个实际例子。假设系统里只有两个 CPU 密集型进程 A 和 BA 的 nice 值是 0权重 1024B 的 nice 值是 5权重约 335。两个进程都很饥饿那么 CFS 给它们分配 CPU 时间的比例大约是A 获得的比例 1024 / (1024 335) ≈ 75.3% B 获得的比例 335 / (1024 335) ≈ 24.7%A 跑 100ms 物理时间vruntime 增加约 100msB 跑 100ms 物理时间vruntime 增加约 305ms。所以调度器观测到的是两者比较虚拟时间而不是物理时间。跑一段时间后A 的 vruntime 和 B 的 vruntime 会在某个动态平衡点上不断交错这个平衡点对应的物理时间比例恰好就是权重比例。这个公式是整个 CFS 的基石理解它之后再去看内核源码里那些update_curr、calc_delta_fair函数就不会迷路。update_curr做的事就是拿当前时间和上次调度点相减得到delta_exec然后代入上面的公式更新当前任务的 vruntime同时更新 cfs_rq 的min_vruntime。2.2 nice 值与权重映射的关系权重从哪来内核维护了一张长度为 40 的表把 nice 值 -20 到 19 映射成一组权重值。这张表的关键部分是nice 值权重-2088761-109549-533550102453351011015361915有规律吗有的。nice 值每降低 1权重大约乘以 1.25nice 值每增加 1权重大约除以 1.25。比如 nice 0 的 1024 除以 1.25 等于 819.2内核取整成了 820也就是 nice 1 的权重。为什么要取 1.25 这个基数历史原因是 nice 语义设计时希望每差 1 级CPU 时间占用差约 10%实际是 1.25 倍对应 20% 左右的 CPU 比例差“每降 1 级多 10% CPU” 的旧界被进一步精确化。注意这张表是为了在数学上用整数运算模拟浮点指数关系所以相邻项并不严格是 1.25 倍但整体趋势一致。从这张表还能看出一个实用规律nice 值差分得太细实际效果增长越来越不明显。nice 0 到 nice 5权重差了大约 3 倍对 CPU 密集任务来说效果立竿见影但从 nice 15 到 nice 19权重差只有 2.4 倍绝对值都很小调度器对这些低权重进程的照顾程度差别已经不大。所以在实际调优时没必要把某个进程拉到 nice -20 那样的极端值多数情况下 nice 差 5 级以内就能解决问题尤其不要轻易给核心进程设负优先级——它会挤占所有普通任务包括系统管理用的进程。2.3 调度周期与“延迟”边界CFS 里还有个和 vruntime 强相关的参数调度周期sched_period。它刻画的是“CPS 保证所有可运行任务至少在它对应的周期内各运行一次”的时间窗口。内核里维护了sysctl_sched_latency默认 6ms和sysctl_sched_min_granularity默认 0.75ms。调度周期计算逻辑大致是period sched_latency_ns if (nr_running * min_granularity period) period nr_running * min_granularity也就是说当运行队列里的任务数不多时调度周期就是固定的 6ms任务非常多时周期自动拉长保证每个任务至少能运行一个 min_granularity。比如 100 个任务period 就被拉长到 75ms每个任务转一圈的时间变长但至少都有 0.75ms 的 CPU 时间。系统中立地看CFS 的目标并不是“每个任务每毫秒都被执行”而是“每个任务在每个调度周期内获得它应得的 CPU 比例”。响应延迟的改善靠的是“让没有用完自己 vruntime 配额的进程尽快获得 CPU”低频等待任务在唤醒时会带着很小的 vruntime 进入红黑树自然会插到最左边从而快速被选中。这也是为什么 CFS 不需要专门猜谁是交互进程——等待时间长、需求量小的进程天然 vruntime 落后天然优先。3. 红黑树与调度栈CFS 一次调度决策的完整路径3.1 调度实体与调度类CFS 在调度器中的位置CFS 不是孤立的一套逻辑它由sched_class这个抽象接口接入 Linux 的调度框架。Linux 内核按优先级维护了五个调度类调度类实现文件说明stop_sched_classsched_stop.c最高优先级CPU 停机和自旋等待迁移任务用dl_sched_classsched_deadline.c实时截止时间调度rt_sched_classsched_rt.c实时进程调度fair_sched_classsched_fair.cCFS普通时间片任务idle_sched_classsched_idle.c空闲任务调度器在挑选下一个任务时从 stop 到 idle 逐级询问每个调度类“你有没有可运行的任务”第一个答复有的类负责产出任务。普通进程默认进入 fair 类即 CFS。CFS 管理的对象是调度实体sched_entity它并不是 task_struct 本身task_struct 里内嵌了一个 sched_entity通常叫 se。组调度模式下每个 cgroup 也对应一个 se可以嵌套——一个 se 可以是一个任务也可以代表一个子调度实体队列。sched_entity的关键字段很简单struct sched_entity { struct load_weight load; /* 权重 */ struct rb_node run_node; /* 红黑树节点 */ struct list_head group_node; unsigned int on_rq; /* 是否在运行队列 */ u64 vruntime; /* 虚拟运行时间 */ u64 prev_sum_exec_runtime; ... };3.2 红黑树的键值与操作开销为什么选红黑树而不是简单的数组或者链表因为 CFS 的核心操作有两个频繁插入/删除新唤醒的任务频繁取“vruntime 最小”的任务。链表取最小点是 O(n)数组插入要移动大量元素都不适合。红黑树是自平衡二叉搜索树插入和删除都是 O(log n)而且 CFS 缓存了红黑树的最左节点指针leftmost直接指向最小 vruntime 的节点这样取下一个任务的复杂度是 O(1)。红黑树里节点的排序键是 se-vruntime 与 cfs_rq-min_vruntime 的差或者说比较两个 vruntime 的先后。为了避免 vruntime 数值无限增长内核这里有个非常容易忽略的细节min_vruntime会单调递增新入队的进程如果 vruntime 太老比如刚从睡眠中醒来系统会做钳制把它拉到不小于 min_vruntime 减去某个阈值的水平。这个机制叫“vruntime 规范化/钳制”防止一个长时间睡眠的进程醒来时 vruntime 远小于所有运行进程从而独占 CPU 很久。从读源码角度红黑树操作集中在kernel/sched/fair.c的__enqueue_entity和__dequeue_entity。__enqueue_entity调用rb_add_augmented_cached通过leftmost缓存保证插入后如果有更小的 vruntime 节点会更新最左指针。__dequeue_entity会检查被删的是不是当前最左节点如果是就更新leftmost为后继节点。3.3 一次时钟中断引发的调度决策链把 CFS 放到调度主链路里看一次典型调度决策是这样发生的硬件定时器触发时钟中断进入内核最后走到scheduler_tick()scheduler_tick找到当前 CPU 上的运行队列的 fair 类调用task_tick_fair()task_tick_fair判断当前进程是否运行超过理想配额如果超过设置TIF_NEED_RESCHED标志中断返回用户态或内核态时检查TIF_NEED_RESCHED触发schedule()schedule()调用__schedule()其中核心两步是pick_next_task_fair()和put_prev_task()严格说一个新的调度周期会先 dequeue 再 pick但大体类似pick_next_task_fair从当前 cfs_rq 的红黑树拿到最左节点取它的 task_struct完成 context switch。除了时钟中断还有一类是唤醒抢占wakeup preemption。当进程 A 唤醒进程 B 时调度器会计算 B 的 vruntime如果 B 的 vruntime 比当前进程小并且差值超过sched_wakeup_granularity_ns就会触发抢占设置当前进程的TIF_NEED_RESCHED。这套机制让刚醒来的、有交互性质的进程能很快上 CPU而不是排到队尾。内核 5.x 里这个开关由sched_feat WAKEUP_PREEMPTION控制。如果你在 perf 里看到定时器中频繁发生调度切换可以沿着这条链去抓sched_switch事件结合sched_wakeup事件判断每次调度是被谁触发的。实际排查中我经常用 bpftrace 挂sched_switch的 tracepoint直接打印 prev_comm/next_comm先看哪些进程在互相切换再结合 vruntime 分析谁抢谁。4. 多核伸缩、组调度与负载均衡的真实挑战4.1 从单核公平到多核均衡调度域的作用CFS 的单核逻辑只维护一个运行队列但现代服务器都是几十核起步不可能所有进程都放进一个队列里全局排序——那会有巨大的锁竞争和缓存失效。所以内核的做法是每个 CPU 都有自己的运行队列cfs_rqCFS 在每个队列上独立选下一个进程。跨 CPU 之间的公平由负载均衡器负责。负载均衡器按调度域sched domain分层组织 CPU从 SMT 域、MC 域一个物理 CPU 内的多个核心、NUMA 域逐层向上。不同域层级有着不同距离的“迁移代价”越往上层迁移代价越高。内核会定期检查各 CPU 的运行队列负载把负载高的一侧任务迁移到负载低的一侧。“负载”在现代内核里用的是 PELT即 Per-Entity Load Tracking每个调度实体维护一个基于半衰期衰减的负载均值。它更新时按 1ms 粒度历史负载以约 32ms 的半衰期衰减。注意 PELT 的“负载估算”和 vruntime 不是一回事vruntime 管公平顺序PELT 管的是迁移决策。多核 CFS 有个经典问题就是“全局怎么看都均衡、局部却延迟高”。一个进程被迁移到新 CPU 之后它的vruntime会和目标 CPU 的min_vruntime做一次归一化校准避免因为两 CPU 调度进度不同导致新 CPU 上的进程永远被“老居民”压制。这个细节在阅读kernel/sched/fair.c的migrate_task_rq_fair时能看到。4.2 用 nice 调不够了就要靠 cgroup 组调度单机场景下 nice 值可以手工调但真实服务器上会有几十上百个进程而且分属不同业务方。比如一台机器同时跑 Nginx、Java 微服务、日志采集 Agent 和凌晨的批处理任务给每个进程调 nice 是非常脆弱的方案——进程起来就没了运维根本追不上。这时候要用 CFS 的组调度能力也就是 cgroup 的 cpu 子系统。你可以把一个业务组的所有进程放进同一个 cgroup给这个 cgroup 设置cpu.shares组内的进程再算各自权重。组调度不会管组内用不用得完配额但它会保证“按权重比例公平分配组间 CPU”组内谁饥饿谁先跑。比较典型的是把 Web 组和批处理组各自建 cgroupWeb 共享 70% 的 CPU 份额批处理共享 30%。如果 Web 组负载低批处理仍然可以用闲置的 CPU不会白费算力。这比按进程设 nice 粒度粗糙但极其省心。从数据结构上看组调度就是让 cfs_rq 挂到一个更高层次的队列里去参与竞争。task_group结构体里有自己的 cfs_rq 和 se层层嵌套公平性在每一层递归下去。初学的时候建议从扁平结构开始理解组嵌套时最上层公平影响比组内优先级影响更大先设好组间权重再考虑组内 nice。4.3 睡眠进程与唤醒抢占的“补偿”机制CFS 对睡眠进程有一种隐含的“补偿效果”。进程睡眠时它不再运行vruntime 不增长别的进程继续跑vruntime 持续上涨。等它醒过来它的 vruntime 自然就显得“远远落后”红黑树排序后它插到很靠左的位置于是获得了优先运行权。这本身是一种对 I/O 密集/交互型任务的天然优待但太激进也会有副作用。一个进程如果频繁短期睡眠比如每次只睡 200us它会反复获得先发优势长时间霸占 CPU导致其他 CPU 密集进程被饿着。内核历史上专门处理过“睡眠偷跑”问题用sched_feat GENTLE_FAIR_SLEEPERS控制睡眠补偿强度。在新内核里唤醒时钳制 vruntime 的机制会对补偿幅度做限制避免过度补偿。你在调优时遇到“某个常睡进程反而占用高 CPU”时不要先怀疑调度器坏了先看看是不是睡眠时间极短、唤醒极其频繁的模式触发了补偿放大器这种场景直接用 cgroup 的 shares 限制它的权重收益会更有效。5. 观察与调优用纪律代替玄学5.1 用 proc 和 trace 看穿 CFS 的真实状态很多人在排查调度延迟时第一反应是改参数但我建议顺序反过来——先观察再调参。最简单的观察手段是/proc/sched_debug它输出每个 CPU 的运行队列情况以及当前任务和可运行任务的详细信息。里面的关键字段包括runnable tasks: task vruntime ... nginx-1234 1112345.123 ...重点关注每个任务的 vruntime 差值如果某些任务 vruntime 长期远远落后数量级差异巨大说明它们被持续抢占可能是该调高优先级也可能是负载已经远超 CPU 能力。更精细的观察用 tracepoint。可以用 perf 记录调度事件sudo perf sched record -- sleep 5 sudo perf sched latency --sort max输出里会列出每个任务的调度延迟分布包括平均延迟和最大延迟这是找“谁拖累了谁”的最快路径。嫌 perf sched 太重的话bpftrace 也可以sudo bpftrace -e tracepoint:sched:sched_switch { [comm] count(); }先确认哪些进程参与切换再对着/proc/sched_debug逐个查 vruntime。如果看到某个交互进程被某个后台任务反复抢占而且后台任务的 vruntime 已经很落后说明后台任务确实长期饥饿抢占是有道理的问题根源在 CPU 总资源不足如果后台任务 vruntime 明明领先还频繁抢占那就需要检查sched_wakeup_granularity_ns是不是设得太大准确说是要检查唤醒抢占的参数设置。5.2 几个值得调整的调度参数与适用场景CFS 的可调参数入口是多级既有 sysctl 又有/sys/kernel/debug/sched/features还有 cgroup 内的cpu.shares。下面这几个是我实际项目里用过的sysctl_sched_min_granularity_ns单次运行最小量子默认 750000ns。调小它可以让进程切换更频繁、响应更快但会增加上下文切换开销调大它适合批处理场景减少切换。注意不建议设到 1ms 以下甚至几百 us除非你充分评估过 syscall 和 cache miss 的代价。sysctl_sched_wakeup_granularity_ns默认 1ms 左右。这个值越大唤醒抢占越不积极越小刚醒来的进程越容易立刻抢 CPU。延迟敏感服务希望小但小到 0.5ms 以下可能造成频繁乒乓切换。sched_child_runs_first默认关闭。开启后 fork 出来的子进程先运行对某些 fork 频繁且子进程快速消费 CPU 的场景有收益但可能让父进程的唤醒延迟变高。sysctl_sched_autogroup_enabled很多发行版默认开。它把“同一会话终端/SSH 登录的进程”自动分成一组防止一个 make 进程把整个系统的交互性拉垮。如果你发现明明只有一个用户跑构建任务桌面却卡成 PPT先看看 autogroup 有没有被关掉。线上排查时还有一个非常重要的纪律每次只调一个参数记录前后数据对比。调度器参数之间存在相互作用比如 min_granularity 调小后 wakeup_granularity 可能也需要相应调整两个一起动出了问题你根本不知道是谁的锅。5.3 我个人在项目里用过的调优策略最后分享一个实际案例。那个转码服务器最后没有调任何全局参数而是给 Web 网关进程和转码进程各建了一个 cgroupWeb 组 shares 设为 2048转码组 shares 设为 1024。同时把转码进程组里的线程 nice 值整体设为 10而不是给进程设 0。效果是 API 网关的 p99 延迟从 300ms 量级降到 40ms 左右转码吞吐只损失了约 8%。这背后的逻辑很简单cgroup shares 保证组间的比例组内用 nice 拉低批处理优先级两层叠加比只调全局 sysctl 更可控。调参不要追求极限要追求可预期。全局参数会影响所有任务一旦误调整个主机交互性都崩。用 cgroup 把调优范围圈在一个业务组内出问题也只是影响那个组回滚成本极低。另外一个容易被忽视但很实用的细节是在 NUMA 机器上CFS 的负载均衡会尽量保持进程在同一个 NUMA 节点内迁移跨节点迁移会带来内存访问延迟的大幅上升。所以调优时除了看调度参数还要用numactl --hardware和perf stat -e remote_node_loads检查跨节点访问比例。如果发现远程内存访问占比高优先修复内存绑定问题调度器参数救不了内存距离。这套 CFS 的机制看起来复杂核心就三件事权重决定比例、vruntime 决定顺序、红黑树保证效率。把这三根支柱搞透再遇到 CPU 延迟抖动你就知道该看哪一层了——先看是不是负载过载再看是不是参数不合适最后看是不是组间配置问题。照这个顺序排查绝大多数调度层面的幺蛾子都能在半小时内定位。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

快速搭建网站的工具怎么选?3个方案省下5万冤枉钱 2026/9/28 9:42:31

快速搭建网站的工具怎么选?3个方案省下5万冤枉钱

快速搭建网站的工具怎么选?3个方案省下5万冤枉钱 网站做好了没人访问,这是很多老板最头疼的事。你花大价钱做的官网,设计精美、功能齐全,但打开一看,流量为零,咨询为零。这时候你才意识到,问题不在“做没做”,而在“怎么快速做出来并推向市场”。面…

阅读更多 →
昇腾910B多机分布式推理:从HCCL到MindIE的DeepSeek部署实践 2026/9/28 9:42:24

昇腾910B多机分布式推理:从HCCL到MindIE的DeepSeek部署实践

昇腾910B上跑DeepSeek多机分布式推理,很多人卡在第一眼:MindIE、HCCL、ranktable、hccn_tool,每个词都眼熟,串起来就不是那么回事。实际踩过一圈之后你会发现,真正决定能不能跑起来的不是模型代码,而是通信…

阅读更多 →
从CANoe到TSMaster:车载总线测试工具链迁移实战指南 2026/9/28 9:42:24

从CANoe到TSMaster:车载总线测试工具链迁移实战指南

搞车载总线测试的工程师,电脑里大概率都装着一套CANoe。我最早接触CANoe是刚入行那会儿,跟着前辈在项目里做网络测试,从报文发送、DBC解析到UDS诊断,基本全是靠Vector这套工具撑起来的。说实话,CANoe确实是这个行业的标…

阅读更多 →
从刷榜到用榜:GitHub Trending 的增量逻辑、项目筛选与高效落地 2026/9/28 9:42:23

从刷榜到用榜:GitHub Trending 的增量逻辑、项目筛选与高效落地

1. 日榜的"热度"到底是怎么算出来的先别急着收藏仓库。每天打开 GitHub 的 Trending 页面,你看到的是过去 24 小时内 Star 增量最高的仓库,周榜和月榜则分别看一周、一个月内的增量。官方没有公开完整排序算法,但用久了会发现&…

阅读更多 →
【Java开发MCP】SSE模式开发并集成MCP:TaoToken统一Key接入与SpringAI WebFlux配置骨架 2026/9/28 9:42:23

【Java开发MCP】SSE模式开发并集成MCP:TaoToken统一Key接入与SpringAI WebFlux配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
OpenCompass 高效评测:Partitioner 任务切分与 Runner 执行后端实战指南 2026/9/28 9:42:23

OpenCompass 高效评测:Partitioner 任务切分与 Runner 执行后端实战指南

模型评测人工智能大模型AI 评测 【免费下载链接】opencompass OpenCompass is an LLM evaluation platform, supporting a wide range of models from OpenAI, Anthropic, Gemini, Qwen, GLM, DeepSeek, etc, across 100 datasets covering knowledge, reasoning, coding, scie…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉