先搞清楚一个概念:内核态抢占到底意味着什么
很多开发者在看内核配置时看到 CONFIG_PREEMPT 相关选项,但不太理解它为什么会影响系统行为。简单说,抢占模型决定了一个正在内核态执行系统调用的进程,能不能被更高优先级的任务打断。如果是不可抢占内核,进程即使陷入内核执行一个耗时很长的遍历,也只能等这个系统调用自然结束或主动调用调度函数,才会让出 CPU。这个等待时长,就是调度延迟的一部分。

用户态的抢占大家很熟悉,时间片到点就切换。内核态抢占不是简单地把内核也“时间片分片”,它意味着在内核代码的任意点,可能需要考虑并发的其他任务。这不仅仅是开关,它会影响同步原语的实现、锁的代价、缓存行为,甚至驱动代码的兼容性。所以 Linux 在很久以前就在编译期提供了三种模型:PREEMPT_NONE、PREEMPT_VOLUNTARY 和 PREEMPT。
三种模型是怎么设计出来的
从内核 2.6 开始,抢占模型一直是构建配置里的核心选项。三者并不是某个功能的有无,而是“内核代码里哪些位置允许发生调度”的尺度。
PREEMPT_NONE:优先保证吞吐量
这种模型下,内核态代码基本是不可抢占的。当一个任务进入内核执行系统调用时,它会一直运行到主动调用 schedule、发生阻塞、或者通过中断/异常返回路径中的调度检查。普通网络包处理、磁盘IO等在大多数路径上都能很快返回,但一些文件系统遍历、内存分配和迭代操作,可能花费数毫秒甚至更长时间。如果系统里有优先级极高的实时任务,它会因为这颗 CPU 正在忙别的事而白白等待。
选择 PREEMPT_NONE 的主要理由是为吞吐量和 CPU 亲和性。减少抢占点意味着更少的上下文切换、更稳定的 cache 状态,对数据库、存储服务等负载通常更友好。
PREEMPT_VOLUNTARY:在关键耗时路径上主动让位
这层的思路比较务实:不要求内核处处可抢占,但在那些明显可能耗时的循环或操作里,显式调用 cond_resched() 检查调度需求。从内核 2.6.36 开始,当时的内核已经逐步把很多长循环改成通过 cond_resched() 给调度留出窗口。
效果是:大多数交互场景下,这样的调度点能明显降低最长调度延迟,但还不够严谨。因为调度点不是全覆盖的,一个没有显式让位的内核路径仍然可能让高优先级任务等很久。可以说 PREEMPT_VOLUNTARY 是介于吞吐量和响应性之间的折中,比 PREEMPT_NONE 稍微牺牲一点吞吐,换来更平滑的交互体验。
PREEMPT:内核态也允许随时抢占
这个模型下,只要内核没有处于中断上下文,并且没有持有某些关键锁(如自旋锁),当前任务可以被高优先级任务立即抢占。换句话说,一个普通进程在内核里跑计算时,实时任务可以在很短的时间内拿到 CPU。这通常将最坏调度延迟从毫秒级降低到几十微秒甚至更低。
代价也明显:可抢占内核需要更精细的锁机制,spinlock 等同步原语的开销变大,很多内核代码路径要考虑更多并发场景。同时,编译出的内核调度路径更频繁,吞吐量会有所下降。社区用一个专用术语“PREEMPT_RT”表示在上面进一步增强为完全实时抢占,但常规的 CONFIG_PREEMPT 已经能覆盖大部分桌面与实时敏感负载。
一张表看清 PREEMPT_NONE、PREEMPT_VOLUNTARY 与 PREEMPT
这里列一个简单对比,方便在选型时快速判断。
| 对比项 | PREEMPT_NONE | PREEMPT_VOLUNTARY | PREEMPT |
|---|---|---|---|
| 内核态可抢占性 | 基本不可抢占 | 仅显式调度点可让出 | 大多数内核代码可抢占 |
| 调度点类型 | 返回用户态、阻塞、主动schedule | 外加 cond_resched() 等 | 任意安全点,几乎全路径 |
| 最坏调度延迟 | 高,可达数十毫秒 | 中,但无严格上界 | 低,可达微秒级 |
| 同步原语开销 | 最低 | 较低 | 较高 |
| 系统吞吐量 | 最高 | 中等 | 略降 |
| 典型场景 | 数据库、存储、批处理 | 桌面、交互式负载 | 实时工业、音频、嵌入式 |
| 常见内核配置 | CONFIG_PREEMPT_NONE=y | CONFIG_PREEMPT_VOLUNTARY=y | CONFIG_PREEMPT=y |
注意最坏调度延迟并没有绝对数值,它取决于内核代码路径长度。这里的“微秒级”通常指在 PREEMPT 模型下,高优先级任务从被唤醒到开始运行的中断延迟,实际也需要硬件和负载支撑。
怎么确认当前内核用的是哪种模型
不必重新编译内核,大多数发行版可以直接从配置目录里读到 CONFIG_PREEMPT 相关项。先用这组命令确认。
# 如果发行版开启 /proc/config.gz
zcat /proc/config.gz | grep -E 'CONFIG_PREEMPT'
# 或者从 /boot/config-$(uname -r) 查看
grep CONFIG_PREEMPT /boot/config-$(uname -r)
# 动态抢占模式还可以通过内核命令行查看
cat /proc/cmdline | tr ' ' '\n' | grep preempt
输出一般会看到类似这种结果:CONFIG_PREEMPT_VOLUNTARY=y,说明当前是自愿抢占模型。如果编译时开启了 CONFIG_PREEMPT_DYNAMIC,则启动参数 preempt=none/voluntary/full 可以在运行时覆盖模型选择,此时 uname 显示 “PREEMPT_DYNAMIC”。
换一个角度:为什么 PREEMPT_NONE 不是你理解的“没有抢占”
很多文章把 PREEMPT_NONE 翻译成“非抢占内核”,容易让人误以为它完全没有调度能力。实际上它依然保留硬实时和用户态抢占,调度器照常运作。只是对于已经在内核态运行的任务,它不会因为一个更高优先级任务而被打断。比如一个进程正在内核做 page cache 回收,即使另一个更高优先级进程需要 CPU,也得等它完成当前临界区或主动让出。
这意味着 PREEMPT_NONE 下的调度延迟是“不可预测”的,但并不是“特别差”。因为大多数系统调用很短,只有在极端情况下(比如遍历百万级 inode、长时间关中断),高优先级任务才可能等待很久。对于很多服务器场景,这种不可预测性可以接受。
选择 PREEMPT 前,先把这两个误区放一边
误区一:PREEMPT_VOLUNTARY 是 PREEMPT 的轻量版。它的确更轻,但不是同一机制的简化,而是“选择性让位”。它不能提供最坏延迟保证,对于严格实时任务,它并不能满足需求。如果你需要确定性,就该直接上 PREEMPT,甚至 PREEMPT_RT。
误区二:开启了 CONFIG_PREEMPT,所有内核代码都能立即被抢占。这个说法不完全对。中断、NMI、自旋锁临界区、某些 RCU 读临界区仍然不可抢占。只是绝大多数系统调用路径是安全的。所以千万别以为 PREEMPT 是万能灵药,它解决的问题是高优先级任务被普通任务拖住,而不是干掉所有延迟根源。
在真实项目里,这三个模型分别出现在哪
设想一下你在一家做边缘网关的团队,设备需要同时处理传感器采集、数据上报和本地维护交互。这种系统一般会选 PREEMPT_VOLUNTARY,既能保证数据吞吐,也不会让页面点击产生明显卡顿。而如果你在做数控机床控制器,控制周期甚至小于 1 毫秒,那必须用 CONFIG_PREEMPT 甚至 PREEMPT_RT 补丁,把调度延迟控制在一个稳定的范围内。
再比如常见的分布式存储服务器。这类机器 CPU 资源大部分消耗在网络协议栈和存储引擎,上下文切换会直接影响 cache 命中率。通常保持编译时的默认 PREEMPT_NONE 或 PREEMPT_VOLUNTARY,再用 CPU 绑核、中断亲和等手段解决延抖,而不是盲目打开内核抢占。
如何动手切换和验证?给你一套简单的流程
如果你的内核支持 CONFIG_PREEMPT_DYNAMIC,工作会比较简单。修改 GRUB 启动参数即可,不用重新编译。
# 编辑 /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash preempt=full"
# 更新引导
sudo update-grub
sudo reboot
重启后可以通过 cat /proc/cmdline 确认参数是否生效。如果还想更合理地评估延迟,推荐用 rt-tests 里的 cyclictest。这是一个标准测试,它会产生实时线程并通过定时唤醒测量调度延迟。
# 安装 rt-tests 后,用 root 运行
sudo cyclictest -m -n -p80 -i1000 -l100000
运行完后会输出 min/max/avg 延迟,重点看 max 值。不同抢占模型下的 max 差异会非常明显。在同样的硬件和负载上,PREEMPT_NONE 可能轻松看到几毫秒的峰值,而 PREEMPT 往往能维持在微秒级别。不过测试环境要保证 CPU 不被干扰,比如禁用屏幕保护、避免频率调整等。
选型建议:别急于往“全可抢占”靠
很多开发者第一次接触这些选项时,会觉得 PREEMPT 肯定更好,毕竟延迟低。但实际上,内核可抢占性必须与驱动、实时性要求、业务负载一起看。
- 如果跑的是数据库、文件服务器、大数据节点,核心诉求是吞吐和稳定性,选 PREEMPT_NONE 或 PREEMPT_VOLUNTARY 更合理。
- 如果设备是桌面、笔记本或交互式终端,选 PREEMPT_VOLUNTARY 即可,极少数需要音频/多媒体实时场景才值得开 PREEMPT。
- 如果做工业控制、仪器仪表、机器人,调度延迟不能出现尖峰,那 PREEMPT 是起点,需要极致实时再上 PREEMPT_RT。
归根结底,这是一个工程取舍,不是“更高版本”。内核维护者之所以保留三种模型,就是因为它们在不同负载下各自有价值。先看清自己业务对延迟的容忍度,再决定要不要为额外抢占能力付出吞吐和兼容性的成本。
收尾:理解抢占模型,就不容易被“性能参数”牵着走
重新看一遍内核编译选项,你会发现 PREEMPT_NONE、PREEMPT_VOLUNTARY、PREEMPT 其实是在回答一个问题:当高优先级任务需要 CPU 时,当前普通任务是否愿意“立刻放下手里的内核工作”。答案不同,内核的结构设计和运行行为也不同。
对绝大多数应用来说,Linux 自带的默认模型已经够用。只有当你面对延迟抖动无从下手,或者需要满足产品实时规格时,才值得重新审视这几个选项。把本文中的对比表收藏好,下次编译内核或调优系统时,你会知道该把注意力放在哪里。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/1032/