最近在调一块多轴运动控制卡,板子上跑的是Linux。负载一上来,伺服周期抖动从几十微秒飙到几百微秒,控制曲线肉眼可见地变形。最开始我怀疑是驱动问题,查了一整天才把根因定位到调度器配置上——发行版默认内核为了兼容五花八门的硬件和负载,调度相关的编译选项全都在"不出错"和"通用"之间取平衡,跟"低延迟"三个字没有半点关系。后来我把内核重新编译了一遍,调度器配置从通用取向改成明确的低延迟取向,同一个程序,抖动直接降了一个数量级,整个系统像换了台机器。
这篇文章就把我这次折腾中梳理过的10个Linux调度器编译配置项完整过一遍,每个都讲清楚:它是什么、影响调度器哪部分行为、什么场景该开、什么场景该关、实测结果如何。适合三类人看:给嵌入式Linux项目做实时性兜底的嵌入式开发,维护高并发服务器想榨干性能的运维,以及用桌面Linux不想被编译任务卡死的普通用户。读完你不仅能照着配置出一版适合自己的内核,还能明白这些选项背后的调度原理,而不是只会抄别人的 menuconfig 操作。
1. 为什么调度器值得你单独为它编译一次内核
1.1 发行版内核的调度器是"通用妥协品"
很多人第一次听说要为了调度器去编译内核,第一反应是"有必要吗?"我理解这个疑问,因为大部分人在日常使用中根本感受不到调度器的存在,它就像一个默默排队的管家,把CPU时间分配给各个进程。但恰恰是这个"分配"策略,决定了你的系统在高负载下的响应速度、吞吐量和实时性。
发行版内核的问题在于它必须覆盖几乎所有硬件平台和几乎所有负载类型,所以调度器相关选项全部取的是"不会出大错"的中间值:时钟频率可能选300Hz而不是1000Hz,抢占模型可能选Voluntary而不是Full Preemption,CPU隔离、全无时钟这些面向极端场景的优化更是默认关闭。这样的内核放在普通桌面或一般服务器上没问题,可一旦你的负载有明确的实时性或低延迟要求,通用配置就会成为瓶颈。
我举个具体的例子:发行版内核默认的调度器时钟频率如果是300Hz,意味着调度器每3.3毫秒才醒来一次检查任务状态。对于要求1毫秒内完成一次控制计算的实时任务,这个粒度太粗了,任务就算就绪了,也可能要等一个tick周期才被调度到。这就是为什么很多做实时控制的工程师最终都走向自行编译内核。
1.2 这10个配置项在整个调度器里扮演什么角色
调度器不是单一个模块,它由"时钟心跳""调度算法""分组控制""计数统计""隔离机制"等好几层组成。我这次梳理的10个配置项刚好覆盖了这几层:
- 基础层:决定调度器"多久醒一次""能否随时抢走CPU"——CONFIG_HZ、CONFIG_PREEMPT、CONFIG_SCHED_EEVDF
- 分层控制层:决定多个进程组之间如何分配CPU、实时任务会不会被限流——CONFIG_FAIR_GROUP_SCHED、CONFIG_RT_GROUP_SCHED、CONFIG_SCHED_AUTOGROUP
- 低延迟增强层:决定时钟中断是否打扰任务、调度统计是否精确、SMT和NUMA架构下的隔离与迁移——CONFIG_NO_HZ_FULL、CONFIG_IRQ_TIME_ACCOUNTING、CONFIG_SCHED_CORE、CONFIG_NUMA_BALANCING
我没有把 CONFIG_SCHED_MC、CONFIG_SCHED_SMT 这类调度域拓扑选项算进来,因为它们在大多数场景下用默认值就够,改了收益不明显,反而可能引入新问题。真正影响大、容易踩坑、值得花时间权衡的就是上面这10个。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先定基调:HZ与抢占模型决定系统的"响应性格"
2.1 CONFIG_HZ:调度器多久醒来一次
Linux调度器是时间驱动的,内核通过周期性的时钟中断(tick)来唤醒调度器检查当前CPU上运行的任务是否该被换下去。CONFIG_HZ就定义了这个tick每秒发生多少次。可选值一般是100、250、300、1000,少数发行版也有200。
这个数字直接影响调度粒度和系统开销。HZ=100时,tick每10毫秒一次,调度器每10毫秒才能重新评估一次CPU分配,这对高吞吐服务器能节省可观的调度开销,但也意味着一个实时任务就绪后,最坏情况要等10毫秒才轮到它。HZ=1000时,tick每1毫秒一次,调度更及时、延迟更可控,代价是CPU空转时也要处理10倍数量的时钟中断,白白消耗一些性能。
我实测过同一台8核机器上不同HZ的差异:在HZ=100的配置下用 cyclictest 测1000微秒周期的实时任务,最大延迟大概在300微秒左右;切到HZ=1000后,同样任务的最大延迟降到80微秒以内,效果非常明显。如果你做嵌入式实时控制、音频处理、高频交易,直接上1000;如果是高并发Web服务器,250到300比较合理;如果追求极限吞吐、负载周期性不强的批处理,100也够用。
2.2 CONFIG_PREEMPT:内核态什么时候可以被抢
HZ决定调度器多久醒来一次,但还有一个更关键的问题:一个进程正在内核态执行系统调用时,另一个高优先级任务来了,内核能不能打断当前这个系统调用?这就是抢占模型要回答的问题。
Linux的抢占模型分几档,从弱到强大致是:PREEMPT_NONE(内核态不可抢占)、PREEMPT_VOLUNTARY(在内核长时间耗时路径上增加主动让出点)、PREEMPT(内核态支持随时抢占)、PREEMPT_DYNAMIC(启动时动态切换)、PREEMPT_RT(完整实时化改造,6.12之后已并入主线)。这几档的关键差异在于:不可抢占时,一个进程进去内核态后,可能要完成整个系统调用才出来,如果这个系统调用恰好很耗时,后面排队的实时任务就只能干等;可抢占时,只要满足条件,高优先级任务可以立刻把低优先级任务从内核态拽下来。
给非内核背景的读者打个比方:PREEMPT_NONE像老式绿皮火车,到站才停,谁中途想上车都得等;PREEMPT像出租车,随时可以踩刹车靠边停,让更急的人先上车;PREEMPT_RT则是给乘客配了优先通道,重要人物到了,其他车都得让路。
我个人的经验是,做桌面或嵌入式应用,至少选PREEMPT;做实时控制,直接用PREEMPT_RT(编译6.12以上内核就能直接在menuconfig里选)。服务器的取舍也不绝对,如果你跑的是延迟敏感的搜索、缓存集群,用PREEMPT未必会损失多少吞吐,但延迟尾巴会好看很多。建议用PREEMPT_DYNAMIC做实验,启动时通过参数 preempt=full 或 preempt=none 切换,实测后再定。
2.3 实测对比:同一台机器不同HZ/PREEMPT的延迟差异
为了让大家直观感受这两个配置项的影响,我把一张实测表放出来,测试环境是Intel i5四核、内核6.12、cyclictest单线程跑100000次循环,统计最大调度延迟:
| 配置 | HZ=300,PREEMPT_VOLUNTARY | HZ=1000,PREEMPT_VOLUNTARY | HZ=1000,PREEMPT | HZ=1000,PREEMPT_RT |
|---|---|---|---|---|
| 最大延迟(微秒) | 约350 | 约120 | 约70 | 约25 |
| 最小延迟(微秒) | 约8 | 约6 | 约4 | 约3 |
| 平均延迟(微秒) | 约25 | 约15 | 约10 | 约6 |
注意这里是普通桌面环境,没有做CPU隔离和中断亲和性优化,延迟绝对值不算低,但相对差异很清楚:HZ和抢占模型这两个基础项不改,后面做再多精细优化都白搭。我后来做隔离优化时,rt任务的最大延迟能压到10微秒以内,那是后话。所以编译内核第一步,先把HZ和PREEMPT定下来。
3. 默认调度算法与分组控制:CFS/EEVDF和cgroup份额
3.1 CONFIG_SCHED_EEVDF:新内核里的公平调度器怎么变了
先说调度器算法本身。Linux桌面和服务器上最常用的调度策略是CFS(完全公平调度器),核心思路是给每个任务记录一个虚拟运行时间vruntime,每次调度都选择vruntime最小的任务运行,让所有任务在时间维度上尽量公平。这个设计对交互和普通计算任务都不错,但有个历史问题:当系统里同时存在大量短任务和长任务时,长任务因为有累积的vruntime优势,会连续霸占CPU,短任务迟迟得不到调度,交互端就感觉卡顿。
Linux 6.6起,CFS被一种叫EEVDF(Earliest Eligible Virtual Deadline First,最早可用虚拟截止时间优先)的算法增强或替代。EEVDF在保留vruntime公平性的基础上,给每个任务引入一个虚拟截止时间概念。选下一个任务时,不只是比"谁跑得少",还要看"谁快到截止时间了",这样既保证了长期公平,又让短任务、交互任务能更快被调度到。这个改动对桌面和延迟敏感负载是实打实的提升。
在menuconfig里,较新内核会看到CONFIG_SCHED_EEVDF这个选项,默认是开启的,一般不需要手动改。但有一点要注意:如果你是从老内核迁移过来的服务器,升级到6.6以后会发现某些负载的调度行为变了,例如原本靠CFS的vruntime积分来保证优先级的场景会有些许差异。遇到这种情况先别急,EEVDF依然尊重nice值,只是任务选择顺序变了,观察一段时间再用数据判断,实在不行才考虑往回调整优先级设置。
3.2 CONFIG_FAIR_GROUP_SCHED与CONFIG_CGROUP_SCHED:CPU份额隔离的前提
上面讲的是单个任务之间的调度,但现实里我们更常需要管理的是"一组任务",比如一个Docker容器里的所有进程、一个cgroup里的整套服务。如果不开启组调度,所有任务都直接挂在根调度组下面,你没法通过cgroup的cpu.weight(老内核是cpu.shares)给他们分配CPU份额,想限制某个容器最多占用50%CPU都做不到。
CONFIG_FAIR_GROUP_SCHED是组调度开关,CONFIG_CGROUP_SCHED是cgroup支持底座,两者配合才能在cgroup v2里使用cpu.weight控制组间CPU权重。容器平台、KVM虚拟化、多租户服务器属于必须开的项;反过来,如果你是单一业务、所有进程一个优先级体系,关闭它每年可以省掉微不足道的调度开销——说实话这点开销很小,所以我倾向于默认开着,收益远大于损失。
打开路径在menuconfig里比较容易找:General setup -> Control Group support,进去后勾选Group CPU scheduler和Group scheduling for SCHED_RR/CAP?注意别把RT group scheduling混进来,那是下面要讲的另一个坑。开了之后,配合systemd的Slice或者直接写cgroup文件,就能把关键业务的CPU权重和后台任务隔离开。
3.3 CONFIG_RT_GROUP_SCHED:实时任务被"隐形限流"的元凶
这个配置项是我这次调控制卡时踩的最大坑,也是很多做实时Linux的人一开始完全没意识到的隐形陷阱。CONFIG_RT_GROUP_SCHED的作用是让RT调度类任务(SCHED_FIFO/SCHED_RR)也能按组做cgroup限制。听起来挺安全,但它有一个默认行为:RT任务在每个周期内的运行时间被限流在95%以内,具体参数就是cgroup里的cpu.rt_period_us和cpu.rt_runtime_us,默认分别是1秒和950毫秒。
也就是说,就算你辛辛苦苦开了PREEMPT_RT、配了高优先级,只要系统里启用了RT group scheduling且没有修改默认参数,你的实时任务每跑950毫秒就会被强制挂起50毫秒,这50毫秒里无论优先级多高都得不到调度。实时控制周期是毫秒级的话,影响可能还能扛,但如果是高频控制或精密运动控制,这50毫秒就是灾难。更隐蔽的是,这个问题在生产环境很难查,因为日志里压根不会报错,任务只是周期性卡顿。
解决方式有两种。第一种,如果你明确不需要对RT任务做CPU限流(绝大多数嵌入式实时场景都不需要),在编译内核时直接关掉CONFIG_RT_GROUP_SCHED,一了百了。第二种,如果你要保留,那就必须配好cgroup参数,把当前RT任务所在cgroup的cpu.rt_runtime_us改成-1,表示不限制。我在测试机器上就是直接把CONFIG_RT_GROUP_SCHED关了,因为容器平台才需要它,普通实时控制根本用不上。
注意:开启CONFIG_RT_GROUP_SCHED后,如果忘记配置cgroup的cpu.rt_runtime_us,RT任务的CPU占用会被悄悄限制在95%,排查这类问题看 /proc/sched_debug 里的rt_throttled字段,如果为1,说明确实被限流了。
3.4 CONFIG_SCHED_AUTOGROUP:桌面交互优先的锦上添花
CONFIG_SCHED_AUTOGROUP是一个很容易被忽略但对桌面体验影响巨大的配置。它做的事情是:不需要你手动配任何cgroup文件,自动把来自同一个终端会话、同一个登录会话的进程分成一组,组内竞争共享的CPU权重。这样做的直接好处是,如果你在终端里执行 make -j$(nproc) 编译内核,它的所有子进程会被自动归为一个大组,不会把整个CPU时间吃光,桌面的鼠标、浏览器、音频进程依然能实时拿到调度资源。
我经常用这个功能调试桌面卡顿问题。开启AUTOGROUP后,同样进行大型编译任务,桌面窗口的响应延迟能缩短几百毫秒甚至更多。对服务器来说情况相反,服务器通常是单一批处理负载,自动分组反而可能引入不确定的调度分层,我一般建议关掉,让所有进程公平竞争。配置项位置:General setup -> Automatic process group scheduling。
4. 低延迟的隐形推手:动态时钟、计时精度、CPU隔离
4.1 CONFIG_NO_HZ_FULL:让tick彻底远离你的实时任务
前面讲过CONFIG_HZ决定tick频率,但还有一种更极端的情况:能不能让特定CPU干脆没有周期性tick?这就要说到CONFIG_NO_HZ_FULL,全无时钟模式。
默认内核处于CONFIG_NO_HZ_IDLE状态,即CPU空闲时才停止周期tick,一旦有任务运行,tick照常。NO_HZ_FULL则更进一步,允许通过启动参数nohz_full=指定一个CPU列表,这些CPU在只有一个可运行任务时,彻底关闭周期tick。对这个CPU上的任务来说,再也没有一个时钟中断每隔1毫秒或3.3毫秒打扰它,系统调用按需执行,性能稳定性和实时确定性都能大幅提升。
这个功能听起来很香,但有几个前置条件。第一,必须同时配合isolcpus=参数做CPU隔离,否则内核还是会因为其他维护任务把IPI中断发送到隔离CPU上,tick停了也没用。第二,编译时建议同时打开CONFIG_IRQ_TIME_ACCOUNTING和CONFIG_VIRT_CPU_ACCOUNTING_NATIVE,否则全无时钟模式下的任务时间统计会不准确,调度器可能做出错误判断。第三,隔离CPU上不能随便跑多个任务,NO_HZ_FULL只在单任务时才能停tick,任务多了内核会重新开启tick。
实际应用中,我会把最后两个核 isolcpu=2,3 nohz_full=2,3,把实时控制线程绑定到3号核,然后让2号核专门处理hrtimer和外围事件。这样跑出来的实时任务延迟分布非常稳定,几乎看不到周期性的tick尖峰。需要注意的是,NO_HZ_FULL并不是什么负载都适合,普通多任务服务器开了反而因为统计不准导致调度质量下降,所以生产环境要谨慎。
4.2 CONFIG_IRQ_TIME_ACCOUNTING:调度公平性离不开精确记账
调度器做决策依据是每个任务累计的运行时间。默认情况下,内核把CPU时间只记到任务头上,但有一个漏洞:当硬件中断、软中断发生并打断当前任务时,这段时间并没有单独记到中断头上,而是被算进了当前任务的运行时间。这样一来,一个频繁被打断的任务会被调度器误判为"已经跑了很多",从而在下一轮被调低优先级,实际上它真正执行用户代码的时间远没那么长。对实时任务来说,这种误判会直接反映为延迟抖动。
CONFIG_IRQ_TIME_ACCOUNTING解决的就是这个问题。开启后,内核会把中断处理时间单独记账,调度器计算vruntime时会把中断时间抠出去,任务不会因为被打断而被惩罚。我测过开启和关闭对实时任务的影响,在同样的高中断负载下,关闭时cyclictest最大延迟能到200微秒以上,开启后压到60微秒左右,改善非常明显。
代价是每次中断处理都要多做一些记账操作,对中断极频繁的万兆网卡或存储场景,吞吐会有小几个百分点的损失。我的建议是,凡是做实时、低延迟的系统,必须开;普通Web服务器看情况,如果中断压力大且没有实时要求,可以关掉换吞吐。编译选项位置:General setup -> CPU/Task time and stats accounting -> IRQ time accounting。
4.3 CONFIG_SCHED_CORE:SMT场景下的确定性隔离
CONFIG_SCHED_CORE是相对较新的功能,专门针对SMT(同步多线程)架构。一个物理核心上有两个逻辑线程(比如Intel的超线程),默认调度器会把这两个逻辑线程当成两个独立CPU,分别指派不同任务。问题在于,这两个逻辑线程共享执行单元和部分缓存,一个线程跑高负荷计算时,另一个线程的任务会被拖慢,延迟波动很大,而且存在通过共享资源侧信道窃取数据的风险。
核心调度的思路是让同一个物理核上的两个SMT线程进入同一个调度组(通过cgroup或prctl设置cookie),这样可以确保共享同一核的线程一起运行、一起退出,强制隔离出独立的执行资源。它不是为了提升吞吐,而是为了提升确定性和安全性。
启用这个功能需要平台支持,x86上没问题,ARM部分平台需要确认CONFIG_ARCH_HAS_SCHED_CORE。我的测试结果:开启SCHED_CORE后,SMT场景下实时任务的延迟抖动比开启前小了不少,尤其两个线程都在忙的时候,不会出现"邻居线程抢资源导致延迟暴涨"的情况。代价是调度器的选择逻辑变复杂,普通负载下总吞吐会有一点下降,所以是否开启取决于你对确定性的要求有多高。云服务商、金融高频、安全合规场景建议开,普通桌面服务器就算了。
4.4 CONFIG_NUMA_BALANCING:自动搬迁任务不一定总是好事
多路服务器上,内存访问有本地和远程之分,访问远端内存的延迟比本地高不少。CONFIG_NUMA_BALANCING做的事情就是自动监测任务对内存的访问模式,把任务迁移到它频繁访问的内存所在的NUMA节点,减少远程内存访问次数,提升吞吐。
听起来很美好,但自动迁移本身是异步的,它会在后台扫描任务的内存映射,触发页表修改、任务迁移、内存页迁移,这些操作都需要持锁运行,会引入不可预测的调度延迟。我在一台双路服务器上测过Redis的p99.9延迟:开启NUMA_BALANCING时,偶尔能看到几十微秒的尖峰,关掉之后尖峰消失。原因就是这个后台迁移机制在繁忙时抢占了任务。
所以我的处理原则是:大内存、多路数据库、后端服务,可以开,吞吐收益明显;低延迟缓存、实时控制、网关转发类任务,建议编译时关闭,或者用启动参数 numa_balancing=disable 在运行时禁用。反正我调控制卡那台机器是单路平台,直接关,省心。
5. 三种实战场景的配置组合:服务器、嵌入式实时与桌面
单独讲完每个配置项后,更重要的是把它们组合起来。同一个配置在A场景的正确选择,到B场景可能是错误答案。下面分享三个典型场景的实测配置组合,直接给出menuconfig路径和启动参数,方便照抄。
5.1 嵌入式实时控制场景:低延迟优先
这是我最熟悉的场景,比如工业控制、无人机飞控、机器人运动控制。核心诉求是调度延迟越低越稳,宁可牺牲一点吞吐。
编译配置建议:
- CONFIG_HZ_1000:时钟周期1ms,调度粒度最细
- CONFIG_PREEMPT=y 或 CONFIG_PREEMPT_RT=y:内核态可抢占,让高优先级任务能随时抢到CPU
- CONFIG_FAIR_GROUP_SCHED=y:保留组调度能力,便于隔离其他负载
- CONFIG_RT_GROUP_SCHED=n:关闭RT限流,避免实时任务被隐形限制
- CONFIG_SCHED_AUTOGROUP=n:不需要自动分组,统一调度更确定
- CONFIG_IRQ_TIME_ACCOUNTING=y:精确记账,任务不会因中断被打扰而吃亏
- CONFIG_NO_HZ_FULL=y + 启动参数
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3:隔离专用核给实时任务 - CONFIG_NUMA_BALANCING=n:单路嵌入式平台用不上,关了避免迁移抖动
启动后配合 cpuset 把实时线程绑定到隔离核上,同时用 chrt -f 80 设置实时优先级,cyclictest在隔离核上最大延迟能压到10~15微秒。这个水平对绝大多数工业控制都够用了。
5.2 高并发服务器/云原生化场景:吞吐与隔离并重
服务器场景的诉求是在一定延迟保证下尽量提升吞吐,同时容器之间要做CPU隔离。
编译配置建议:
- CONFIG_HZ_250 或 CONFIG_HZ_300:平衡调度粒度和中断开销
- CONFIG_PREEMPT_VOLUNTARY=y:内核态偶尔让出即可,别让高优先级任务随便打断长尾系统调用
- CONFIG_FAIR_GROUP_SCHED=y + CONFIG_CGROUP_SCHED=y:容器CPU权重隔离的前提
- CONFIG_RT_GROUP_SCHED=n:服务器上一般不跑RT调度类任务,开着反而多一层限制
- CONFIG_SCHED_AUTOGROUP=n:避免自动分组干扰容器的分组权重逻辑
- CONFIG_IRQ_TIME_ACCOUNTING=y:网络和存储中断较多的场景,精确记账让调度更合理
- CONFIG_NUMA_BALANCING=y:多路服务器建议开,内存访问优化收益明显
- CONFIG_SCHED_CORE=n:除非业务对SMT隔离有硬性要求,否则会拖累吞吐
另外,如果服务器运行了EMQX、Nginx这类延迟敏感的转发服务,可以保留PREEMPT_DYNAMIC并在启动时选择full,其余业务走voluntary,兼顾吞吐和响应。这个灵活度只有6.12以上内核才有的,建议升级到新内核再用。
5.3 桌面/通用工作站:交互流畅优先
桌面用户的核心痛点就是"编译任务一跑,鼠标卡成PPT",我把AUTOGROUP和HZ的收益看得最重。
编译配置建议:
- CONFIG_HZ_1000:调度粒度细,输入响应更快
- CONFIG_PREEMPT=y:内核态可抢占,保证交互任务优先
- CONFIG_FAIR_GROUP_SCHED=y + CONFIG_SCHED_AUTOGROUP=y:自动分组,编译任务不会吃光CPU
- CONFIG_RT_GROUP_SCHED=n:桌面没有RT限流需求,关了省心
- CONFIG_IRQ_TIME_ACCOUNTING=y:音频、视频场景对延迟敏感,精确记账有好处
- CONFIG_NO_HZ_FULL=n:桌面任务多而杂,全无时钟反而会让统计失真
- CONFIG_NUMA_BALANCING=y:如果工作站是双路,开着能改善内存访问效率
这套配置下,一边开着浏览器和IDE,一边 make -j$(nproc) 编译内核,桌面依然能保持流畅拖动窗口,音频也不会有爆音。比起关掉编译任务的CPU限制,温和得多也公平得多。
5.4 配置组合速查表
| 配置项 | 嵌入式实时 | 高并发服务器 | 桌面/工作站 |
|---|---|---|---|
| CONFIG_HZ | 1000 | 250/300 | 1000 |
| CONFIG_PREEMPT | PREEMPT / PREEMPT_RT | VOLUNTARY / DYNAMIC | PREEMPT |
| CONFIG_FAIR_GROUP_SCHED | 开 | 开 | 开 |
| CONFIG_RT_GROUP_SCHED | 关 | 关 | 关 |
| CONFIG_SCHED_AUTOGROUP | 关 | 关 | 开 |
| CONFIG_IRQ_TIME_ACCOUNTING | 开 | 开 | 开 |
| CONFIG_NO_HZ_FULL | 开 | 关 | 关 |
| CONFIG_SCHED_CORE | 视平台而定 | 默认关 | 关 |
| CONFIG_NUMA_BALANCING | 关 | 开 | 单路关/双路开 |
6. 从make menuconfig到实测数据:验证调度器变化的完整流程
6.1 编译内核的完整步骤与三个容易踩的坑
配置项定了之后,编译和安装本身也有不少细节。这里给一份Debian/Ubuntu系可直接用的命令序列,其他发行版依赖包名略有不同,但流程一致。
bash复制# 1. 安装依赖
sudo apt install build-essential flex bison libssl-dev libncurses-dev libelf-dev dwarves
# 2. 下载内核源码,建议直接用主线或LTS版本
curl -L https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.tar.xz | tar -xJ
cd linux-6.12
# 3. 以当前系统config为基线,再调整
cp /boot/config-$(uname -r) .config
make olddefconfig
make menuconfig
menuconfig里改完保存后,开始编译:
bash复制# 编译,-j后面跟CPU核心数
make -j$(nproc)
# 安装模块和内核
sudo make modules_install
sudo make install
# 更新grub引导项,确保能切回旧内核
sudo update-grub
整个过程里最容易踩的坑有三个。第一个是依赖没装全,编译到一半报"fatal error: libssl.h: No such file",装一下libssl-dev就解决。第二个是缺少dwarves包,编译时BTF相关步骤会报错,因为这个包管debug信息生成。第三个是make install后grub没有正确更新,重启后直接进了旧内核或者黑屏——所以update-grub一定要执行,并且确认启动项里出现了新内核版本号,再用旧内核兜底。
还有一个和编译无关但和硬件相关的问题是PCI地址空间分配,我在某些主板上遇到过 pci bridge: not enough MMIO resources 的报错,这通常是BIOS预留资源不够,可以在启动参数里加上 pci=realloc 或 pci=assign-busses 缓解,不影响调度器配置本身,但会挡住内核启动。
6.2 cyclictest与perf sched:量化调度延迟与吞吐
配置改完,没有数据支撑都是玄学。我推荐两个工具:cyclictest用来测实时调度延迟,perf sched用来分析调度行为。
cyclictest的用法很简单,单线程、实时优先级95、间隔1毫秒、循环10万次:
bash复制sudo cyclictest -t 1 -p 95 -i 1000 -l 100000
输出里的Max值就是最坏情况调度延迟,这个数字越小说明实时性越好。我把同一块板子从发行版内核换成自己编译的HZ=1000+PREEMPT配置后,Max从几百微秒稳定降到几十微秒,数字摆在那里,比任何口头解释都有说服力。
perf sched可以看调度器的等待时间分布:
bash复制# 采集10秒调度事件
sudo perf sched record -- sleep
