4. PREEMPT_NONE 模型:设计目标、适用场景(服务器)与性能特征
PREEMPT_NONE 是 Linux 内核默认的抢占模型(对应内核配置 CONFIG_PREEMPT_NONE=y),其核心设计哲学是“吞吐优先,延迟让位”。该模型几乎完全禁用了内核态抢占,仅在返回用户态时执行调度检查,是四种抢占模型(NONE / VOLUNTARY / LL / RT)中抢占开销最低、但最差情况延迟最高的配置。
4.1 设计目标:最大化吞吐量
PREEMPT_NONE 的设计目标可归纳为三个层次:
- 消除内核抢占开销:进程在内核态执行系统调用时,不会被其他进程抢占,避免了抢占点检查、上下文切换和缓存污染。
- 保持内核路径原子性:系统调用、中断下半部(softirq / tasklet)和内核线程在临界区内连续执行,无需考虑被中途打断后的数据一致性。
- 优化 CPU 缓存利用率:长时运行的内核路径可以充分利用 L1/L2 缓存的热数据,减少因频繁切换导致的缓存失效。
核心权衡: PREEMPT_NONE 用“不可预测的调度延迟”换取“最高的系统吞吐量”。在服务器场景中,这种权衡通常是合理的——用户更关心每秒处理多少请求,而非单个请求的尾部延迟。
4.2 适用场景:吞吐优先的服务器工作负载
PREEMPT_NONE 最适合以下服务器场景:
| 场景 | 典型负载 | 为什么适合 PREEMPT_NONE |
|---|---|---|
| Web 服务器 | Nginx / Apache 处理 HTTP 请求 | 大量短连接,吞吐量是首要指标;毫秒级延迟波动可接受 |
| 数据库服务器 | MySQL / PostgreSQL 查询处理 | 磁盘 I/O 和内存访问占主导,内核抢占节省的开销可忽略 |
| 文件服务器 | NFS / Samba 文件共享 | 批量数据传输,延迟不敏感,吞吐量决定用户体验 |
| 批处理 / 离线计算 | Hadoop / Spark 作业 | 任务级并行,单个任务的完成时间不重要,整体吞吐是关键 |
| 虚拟化宿主机 | KVM / Xen 管理虚拟机 | 虚拟机调度由 hypervisor 管理,宿主机内核抢占模型影响较小 |
经验法则: 如果你的系统对 99.9 百分位延迟 没有硬性要求(例如 < 10ms),且 CPU 密集型或 I/O 密集型工作负载占主导,PREEMPT_NONE 通常是默认且最优的选择。
4.3 性能特征:吞吐量优势与上下文切换成本
PREEMPT_NONE 的性能特征可以通过以下关键指标量化:
- 上下文切换率:相比 PREEMPT_VOLUNTARY 降低约 30%~50%,相比 PREEMPT_RT 降低 60%~80%。
- 系统调用延迟:平均延迟最低,因为无需在每次系统调用入口/出口检查抢占标志。
- 中断响应延迟:中断处理完成后,高优先级任务必须等待当前内核路径执行完毕(直到返回用户态)才能获得 CPU。
- 缓存污染:最小化,因为内核路径连续执行,不会因抢占导致 TLB / cache 刷新。
4.4 延迟分析:最差情况延迟的来源
PREEMPT_NONE 的延迟问题主要源于以下三个内核路径:
- 系统调用长路径:例如
read()从磁盘读取大量数据时,内核可能在 D state 下阻塞数毫秒甚至数十毫秒,期间高优先级任务无法抢占。 - 中断下半部(softirq):网络栈的 NET_RX_SOFTIRQ 可能一次处理数百个数据包,占用 CPU 数百微秒到数毫秒。
- 内核锁竞争:当多个 CPU 同时争用同一把锁(如
tasklist_lock),等待锁的 CPU 可能自旋等待数百微秒。
关键警告: 在 PREEMPT_NONE 下,最差情况延迟(Worst-Case Latency)可能达到 10ms ~ 100ms 级别。对于音频处理、工业控制、机器人等实时应用,这种延迟是不可接受的。即使对于某些软实时服务器场景(如高频交易),PREEMPT_NONE 也可能导致尾部延迟超标。
4.5 内核配置与运行时验证
确认当前系统是否使用 PREEMPT_NONE 模型:
# 方法一:查看内核配置
zcat /proc/config.gz | grep CONFIG_PREEMPT
# 方法二:查看 /proc/sys/kernel/sched_features
cat /proc/sys/kernel/sched_features
# 方法三:使用 uname 查看内核抢占标记
uname -a
# 输出示例(PREEMPT_NONE 无特殊标记):
# Linux server01 5.15.0-91-generic #101-Ubuntu SMP ... x86_64 GNU/Linux
# 注意:PREEMPT_NONE 内核在 uname 输出中不会出现 "PREEMPT" 字样
测量 PREEMPT_NONE 下的调度延迟:
# 使用 cyclictest 测量(需要安装 rt-tests)
sudo cyclictest -l 100000 -m -n -p 99 -i 1000 -h 100
# 输出示例(重点关注最大延迟):
# T: 0 (12345) P: 99 I: 1000 Min: 2 Act: 5 Avg: 4 Max: 1876
# 最大延迟 1876 µs ≈ 1.9 ms,远高于 PREEMPT_RT 的典型值(< 50 µs)
性能数据参考: 在相同的 x86_64 服务器硬件上(Intel Xeon Gold 6248,2.5GHz),使用 cyclictest 测量 100 万次循环:
- PREEMPT_NONE:最大延迟 1.5ms ~ 5ms,平均延迟 3µs ~ 8µs
- PREEMPT_RT:最大延迟 15µs ~ 50µs,平均延迟 2µs ~ 5µs
PREEMPT_NONE 的最大延迟比 PREEMPT_RT 高出 30~100 倍。
- PREEMPT_NONE:最大延迟 1.5ms ~ 5ms,平均延迟 3µs ~ 8µs
- PREEMPT_RT:最大延迟 15µs ~ 50µs,平均延迟 2µs ~ 5µs
PREEMPT_NONE 的最大延迟比 PREEMPT_RT 高出 30~100 倍。
4.6 何时应该避免使用 PREEMPT_NONE
以下场景应主动切换到 PREEMPT_VOLUNTARY 或 PREEMPT_LL:
- 交互式桌面系统:用户期望鼠标点击、键盘输入得到毫秒级响应。
- 音频/视频播放:缓冲区欠载(underrun)会导致卡顿或爆音。
- 软实时服务器:如游戏服务器、视频流服务器,尾部延迟影响用户体验。
- 嵌入式控制:即使非硬实时,也需要可预测的响应时间。
对于这些场景,PREEMPT_NONE 的“吞吐优先”策略反而会降低整体服务质量——因为单个慢请求可能阻塞后续所有请求的处理。