这两年不管是在嵌入式行业还是云原生场景,群里聊调度器的频率明显高了。有人问为什么自己的高并发服务在低负载阶段还是会偶尔卡一下,有人问同一个内核在实时设备上跑,中断延迟为什么动不动就飙到几十毫秒,还有人干脆说换了最新内核,性能反而倒退了。这些问题十有八九不是内核代码的锅,而是编译内核时那几十个调度器相关配置项没选对。
这篇文章整理的是我在编译定制 Linux 内核时重点盯着的 10 个调度器配置项,覆盖了常规性能、响应延迟、实时确定性这几个维度。每个配置项我会讲清楚它到底控制什么、改前改后有什么差异、实测中容易踩什么坑,最后附上一套可落地的编译和验证流程。适合手里有服务器要做内核调优、或者在做嵌入式 RTOS/Linux 实时性改造的同学参考,刚接触内核编译的新手也能按步骤操作下来。
1. 调度器编译配置的本质:发行版内核是“平均值”,不是“最优解”
1.1 为什么默认内核往往性能平庸
绝大多数发行版自带的 kernel 二进制包,是为了在尽可能多的机器上稳定运行而构建的。这个出发点是“兼容性优先”,所以调度器相关的配置基本都选了中间值:CONFIG_HZ=250、CONFIG_PREEMPT_VOLUNTARY、CONFIG_NO_HZ_IDLE 这类组合,放在普通桌面和通用服务器上都很稳,但放到具体业务场景里就不够看了。
我经常打一个比方:发行版内核相当于食堂大锅饭,保证每个人都能吃饱,但不会为某个人的口味单独开火。你要做高吞吐的 Web 网关,调度粒度 250Hz 可能让短任务排队时间偏高;你要做机械臂控制,非抢占内核的调度延迟可能直接让控制周期抖动超标。自己编译一次内核,把调度器配置从“平均值”调整到“匹配负载”,效果往往比升级 CPU 还明显。
1.2 性能与实时性:两套不同思维模式
性能优化关注的是吞吐量和平均延迟,团队里常说“单位时间能处理多少个任务”;实时性优化关注的是确定性和最坏情况延迟,核心指标是“任何一个任务能不能在 deadline 前完成”。这两套目标在调度器配置上经常是互斥的。
拿抢占模型举例。要实现高吞吐,内核希望减少上下文切换,让进程让出 CPU 的时机尽量少;要实现强实时,内核必须允许高优先级任务随时打断低优先级任务,哪怕这会增加总体的切换次数。类似这种取舍会贯穿下面所有配置项,所以动手之前一定要先问自己一个问题:我这次调优,是要让平均响应更快,还是要让最坏延迟可控?
| 目标 | 核心指标 | 典型配置倾向 |
|---|---|---|
| 高性能吞吐 | 吞吐量、平均延迟 | 低 HZ、少抢占、关闭部分调试统计 |
| 实时响应 | 最坏延迟、确定性 | 高 HZ、可抢占或实时抢占、专用调度类 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 编译前准备:源码、工具链与基线验证
2.1 拿到一套干净的内核源码
我建议优先从 kernel.org 下载主线或 stable 版本,或者下载你所使用的发行版维护的内核源码包。很多人喜欢到处找第三方魔改内核,但在调优阶段这是大忌——你根本不知道别人在 config 里动了什么,出了问题也没法对照上游行为。
下载后必须校验签名或做 SHA256 校验,防止拉到被篡改的包。这一步看着多余,但我确实见过有人解压后编译到一半才发现源码目录里混入了无关文件。
bash复制wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.7.tar.xz
sha256sum linux-6.6.7.tar.xz
# 对比 kernel.org 官方页面提供的 checksum 一致后继续
tar -xf linux-6.6.7.tar.xz
cd linux-6.6.7
2.2 编译工具链与依赖检查
编译内核依赖的工具不多,但缺一个就会在中途报错。Debian/Ubuntu 系可以一次性装齐:
bash复制sudo apt install build-essential libncurses-dev flex bison libssl-dev \
dwarves libelf-dev bc cpio
其中 libncurses-dev 是 menuconfig 的文本界面依赖,dwarves 是生成 BTF 信息用的,新版内核在开启 CONFIG_DEBUG_INFO_BTF 时会强制要求。缺编译器头文件这类问题最常见,提前装好能省半小时。
还要提醒一句:磁盘空间至少留 20GB 以上,编译中间产物比最终内核大很多。如果是在虚拟机上编,建议给到 4 核 + 8GB 内存起步,否则一个 make -j$(nproc) 很容易把机器卡死。
2.3 编译前跑一次基线测试
这一步很多人会跳过,但我强烈建议不要跳。没有基线,后面改完配置到底有没有提升、是提升还是倒退,全靠感觉判断,这在性能调优里是最危险的。
最少要做两件事:第一,用 stress-ng 或负载工具记录当前 CPU 空闲比例和任务排队等待时间;第二,如果是实时性测试,用 cyclictest 或 perf sched 记录调度延迟和最大延迟。
bash复制# 延迟基线,记录 max 值
sudo cyclictest -t1 -p 99 -i 1000 -l 100000 -m
# CPU 压力下的调度延迟观察
stress-ng --cpu 4 --timeout 60s &
sudo perf sched record -- sleep 20
sudo perf sched latency
编译前测试不需要太复杂,但必须可重复。我习惯把所有输出存到一份日志里,后面每次启动新内核都跑同一套命令,对比才客观。
3. 十个关键配置项逐项拆解
下面是我在实际项目中反复调整过的 10 个调度器配置项,前四个对应实时性改造最常用,后六个更多影响整体性能与隔离效果。每个都会给出选项位置和配置建议。
3.1 CONFIG_HZ:内核节拍率
CONFIG_HZ 是内核时钟中断的频率,可选值一般是 100、250、300、1000。它决定调度器多长时间检查一次任务是否需要重新调度,也影响时间片精度和计时粒度。
- 100Hz:适合高吞吐批处理服务器,开销最小,但短任务的调度延迟最大。
- 250Hz:发行版常用默认值,兼顾吞吐和响应。
- 1000Hz:桌面、交互式系统和实时性要求较高的场景,调度延迟低,但中断更频繁。
我做实时控制时一般直接选 1000,宁可多付一点 CPU 开销,也要把调度延迟压下去。做高并发网关,我测试过 250 和 1000 的吞吐差异大约有 3%-5%,这时就没必要盲目追高。
另外注意 CONFIG_HZ 会影响音频、视频播放这类定时敏感应用,改完后建议用 cyclictest 对比最大延迟,不要只看系统跑不跑得动。
3.2 CONFIG_NO_HZ_FULL:完全无时钟节拍
CONFIG_NO_HZ_IDLE 让空闲 CPU 不触发时钟中断,大多数发行版默认开启。更激进的是 CONFIG_NO_HZ_FULL,它可以让某个繁忙 CPU 也完全摆脱周期时钟中断,只有当任务发生状态变化时才产生 tick。
这对绑定 CPU 的高性能计算、DPDK 这类轮询模式非常有用。但启用有严格前提:系统里至少要保留一个 CPU 专门处理传统时钟中断和 RCU 回调,同时需要在内核命令行里显式声明哪些 CPU 进入 full dynticks。
bash复制grubby --update-kernel=/boot/vmlinuz-$(uname -r) \
--args="nohz_full=2-3 rcu_nocbs=2-3"
我在自己的测试机上把 2-3 号 CPU 设为 full dynticks 后,那几个核上绑定的计算任务抖动明显降低。但代价是 0-1 号核的中断压力变大,所以要充分评估其余 CPU 的负载能力,别把系统里重要的中断处理核压垮。
3.3 CONFIG_PREEMPT 抢占模型
抢占模型决定内核态代码能否被高优先级任务打断。menuconfig 里通常是三选一或四选一:
CONFIG_PREEMPT_NONE:基本不抢占内核态,吞吐最好,但调度延迟可能很大。CONFIG_PREEMPT_VOLUNTARY:内核在特定调度点主动让出,适合桌面,延迟比 NONE 好。CONFIG_PREEMPT:内核绝大多数位置可抢占,调度延迟明显改善,实时性更好。CONFIG_PREEMPT_RT(实时补丁后):几乎全内核可抢占,专门为硬实时场景设计。
发行版服务器一般是 NONE 或 VOLUNTARY。我遇到过一个数据库项目,因为内核态网络处理时间过长,导致高优先级任务偶发几十毫秒延迟,把调度模型改成 CONFIG_PREEMPT 后最坏延迟降到个位数毫秒,代价是吞吐量下降约 2%。性能调优是取舍,这个模型选型必须和业务匹配。
3.4 CONFIG_PREEMPT_RT:实时抢占
这个选项通常需要应用 RT 补丁(-rt 分支),它会进一步把自旋锁、中断处理等关键路径改造成可抢占,并且把中断线程化、引入 PI(优先级继承)互斥锁,让高优先级任务不会被低优先级任务持有的锁卡住。
很多刚接触实时 Linux 的同学以为开了 CONFIG_PREEMPT_RT 就等于“所有任务都不会被抢断”,这是误解。它是把最坏情况延迟大幅压缩,比如从几十毫秒降到几十微秒级别,但你必须配合正确的任务优先级、CPU 绑核和避免内存锁页等工程手段。
我做过一个视觉引导机械臂项目,控制器跑在 RT 线程里,要求 1ms 周期内完成位置计算和指令下发。普通内核在负载高时抖动到 6-7ms,切到 PREEMPT_RT 后最坏延迟稳定在 200µs 以内。前提是编译时同时开启 CONFIG_IRQ_FORCED_THREADING,并且把不必要的驱动中断做成线程,否则中断优先级仍然会抢占控制任务。注意,RT 补丁在硬件兼容性上要求更高,老网卡驱动、部分 GPU 驱动可能在 RT 模式下表现异常,上生产前一定要做稳定性回归。
3.5 CONFIG_SCHED_DEADLINE:最严格的确定性调度类
CONFIG_SCHED_DEADLINE 开启 SCHED_DEADLINE 调度策略,它给每个实时任务设置三个参数:运行时间(runtime)、周期(period)、截止时间(deadline)。内核按照截止时间优先进行调度,理论上能比 SCHED_FIFO / SCHED_RR 提供更强的时序保证。
适合 PID 控制、音视频同步这类确定周期任务。举个例子:一个任务周期是 4ms,每个周期内最多运行 1ms,那 deadline 就可以设成 4ms。用 chrt 可以切换:
bash复制sudo chrt -d --sched-runtime 1000000 \
--sched-deadline 4000000 \
--sched-period 4000000 0 ./control_loop
这个调度类在主流内核里已经相当稳定了,但使用时需要先确认 CPU 负载不能超过 100% 的可调度分析边界。如果 CPU 已经被其他实时任务占满,即使开了 SCHED_DEADLINE,也一样会出现超时。项目里最好保留一部分 CPU 余量用于系统调用和中断线程。
3.6 CONFIG_FAIR_GROUP_SCHED 与 CONFIG_CFS_BANDWIDTH:组调度与带宽控制
CONFIG_FAIR_GROUP_SCHED 允许把进程放进 cgroup 的 cpu 子系统中,实现按组分配 CPU 份额。它解决的是“多个业务队抢占同一个 CPU”的问题,在多租户场景下非常实用。
配合 CONFIG_CFS_BANDWIDTH,还能设置某个 cgroup 的 CPU 使用上限。我在容器化环境里通常用这两个选项为不同业务配置不同的 cpu.weight 和 cpu.max,避免某个异常容器把整台机器的 CPU 耗光。
bash复制# 限制某个 cgroup 最大使用 2 个 CPU
echo "100000 50000" > /sys/fs/cgroup/mygroup/cpu.max
但有个坑:cfs bandwidth 的统计粒度是 1ms 周期,如果某业务的 CPU 使用率在毫秒级波动,会看到限流并不平滑。从性能角度,频繁的带宽限流也会引入额外的调度唤醒开销,所以不要为了省事给所有容器都加上 max 限制,能靠权重分配解决的就别上硬上限。
3.7 CONFIG_SCHED_AUTOGROUP:自动组调度
CONFIG_SCHED_AUTOGROUP 是从桌面场景演进出来的特性,作用是把同一个会话/终端开启的任务自动分到一个调度组,防止大量后台任务把前台交互饿死。
这个功能在桌面发行版默认开启,对普通用户很友好。但如果你的机器是纯服务器,没有交互式任务需求,建议关闭它。我在高负载服务器上测试过,AUTOGROUP 开启后,内核需要为每个会话维护自动分组,进程较多时会引入额外的调度开销,延迟尾值波动会高一点。
另外要配合同步检查 systemd 是否依赖该特性。很多 systemd 服务默认情况下不依赖它,所以关掉影响通常不大。
3.8 CONFIG_NUMA_BALANCING:NUMA 感知调度
多路服务器里,CPU 访问不同内存节点的延迟差异很大。CONFIG_NUMA_BALANCING 会定期扫描页表,把进程调度到更靠近其内存的 CPU 上,并自动迁移内存页,降低远端内存访问延迟。
对多路 CPU 的数据库、虚拟机场景提升明显。但也带来额外开销:页表扫描和页面迁移本身会消耗 CPU。我在一台双路服务器上测试过,默认自动均衡可以带来 5%-10% 的吞吐提升,但延迟波动会增大。
实时场景要格外小心。NUMA balancing 的页面迁移可能让一个实时线程在节骨眼上发生缺页,引入不可控延迟。所以我做实时系统时直接关掉它,配合 numactl --cpunodebind 手动绑定进程和内存。
3.9 CONFIG_SCHED_SMT:超线程调度策略
CONFIG_SCHED_SMT 控制调度器对 Intel Hyper-Threading / AMD SMT 的判断方式。开启后,调度器在选核时会尽量避免把任务放到同一个物理核的两个逻辑线程上,除非没有空闲物理核可用。
这种“先填满不同物理核”的策略对大多数计算密集任务是好消息,能明显改善同核争抢带宽的问题。但我测试过一些场景,比如短生命周期线程特别多的网关服务,开启 SMT 调度反而增加了选核复杂度,吞吐略降。
选择上没有标准答案,比较实用的做法是编译时保持开启,运行时用刷核参数或 taskset 做实验。如果你的机器开启了超线程但 core 的数量足够大,也可以在内核命令行加 smt=off 直接关闭 SMT,某些高吞吐场景下散热和电源表现会更好。
3.10 CONFIG_SCHEDSTATS 与 CONFIG_SCHED_DEBUG:调度器统计与调试开关
CONFIG_SCHEDSTATS 和 CONFIG_SCHED_DEBUG 一个负责收集调度延迟、负载均衡等统计信息,一个提供 sched_features、sched_debug 等调试接口。它们排解疑难问题非常有用,但都有额外开销。
我在定位调度延迟问题时,会临时开这两个选项重新编译一次,然后通过 /proc/schedstat 和 /sys/kernel/debug/sched/debug 查看数据。定位后立刻关掉并重新编译。生产内核保持开启调试选项是最常见的性能损耗来源之一,很多人不知道自己的延迟尾值是被这个拖高的。
如果必须保留调试能力,至少把 CONFIG_SCHEDSTATS 关掉,CONFIG_SCHED_DEBUG 在需要时再加载 debugfs 动态观察。
3.11 配置落盘与编译前验证
上面这些选项都在 make menuconfig 的 Kernel Features 或 General Setup 下。手动改完一定要重新检查 .config,确认项没有被依赖规则自动关闭。
我习惯用 scripts/config 做批量调整,可读性好且不容易误触菜单:
bash复制./scripts/config --enable CONFIG_SCHED_DEADLINE \
--enable CONFIG_CFS_BANDWIDTH \
--disable CONFIG_SCHED_AUTOGROUP \
--set-str CONFIG_HZ 1000 \
--enable CONFIG_NO_HZ_FULL
make olddefconfig
grep -E 'CONFIG_HZ|CONFIG_SCHED_DEADLINE|CONFIG_NO_HZ_FULL' .config
make olddefconfig 会用现有配置生成新内核支持的默认补全,这个步骤能避免因为新老内核配置项变化导致的编译失败。改完检查一下目标项是否为 =y、=m,特别留意 CONFIG_HZ=1000 时还要确认 CONFIG_HZ_1000=y。
4. 编译安装与启动校验
4.1 正式编译
配置确认无误后就进入编译阶段。我不建议一上来就 make -j$(nproc) 拉满,除非你的机器内存足够大。内存不足时 CPU 并行编译很容易 OOM,我通常会留一些余量:
bash复制make -j$(($(nproc) - 2))
编译过程中第一次出错很常见。除了缺依赖,最常见的就是编译器版本太新或太老,导致某些内联函数展开报错。这个时候优先检查工具链版本是否在源码目录 Documentation/process/changes.rst 的要求范围内,再去搜报错信息。
4.2 安装模块与内核
编译完成后按顺序安装模块和内核镜像:
bash复制sudo make modules_install
sudo make install
sudo update-grub # 或 grub-mkconfig -o /boot/grub/grub.cfg
make install 会把新内核安装到 /boot 并更新 initramfs。如果机器上有多个内核,启动菜单里默认会选新的,但最好进 GRUB 确认一下,避免出现启动后才发现版本不对的情况。
4.3 确认配置生效
启动新内核后,第一步确认运行版本,第二步确认编译时的配置是否真的被读入。
bash复制uname -r
zcat /proc/config.gz | grep CONFIG_HZ
cat /sys/kernel/debug/sched/preempt 2>/dev/null
# 查看命令行中 nohz_full 等参数
cat /proc/cmdline
如果 /proc/config.gz 不可用,说明 CONFIG_IKCONFIG_PROC 没开。我建议平时保持开启,排查问题时能直接确认配置,不用重新翻 .config 文件。
5. 常见问题与排查技巧实录
5.1 高 HZ + NO_HZ_FULL 不等于实时
有次一个同事把 HZ 改成 1000、开启 NO_HZ_FULL 后在文档里写“已完成实时优化”。我们跑 cyclictest 发现最大延迟还是接近 10ms,排查后才发现抢占模型还是默认的 CONFIG_PREEMPT_VOLUNTARY。实时性改造的核心是抢占模型和实时调度策略,HZ 和 NO_HZ_FULL 只是辅助,绝不能混为一谈。
5.2 编译失败但找不到原因
内核编译报错信息里经常看不到具体原因,只会在日志末尾给一个错误层级。我在编译 6.x 内核时遇到过 BTF 相关错误,当时报错信息指向 pahole 版本过低,升级 dwarves 后解决。遇到这种问题先确认工具链版本,再看 .config 中是否存在依赖项冲突,不要急着回去改代码。
5.3 启动后配置没生效
如果改了配置、重新编译、启动的新内核,但 zcat /proc/config.gz 显示某些选项仍旧是旧值,基本是因为启动时 GRUB 加载了旧内核镜像。检查 /boot/vmlinuz-$(uname -r) 是否指向新编译出的文件即可。另外确认 make install 没有因为磁盘 /boot 空间不足而失败。
5.4 性能反而下降
改完配置性能下降通常有两个原因:一是打开了其他调试选项(比如 SCHEDSTATS)没有关;二是 CPU 进入节能状态,调度频率变化导致延迟变大。先到 /sys/devices/system/cpu/cpufreq/ 看下当前调频策略,如果被切到 powersave,试试把性能策略固定下来再跑一次测试。
我这边踩过一次很典型的坑:只为了看调度器调试信息开启了 CONFIG_SCHED_DEBUG,结果生产内核的上下文切换开销多了 4%,延迟尾值涨了一截。后来把调试选项关掉重编,问题立刻消失。这就是为什么我一直强调“性能验证必须和配置变更一一对应”,否则你永远不知道提升到底是谁带来的。
写在最后:调优前先想清楚目标
编译调度器配置不是把网上的“性能优化清单”抄一遍就完事。我见过很多人把所有实时相关选项全部打开,结果系统从启动到关机都处于高负载、高切换状态,吞吐量惨不忍睹。真正的做法是先明确目标——你是要吞吐、要低延迟,还是要确定性,然后围绕目标选配置项,再通过基线测试验证效果。
如果你刚接触这块,我的建议是从一台备用的物理机或测试虚拟机开始,先备份原来的内核,再按这篇文章的流程改 1-2 个最相关的配置项,跑几天业务观察效果稳定后再继续下一步。内核编译和调优不是一次性的操作,而是一个持续验证、不断迭代的过程,耐心比技术本身更重要。
