news 2026/10/22 4:20:09

Linux内核调度器源码深度解析:从CFS到EEVDF的演进与实战调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核调度器源码深度解析:从CFS到EEVDF的演进与实战调优

1. 为什么每个后端人都该啃一次调度器源码

做后端开发到一定年限,迟早会撞上一堵墙:线上服务明明CPU利用率不高,但请求延迟就是压不下去;或者某个线程莫名其妙被"饿死",日志里看不出任何异常。你翻遍应用层代码,加锁、改线程池、调JVM参数,折腾一圈发现收效甚微。这时候问题往往已经下沉到了操作系统层面——具体说,就是**Linux内核的进程调度器(sched)**在起作用。

我最初接触sched这块源码,是因为一个真实的性能问题:一个计算密集型的服务在混部环境下,P99延迟比独占环境高了将近8倍,但CPU使用率却只有40%出头。当时团队里没人能解释这个现象,直到我把kernel/sched/目录下的代码翻了一遍,才明白是CFS(完全公平调度器)的vruntime机制在作祟——计算密集线程的虚拟运行时间增长极快,导致它频繁被抢占,而每次抢占带来的上下文切换开销,在高并发场景下被放大了。

这就是我想写这个系列的原因。sched是Linux内核里最核心、也最容易被应用层开发者忽视的子系统之一。它不像文件系统、网络协议栈那样有直观的API,但它的每一个设计决策,最终都会以延迟、吞吐、抖动的形式反映在你的服务指标上。这个系列我打算从源码结构入手,把调度器的核心机制一层层拆开,讲清楚它到底怎么工作、为什么这么设计、以及我们能从中获得什么。

适合读这个系列的人:有C语言基础、了解操作系统基本概念(进程、线程、上下文切换)的后端或系统开发者。如果你只是想知道"怎么调nice值",那网上教程一大把;但如果你想搞清楚"为什么调了nice值效果不明显",那跟着源码走一遍是绕不开的路。我会尽量用生活化的类比来解释抽象概念,同时保证每个关键数据结构都落到具体的代码位置上,让你看完能自己动手去读。

2. 调度器源码的目录地图与阅读顺序

2.1 kernel/sched/ 下每个文件的职责边界

很多人第一次打开kernel/sched/目录会懵——十几个文件,名字还都挺像。我建议先别急着看代码,花十分钟把文件职责理清楚,后面读起来会顺畅很多。下面这张表是我自己整理的核心文件清单,按重要程度排序:

文件名核心职责阅读优先级
core.c调度器主框架,schedule()入口、上下文切换、抢占逻辑最高
fair.cCFS完全公平调度器的全部实现最高
sched.h核心数据结构定义(task_struct相关、rq运行队列)最高
rt.c实时调度器(SCHED_FIFO/SCHED_RR)实现中
deadline.cDL调度器(SCHED_DEADLINE)实现中
idle.c空闲调度器,CPU没事干时跑什么低
stats.c调度统计信息导出低
clock.c调度相关的时钟封装低
cpufreq.c调度器与CPU频率调节的交互低
wait.c等待队列实现中

读的顺序我建议是:先看sched.h把数据结构混个脸熟,然后直接啃core.c里的__schedule()函数,这是整个调度器的"心脏"。搞懂它之后,再去看fair.c里CFS怎么挑选下一个任务。这个顺序的好处是,你先建立了"调度器要做什么"的全局观,再去看"具体怎么做",不会一上来就陷进红黑树的细节里出不来。

提示:不同内核版本的目录结构会有差异。比如5.x之后fair.c里的一些功能被拆到了pelt.c(负载跟踪)和fair.c配合。读之前先确认你手上的内核版本,用uname -r看一眼,然后去对应版本的源码树里找。

2.2 从 schedule() 入口倒推调用链

schedule()是调度器对外的门面函数,但它本身逻辑很简单,核心就是调用__schedule()。真正有意思的是谁在什么时候调用了schedule()。我习惯用"倒推法"来读:先找到入口,然后往上追调用者,这样能快速理解调度器在整个内核里的位置。

