最近在调一块多轴运动控制卡,板子上跑的是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之后已并入主线)。这几档的关键差异在于:不可抢占时,一个进程进去内核态后,可能要完成整个系统调用才出来,如果这个系统调用恰好很耗时,后面排队的实时任务就只能干
