做飞腾平台实时Linux方案这么久,前面二十篇把内核移植、驱动适配、文件系统这些底座问题都解决得差不多了,但“系统能实时运行”和“系统达到实时指标”之间还差着大量的测试与调优工作。第二十一篇正好是承上启下的节点,单开一篇专门聊性能测试与优化总结,就是想回答三个问题:实时延迟到底怎么量才可信,默认配置下的表现离工业场景的要求还差多远,以及通过哪些可控手段能把最大延迟真正压下去。
这篇文章覆盖从测试环境搭建、cyclictest参数设计、数据解读,到CPU隔离、中断亲和性、RCU回调、核间干扰等优化手段的完整链路,并附上我实际操作中的问题排查表。适合手里正好有飞腾平台开发板、准备上PREEMPT_RT方案做运动控制或数据采集的工程师参考;刚接触实时Linux的读者也能从中拿到可复现的实验步骤。
1. 实测之前先搭干净的基线环境
1.1 被测平台与内核版本
先交代被测环境。这里以一块飞腾四核处理器的开发板为例,板卡上运行的操作系统是我在系列前几篇中从源码构建的Linux发行版,内核选用长维护版本5.10.x,并打了对应的PREEMPT_RT补丁。之所以选5.10这个版本,主要是它在工业场景被验证得最多,RT补丁的成熟度比新内核更高,很多驱动补丁也是基于这个版本做的,后续迁移成本低。
关于PREEMPT_RT,简单说一句原理:它通过把中断处理线程化、用rtmutex替换普通mutex、让spinlock在RT路径里变成可抢占的优先级继承锁等一系列改动,把内核的不可抢占区间压缩到极致。普通Linux在大部分时间也能保证几十毫秒甚至几毫秒的调度延迟,但实时系统要的是“最坏情况下也能在限定时间内响应”,也就是看尾巴,而不是看平均。
这里把测试基线整理成表格,方便后续复现时一条条对照。
| 项目 | 配置 |
|---|---|
| 处理器 | 飞腾四核ARMv8处理器,主频2.x GHz |
| 内存 | 16GB DDR4,双通道 |
| 内核 | Linux 5.10.x + PREEMPT_RT完整补丁 |
| 存储 | eMMC,根文件系统使用initramfs加载 |
| 工具链 | gcc 10.x,glibc 2.31 |
| 测试工具 | cyclictest、hwlatdetect、perf、trace-cmd |
| 运行模式 | 最小化启动,仅保留串口观察日志 |
1.2 为什么必须搭建隔离环境
实时测试最忌讳“脏环境”。如果机器上同时跑着GUI、日志服务、网络协议栈,测得的结果根本没法归因。你测出最大延迟300us,搞不清楚是调度器的问题、中断的问题还是某个后台进程抢了CPU,后边的优化就只能靠猜。
我这里把系统设置成最小化启动,只保留一个串口用于观察,网络服务全部关闭,CPU调频策略先固定成性能模式。隔离环境的关键点包括:单核运行实时任务,其余核承载系统杂活,中断全部挪到非实时核,关闭看门狗和内核打印。这些我在优化章节会展开讲,这里先把环境整干净,保证所有对比数据都来自同一个基准。
隔离的目的不是让数据好看,而是让数据能复现。我在实际项目中吃过亏:第一次测出最大延迟150us,高高兴兴准备写报告,第二次同样条件测出380us,直接把结论推翻。后来发现是测试时系统里多了个NTP同步进程,偶尔跑一下就把尾部延迟拉高了。环境不干净,测试结果就是随机数。
2. 延迟指标与cyclictest参数设计
2.1 三类关键延迟要分清
做实时性能评估,至少要区分三个层面的延迟,很多人混为一谈,导致问题定位跑偏。
第一是中断响应延迟,从硬件中断触发到中断处理程序开始执行的时间。它受中断控制器、处理器流水线、缓存状态影响。在ARM平台上,GIC中断控制器本身的延迟通常在微秒级别,主要变量在CPU侧的缓存和分支预测。
第二是调度器延迟,从任务被唤醒到它在目标核上真正拿到CPU的时间。PREEMPT_RT要优化的主要就是这一环。调度延迟里包含了内核锁竞争、当前任务的抢占点、以及CPU是否处于可快速唤醒的状态。
第三是任务完成时间,从事件发生到应用代码跑完关键段落的墙钟时间,等于前两者之和再加上应用自身的执行时间。
在飞腾平台上我主要用cyclictest测调度和唤醒延迟,用hwlatdetect测硬件层面的延迟毛刺,用perf和trace-cmd定位具体是哪个中断或者哪个内核路径把时间吃掉了。三类指标各自有适用场景,运动控制回路关心的是任务完成时间,数据采集系统可能更关心中断响应延迟,测试之前想清楚自己要什么。
2.2 cyclictest参数的坑与选择逻辑
cyclictest的用法并不复杂,但参数选不对,结果就没有参考价值。下面是我在飞腾平台上常用的测试命令:
cyclictest -t 1 -p 90 -i 1000 -l 100000 -n -m -a 3参数含义逐个说:
-t 1:创建1个测量线程。工业控制里如果只有一个关键任务,单线程就够;多线程场景建议再跑-t 4对比,但要注意多线程会引入核间迁移的额外延迟。-p 90:线程优先级设为SCHED_FIFO 90。90这个值比大多数内核线程和中断线程高,能确保测量线程本身抢占大多数普通任务。设太低会被别的实时线程抢走,测出来的是别人的延迟;设太高又有可能掩盖真实负载下的情况。-i 1000:测量间隔1000us,对应1ms的工业控制节拍,这是运动控制、电源控制等场景最常见的周期。-l 100000:循环10万次,在1000us间隔下大约跑100秒,足够把各种偶发干扰暴露出来。如果只循环几千次,测出来的少量样本根本抓不到尾部特征。-n:使用clock_nanosleep定时,比普通sleep更精准。-m:锁定内存防止换页。不锁的话,如果实时线程的栈或堆被换出,一次缺页就能带来几十到几百微秒的延迟,而且这种延迟完全随机,无法通过调度优化解决。-a 3:绑定在核3上运行。绑定核是实时测试的前提,否则线程自己就可能在核间迁移。
跑完之后关键看输出里的max字段。很多人习惯看平均延迟,但平均延迟好看不代表系统实时。只有max值直接决定你是否能满足最坏情况约束。比如一个运动控制回路要求在1ms周期内完成位置计算,调度器平均延迟5us但最大延迟300us,那你的任务最坏情况下可能是300us后才开始执行,这个数必须小于任务预算里给调度预留的那部分。
另有一个实用参数-h,可以输出直方图:
cyclictest -t 1 -p 90 -i 1000 -l 100000 -n -m -a 3 -h 500直方图能告诉你延迟分布是集中在几个us的狭窄区间,还是有一个明显的长尾。工业验收时,除了max值,我通常还会统计99.99分位数,这个数比max更稳定,适合做批次间的对比。
3. 默认配置实测:数据的说服力
3.1 只打补丁不优化,到底能跑成什么样
在只打RT补丁、没有做隔离优化的默认配置下,我跑了一次完整的cyclictest。当时的输出类似下面这样:
T: 0 ( 1897) P: 90 I: 1000 C: 100000 Min: 8 Act: 20 Avg: 12 Max: 278解释一下:最小延迟8us,平均延迟12us,平均值相当不错,但最大延迟278us。这个数字在要求苛刻的伺服控制里已经偏大了。278us意味着在1ms的周期里,可能有接近三分之一的周期被调度延迟吃掉,留给应用逻辑的余量很小。
为了定位延迟毛刺的来源,我加跑了hwlatdetect:
duration: 3600s, threshold: 10us samples: 3 max latency: 32ushwlatdetect测的是硬件层面中断被禁用的窗口,32us说明硬件层面有非屏蔽中断或者其他原因导致的短窗口,虽然不长,但会与调度器延迟叠加,最终体现在cyclictest的max值上。
3.2 抓毛刺:trace-cmd与perf的配合
平均延迟低、最大延迟高,这类现象十有八九是“偶发干扰”造成的。直接上trace-cmd抓调度事件,分析最大延迟样本附近发生了什么。
trace-cmd record -e sched_switch -e sched_wakeup -e irq_handler_entry -e irq_handler_exit -b 8000抓完用report查看,重点看几个特征。我在实际排障中见过几类典型问题:
第一类是DMA中断风暴。网卡或USB控制器在大量数据下频繁触发中断,会抢占当前核。即便实时任务所在核做了隔离,如果IRQ的亲和性没有设置,中断仍然可能落到实时核上。
第二类是内核定时器过期负载。大量短周期timer集中在一个CPU上,hrtimer回调处理堆叠,一次hwlatdetect捕捉到的窗口往往就是这类timer回调合并执行造成的。
第三类是页错误。未锁定内存的任务首次访问页面时触发缺页,如果恰好发生在实时任务的路径上就会卡一下。cyclictest的-m参数就是干这个用的。
第四类是同核共用中断。多个设备共用同一个中断号,中断处理程序里要依次识别设备,处理时间成倍增加。ARM平台的GPIO组中断经常出现这类问题。
结论是:max值偏大基本都能从“某个核上有不该出现的活动”这个方向找到线索。实时优化说白了就是把实时任务和一切干扰源在物理上隔开,而不是指望内核自己把活干好。
4. 从内核参数到绑核中断的优化清单
4.1 内核启动参数:隔离、无时钟与RCU回调
内核启动参数是我做优化的第一站。直接在bootloader的bootargs里追加:
isolcpus=3 nohz_full=3 rcu_nocbs=3逐个解释:
isolcpus=3:把核3从内核调度器的普通负载均衡中剔除,普通任务不会自发跑到核3上。这一步保证实时任务独占一个核。nohz_full=3:核3进入自适应无时钟模式,空闲时关闭周期性的时钟tick。每秒钟上千次的时钟中断在实时场景里是持续的微小干扰源,去掉之后尾部延迟会明显收紧。rcu_nocbs=3:核3上的RCU回调不本地处理,挪到其他核的RCU kthread去跑。RCU回调如果不挪走,会在CPU进入内核时排队执行,造成延迟毛刺。
编译内核时还要确保CONFIG_RCU_NOCB_CPU、CONFIG_NO_HZ_FULL这些选项已经打开,否则命令行参数不生效。检查办法是启动后看/proc/cmdline,再确认对应sysfs节点确实存在。
配合sysfs把实时任务的时间配额限制关掉:
echo -1 > /proc/sys/kernel/sched_rt_runtime_us默认情况下实时任务每1秒周期里最多只能占0.95秒,剩下0.05秒预留给非实时任务,这是防止实时任务饿死系统的保护机制。但对于纯实时系统来说,这个配额反而成了干扰源——当配额耗尽时,高优先级RT任务会被强制让出CPU,哪怕当时没有其他任务要跑。
这里有个细节:如果系统里还有其他普通任务在用剩下来的CPU配额,设置-1可能导致它们得不到调度。所以必须确认隔离核之外的核足够处理系统杂务。我在实验机上是用核0做管理、核1和核2跑普通应用、核3跑实时任务,这样即使把限额关掉也不影响交互。
4.2 绑核、优先级与中断亲和性
实时任务绑核用taskset,但注意顺序:
chrt -f 90 taskset -c 3 ./realtime_task先设置调度策略和优先级,再绑定CPU,这样任务一开始就以正确的调度参数运行。生产环境我通常直接在应用代码里通过pthread_attr_setaffinity_np来绑,避免依赖外部命令,也防止人为遗漏。
中断亲和性则是实时优化的重头戏。先把所有中断列出来:
cat /proc/interrupts找到网卡、USB、eMMC、串口这些设备产生的中断号,然后把它们的/proc/irq/<irq>/smp_affinity设置为非实时核的CPU掩码。核3对应的掩码是0x8,设置前先读取当前值,按位去掉核3所在位再写回,避免误伤其他设置。
举个实际遇到的例子:那块实验板上的eMMC控制器中断默认允许在所有核上触发,一旦有写操作,IRQ可以选择任何空闲核,三天里有两天会落到核3上。虽然单次延迟只增加几十微秒,但周期性写盘会导致实时抖动稳定地出现。把IRQ的smp_affinity固定成非实时核之后,这个问题立刻消失,最大延迟直接降了一个量级。
4.3 调频、深度睡眠、打印与内存锁
CPU动态调频是很容易被忽略的干扰源。任务突发时,调频器需要从低频往上爬,过渡时间可能达到几百微秒,这对实时任务来说不可接受。固定成性能模式:
echo performance > /sys/devices/system/cpu/cpufreq/policy0/scaling_governorARM平台的cpuidle深度睡眠也要处理。CPU进入WFI或者更深的睡眠状态后,唤醒延迟会显著增加。实时场景建议通过cpuidle的disable参数让CPU保持轻量睡眠状态。代价是功耗上升,但实时系统本来就是在用功耗换确定性。
内核打印console输出是另一个隐蔽的干扰源。printk本身分等级过滤,但console输出一旦触发就是同步写串口,一次可能占掉几百微秒。测试阶段把console_loglevel调到最低,或者干脆在内核cmdline里去掉console设备,日志只写内存缓冲,出问题再导出。
内存锁定我前面提过,应用代码里调用:
mlockall(MCL_CURRENT | MCL_FUTURE);确保实时线程的栈和堆不会被换出,避免缺页中断。cyclictest的-m参数就是干这个的。还有一个容易被忽略的就是缓存预热的建议。任务启动后先跑一段热身,把热点代码和数据载入cache,正式测试前再清零统计。
4.4 中断线程化之后的优先级管理
PREEMPT_RT的核心机制之一是把中断处理线程化,中断线程以实时优先级运行。但这里有个坑:默认情况下中断线程的优先级继承自设备驱动注册时的值,很多驱动根本没有设过,导致中断线程可能以较低的实时优先级运行,在高负载下被其他RT任务长时间饿着。
查看和调整中断线程优先级:
cat /proc/interrupts chrt -p -f 85 <irq线程pid>把关键设备的中断线程优先级拉高到合适水平。建议原则是:关键传感器相关中断设为85到90,网络类中断设低一些,避免高优先级中断反过来成为新的干扰源。我在飞腾平台上曾把串口中断线程优先级设得过高,结果串口数据一频繁,实时任务反而被挤出去了,这个平衡点需要根据实际负载反复调。
5. 优化前后数据对比与可重复性验证
5.1 同样的命令,不一样的结果
完成上述优化之后,我重新跑同样的cyclictest,参数保持一致:-t 1 -p 90 -i 1000 -l 100000 -n -m -a 3。
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 最小延迟 | 8us | 6us |
| 平均延迟 | 12us | 9us |
| 最大延迟 | 278us | 67us |
| 99.99分位 | 145us | 28us |
最大延迟从278us降到67us,各项指标都明显收紧。主要收益来源我拆解过:
| 优化项 | 预估收益 |
|---|---|
| IRQ亲和性修正,消除eMMC周期性中断 | 约140us |
| CPU调频固定performance模式 | 约50us |
| 关闭console打印 | 约40us |
| rcu_nocbs与nohz_full减少核间干扰 | 约20us |
| mlockall锁定内存 | 约10us |
需要说明的是,这些收益是叠加之后的估算值,逐项单独验证会相互影响,但大方向不会变。每块板子的外设不同,占比会不一样,别照抄结论,要照抄方法。
5.2 可重复性:连续5轮测试的意义
测一次不可信。我把同一测试连跑5轮,每轮10万次循环,观察最大值和99.99分位的稳定性:
| 轮次 | Max | 99.99%分位 |
|---|---|---|
| 第1轮 | 67us | 27us |
| 第2轮 | 55us | 24us |
| 第3轮 | 71us | 29us |
| 第4轮 | 63us | 25us |
| 第5轮 | 59us | 26us |
5轮最大值分布在55到71us之间,没有出现某一轮突然跳到几百us的情况,说明优化已经把这个平台上的主要尾部干扰源都排除了。这种可重复性成立,才敢说优化结果是系统性的,而不是运气好没撞上尖峰。
另外我还会加跑一个高负载压力测试:在非实时核上用stress-ng把CPU、内存、IO都压满,再观察实时核上的延迟。负载场景下的延迟变化比空载场景更能反映系统有没有把隔离做扎实。
6. 高频问题排查速查表
6.1 问题现象与定位手段
把这段时间踩过的坑整理成一张速查表,遇到类似现象可以直接按表操作:
| 现象 | 可能原因 | 排查手段 | 解决办法 |
|---|---|---|---|
| max延迟偶发超过200us | 未隔离的IRQ落到实时核 | cat /proc/interrupts 看CPU列分布 | 设置smp_affinity掩码 |
| 周期性抖动规律出现 | 定时器tick未关闭 | /proc/timer_stats或trace-cmd抓hrtimer | 加入nohz_full配置 |
| 唤醒后延迟居高不下 | cpuidle深睡眠状态过深 | 查看cpu3/cpuidle各state的usage计数 | 禁用深睡眠state |
| 一开网络就性能恶化 | 网卡中断风暴抢占CPU | perf top查软中断占比 | 中断亲和性+降低网络中断优先级 |
| 高负载下RT任务被饿着 | sched_rt_runtime_us配额限制 | cat /proc/sys/kernel/sched_rt_runtime_us | 设为-1并按核分配负载 |
| 应用首次运行出现卡顿 | 缺页导致延迟 | 查看perf的page fault事件 | mlockall锁内存 |
| 测试过程中尾部延迟缓慢爬升 | 内存碎片或缓存污染 | 观察free与cache变化 | 定期清理缓存或重启测试进程 |
6.2 抓现场的几个关键手法
排查实时问题,最怕的就是“问题复现了但没抓到现场”。我习惯的做法是先用trace-cmd开一个环形缓冲区,循环记录调度、中断、定时器事件,等最大延迟样本出现后,用trace-cmd stop冻结现场,再慢慢分析。
trace-cmd record -e sched_switch -e sched_wakeup -e irq_handler_entry -e irq_handler_exit -e syscalls:sys_enter_nanosleep抓到最大延迟附近的事件后,重点看几个特征:是否有irq_handler在实时核上执行、是否有高优先级线程抢占测量线程、是否刚好碰到周期tick中断。如果最大延迟样本周围干干净净,什么都没有,那问题大概率在硬件层或者缓存层,要看hwlatdetect的结果或者检查功耗管理路径。
还有一个容易踩的坑:测试脚本本身不能干扰测试。如果把cyclictest的输出重定向到一个文件,而这个文件所在存储是实时任务同一块盘,写日志本身就会产生IO中断。我习惯把测试结果输出到tmpfs内存盘上,测试结束再拷回持久化存储。
另一个建议是测量线程和记录线程分离。让cyclictest只做测量,把结果通过共享内存交给另一个低优先级的记录线程去保存,避免测量线程因为写文件被中断。虽然可以把日志写到别的地方,但最干净的做法永远是测量与记录彻底分离。
写到这里,第二十一篇的核心内容已经讲完。我个人的体会是,飞腾平台在ARMv8架构基础上做实时方案,优化的底层逻辑和通用ARM平台完全一致,但每块板子的具体外设差异,决定了最终要调的细节各不相同。最大的教训就是不要凭经验猜干扰源,一定先用工具把毛刺的上下文抓出来,再做针对性修改。
最后再分享一个小技巧:所有优化参数和测试结果建议一开始就用脚本固化下来,包括bootargs全文、smp_affinity原始值、优化前后的cyclictest输出。我在项目里被反复要求“恢复到优化前的配置再复测一遍”,如果没有完整记录,光凭记忆根本找不回来。系列后面如果不间断,我打算继续更新负载场景下的实时性验证,也就是在真实运动控制负载下再压一轮,有兴趣的朋友可以留意后续。