主要的调用场景有这么几类:

  • 主动让出:任务调用sched_yield()或者等待某个资源(比如mutex_lock失败)时,会主动触发调度。
  • 被动抢占:时钟中断到来,发现当前任务的时间片用完了,或者有更高优先级的任务被唤醒,就会设置TIF_NEED_RESCHED标志,在中断返回用户态或内核态时触发调度。
  • 唤醒抢占:一个任务被wake_up_process()唤醒时,如果它的优先级比当前运行的任务高,会尝试抢占当前任务。

理解这三类场景很关键,因为它们对应了调度器要解决的三类问题:公平性(主动让出)、实时性(被动抢占)、响应性(唤醒抢占)。后面讲CFS的vruntime和check_preempt_*系列函数时,你会发现所有逻辑都是围绕这三类场景展开的。

2.3 读源码前必须搞清楚的几个前置概念

在正式进入代码之前,有几个概念必须先说清楚,否则后面会一直卡壳。

第一个是"运行队列"(runqueue,简称rq)。每个CPU核心都有一个独立的rq结构,里面挂着这个核心上所有可运行的任务。注意是"每个CPU一个",不是全局一个——这是Linux调度器可扩展性的基石。你可以把rq想象成一个餐厅里每个服务员手里的订单夹,每个服务员只管自己夹子里的订单,不需要去抢别人的。

第二个是"调度类"(sched_class)。Linux不是只有一种调度算法,而是把调度器抽象成了"类",每个类实现一套接口(enqueue_task、dequeue_task、pick_next_task等)。优先级从高到低依次是:stop_sched_class>dl_sched_class>rt_sched_class>fair_sched_class>idle_sched_class。调度时从高优先级类开始问"你有任务要跑吗",有就交给它,没有就问下一个。这种设计叫"调度类链",是理解整个调度框架的钥匙。

第三个是"虚拟运行时间"(vruntime)。这是CFS的核心概念,我后面会用一整节来讲,这里先记住一句话:CFS不直接看任务跑了多久,而是看任务"应该"跑多久,用权重把实际运行时间换算成一个虚拟值,谁的小谁就先跑。

3. CFS完全公平调度器的核心机制拆解

3.1 vruntime 到底在算什么:一个生活化类比

CFS全称Completely Fair Scheduler,翻译过来叫"完全公平调度器"。但"公平"这个词很容易让人误解——它不是让每个任务跑一样长的时间,而是让每个任务按权重比例分享CPU。权重高的任务分到的CPU时间多,权重低的分到的少,但所有任务在"虚拟时间"这个维度上是公平的。

我用一个类比来解释vruntime。假设有三个学生(任务)要分一块蛋糕(CPU时间),他们的"饭量权重"分别是1、2、4。如果直接按权重分,饭量4的学生拿到的蛋糕是饭量1的4倍,这看起来合理,但问题是:如果饭量1的学生先到,他先吃了一块,饭量4的学生后到,他得等多久才能吃到?CFS要解决的就是这个"等待公平性"问题。

vruntime的做法是:给每个学生记一个"虚拟等待时间"。学生实际吃了多久,除以他的饭量权重,得到虚拟时间。饭量大的学生,实际吃同样久,虚拟时间增长得慢;饭量小的学生,虚拟时间增长得快。调度器每次挑虚拟时间最小的那个学生来吃。这样,饭量小的学生虽然每次吃得少,但他虚拟时间涨得快,很快又会轮到他;饭量大的学生虚拟时间涨得慢,可以一次吃久一点。最终结果是:每个学生的虚拟时间趋于相等,而实际吃到的蛋糕量与权重成正比。

对应到代码里,vruntime的计算公式大致是:

vruntime += delta_exec * (NICE_0_LOAD / weight)

其中delta_exec是实际运行时间,weight是任务权重(由nice值映射而来),NICE_0_LOAD是nice为0时的基准权重(1024)。nice值每降低1,权重增加约1.25倍;每升高1,权重减少约0.8倍。这个1.25倍不是随便定的,它保证了nice值相差1的两个任务,CPU占比大约相差10%。

3.2 红黑树:CFS 挑选下一个任务的底层结构

CFS用一棵红黑树来组织所有可运行的任务,树上的节点按vruntime排序,最左边(最小值)的节点就是下一个该跑的任务。为什么用红黑树而不是最小堆?因为红黑树的插入、删除、查找最小值都是O(log n),而且它支持高效的"删除任意节点"操作——任务被唤醒、睡眠、迁移时都需要从树上摘下来或插进去,红黑树的综合性能最均衡。

