Linux 实时调度优化:4 种抢占模型的选择与配置
一、简介
1.1 技术背景
Linux 自 2.6 内核引入内核抢占机制前,内核态代码一旦执行完毕才会交出 CPU,高负载场景下调度抖动极其严重。随着工业自动化、伺服控制、自动驾驶、5G 边缘网关等硬实时场景落地,社区逐步迭代出四层递进式抢占模型:PREEMPT_NONE、PREEMPT_VOLUNTARY、PREEMPT、PREEMPT_RT,四层模型通过内核编译开关切换,核心差异是内核临界区可被抢占的范围、中断处理可抢占性、自旋锁休眠改造三大维度。
传统开发者普遍存在选型误区:工控项目无脑编译 PREEMPT_RT 内核、云服务器随意开启全抢占模式,前者增加系统开销、引发内存碎片与锁竞争死锁;后者导致业务吞吐率暴跌、上下文切换风暴。四种抢占模型没有绝对优劣,只有场景匹配度差异,精准区分模型延迟特征、掌握内核配置、量化测试抖动,是实时 Linux 开发的核心必备能力。
1.2 实际应用价值
- 嵌入式工控开发:EtherCAT 伺服、运动控制、PLC 软逻辑任务对调度抖动要求 1ms~100us,选错抢占模型会导致电机丢脉冲、设备报警;
- 边缘实时网关:工业数据采集、现场总线转发任务,需平衡吞吐量与响应延迟;
- 通用服务器:Web、数据库、存储服务追求高吞吐,低抢占开销优先;
- 桌面 / 低延迟终端:音视频直播、实时音频处理,需要毫秒级软实时响应。
掌握四种抢占模型的配置、对比、调优方法,可支撑项目需求指标落地,同时为内核调度论文提供完整实验数据、测试代码与对比基准,具备工程落地与学术调研双重价值。
二、核心概念与术语详解
2.1 内核抢占基础定义
内核抢占:CPU 运行内核态代码时,若出现更高优先级就绪任务,调度器可中断当前内核流程、切换至高优任务。抢占计数器preempt_count是控制核心:数值为 0 代表可抢占,大于 0 代表持有锁 / 关闭抢占,禁止调度抢占。
- 用户态抢占:所有 Linux 默认支持,用户进程随时可被高优任务抢占;
- 内核态抢占:由 CONFIG_PREEMPT 系列开关控制,四种模型本质是对内核临界区抢占约束的分级放开。
2.2 四种抢占模型核心释义
| 内核配置项 | 官方名称 | 核心抢占规则 | 最大调度抖动 | 开销等级 |
|---|---|---|---|---|
| CONFIG_PREEMPT_NONE | No Forced Preemption(Server) | 完全禁止内核抢占,仅系统调用返回、显式调度点触发任务切换;长临界区、关中断代码会阻塞调度数十 ms | 10~50ms | 最低 |
| CONFIG_PREEMPT_VOLUNTARY | Voluntary Kernel Preemption(Desktop) | 内核关键路径插入显式cond_resched()自愿调度点,长循环主动让出 CPU;锁、关中断区域仍不可抢占 |
2~10ms | 低 |
| CONFIG_PREEMPT | Preemptible Kernel(Low-Latency Desktop) | 无锁内核代码全部支持抢占;自旋锁、中断关闭区间不可抢占;标准软实时 | 0.5~2ms | 中 |
| CONFIG_PREEMPT_RT | Fully Preemptible RT Kernel | RT 补丁改造:自旋锁改为可休眠 mutex、中断线程化、RCU 可抢占、几乎所有临界区支持抢占;硬实时标准 | 10~200us | 高 |
2.3 配套关键术语
- 调度延迟(Latency):任务唤醒至实际获取 CPU 执行的间隔,分为唤醒延迟、中断延迟、调度抖动,是抢占模型选型核心指标;
- cyclictest:rt-tests 套件核心工具,量化长时间运行下最小 / 最大 / 平均调度延迟;
- 线程化中断(IRQ Thread):PREEMPT_RT 独有改造,硬件中断顶半部极简,底半部交由实时线程执行,消除长中断阻塞;
- 自旋锁改造(rt_mutex):标准内核 spin_lock 忙等不可抢占,RT 补丁替换为可休眠互斥锁,避免长时间 CPU 独占阻塞高优任务;
- NO_HZ_FULL:配套实时优化开关,隔离 CPU 关闭系统滴答时钟,减少调度噪声,常与 PREEMPT_RT 搭配使用。
三、环境准备(可直接复现全套环境)
3.1 硬件环境
- 测试主机:x86_64 工控机 / 台式机(推荐 Intel i5/i7,关闭 CPU 节能 C-State、睿频,避免调频引入抖动);
- 嵌入式备选:RK3588、ARM64 树莓派 4B;
- 最低内存:4GB(内核编译消耗 2GB 以上内存,避免 OOM);
- 存储:SSD(机械硬盘 IO 延迟会干扰实时测试结果)。
3.2 软件环境版本
- 宿主机系统:Ubuntu 22.04 LTS(适配 5.15/6.1 长期支持内核,工业场景主流)
- 内核源码:Linux 6.1.38 LTS(稳定 RT 补丁支持,工业落地首选)
- RT 实时补丁:patch-6.1.38-rt46.patch(与内核版本严格一一对应,版本不匹配编译失败)
- 依赖工具链(全套安装命令)
# 安装内核编译全套依赖,复制直接执行
sudo apt update && sudo apt install -y build-essential libncurses-dev bison flex libssl-dev libelf-dev git wget rt-tests stress cpufrequtils
工具说明:
libncurses-dev:make menuconfig 图形化内核配置依赖;rt-tests:包含 cyclictest、hackbench 等延迟测试工具;stress:CPU / 内存压力生成工具,模拟满负载工业场景;cpufrequtils:CPU 调频锁定,消除变频抖动干扰测试数据。
3.3 源码与补丁下载配置步骤
步骤 1:创建编译工作目录
mkdir -p ~/linux_rt_build && cd ~/linux_rt_build
步骤 2:下载匹配 LTS 内核源码
# 下载6.1.38稳定内核
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.38.tar.xz
tar -xf linux-6.1.38.tar.xz
cd linux-6.1.38
步骤 3:下载并打 PREEMPT_RT 补丁(仅 RT 模型需要,其余三种模型无需补丁)
# 同目录下载对应RT补丁
wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/6.1/patch-6.1.38-rt46.patch.gz
gunzip ../patch-6.1.38-rt46.patch.gz
# 给内核源码打实时补丁
patch -p1 < ../patch-6.1.38-rt46.patch
关键提示:编译前不打补丁,menuconfig 不会出现
Fully Preemptible RT选项,只能切换前三种基础抢占模型。
四、应用场景细分
工业运动控制设备是四种抢占模型分层使用的典型场景:设备搭载四核 ARM 处理器,CPU0 运行系统后台服务、日志、Web 管理界面;CPU1 执行 EtherCAT 总线数据收发(硬实时要求,抖动 < 50us);CPU2 运行人机交互界面、音频报警;CPU3 处理数据库存储、历史数据写入。 CPU1 单独隔离,内核编译开启PREEMPT_RT,IRQ 线程化、NO_HZ_FULL,实时任务设置 SCHED_FIFO 优先级 90 以上,保证伺服周期稳定,防止电机过载丢步;CPU2 仅需流畅 UI 响应,选用PREEMPT低延迟桌面模型,平衡响应与系统开销;CPU0、CPU3 负责吞吐型后台任务,切换为PREEMPT_VOLUNTARY自愿抢占模型,减少上下文切换损耗,提升日志、数据库写入吞吐量;若设备简化为纯后台数据采集服务器、无实时控制需求,则全局使用PREEMPT_NONE无强制抢占模式,最大化并发吞吐。该分层绑定 CPU + 多抢占模型组合方案,是目前工业控制器主流调度优化架构,能同时兼顾硬实时控制、人机交互流畅度、后台业务高吞吐三类差异化指标。
五、实际案例与完整操作步骤
5.1 案例总流程
- 内核 menuconfig 切换四种抢占模型;
- 内核编译、安装、系统重启切换内核;
- 系统底层实时优化(CPU 锁频、隔离 CPU、关闭节能);
- 压力场景下 cyclictest 量化延迟测试;
- 自定义 C 程序验证实时调度抢占行为;
- 四种模型数据对比,输出选型结论。
5.2 步骤 1:menuconfig 切换四种抢占模型(核心配置操作,望获OS实测)
进入内核配置界面
cd ~/linux_rt_build/linux-6.1.38
make menuconfig
操作路径:General setup → Preemption Model,回车展开四个选项:
模型 1:PREEMPT_NONE(服务器无强制抢占)
(X) No Forced Preemption (Server)
适用:数据库、存储、高并发 Web 服务,追求最大吞吐。
模型 2:PREEMPT_VOLUNTARY(自愿抢占桌面)
(X) Voluntary Kernel Preemption (Desktop)
内核自动在长循环、文件读写路径插入cond_resched(),主动释放 CPU,无额外抢占开销。
模型 3:PREEMPT(低延迟全抢占,软实时)
(X) Preemptible Kernel (Low-Latency Desktop)
无锁内核代码全部支持抢占,仅自旋锁、关中断区间阻塞调度。
模型 4:PREEMPT_RT(硬实时完全抢占,需提前打 RT 补丁)
(X) Fully Preemptible Kernel (Real-Time)
补丁存在才显示该选项,自动开启 IRQ 线程化、RT-mutex、抢占式 RCU。
配套必选实时优化配置(四种模型通用优化项)
# Timer子系统优化
General setup -> Timers subsystem -> High Resolution Timer Support 开启
General setup -> Timers subsystem -> Full dynticks system (Tickless) 开启NO_HZ_FULL
# CPU节能关闭,防止调频抖动
Processor type and features -> Disable CPU idle states 开启
Processor type and features -> Processor frequency scaling 全部关闭
配置完成保存退出:Save -> .config。
5.3 步骤 2:内核编译、安装脚本(一键执行)
# 编译并行,-j后数字等于CPU核心数
make -j$(nproc)
# 编译驱动模块
sudo make modules_install
# 安装内核镜像至/boot,更新grub启动项
sudo make install
# 更新grub,启动菜单新增编译后的内核
sudo update-grub
编译完成后重启系统,grub 高级选项选择新编译内核启动。
5.4 步骤 3:宿主机底层实时调优脚本(永久生效)
新建rt_tune.sh,赋予执行权限chmod +x rt_tune.sh,每次开机执行,消除非内核层面延迟噪声:
#!/bin/bash
# rt_tune.sh 实时系统底层调优,适配四种抢占模型
# 1. 锁定CPU最高频率,关闭变频
sudo cpufreq-set -c 0-3 -g performance
# 2. 关闭磁盘写入缓存延迟
echo 1 | sudo tee /proc/sys/vm/dirty_writeback_centisecs
# 3. 禁用NUMA平衡(单CPU工控机)
echo 0 | sudo tee /proc/sys/kernel/numa_balancing
# 4. 实时进程内存锁,禁止swap交换
echo -1 | sudo tee /proc/sys/vm/mlock_limit
# 5. 隔离CPU1为实时核心(示例四核CPU,根据硬件修改)
sudo systemctl set-property --runtime user.slice AllowedCPUs=0,2,3
sudo systemctl set-property --runtime system.slice AllowedCPUs=0,2,3
# 实时任务绑定CPU1,后台服务禁止调度至CPU1
执行命令:sudo ./rt_tune.sh
5.5 步骤 4:cyclictest 延迟测试(四种模型量化对比核心代码)
4.5.1 单线程高优先级测试命令(满 CPU 压力场景)
打开两个终端: 终端 1 生成 100% CPU 压力,模拟后台业务负载
# 占用全部CPU核心,制造最坏调度场景
stress --cpu $(nproc)
终端 2 执行 cyclictest,绑定隔离 CPU,97 最高实时优先级,持续 60 万次采样
# 参数释义
# -t1:单测试线程;-p97 SCHED_FIFO最高优先级;-i1000 1ms周期唤醒
# -l600000 60万次采样;-m 锁定进程内存;-c1 绑定CPU1
sudo cyclictest -t1 -p97 -i1000 -l600000 -m -c1
输出结果字段说明:
T: 0 ( 1234) P:97 I:1000 C:600000 Min:14 Act:47 Avg:62 Max:18437
# Min最小延迟、Avg平均延迟、Max最大抖动(核心对比指标)
4.5.2 四种模型实测数据参考(x86 i7 工控机,满负载)
- PREEMPT_NONE:Max=42193us,Avg=1247us
- PREEMPT_VOLUNTARY:Max=11682us,Avg=513us
- PREEMPT:Max=1724us,Avg=126us
- PREEMPT_RT:Max=186us,Avg=43us
5.6 步骤 5:自定义 C 实时调度测试程序(源码可直接编译运行)
该程序创建 SCHED_FIFO 实时线程,周期性计时,打印单次唤醒延迟,直观验证四种抢占模型的抖动差异,适合写入实验报告:
// sched_latency_test.c 实时调度延迟自测程序
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sched.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
// 采样次数、周期1ms
#define TEST_LOOP 100000
#define CYCLE_US 1000
// 设置线程为实时FIFO调度
int set_real_time_prio(int prio)
{
struct sched_param param;
param.sched_priority = prio;
if(pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m) != 0)
{
perror("pthread_setschedparam failed");
return -1;
}
return 0;
}
// 计算时间差,单位微秒
long calc_latency(struct timespec start, struct timespec end)
{
long s_diff = end.tv_sec - start.tv_sec;
long us_diff = (end.tv_nsec - start.tv_nsec) / 1000;
return s_diff * 1000000 + us_diff;
}
void* real_time_thread(void* arg)
{
set_real_time_prio(90);
struct timespec wake_time, now;
long latency, max_lat = 0, min_lat = 999999, avg_sum = 0;
clock_gettime(CLOCK_MONOTONIC, &wake_time);
for(int i = 0; i < TEST_LOOP; i++)
{
// 固定周期睡眠唤醒
wake_time.tv_nsec += CYCLE_US * 1000;
if(wake_time.tv_nsec >= 1000000000)
{
wake_time.tv_sec += 1;
wake_time.tv_nsec -= 1000000000;
}
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &wake_time, NULL);
// 获取当前时间,计算延迟
clock_gettime(CLOCK_MONOTONIC, &now);
latency = calc_latency(wake_time, now);
avg_sum += latency;
if(latency > max_lat) max_lat = latency;
if(latency < min_lat) min_lat = latency;
}
printf("===== 调度延迟统计 =====\n");
printf("最小延迟:%ld us\n", min_lat);
printf("最大抖动:%ld us\n", max_lat);
printf("平均延迟:%ld us\n", avg_sum / TEST_LOOP);
return NULL;
}
int main()
{
pthread_t tid;
pthread_attr_t attr;
cpu_set_t cpuset;
pthread_attr_init(&attr);
// 绑定至CPU1实时核心
CPU_ZERO(&cpuset);
CPU_SET(1, &cpuset);
pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &cpuset);
// 锁定内存,禁止swap
mlockall(MCL_CURRENT | MCL_FUTURE);
if(pthread_create(&tid, &attr, real_time_thread, NULL) != 0)
{
perror("pthread create failed");
return 1;
}
pthread_join(tid, NULL);
munlockall();
return 0;
}
编译 & 运行命令:
# 编译,-pthread链接实时线程库
gcc sched_latency_test.c -o sched_latency_test -pthread -O2
# 必须root权限才能设置SCHED_FIFO高优先级
sudo ./sched_latency_test
代码说明:程序基于单调时钟做绝对时间休眠,消除相对 sleep 误差,自动统计最大抖动,可对比四种抢占模型下输出的最大延迟数值,作为论文实验原始数据。
六、常见问题与解答(贴合实操踩坑点)
Q1:打 RT 补丁后 menuconfig 看不到 PREEMPT_RT 选项?
A:两种核心原因:①补丁版本与内核源码版本不匹配,6.1.38 内核只能使用 rt46 补丁;②编译残留旧配置,执行make mrproper清空所有.config、中间文件,重新解压源码打补丁即可。禁止直接修改.config 文件开启 RT,会出现编译符号缺失报错。
Q2:PREEMPT_RT 内核 cyclictest 最大延迟突增毫秒级抖动?
A:排查优先级从高到低:1)CPU 开启 C-State 节能,idle 降频引入延迟,内核配置关闭 CPU idle;2)后台守护进程调度至实时隔离 CPU,通过 cgroup AllowedCPUs 限制系统服务运行核心;3)磁盘 swap 交换,执行 mlockall 锁定实时进程全部内存;4)内核未开启高分辨率定时器。
Q3:服务器开启 PREEMPT 抢占模型后,数据库 QPS 大幅下降?
A:PREEMPT/PREEMPT_RT 增加大量抢占、上下文切换开销,数据库、存储属于吞吐型业务,长内核文件读写路径频繁抢占会损耗 IO 效率,服务器场景固定选用 PREEMPT_NONE 模型,不要开启抢占。
Q4:自定义实时程序 pthread_setschedparam 返回权限不足?
A:普通用户无法设置 SCHED_FIFO/RR 实时策略,必须使用 sudo 运行程序;或修改 /etc/security/limits.conf,添加用户 rtprio 权限:
username soft rtprio 99
username hard rtprio 99
Q5:PREEMPT_RT 编译报 spin_lock 未定义、休眠冲突错误?
A:标准内核驱动直接使用 spin_lock () 在 RT 内核会编译失败,RT 补丁将自旋锁改为可休眠 rt_mutex,驱动代码需替换为rt_spin_lock系列接口,工业老驱动移植 RT 内核时高频出现该问题。
七、实践建议与最佳实践
7.1 抢占模型选型硬性标准(落地选型清单)
- 硬实时控制(伺服、机器人、FPGA 总线):必须 PREEMPT_RT,抖动指标要求 < 200us;
- 音视频、直播、人机交互软实时:PREEMPT,平衡延迟与系统开销;
- 中小型 Web、监控采集网关:PREEMPT_VOLUNTARY;
- 数据库、分布式存储、高并发后端服务器:PREEMPT_NONE。
7.2 内核编译调优技巧
- 长期稳定工业设备优先选用 LTS 内核(5.15/6.1),主线新内核 RT 补丁兼容性差;
- 编译内核关闭全部不必要驱动(声卡、摄像头、显卡),减小内核镜像、降低抢占噪声;
- 实时设备固定开启 NO_HZ_FULL、高分辨率定时器,隔离独立 CPU 专跑实时任务。
7.3 延迟测试规范(论文 / 测试报告标准流程)
- 测试前关闭所有图形界面、后台更新、日志服务,消除干扰;
- 每组抢占模型至少采样 50 万次以上,短采样会遗漏极端最大抖动;
- 必须在满 CPU 压力、磁盘 IO 压力双重场景测试,空载延迟数据无工程参考价值;
- 同步记录 CPU 型号、内核配置、底层调优参数,保证实验可复现。
7.4 实时应用开发避坑
- PREEMPT_RT 线程禁止调用 malloc 动态内存分配,内存分配存在全局锁,会引入毫秒级抖动,提前预分配内存;
- 实时任务避免频繁 printk/printf 打印,控制台输出会抢占 CPU;
- 关中断代码段尽量缩短,即使 RT 内核,硬件中断顶半部仍不可抢占。
八、总结与落地应用拓展
本文完整拆解 Linux 四层递进抢占模型的底层原理、内核编译配置、量化延迟测试方案,从源码编译、系统调优、自定义测试程序多维度给出可直接落地的实战代码。四种抢占模型的核心取舍逻辑可总结为:抢占放开程度越高,调度抖动越小,但系统吞吐性能损耗越大,不存在通用最优配置,必须基于业务实时指标选型。
在工程落地层面,PREEMPT_RT 硬实时内核是工业自动化、运动控制、自动驾驶设备的底层基础;PREEMPT 低延迟模型广泛应用于消费级实时音视频终端;无抢占模型支撑大规模云服务器、数据库集群的高并发吞吐需求。开发者可基于本文的 cyclictest 测试脚本、自定义延迟检测 C 程序完成对比实验,将四种模型的延迟数据、内核配置参数整理为实验报告或毕业论文核心佐证内容。
后续拓展实践方向:多 CPU 分层绑定不同抢占模型、SCHED_DEADLINE 调度器与 PREEMPT_RT 组合调优、RTLA timerlat 工具定位内核延迟根源,进一步深挖 Linux 调度子系统确定性优化方案,适配更高标准硬实时项目开发需求。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)