2、内核抢占模型:Linux内核抢占模型详解
说到实时性,内核抢占模型是绕不开的话题。我刚开始接触Linux实时系统时,也被PREEMPT_NONE、PREEMPT_VOLUNTARY、PREEMPT、PREEMPT_RT这几个选项搞得一头雾水。说白了,它们决定了内核在什么时机可以被打断,直接影响了系统的实时响应能力。
今天我就把这四种模型掰开揉碎了讲清楚。你想想看,一个任务正在内核里跑着,来了个更高优先级的任务要执行——内核能不能立刻停下当前工作去响应它?这就是抢占模型要解决的核心问题。
2.1 四种抢占模型的核心差异
先看一张总览图,把四种模型的关系和特点理清楚。
2.2 PREEMPT_NONE:服务器模式
这是内核的默认配置,也叫"无抢占"模式。内核代码在执行时,除非主动调用调度器,否则不会被高优先级任务打断。说白了,内核认为自己的活儿最重要,外部任务等着就好。
特点:
- 抢占点最少:只在系统调用返回用户空间时检查是否需要调度
- 吞吐量最高:没有额外的抢占检查开销
- 延迟最大:一个低优先级任务在内核里跑,高优先级任务可能等很久
我在项目中遇到过一台数据库服务器,跑的是PREEMPT_NONE。DBA抱怨偶尔有查询延迟抖动,查了半天发现是某个磁盘IO操作在内核路径里卡了上百毫秒。嗯,对于服务器来说这通常不是问题,但如果你对响应时间有要求,就得换模型了。
2.3 PREEMPT_VOLUNTARY:桌面模式
这个模型在PREEMPT_NONE的基础上,在内核代码中显式插入了更多的抢占点。说白了就是内核开发者手动标注了一些"可以安全抢占"的位置,比如在等待锁、等待IO完成时主动让出CPU。
特点:
- 抢占点增加:在cond_resched()等显式调用处可抢占
- 延迟降低:从几十毫秒降到几毫秒级别
- 吞吐量略有下降:多了些调度检查
我的经验:桌面发行版(如Ubuntu、Fedora)默认就是PREEMPT_VOLUNTARY。它兼顾了日常使用的流畅度和吞吐量。如果你做的是普通嵌入式设备,比如智能家居网关,这个模型通常够用。
2.4 PREEMPT:低延迟抢占
这个模型就激进了。它允许在内核空间的几乎所有位置进行抢占,只要当前没有持有自旋锁。内核代码在执行过程中,随时可能被高优先级任务打断。
特点:
- 抢占点遍布内核:除了持有自旋锁的临界区,其他地方都可抢占
- 延迟进一步降低:典型延迟在100μs~1ms
- 吞吐量下降明显:每个指令路径都要检查抢占标志
我记得有个做音频处理的客户,用PREEMPT模型跑ALSA驱动,延迟抖动从原来的5ms降到了200μs左右。但代价是CPU占用率高了约5%。
注意:PREEMPT模型下,如果驱动代码写得不好——比如在自旋锁保护的临界区里做了耗时操作——整个系统的实时性会瞬间崩塌。我曾经调试过一个网卡驱动,就是在临界区里做了内存分配,导致高优先级任务被阻塞了3ms。
2.5 PREEMPT_RT:完全实时抢占
这是实时性的终极方案。PREEMPT_RT通过一系列补丁,实现了内核的完全可抢占。它做了几件关键的事:
- 中断线程化:把中断处理程序变成内核线程,可以被调度
- 自旋锁替换:大部分自旋锁被替换成可睡眠的互斥锁
- 优先级继承:防止优先级反转
- 高精度定时器:支持微秒级定时精度
特点:
- 抢占点无处不在:内核所有位置都可抢占
- 延迟极低:典型延迟5~100μs
- 吞吐量损失最大:约10%~30%
关键区别:PREEMPT_RT不是主线内核的一部分,需要打补丁。不过好消息是,从Linux 5.x开始,大部分PREEMPT_RT特性已经逐步合入主线。到了6.x时代,PREEMPT_RT的配置选项已经可以直接在主线内核中启用。
2.6 如何选择与配置
选择哪种模型,说白了就是回答三个问题:
- 你的系统对延迟有多敏感?——毫秒级?微秒级?还是无所谓?
- 你能接受多少吞吐量损失?——CPU资源够不够挥霍?
- 你的硬件平台是什么?——ARM Cortex-A系列和x86的实时表现差异很大
我整理了一个选择指南,方便你快速决策:
| 应用场景 | 推荐模型 | 理由 |
|---|---|---|
| Web服务器、数据库 | PREEMPT_NONE | 追求吞吐量,延迟不敏感 |
| 桌面系统、普通嵌入式 | PREEMPT_VOLUNTARY | 平衡流畅度和性能 |
| 多媒体播放、游戏 | PREEMPT | 需要毫秒级响应 |
| 工业控制、机器人 | PREEMPT_RT | 需要微秒级确定性 |
| 航空航天、医疗设备 | PREEMPT_RT | 硬实时要求,不容闪失 |
2.7 配置方法
配置抢占模型有两种方式:
方式一:内核编译配置
# 在menuconfig中设置
Kernel Features --->
Preemption Model --->
( ) No Forced Preemption (Server)
( ) Voluntary Kernel Preemption (Desktop)
( ) Preemptible Kernel (Low-Latency Desktop)
( ) Fully Preemptible Kernel (Real-Time)
方式二:命令行参数(仅限PREEMPT_RT)
# 在grub中追加
preempt=full
我的建议:如果你不确定选哪个,先从PREEMPT_VOLUNTARY开始。用cyclictest跑一下延迟数据,如果不能满足需求,再升级到PREEMPT或PREEMPT_RT。我曾经帮一个客户做方案选型,他们一开始就上了PREEMPT_RT,结果发现硬件太弱,吞吐量掉得厉害。后来换成PREEMPT,延迟只多了50μs,但吞吐量提升了20%。
2.8 避坑指南
最后分享几个我踩过的坑:
- 别在PREEMPT_RT下用老的驱动:有些驱动在自旋锁里做了schedule(),这在PREEMPT_RT下会死锁。我曾经调试过一个USB驱动,花了三天才找到问题。
- 注意中断负载:PREEMPT_RT把中断线程化了,但如果某个中断来得特别频繁,线程调度开销会很大。我建议把高频中断绑到特定CPU上。
- 测试要上真实硬件:QEMU里跑PREEMPT_RT看不出真实延迟。我见过有人在模拟器上测出10μs的延迟,上了真机直接飙到200μs。
嗯,抢占模型就讲到这里。记住一句话:没有最好的模型,只有最适合你场景的模型。选型时多做测试,别偷懒。