红黑树的节点是sched_entity结构,每个任务(或任务组)都有一个。关键字段包括:

  • vruntime:虚拟运行时间,树的排序依据。
  • rb_node:红黑树节点,把自己挂到树上。
  • load:权重,用于计算vruntime增量。
  • on_rq:标记是否在运行队列上。

挑选下一个任务的函数是pick_next_task_fair(),它最终会调用__pick_first_entity(),也就是取红黑树最左边的节点。这个过程非常快,就是沿着左指针一路走到底。

但这里有个细节值得注意:新创建的任务的vruntime初始值是多少?如果设成0,那它会一直霸占CPU直到追上其他任务,这显然不公平。内核的做法是把它设成当前运行队列的min_vruntime(树中最小的vruntime),并且做一点偏移,保证新任务不会一上来就抢占,也不会被饿死。这个细节在place_entity()函数里,读的时候可以留意。

3.3 调度周期与时间片:sysctl_sched_latency 的取舍

CFS没有固定时间片的概念,它用的是"调度周期"(sched_latency)。默认情况下,这个周期是6ms(sysctl_sched_latency),意思是:如果运行队列上有n个任务,那么在6ms内,每个任务至少应该被调度一次。每个任务分到的时间片就是调度周期 / 任务数,再按权重调整。

但这里有个问题:如果任务数很多,比如100个,那每个任务只能分到60微秒,上下文切换的开销可能比实际执行时间还大。所以内核设了一个下限sysctl_sched_min_granularity,默认是0.75ms。如果算出来的时间片小于这个值,就强制设为这个值,同时调度周期也会相应拉长。

这两个参数的取舍很能体现调度器的设计哲学:

  • sched_latency调小:任务轮转更快,交互式任务响应更好,但上下文切换开销增大,吞吐下降。
  • sched_latency调大:吞吐提升,但延迟敏感的任务可能等太久。
  • min_granularity调小:任务数多时每个任务分到的时间更少,公平性更好,但切换开销大。
  • min_granularity调大:减少切换,但任务数多时延迟会明显上升。

我在实际调优中遇到过这样一个案例:一个服务在容器里跑,容器限制了CPU配额,导致运行队列上的任务数看起来很多(因为cgroup的throttle机制),结果每个任务分到的时间片被压得很小,上下文切换率飙升。后来把min_granularity适当调大,切换率降了一半,P99延迟改善了30%多。这个经验说明,调度参数不是孤立的,它和cgroup、CPU亲和性等机制会相互影响。

4. 调度类链与优先级:谁先谁后的完整逻辑

4.1 sched_class 结构体里的函数指针表

sched_class是Linux调度框架的抽象接口,定义在kernel/sched/sched.h里。它本质上就是一张函数指针表,每个调度类实现自己的一套。核心方法包括:

struct sched_class { void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags); void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags); void (*yield_task)(struct rq *rq); struct task_struct *(*pick_next_task)(struct rq *rq); void (*task_tick)(struct rq *rq, struct task_struct *p, int queued); void (*task_fork)(struct task_struct *p); void (*task_dead)(struct task_struct *p); void (*switched_from)(struct rq *rq, struct task_struct *p); void (*switched_to)(struct rq *rq, struct task_struct *p); // ... 还有更多 };

读这张表的时候,我建议重点关注三个方法:enqueue_task(任务怎么进队列)、pick_next_task(怎么挑下一个)、task_tick(时钟中断来了怎么处理)。这三个方法构成了调度器的主循环。

enqueue_task在任务被唤醒或新建时调用,负责把任务挂到对应的数据结构上(CFS是红黑树,RT是优先级链表)。pick_next_task在调度时调用,返回下一个该跑的任务。task_tick在每个时钟中断里调用,用来更新运行统计、检查是否需要抢占。

4.2 从 stop 到 idle:五个调度类的优先级顺序

