飞腾平台实时Linux性能测试与优化总结
做实时Linux方案做到第二十一篇,说实话已经不只是“能不能跑”的问题了。内核起来了,驱动能通了,中断也能响应了,越到后面越让人头疼的是另一件事:性能到底稳不稳,延迟到底有没有隐藏的尖峰,应用能不能信得过这套系统去扛关键任务。
这一篇不讲怎么移植,也不讲怎么裁剪内核,直接聚焦飞腾平台实时Linux的性能测试与优化总结。把我在调试过程中踩过的坑、测过的数据、改过的配置、最后沉淀下来的优化套路一次说透。适合那些已经把PREEMPT_RT跑起来、正被偶发延迟尖峰折磨的团队参考。
1. 项目复盘视角:实数性不该只盯“跑得快”
很多项目组一提到实时性能,第一反应就是拿出cyclictest跑一个均值,看到平均几十微秒,就认为系统已经“实时”了。这是我在多个项目里反复纠正过的误区。实时系统考核的核心不是平均值,而是最坏情况响应时间,也就是最大延迟,以及这个最大延迟是否可预期、可重复、可兜底。
在飞腾平台这种ARM架构的多核处理器上,实时性能要拆成几个维度来看:中断响应延迟、调度延迟、上下文切换开销、内核临界区不可抢占的时间,以及用户态线程从唤醒到真正运行的完整链路延迟。cyclictest这类工具测的是最后一条链路,但如果中断处理、时钟源、串口打印这些环节没有治理好,最终呈现出来的最大延迟会非常难看。
我这次复盘的具体对象是一块飞腾四核处理器的主板,自带4GB内存,板载网口和串口,跑的是某5.x版本内核加PREEMPT_RT补丁。测试场景模拟的是工业设备里最常见的模式:一个高优先级实时控制线程周期采集数据并输出控制信号,其余CPU核跑普通业务和网络通信。整个系列前面二十篇解决了移植和驱动问题,这一篇就是回答一个核心疑问:这套方案在真实负载下能守住多少微秒的底线,以及守不住时该怎么调。
之所以把测试和优化放在一起总结,是因为在飞腾平台上孤立地看某个数字没有意义。处理器本身的性能足够,短板往往出现在系统配置、中断分布、电源管理这些“软环节”。只要把跟实时性冲突的机制逐个找出来关掉或挪走,延迟数据会有肉眼可见的改善。
注意:实时性不等于“CPU跑得快”。实时性追求的是确定性,也就是每次响应的时间波动尽可能小。性能再强的处理器,如果调度器里有一个长时间关抢占的路径,照样会让实时线程错过截止时间。
2. 测试环境与测试方法:能复现的数据才算数
性能测试最忌讳的就是“这次测的和下次测的对不上”。为了复盘结论可信,我把测试环境固定成一套标准配置,每次改动只允许一个变量,并且全程记录环境信息。这不是繁琐,而是后续判断优化是否有效的唯一依据。
2.1 硬件与内核基线
测试板的BIOS设置做了统一约束:关闭看门狗,关闭不必要的远程管理功能,串口保留但控制台输出单独处理。内核基线是PREEMPT_RT全抢占配置,实时线程优先级统一使用SCHED_FIFO策略,优先级80。这里额外说明,SCHED_FIFO的优先级范围是1到99,80这个值足够高于绝大多数内核线程,又不会干扰少数关键内核线程,属于比较折中的选择。
内核配置里几个与实时直接相关的开关也必须确认:
| 配置项 | 期望状态 | 作用 |
|---|---|---|
| CONFIG_PREEMPT_RT | 开启 | 把内核中不可抢占区间缩到最小 |
| CONFIG_HZ | 1000 | 时钟中断频率,影响调度粒度 |
| CONFIG_HIGH_RES_TIMERS | 开启 | 提供高精度定时唤醒 |
| CONFIG_NO_HZ_FULL | 按需开启 | 自适应tick,适合隔离核运行任务 |
| CONFIG_RCU_NOCB_CPU | 按需开启 | 将RCU回调转移,减少隔离核的干扰 |
这里提醒一句:CONFIG_HZ并不是越高越好。1000即1毫秒的调度定时器中断,已经是大多数实时场景的折中值。再高会增加中断负载,反而可能引发新的抖动。
2.2 标准压测命令与步骤
我用的测试工具是cyclictest,配合stress-ng做后台负载注入。cyclictest的常用参数含义如下:
# 固定线程到CPU2,优先级80,间隔1毫秒,执行10万次,锁内存 taskset -c 2 cyclictest -t 1 -p 80 -i 1000 -l 100000 -m参数说明:
- -t 1:只创建一个测量线程,避免多个线程互相干扰造成的误解。
- -i 1000:测量线程的唤醒周期为1000微秒。
- -l 100000:总共测量10万次,约等于连续跑100秒。
- -m:锁定当前进程的内存,避免运行过程中出现缺页异常。
- taskset -c 2:把线程绑定到CPU2,保证运行核固定。
后台负载用stress-ng模拟,一般开4到8个CPU压测线程,再配合一个网络压力任务,模拟真实业务里“系统很忙”的状态。压测命令如下:
stress-ng --cpu 4 --io 2 --vm 1 --hdd 1 --timeout 300s &2.3 为什么数据集要单独隔离
第一次测试我直接在整个系统上跑,结果数据非常乱,最大延迟动不动冲到几毫秒。后来逐个排查才发现,普通业务线程的负载均衡器把任务甩到各个CPU上,中断处理也频繁切换核心,实时线程的运行环境一直不稳定。
标准做法是用内核cpuset隔离。把普通负载固定在0到1号核,实时测量线程固定在2号核,剩余一个核处理网络和文件系统等不紧急的任务。如果有条件,还可以把中断全部定向到0号核。
# 假设四核处理器,CPU编号0-3 # 创建cpuset并分配普通负载 mkdir -p /sys/fs/cgroup/cpuset/regular echo 0-1 > /sys/fs/cgroup/cpuset/regular/cpuset.cpus echo 0 > /sys/fs/cgroup/cpuset/regular/cpuset.mems # 将当前shell和压测进程放入该cpuset echo $$ > /sys/fs/cgroup/cpuset/regular/cpuset.tasks数据隔离的意义不只是让数字变好看,而是让每一次测试都能在相同条件下复现。你总不希望今天测出来的最大延迟是300微秒,明天同样命令测出来变成1.2毫秒,却根本说不清差别在哪。
3. 基线数据里的异常尖峰:排查链路全记录
基准测试跑完第一轮,结果在意料之中但依然让人头疼:平均延迟只有几十微秒,最大延迟却达到2毫秒以上。这类“尾巴”问题在实时系统里最致命。平均值再漂亮,只要最大延迟超了任务截止时间,系统就算不合格。
3.1 第一步:确认测量线程真正拿到运行权
最开始我怀疑是不是cyclictest线程的优先级不够,导致被其他线程抢占。检查的方式很简单,在测试过程中用实时调度器查看工具观察线程状态,确认它处于TASK_RUNNING,抢占次数是否异常。如果实时优先级已经最高,但延迟依然大,说明问题不在调度器的优先级排队上,而在更底层的中断或者内核临界区。
这一步实际上是在筛选:延迟尖峰是“线程醒了但没被调度”,还是“线程根本没醒”。前者指向调度器和优先级问题,后者指向定时器、中断或CPU隔离问题。两条路的优化方向完全不同。
3.2 第二步:排查中断风暴
打开/proc/interrupts,重点看两个信息:每个中断号在哪个CPU上处理,以及中断次数是否出现急剧增长。我在测试中发现板载千兆网卡的中断集中在一个核上,每当网络流量大的时候,那个核就被中断淹没,而实时线程恰好也跑在那个核的相邻位置,缓存和总线资源都被争抢。
方向定为:中断尽量从实时CPU上挪走,尤其是网卡这种高频中断源。如果中断不能完全移走,至少保证实时CPU上只有必要的内部定时器中断。
3.3 第三步:串口打印与内核日志是隐形杀手
这是复盘里最值得记录的一处。因为测试板的串口默认作为内核控制台,内核日志printk输出会经过串口驱动发送。串口属于慢速外设,一个字符一个字符往外送,期间还涉及到锁机制和中断。当后台负载触发大量日志打印时,个别实时线程的唤醒路径可能被串口驱动的临界区卡住,产生毫秒级的延迟尖峰。
处理方法分两层:一是启动参数里去掉控制台串口,或者把console级别调低,只在必要时保留输出;二是把内核日志级别设为只输出紧急信息,减少运行时打印。
# 内核启动参数示例:关闭串口控制台,减少printk输出 console="" loglevel=3这里强调一句:如果项目必须保留串口日志,就把console作为外设驱动的独占通道,不要让它和内核printk机制搅在一起。否则日志越多,延迟越不可控。
3.4 第四步:检查页错误和内存锁定
cyclictest加上-m参数锁页后,最大延迟明显下降,但仍有少数尖峰。用perf或者ftrace抓取这些尖峰的调用栈,发现有些来自于定时器软中断处理路径里的页表遍历,有些来自于内存节点的NUMA访问不均衡。飞腾平台的存储控制器架构跟x86不一样,对内存访问延迟更敏感,所以尽量把实时线程的数据和栈锁在同一个内存节点上,避免跨节点访问。
这一步排查链路的价值在于:不要直接跳到“调内核参数”这一步。先通过工具确认尖峰是从哪个路径冒出来的,再对症下药。乱调参数往往越调越乱,最后连基线数据都找不回来。
4. 针对飞腾平台特点的实时优化清单
进入优化阶段后,我把所有生效配置整理成清单。这一节不是理论堆砌,而是每一条都有数据支撑的实战配置。读者可以直接对照自己的项目抄作业,但建议先看懂每条背后的逻辑,再结合自家硬件调整。
4.1 内核配置与启动参数:先定基础盘
实时内核必须启用PREEMPT_RT。这不仅仅是打开一个抢占选项,而是改变内核的同步机制和中断线程化方式。常规内核里,持锁的临界区可能长时间不可抢占,而PREEMPT_RT把多数自旋锁转换成可睡眠锁,让高优先级实时任务能够在更短的时间内获得CPU。
启动参数上,我最终使用的组合是:
isolcpus=2 nohz_full=2 rcu_nocbs=2 irqaffinity=0-1 skew_tick=1- isolcpus=2:把CPU2隔离出调度器的普通负载均衡范围。
- nohz_full=2:让CPU2上的定时器tick减少甚至停止,降低周期性的调度中断干扰。
- rcu_nocbs=2:把CPU2的RCU回调转移出去,避免RCU软中断打扰实时线程。
- irqaffinity=0-1:默认中断只允许发往0号和1号核。
- skew_tick=1:让各个CPU的时钟tick错开,避免同时触发造成瞬时抢资源。
这些参数联动的核心思路只有一个:把实时任务专属CPU从系统的“杂事”里彻底摘出来。隔离度越高,确定性越好。代价是剩下三核的负载压力变大,所以需要评估业务是否扛得住。
4.2 CPU分区与中断亲和性设计
多核实时系统最推荐的布局是ARM大小核调度之外的“静态分区”思路。实时线程独占一个核,中断和管理任务固定在指定核,网络和业务负载放剩余核。
以四核为例,我的分配方案如下:
- CPU0:处理所有外部中断,跑管理任务和网络协议栈。
- CPU1:运行普通业务线程、文件系统、内存回收等内核服务。
- CPU2:实时任务专用,只允许必要的内核线程出现。
- CPU3:预留,处理突发负载或做隔离备份。
中断亲和性通过写/proc/irq/{中断号}/smp_affinity实现,用十六进制掩码表示CPU集合。比如让设备中断只发往CPU0,就把掩码写成0x01。
# 将某个中断绑定到CPU0 echo 1 > /proc/irq/78/smp_affinity实际操作时,不要只盯着网卡中断。串口、USB控制器、存储控制器的中断都可能成为干扰源,逐个查看/proc/interrupts里的计数和归属,把它们统一收拢到非实时核。
4.3 电源管理与CPU调频:确定性的大敌
飞腾平台默认的CPU调频策略会根据负载动态改变运行频率,低频运行时面临突发中断,升频过程需要几百微秒甚至更长,这段窗口期内实时任务的执行时间会被拖长。在实时场景下必须先把调频调速器切到performance模式,锁住最高频率。
# 查看当前调速器 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 设置为performance并固定频率 echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governorCPUIdle相关的睡眠状态也要仔细筛选。深度睡眠状态虽然省电,但唤醒延迟很大。实时任务所在CPU通常建议禁用深度睡眠,或者在测试中记录不同睡眠状态下的最大延迟,确认没有超过指标。
4.4 网卡中断合并与DMA缓冲处理
网络数据包中断合并(interrupt coalescing)会让网卡攒一批数据再产生一次中断,降低普通场景下的CPU占用,但实时性场景下会引入不确定的批量延迟。如果实时控制流和数据采集走网口交互,建议关闭中断合并,让每个包都及时触发中断。
# 以常见网卡为例,关闭中断合并参数 ethtool -C eth0 adaptive-rx off ethtool -C eth0 rx-usecs 0DMA缓冲这一块容易被忽略。实时线程如果直接操作设备驱动分配的内存,要确保这些缓冲区在初始化阶段就完成分配和映射,运行期间不要动态分配。必要时通过内核配置预留显式物理内存区域,把DMA内存固定下来,避免缺页和内存碎片带来的延迟。
4.5 工作队列与内核线程的越界问题
隔离了CPU2之后,还要检查实时CPU上是否残留了不友好的内核线程。用ps命令可以列出每个线程的CPU亲和性和优先级,如果发现kworker、rcuos等工作队列线程出现在实时CPU上,需要主动把它们打发走。
# 查看CPU2上的内核线程 ps -eLo pid,pri,psr,comm | awk '$3==2' # 当发现不应存在的kworker时,调整其亲和性 taskset -pc 0-1 进程号更彻底的方法是在启动参数里用rcu_nocbs隔离RCU,再用cgroup把可抢占的工作队列移到普通核。实时CPU上保留的内核线程越少,调度器在切换实时任务时的负担就越轻。
5. 优化前后的实测数据对比:收益要放在真实负载下看
全部优化落地后,我重新跑了完整测试流程。以下是同一台机器、同一内核版本、同一条压测命令下的数据对比。以某一次稳定运行的测量窗口为准。
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 最小延迟 | 12微秒 | 10微秒 |
| 平均延迟 | 31微秒 | 22微秒 |
| 最大延迟 | 2.37毫秒 | 136微秒 |
| p99延迟 | 87微秒 | 49微秒 |
| p99.9延迟 | 356微秒 | 89微秒 |
最大延迟从2.37毫秒降到136微秒,这是最直观的收益。p99.9从356微秒降到89微秒,说明尾部延迟明显收敛,系统行为变得更加可预测。值得说明的是,这个数据是在后台stress-ng压满4核CPU、同时跑着网络压力的环境下测出来的。空载环境下最大延迟本来就低,优化前后差距没那么大,所以真实业务场景的测试更有说服力。
我还额外测了一种特殊情况:把串口控制台重新打开,让内核日志持续输出时,优化后的最大延迟重新升高到400微秒左右。这进一步验证了串口打印对实时性的破坏力——任何一条慢速I/O路径都可能成为延迟尖峰的来源。
从CPU利用率和吞吐的角度看,CPU隔离让普通业务核的负载上升,这也是预期的代价。实时核的空闲率明显增加,因为中断和RCU都被挪走了,它只需要专心处理实时线程。如果你的业务允许双操作系统或者AMP架构,这种静态分区带来的好处会更明显。
在飞腾平台上,优化不是做一次就结束。调频策略、中断分布、驱动参数都会因为具体外设不同而表现各异,每次改动后都要重新跑基线测试。能守住稳定性的方案,才算真正落地。
6. 看不见的边界条件:哪些场景下优化会失效
优化后的数据很漂亮,但我在总结时更在意的是边界条件。项目上线前如果只按“正常负载”验收,很容易漏掉那些隐藏的失效场景。
6.1 低功耗需求与实时性天然冲突
有客户场景要求设备在空闲时进入低功耗状态,CPU频率降到很低甚至进入深度睡眠。一旦这种状态被外部中断打断,唤醒路径的延迟会明显拉长。实时任务如果要在低功耗模式下继续保证微秒级响应,基本上不现实。我的处理方式是:实时任务运行期间强制关闭深度睡眠,系统空闲时再让非实时核进入低功耗,实时核保持恒定频率。这个取舍必须在需求阶段沟通清楚,否则上线后性能验收会出问题。
6.2 驱动的不可中断路径
PREEMPT_RT能把内核的大部分临界区变短,但某些驱动程序内部仍有关中断或者自旋锁保护的路径。比如某些GPU驱动、外部总线控制器驱动,在处理特定操作时可能长时间关中断。这类路径一旦被实时线程撞上,延迟尖峰就会直接出现。排查方法还是靠ftrace抓取最大延迟点的调用栈,找到具体驱动函数后要么修驱动,要么把相关设备操作分布到非实时核上避免冲突。
6.3 长时间运行后的温漂与老化
有些延迟尖峰不是一开始就出现,而是机器跑了一两个小时后才冒出来。这跟温度升高后内存控制器时序变化、散热导致风扇中断增多有关。所以测试不能只跑几分钟,我习惯让系统至少浸泡24小时以上,中途每十分钟记录一次最大延迟。如果最大延迟随时间推移有缓慢上升趋势,就要检查散热和风扇策略,必要时把风扇控制和温度监控中断移到非实时核。
这个边界条件在嵌入式设备里非常常见。实验室环境温度可控,现场机柜温度可能很高,不把温度因素纳入验收标准,迟早会在现场踩坑。
7. 经验沉淀:优化清单的可持续维护
每次优化落地后,我还会做一件容易被忽视的事:把改动整理成可追溯的配置管理记录。包括内核.config、启动参数、runtime写寄存器命令、每个irq的亲和性设置、以及对应的测试数据。因为现场运维人员不可能每次都调用底层调试接口,他们需要的是一份固化好的部署脚本。
脚本化的思路很简单,把运行时配置整理成启动后自动执行的脚本。比如把irq亲和性、调频策略、cpuset设置写进systemd service,确保每次开机都按同一套配置执行。下面给出一段参考逻辑:
# /etc/rt-tuning.sh 片段 # 设置performance调速器 for cpu in /sys/devices/system/cpu/cpu[0-3]; do echo performance > $cpu/cpufreq/scaling_governor done # 中断亲和性,按实际中断号调整 for irq in /proc/irq/*/smp_affinity; do echo 1 > $irq 2>/dev/null done # 实时核隔离 echo 2 > /sys/fs/cgroup/cpuset/rt/cpuset.cpus重要的一点,脚本必须放在系统服务启动流程前面,并且有幂等性,也就是重复执行不会产生副作用。否则一次误操作可能让整个实时环境失效,排查起来相当费劲。
除了脚本,还要保留每一次性能回归的测试记录。我会为一个版本建立单独的目录,存放cyclictest输出、后台压测配置、内核版本、启动参数快照。将来遇到新问题,直接对比历史数据就能知道是哪次改动引起的异常。这种习惯在项目维保期几乎等于救命稻草。
8. 实时调优之外:应用层还要留意什么
实践中有个现象:内核侧优化做得再完美,应用层如果写得不注意,实时性能照样崩。这里补充几个我在飞腾平台上验证过的应用层约束。
第一,实时线程里不要调用任何可能分配内存的函数。malloc、new在内存不足时可能触发页面回收和直接内存回收,这些路径会让线程卡顿几十毫秒。解决办法是启动阶段先分配好所需内存池,运行期全部复用。cyclictest带-m参数锁内存就是在模拟这种干净的环境。
第二,实时线程之间的通信优先用无锁数据结构。飞腾平台的内存屏障语义和调度模型跟x86有差异,如果多核之间频繁使用锁变量通信,即使内核延迟很小,锁竞争也会把总延迟拉长。我实测过同一个数据交换场景,无锁实现比互斥锁实现的最大延迟降低了一半以上。
第三,用户态的中断处理方式也要迭代。现代Linux允许通过io_uring或者eventfd机制处理设备事件,设计得当的话,实时线程只在事件真正到来时被唤醒,而不是周期性地轮询设备状态。轮询在负载低时看起来没问题,但负载一高,轮询本身就会成为新的干扰源。
如果项目里既有实时控制又有复杂业务逻辑,尽量在架构上做隔离:一个独立进程管实时控制,其他进程跑业务,进程间用共享内存通信。千万不要把实时逻辑和普通业务写进同一个大模块里,那是性能优化永远救不回来的设计缺陷。
9. 最后一轮验证:把优化后的方案压向更激进的场景
优化清单做完、数据也达标之后,我没有直接收工,而是把负载压向更极端的场景验证一份方案的韧性。
首先是中断风暴测试。我用一个脚本制造高频网络小包,让网卡中断频率冲到接近极限。这个场景下平均延迟只上升了大约20微秒,最大延迟仍然控制在250微秒以内。中断亲和性和关闭合并的优化在这一轮体现得最明显。如果不做这两项设置,同样的场景最大延迟早就破毫秒了。
然后是优先级反转测试。我故意让一个低优先级线程占用实时线程需要的资源,测试优先级继承机制是否正常工作。这一步是验证PREEMPT_RT下的实时互斥锁是否有正确的优先级继承行为。如果不继承优先级,高优先级实时线程会被低优先级线程无限阻塞,这在任何实时项目里都是灾难。好在测试结果符合预期。
最后是长时间稳定性验证。系统满载跑72小时,每10分钟统计一次最大延迟,整体波动范围保持在正负30微秒内,没有出现延迟随时间增长的漂移。我认为这套优化才算真正通过验收。
这块内容如果看客是刚接触实时Linux的,可能会觉得“优化后还要压这么狠,是不是小题大做”。但真实项目里,现场环境永远比实验室复杂。中断风暴、内存压力、驱动异常多多少少会出现,系统能承受多大扰动,决定了它能不能从实验室走进生产环境。
10. 写在性能优化总结之后:一些实在话
飞腾平台实时Linux的优化,做到最后拼的不是某个绝招,而是对每个细节的系统性治理。隔离CPU、绑定中断、锁频、防止页错误、控制日志输出,每一件单独看都琐碎,合在一起才是延迟指标改善的根本原因。回头看这次复盘,最大延迟从2.37毫秒降到136微秒的过程,本质上就是把这些琐碎逐一落实的过程。
我自己的体会是,测试方法的重要性不亚于优化手段。数据测不准,优化就无从谈起。务必保证每一次改动都有唯一变量,每一个结论都能被复现。团队里最好固定一个人负责性能基线数据维护,避免不同人用不同姿势测试导致结论互相矛盾。
如果你正准备在自己的飞腾板卡上做类似优化,建议先从启动参数和中断亲和性入手,这两项改造成本低、见效快。服务好之后再检查驱动里的长临界区,这个需要投入更多精力,但往往是最终能否守住指标的关键。希望这一篇总结能帮你少走一些弯路,让实时Linux方案真正从“能跑”变成“敢用”。