调度类链的顺序是硬编码在pick_next_task()里的,从高到低依次尝试:

  1. stop_sched_class:最高优先级,用于CPU热插拔、迁移线程等内核内部机制。普通任务永远不会属于这个类。
  2. dl_sched_class:SCHED_DEADLINE,基于截止时间的调度,用于硬实时场景。
  3. rt_sched_class:SCHED_FIFO和SCHED_RR,实时调度,优先级1-99。
  4. fair_sched_class:SCHED_NORMAL和SCHED_BATCH,普通任务,就是我们平时写的绝大多数程序。
  5. idle_sched_class:SCHED_IDLE,最低优先级,只在CPU完全没事干时跑。

这个顺序意味着:只要有一个RT任务可运行,所有CFS任务都别想跑。这是实时性的保证,但也是很多"CPU明明很闲但我的任务不跑"问题的根源。我曾经排查过一个案例:一个服务在混部机器上延迟极高,最后发现是同一台机器上有个RT任务在周期性运行,每次运行都把CFS任务挤掉了。用chrt -p <pid>一看,那个任务的调度策略是SCHED_FIFO,优先级50。把它的策略改成SCHED_OTHER后,问题立刻消失。

注意:RT任务如果设计不当(比如死循环且不主动让出),会直接饿死同CPU上的所有CFS任务,甚至导致系统无响应。生产环境里给任务设RT策略要非常谨慎,最好配合RLIMIT_RTTIME限制单次运行时间。

4.3 优先级继承与抢占标志的传递路径

当一个高优先级任务被唤醒时,它需要"通知"当前CPU上正在跑的任务让位。这个通知机制就是TIF_NEED_RESCHED标志。具体路径是:

  1. 任务A被wake_up_process()唤醒,进入try_to_wake_up()。
  2. 如果A的优先级高于当前运行的任务B,调用check_preempt_curr()。
  3. check_preempt_curr()根据调度类调用对应的check_preempt_*函数,比如CFS的check_preempt_wakeup()。
  4. 如果判断需要抢占,设置B的TIF_NEED_RESCHED标志。
  5. B在返回用户态或内核态可抢占点时,检查到这个标志,调用schedule()让出CPU。

这个路径里有个容易忽略的点:抢占不是立即发生的。设置标志只是"请求",真正让出要等到下一个抢占点。如果B正在内核态执行一个不可抢占的临界区(比如持有自旋锁),那它必须先把临界区执行完。这就是为什么有时候高优先级任务被唤醒后,延迟还是很高——它在等当前任务释放CPU。

理解这一点对排查延迟问题很重要。如果你发现高优先级任务唤醒后延迟高,可以检查当前任务是否长时间持有自旋锁或处于不可抢占状态。/proc/<pid>/stack和ftrace的preemptirqsoff跟踪器都能帮你定位这类问题。

5. 上下文切换的完整链路与开销来源

5.1 __schedule() 函数逐段拆解

__schedule()是调度器的核心函数,我把它拆成几个关键段落来看:

第一段:关抢占、拿运行队列锁。调用preempt_disable()禁止抢占,然后rq_lock()拿当前CPU运行队列的锁。这一步保证了调度过程的原子性。

第二段:检查是否需要调度。如果当前任务有TIF_NEED_RESCHED标志,或者处于可中断睡眠状态被唤醒,就继续;否则直接返回。这里有个优化:如果当前任务还在运行且没有抢占请求,就不做无谓的调度。

第三段:挑选下一个任务。调用pick_next_task(),从调度类链里找下一个任务。如果找不到(所有任务都在睡眠),就选idle任务。

第四段:上下文切换。调用context_switch(),这是真正"换人"的地方。它又分两步:先switch_mm()切换地址空间(如果两个任务属于不同进程),再switch_to()切换CPU寄存器状态和栈。

第五段:收尾。新任务开始运行后,会从switch_to()返回,然后释放运行队列锁、开抢占。

整个流程里,最耗时的通常是switch_mm(),因为它涉及页表切换和TLB刷新。如果两个任务属于同一个进程(比如两个线程),switch_mm()会跳过,这就是线程切换比进程切换快的根本原因。

5.2 switch_to 宏背后的寄存器与栈切换

switch_to是一个宏,不同架构实现不同。以x86_64为例,它做的事情大致是:

  1. 保存当前任务的callee-saved寄存器(rbp, rbx, r12-r15)到当前任务的内核栈。
  2. 把当前任务的栈指针保存到task_struct->thread.sp。
  3. 把下一个任务的thread.sp加载到栈指针寄存器。
  4. 恢复下一个任务的callee-saved寄存器。
  5. 跳转到下一个任务上次被切换出去的位置继续执行。

这里的关键是:每个任务都有自己的内核栈。任务在用户态运行时用用户栈,进入内核态后切换到内核栈。上下文切换只发生在内核态,所以切换的是内核栈。内核栈的大小默认是16KB(THREAD_SIZE),在x86_64上是4个页。

我踩过的一个坑:有次写内核模块,在中断处理里递归调用导致内核栈溢出,表现就是随机的oops和死机。后来用CONFIG_DEBUG_STACK_USAGE打开栈使用统计,才发现栈快满了。这个经历让我意识到,内核栈是稀缺资源,任何在内核态执行的代码都要控制栈深度。调度器本身在__schedule()里的栈使用是经过精心优化的,但如果你在task_tick回调里加了自己的逻辑,就要小心了。

5.3 上下文切换开销的量化与观测手段

上下文切换的开销到底有多大?我实测过一组数据(不同硬件会有差异,仅供参考):

切换类型典型开销
同进程线程切换1-2微秒
跨进程切换(同CPU)3-5微秒
跨CPU迁移5-10微秒
带TLB刷新的切换额外2-5微秒

这些数字看起来不大,但在高并发场景下会被放大。假设一个服务每秒处理10万请求,每个请求触发2次上下文切换,那就是20万次切换,按3微秒算就是0.6秒的CPU时间纯花在切换上。如果CPU只有4核,这0.6秒就是15%的CPU浪费。

观测上下文切换的手段主要有:

  • vmstat 1:看cs列,就是每秒上下文切换次数。
  • pidstat -w -p <pid> 1:看单个进程的cswch/s(自愿切换)和nvcswch/s(非自愿切换)。
  • perf stat -e context-switches:精确统计。
  • ftrace的sched_switch事件:能看到每次切换的完整调用栈,适合定位"谁在频繁切换"。

自愿切换通常是任务主动等待资源(比如等IO、等锁),非自愿切换是被抢占。如果非自愿切换很高,说明CPU竞争激烈,可能需要加CPU或调整调度策略。如果自愿切换很高,说明任务在频繁等待,要检查是不是锁竞争或者IO瓶颈。

6. 从源码回到生产:几个真实的调优场景

6.1 计算密集任务被频繁抢占的排查过程

回到我开头提到的那个案例。一个计算密集型服务在混部环境下P99延迟飙升,CPU使用率却不高。排查过程是这样的:

第一步,确认现象。用pidstat -w看到非自愿上下文切换高达每秒5万次,而独占环境下只有几百次。这说明任务在被频繁抢占。

第二步,看谁在抢占。用ftrace抓sched_switch事件,发现抢占者是一批短命的线程,它们属于同机器上的另一个服务。这些线程生命周期极短(几毫秒),但数量多,频繁唤醒。

第三步,分析调度行为。计算密集任务的vruntime增长很快(因为它一直在跑),而短命线程每次唤醒时vruntime被设为min_vruntime附近,所以它们一醒来就能抢占计算任务。这就是CFS的"唤醒抢占"机制——为了保证交互式任务的响应性,新唤醒的任务会获得一定的优先级优势。

第四步,解决方案。我们试了几种方案:调大sysctl_sched_wakeup_granularity(增加唤醒抢占的门槛)、给计算任务设CPU亲和性、用cgroup隔离。最终效果最好的是cgroup隔离,把两个服务放到不同的CPU cgroup里,各自有独立的运行队列,互不干扰。

这个案例的教训是:CFS的公平性是"局部"的,它保证同一个运行队列上的任务公平,但不保证跨cgroup的公平。混部场景下,cgroup隔离往往比调调度参数更有效。

6.2 nice 值调了没效果?先看 cgroup 和 CPU 亲和性

很多人调优的第一反应是调nice值,但经常发现没效果。原因通常有两个:

一是cgroup的CPU配额限制。如果任务在cgroup里,cgroup的cpu.max会限制整个组的CPU使用,组内任务的nice值只在组内起作用。如果组本身就被限流了,组内怎么调nice都没用。检查方法:cat /sys/fs/cgroup/<path>/cpu.max。

二是CPU亲和性。如果任务被绑定到某个CPU,而那个CPU上有个RT任务在跑,那nice值再低也没用——RT任务永远优先。检查方法:taskset -p <pid>看亲和性,chrt -p <pid>看调度策略。

我的一般排查顺序是:先看调度策略(是不是RT),再看cgroup限制,最后才看nice值。这个顺序能避免很多无用功。

6.3 容器环境下调度参数的可见性与调整边界

容器环境给调度调优带来了额外的复杂性。容器里的/proc/sys/kernel/sched_*参数通常是只读的,因为它们是全局参数,改了会影响宿主机上所有容器。你能调的主要是cgroup层面的参数:

  • cpu.weight(cgroup v2)或cpu.shares(v1):组内任务的权重分配。
  • cpu.max:组的CPU带宽限制。
  • cpuset.cpus:组的CPU亲和性。

我的经验是:容器里不要试图调全局调度参数,而是通过cgroup的权重和配额来表达你的优先级需求。比如一个延迟敏感的服务,给它更高的cpu.weight和更宽松的cpu.max,比调nice值有效得多。

另外要注意,不同内核版本的cgroup接口有差异。v1和v2的调度参数名字和语义都不一样,调优前先确认你的环境用的是哪个版本。mount | grep cgroup能看出来。

7. 读 sched 源码的几个实用技巧

7.1 用 ftrace 和 perf 验证你的理解

读源码最怕的是"以为自己懂了"。我的习惯是:每理解一个机制,就用ftrace或perf去验证。比如理解了check_preempt_wakeup()的逻辑后,我会写个小程序,创建两个不同nice值的线程,用ftrace抓sched_wakeup和sched_switch事件,看实际行为是否符合预期。

ftrace的sched跟踪器特别有用,它能记录每次调度决策的细节。打开方式:

# 挂载tracefs(如果还没挂) mount -t tracefs nodev /sys/kernel/tracing # 启用sched_switch事件 echo 1 > /sys/kernel/tracing/events/sched/sched_switch/enable # 开始跟踪 echo 1 > /sys/kernel/tracing/tracing_on # 跑你的测试程序 ./your_test # 停止并查看 echo 0 > /sys/kernel/tracing/tracing_on cat /sys/kernel/tracing/trace

输出的每一行会显示:哪个任务被切换出去、哪个任务被切换进来、在哪个CPU上、优先级是多少。对照源码看这些数据,理解会深刻很多。

7.2 版本差异:从 4.x 到 6.x 的关键变化

Linux调度器一直在演进,不同版本差异不小。几个关键变化:

  • 4.14之前:CFS的负载跟踪用的是load_avg,基于运行时间。
  • 4.14之后:引入了PELT(Per-Entity Load Tracking),用指数衰减的方式跟踪负载,对突发负载的响应更好。
  • 5.0之后:fair.c里的部分功能拆到了pelt.c,代码结构更清晰。
  • 5.8之后:引入了SCHED_CORE(core scheduling),用于解决SMT(超线程)场景下的侧信道安全问题。
  • 6.x:EEVDF调度器开始进入主线(6.6+),CFS逐渐被替代。EEVDF用"最早截止时间优先"的思路,解决了CFS在某些场景下的公平性问题。

如果你读的是6.6之后的内核,会发现fair.c里多了很多EEVDF相关的代码。EEVDF的核心思想是给每个任务算一个"截止时间"(deadline),按截止时间排序而不是按vruntime。这个变化对延迟敏感型任务更友好,但理解起来需要先搞懂CFS,因为EEVDF是在CFS基础上演进的。

提示:读源码时一定要对照你实际使用的内核版本。网上很多文章基于4.x或5.x,和6.x的代码差异可能很大。用git log kernel/sched/fair.c看提交历史,能快速了解某个函数的演变过程。

7.3 调试调度问题的常用命令清单

最后整理一份我平时排查调度问题常用的命令清单,都是实战中反复用到的:

# 查看进程的调度策略和优先级 chrt -p <pid> # 查看进程的CPU亲和性 taskset -p <pid> # 查看进程的cgroup cat /proc/<pid>/cgroup # 实时监控上下文切换 pidstat -w -p <pid> 1 # 查看运行队列长度 vmstat 1 # 看r列 # 查看调度延迟(需要root) perf sched latency # 抓调度事件 perf sched record -- sleep 5 perf sched script # 查看CPU频率和调度统计 cat /proc/schedstat cat /proc/<pid>/sched

/proc/<pid>/sched这个文件特别有用,它显示了单个任务的调度统计,包括se.sum_exec_runtime(总运行时间)、se.vruntime(虚拟运行时间)、nr_switches(切换次数)等。对比两个任务的这些值,能直观看出调度器是否"公平"。

我个人在实际操作中的体会是:调度器调优没有银弹,关键是理解机制后,结合具体场景去观测、假设、验证。源码是理解机制的最好途径,但光读源码不够,一定要配合工具去验证。我见过太多人背了一堆参数含义,但真遇到问题时不知道从哪下手。真正的能力是:看到现象,能快速定位到调度器的哪个机制在起作用,然后用工具去证实或证伪。这个系列后面还会继续拆解CFS的负载均衡、组调度、以及EEVDF的演进,感兴趣的话可以跟着一起读源码。

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

AI Agent上下文工程实战:从多轮对话失忆到稳定决策的完整指南

如果你做过几个正经的 AI Agent 项目&#xff0c;迟早会撞上一堵墙&#xff1a;模型明明够聪明&#xff0c;工具也接上了&#xff0c;提示词改了十几个版本&#xff0c;可 Agent 一跑多轮就“失忆”&#xff0c;该调的工具不调&#xff0c;不该编的答案瞎编。我在连续踩了两个月…

作者头像 李华
网站建设 2026/10/22 4:19:57

macOS虚拟机安装最佳方案:从工具选型到性能调优实战汇总

macOS虚拟机安装最佳方案汇总经常有朋友问我&#xff0c;想在macOS里跑一个干净的测试环境&#xff0c;或者临时开一台macOS虚拟机做点编译、自动化验证&#xff0c;到底怎么搞最省事。这个事我前前后后折腾了小半年&#xff0c;踩了不少坑&#xff0c;也试过好几套不同的方案&…

作者头像 李华
网站建设 2026/10/22 4:19:32

多模型AI助手桌面端聚合:Cherry Studio配置与知识库实战

用过一阵子各种网页版AI之后&#xff0c;我的桌面终于变成了这个样子&#xff1a;写代码的时候开着一个对话窗口&#xff0c;查资料的时候切到另一个&#xff0c;翻译一段文字又得换一个页面&#xff0c;电脑稍微开久了&#xff0c;光是浏览器标签就密密麻麻排了一整排。我一直…

作者头像 李华
网站建设 2026/10/22 4:18:41

CUDA独立运行期模块:边界、架构与故障排查实践

1. 模块定位&#xff1a;先搞清楚“独立运行期模块”指什么1.1 这一讲的背景&#xff1a;为什么反复强调“独立”如果你让一个刚接触 GPU 编程的同学说说 CUDA 是什么&#xff0c;大概率会得到“NVIDIA 的并行计算框架”“用来写核函数的语言”这类回答。但很少有人在意一个更基…

作者头像 李华
网站建设 2026/10/22 4:18:40

大模型API厂商能力对比:技术选型与避坑实战指南

做AI应用开发的人&#xff0c;迟早会撞上一个绕不开的问题&#xff1a;项目要接大模型API&#xff0c;到底选哪家。市面上能提供的厂商少说也有十几家&#xff0c;每一家都把自己的模型能力说得天花乱坠&#xff0c;跑分一个比一个高&#xff0c;价格一个比一个低。可真到了要把…

作者头像 李华
网站建设 2026/10/22 4:18:14

LLM上下文工程:模型注意力如何决定Agent成败

做Agent有一个特别容易被低估的环节&#xff1a;上下文工程。这个词听起来像是把提示词调出花样的进阶版&#xff0c;但实际做下来你会发现&#xff0c;它决定了一个Agent是“看起来聪明”还是“真的能干”。我最早丢掉幻想&#xff0c;是在一个很简单的场景里&#xff1a;让模…

作者头像 李华