老内核实时化:ARM平台RT-Preempt补丁移植全记录
老设备要上实时这事儿没那么玄乎。前阵子我手里有一批基于ARM架构的老工控单板出厂带的还是Linux 3.0.101这个版本内核跑的是普通抢占模式业务侧当时反馈控制周期抖动很大偶尔一次任务超过调度时限整个流水线的联动就卡一下。项目需求很直接不换硬件不换主系统版本在这套老内核上把实时能力补上。方案挑来挑去最合适的就是给3.0.101打RT-Preempt补丁然后重新交叉编译刷到ARM开发环境里验证。这篇东西就是那次移植的完整记录包括补丁版本怎么选、内核怎么配、编译踩了什么坑、启动之后怎么确定实时真的生效、延迟数据怎么测以及一批我实际撞上的问题。内容面向的是要在老内核上做实时改造、又不想动不动就升级整个系统的嵌入式开发者。1. 移植前的方案设计与环境确认1.1 为什么要选3.0.101这套组合Linux内核的实时方案大体上有两条路一条是双内核典型代表是Xenomai另一条就是RT-Preempt补丁。双内核方案在工业领域用户不少但它要求应用层走独立出来的实时核驱动模型和普通Linux驱动完全不是一回事迁移成本高。RT-Preempt的思路是在Linux内核自身框架内把不可抢占的地方逐个击破把自旋锁换成可睡眠的rtmutex把中断处理强制线程化让调度器面对的是一个几乎处处可抢占的内核。这么做的最大好处是应用层不用改文件、网络、串口、GPIO这些接口跟原来的Linux行为保持兼容只有调度延迟这个维度发生质变。选3.0.101而不是直接跳到新内核原因很现实板子的BSP、外设驱动、老业务二进制库都绑死在3.0.x这个分支上强行升到4.x或5.x光是把BSP里的平台代码和板级初始化迁移过来就是大工程还要面对新内核里大量驱动接口变动。RT-Preempt在3.0.x时代已经发布得相当频繁官方发布目录里有专门针对该分支的补丁包补丁本身自带arch/arm目录下的改动对当时主流的单核/多核ARM SoC支持都比较成熟。内核老但实时化改造的路径是通的。还有个必须提前说破的认知RT-Preempt补丁不是拿来就能用的通用软件它对内核版本的要求是像素级的匹配。补丁发布方会给每个内核小版本生成专属补丁比如3.0.101对应的补丁编号和基线一定是完全对应的。不同小版本之间虽然差异不大但补丁程序在回打时一旦遇到上下行不匹配就会产生rej文件轻则某个函数没打上重则编译出一堆undefined reference。所以我拿到内核源码之后第一件事不是编译而是去查补丁的基线信息和changelog确认它声称支持的内核版本就是手里的这个3.0.101。1.2 硬件平台与工具链准备我这次的目标板是一块基于Cortex-A8架构的老式核心板单核主频800MHz内存256MB系统烧写在NAND上u-boot引导内核和设备树分开存放。这类板子在七八年前的工控设备里非常常见性能不强但胜在稳定跑RT-Preempt完全没问题唯一要注意的是内存占用会比普通内核多一些后面实测大概多耗20MB左右。交叉编译工具链我选的是arm-linux-gnueabihf系列注意后缀一定带hf即硬浮点。Cortex-A8原生就带VFPv3浮点单元硬浮点工具链编译出来的内核在上下文切换和浮点运算密集的中断回调里都要快不少。GCC版本这里有个讲究我实测用较新的8.x版本GCC编译3.0.101会碰到个别头文件宏兼容问题后来换用4.7.x或4.9.x的旧版工具链反而一路顺畅。所以环境准备阶段千万不要图新选一个与内核年代匹配的老工具链比后面去改代码省太多事。内核源码我直接从官方归档站拉取的linux-3.0.101完整源码包不是某些板厂发布的那个改过的BSP包。板厂BSP包通常已经乱七八糟打过一堆补丁再叠上RT补丁很容易冲突。正确姿势是先拿官方干净内核把RT补丁打上再按板卡的实际外设情况把BSP里的驱动代码单独调度过来。这个次序很重要我在一开始就吃过顺序颠倒的亏。2. 补丁集成与关键配置项调优2.1 给3.0.101打RT补丁的正确姿势补丁文件从官方项目目录下载文件名类似patch-3.0.101-rtXXX.patch.gz。RT补丁的版本号有两种记法一种是跟随内核版本后面加rt后缀一种是单独递增的序列号下载时注意看补丁头部的“tailor”信息确认它对应的就是3.0.101这个基线。我当时拿到的版本是3.0.101-rtXXX压缩包格式直接解压后丢进内核源码根目录执行zcat patch-3.0.101-rtXXX.patch.gz | patch -p1 --dry-run先跑dry-run这一步非常关键。它只做模拟不打真补丁纯粹检查补丁上下文是否匹配。输出里如果有“fail”或“hunk”相关提示说明补丁和源码基线不吻合这时候要先停下来排查而不是强行回打。模拟通过之后才正式执行zcat patch-3.0.101-rtXXX.patch.gz | patch -p1打完之后全库搜索一下有没有残留的.rej文件find linux-3.0.101/ -name *.rej -o -name *.orig有rej文件就说明有部分补丁块没打上多半是内核源码已经被之前的修改污染过。这种情况我一般直接禁用干净源码重新来比花几个小时手工合并rej划算得多。补丁打完之后内核里会多出几个关键的目录和文件比如include/linux/rwsem.h、kernel/rtmutex.c相关文件等ARCH目录下的改动也会体现在arch/arm/kernel和arch/arm/mm里。这些改动的主线逻辑是把所有中断处理中不可屏蔽的临界区替换成可睡眠的同步原语同时把中断处理程序从原子上下文改成线程化上下文。这不是几个文件的替换而是全内核范围内的锁机制替换所以打补丁本身不需要我们写一行代码但后续的配置和编译调试才是真正的重头戏。2.2 内核实时性相关选项的设置补丁打完之后进入配置阶段。我习惯先加载原板卡的defconfig做基础然后在此基础上做微调这样外设驱动、内存布局和文件系统相关的配置项都能保持板卡出厂状态不会因为切换配置而弄丢关键驱动。make ARCHarm board_defconfig make ARCHarm menuconfig进入menuconfig之后直奔Kernel Features这一层。3.0.101打完RT补丁之后抢占模式选项会多出几个新条目选择情况直接决定了最终内核的实时能力配置项推荐值说明CONFIG_PREEMPT_RT_FULLy完整实时抢占RT补丁的核心开关CONFIG_HZ_1000y时钟频率设为1000Hz调度粒度更细CONFIG_HIGH_RES_TIMERSy高精度定时器实测延迟必须依赖它CONFIG_NO_HZy动态时钟空闲时减少无谓的时钟中断CONFIG_IRQ_FORCED_THREADINGy强制中断线程化RT补丁默认依赖CONFIG_PREEMPTn老式的普通抢占与RT_FULL互斥CONFIG_CPU_FREQn实时场景下建议关闭频率切换会引入延迟CONFIG_CPU_IDLEn同上避免进入深度睡眠状态唤醒延迟CONFIG_PREEMPT_RT_FULL和CONFIG_PREEMPT是互斥的你在menuconfig里选中RT_FULL之后系统会自动把普通抢占关掉。这里比较容易迷惑人的是RT_FULL的本质是“完全实时抢占”不是选项字面上看起来的“允许在RT中使用抢占”它和CONFIG_PREEMPT_VOLUNTARY的层次不同。3.0.101这个年代的内核里RT_FULL在Kernel Features下直接可见选上即可。CONFIG_CPU_FREQ和CONFIG_CPU_IDLE这两个我建议在实时验证阶段直接关闭。CPU频率调节虽然能省电但在RT环境下频率切换的短暂停顿会在最不该出现的时候制造延迟毛刺。CPU进入C-state深度睡眠后唤醒路径上的中断响应也会变慢对微秒级延迟目标非常不利。等实时性指标稳定之后如果想保留省电特性可以再把它们打开做一轮对比测试但那就是优化阶段的事了。2.3 ARM中断线程化与时钟机制适配3.0.101的RT补丁在ARM架构下的核心改动集中在中断处理和时钟系统两块。ARM的GIC通用中断控制器在打了RT补丁之后所有中断都会默认走线程化路径。中断到来时硬中断上下文只做最基本的状态保存和唤醒操作真正的处理逻辑进入一个实时内核线程中执行。这个机制直接把长中断回调对系统实时性的影响隔离掉了但代价是某些中断回调里原本依赖原子上下文的操作会报错。比如在中断线程里调用了in_interrupt()判断上下文或者直接调用了mdelay()这类忙等待函数语义都发生了变化。时钟方面RT补丁对ARM的arch_sched_clock做了扩展要求板卡提供足够精度的时钟源来支撑高精度定时器。3.0内核里ARM的时钟源注册通常在arch/arm/plat-xxx/time.c或mach-xxx/timer.c里完成。如果板卡BSP里的定时器驱动还是老式的低精度tick实现RT补丁跑起来之后时钟中断会频繁抖动直接影响调度延迟测试数据。我当时做的适配是把板卡的定时器驱动从旧的struct clock_event_device注册方式检查了一遍确保它的set_next_event回调实现正确并且mult和shift两个参数严格按照时钟源频率计算得出。这里有个容易踩的坑mult/shift参数如果不匹配会直接导致时间不准确甚至完全卡死调制的时候先拿一个已知宽度的GPIO信号输入对比系统时间戳是否一致比盲改快得多。3. 编译、镜像制作与启动验证3.1 交叉编译与常见编译参数配置完成开始编译。我的完整命令是这样的make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- zImage -j4 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules -j4-j参数我一般不开满因为RT补丁下的内核源码有不少大文件并发过高反而会把老GCC的编译服务器内存吃爆实测-j4在老8核服务器上是稳定上限。编译过程中最让人头疼的是3.0.101的RT补丁使用了少量在新版GCC头文件中已移除的语法。如果工具链版本偏新会看到大量类似error: struct task_struct has no member named xxx的报错这时候第一反应不是去改代码而是核查工具链版本。我最后锁定的GCC 4.9.x内核编译完全没有问题所有报错归零。编译产物里zImage是我们要烧到板卡的内核镜像modules目录下会有完整的可加载内核模块。设备树文件如果板卡用的是设备树方式还需要单独编译dtbmake ARCHarm CROSS_COMPILEarm-linux-gnueabihf- board.dtb如果是老式的非设备树平台那就要检查arch/arm/mach-xxx下的板级初始化文件是否和RT补丁冲突。大部分3.0.x老板卡还在用board文件方式这种情况下arch/arm/mach-xxx/mach-xxx.c里那段MACHINE_START宏写的初始化逻辑不需要动但要注意init_irq回调里如果有手工设置中断优先级的操作必须确认它没有和线程化机制冲突。编译完成之后把zImage转成uImage格式给u-boot用。这里我直接用了内核源码里自带的内核镜像生成工具没装额外的mkimage软件包。转完把uImage和dtb一起传到tftp服务器准备网络下载启动调试。3.2 引导参数与设备树调整u-boot环境变量里bootargs的参数设置对RT补丁的实际行为影响很大。我最终使用的关键参数组是这样的consolettyS0,115200 root/dev/nfs nfsroot... rw ipdhcp调试阶段我坚决用NFS根文件系统启动原因很朴素内核映像可以反复快速迭代不用每次改完都往NAND里烧。NFS启动模式下模块加载路径直接指向开发机上的目录改代码调试的循环粒度可以缩短到秒级。RT补丁在启动参数里还有个特殊选项可以临时关闭中断线程化叫threadirqs。如果你在启动过程中怀疑某个驱动和线程化机制冲突可以先加上threadirqs0做对照实验。不过要注意这个参数只是调试辅助RT补丁的完整实时效果依赖路径上其他机制的配合最终交付版本里我是去掉这个参数的。另外bootargs里我还加了isolcpus相关参数的分支版本但那是在多核平台上调测时用的这次的Cortex-A8单核用不上。单核平台RT调优的关键不在CPU隔离而在中断亲和性和线程优先级配置上。3.3 怎么判断RT补丁真的生效了系统启动进入shell之后我习惯用几个命令快速确认实时化是否真正生效。第一落点看内核启动日志。RT补丁在开机时会在dmesg里打出类似的标志性信息dmesg | grep -i -E preempt|rt|thread正常打上RT_FULL之后/proc/version里会带上PREEMPT_RT字样这是最直观的标记。同时可以查cat /proc/version如果编译时选了RT_FULL应该看到#1 SMP PREEMPT_RT ...这类字样的arm内核版本信息。第二落点看内核配置导出接口zcat /proc/config.gz | grep CONFIG_PREEMPT_RT_FULL前提是编译时开了CONFIG_IKCONFIG否则这个文件不存在。很多板卡默认没开我强烈建议在做RT改造时把它打开后面任何配置验证都能直接在板子上查不用再翻源码目录。第三落点看实际行为。启动一个实时优先级较高的线程在它运行时手动对CPU做负载观察它是否还能保持稳定的调度周期。这属于功能验证真正的量化测试放到下一节专门讲。我这边判断RT补丁生效的实践经验是启动日志里出现forced thread irqs相关信息并且/proc/version里明确带PREEMPT_RT字样基本就可以确认内核已经进入实时模式了。4. 实时性测试与性能评估4.1 用cyclictest测量延迟抖动实时内核的成色如何不能靠嘴说必须用数据量化。我用的是实时社区的标准测试工具cyclictest它属于rt-tests工具集里的老牌成员。用法非常简单直接cyclictest -t 1 -p 80 -n -i 1000 -l 100000参数含义分别是指定单线程测试、实时优先级80、使用clock_nanosleep定时、周期1000微秒、跑10万个循环。测试输出会周期性打印一行数据包含当前循环的min、avg、max延迟值。在Cortex-A8单核平台上纯空载状态下的延迟数据我先跑了一轮结果如下最小延迟8微秒平均延迟32微秒最大延迟86微秒这个数据在打RT补丁之前是什么水平呢同平台同样的测试代码普通抢占内核下的最大延迟是2200多微秒差了一个数量还多而且普通内核的延迟分布尾部长最坏情况时不时就冒出来这对控制周期是致命的。RT补丁带来的最直接变化就是最大延迟这项指标被压到了三位数以内抖动尾巴被剪掉了。4.2 数据怎么看优化空间在哪延迟数据不是跑完一版就完事我把测试拆成了三种负载场景空载、网络Ping并发、磁盘读写并发分别记录最大延迟。场景覆盖的目的在于暴露真实工况下的调度最坏情况而不仅仅是实验室数据。Ping并发场景下网络中断的线程化处理会带来一些额外开销实测最大延迟从86微秒涨到了130微秒左右还在可接受范围内。磁盘读写场景更明显因为块设备层的锁和页缓存回收在RT补丁下经过重构偶尔会出现单次调度延迟超过200微秒的毛刺。针对这一点我把块设备层的请求队列调度器从默认的cfq换成了noop中断合并参数也调了毛刺次数下降了不少。还有一个被很多人忽略的优化点中断线程的优先级。RT补丁会把所有中断处理线程创建为实时线程默认优先级是基于中断号静态排序出来的。如果你的系统里有一个高频率中断和一个关键数据采集中断可以通过修改/proc/irq/irq/thread_priority之类的接口看版本或者直接在板级初始化里设定优先级把关键中断的线程优先级调高。我在这块板上把定时器中断线程优先级提到比网卡中断高的位置后最大延迟又压掉了大约20微秒。4.3 RT化带来的性能权衡RT-Preempt不是银弹它是有代价的。3.0.101打补丁后最直观的是内核内存占用增加了约20MB这是因为可睡眠锁的管理结构、线程栈的预分配都比原来重。其次全内核锁机制的替换让某些非实时关键路径变慢比如文件系统元数据操作、进程fork/exec这类频繁创建销毁的过程实测带有10%到15%的吞吐下降。但实时系统的设计目标本来就不是追求最大吞吐而是保证最坏情况下的确定性延迟。我们在项目里对业务方的承诺是控制周期无论负载怎么抖单次调度延迟不超过200微秒。实测数据满足了这个约束所以性能损耗是值得的。如果你的应用是纯转发型、对吞吐极度敏感但对延迟不敏感那RT-Preempt可能并不是最合适的方案。5. 踩坑实测典型问题与排查思路5.1 编译期间最常见的错误第一类错误集中在头文件不兼容。老内核源码配新GCC最容易爆出来的错误是某些内联函数用了已废弃的语法或者__attribute__的写法变了。解决方案前面已经说过换老工具链别让代码为新编译器做适配那是个无底洞。第二类错误是文件系统相关模块的整体编译失败。比如ext4、NFS这类大型模块在打RT补丁后偶尔出现某个函数定义不匹配通常是因为补丁版本和内核分支之间确实存在细微差异。我的经验是先看不匹配的错误是来自补丁改动过的文件还是未改动的文件如果来自未改动的文件很可能是内核源码本身在官方拉取下就有问题重新解压源码再做一遍最稳妥。第三类是变量未定义报错。这类问题多半出现在CONFIG_选项组合完之后某个配置组合把底层依赖选项弄丢了。遇到这类问题先回头检查.config里的依赖项看看那个缺失的符号对应的配置是否没开。5.2 启动即崩与中断风暴第一次刷RT内核进板卡时我在启动阶段遇到过一个非常典型的崩溃内核打印一串与中断相关的点后直接卡死没有任何oops输出。排查过程花了大半天最后定位到的原因是个别驱动在初始化时注册中断使用了IRQF_NO_THREAD标志强制中断处理留在硬中断上下文而这个驱动内部却调用了会睡眠的函数。RT补丁下这种违例不会立刻报警而是拖到运行期某次中断触发时直接卡死。排查方法是用排除法逐类禁用板卡上的外设模块缩小范围后定位到具体驱动。定位到之后驱动代码里必然存在的违规行为无非是三种在硬中断回调里调用了可睡眠函数、中断回调里释放了自旋锁、或者错误使用了in_interrupt()判断上下文。把违规调用改成tasklet或直接改成线程上下文之后问题消失。另外一个我印象深刻的坑是中断风暴。板卡启动后系统负载不高但/proc/interrupts里的定时器中断计数暴涨多核负载高得离谱。这个问题的根源是时钟事件驱动注册时set_next_event回调里没有正确处理单触发模式的重复重挂RT补丁下高精度定时器反复触发导致中断风暴。修复方法是在板级timer驱动里检查clockevents_config_and_register的调用方式是否符合单触发语义。5.3 驱动锁与网络延迟问题网络性能在RT化后会有一段阵痛期。我测试时发现网络吞吐掉了一半多延时不降反升看系统日志完全是正常状态但就是慢。后来用lock_stat工具一看网卡驱动的自旋锁在RT补丁下已经被替换成了rtmutex而驱动里某个热路径上的临界区异常长所有网络包处理被串行化了。解决办法是两选一一是把网卡驱动的NAPI轮询逻辑里的大临界区去掉把多包处理拆成小步二是给网卡开启多队列及RPS/RFS让每个CPU有自己的处理队列。我的平台是单核所以主要走了拆临界区的路子。改完再跑网络吞吐恢复到了原内核水平的八成左右基本满足业务需求。这个问题的排查思路是要意识到RT补丁改变了同步原语的语义原本自旋锁保护的短临界区在RT下变成可睡眠的多个中断线程会同时等待同一把锁如果临界区写得差性能会比普通内核下更糟糕。这也是为什么很多老驱动在RT内核上性能不升反降的原因。5.4 调试工具与日志取证RT环境下出问题比普通内核难排查很多因为很多bug是时序相关的看日志根本复现不出来。我这边用到的几个工具简单列一下都是常规可用不做营销的那种lock_stat配合CONFIG_LOCK_STAT配置开启能输出锁的竞争频率、等待时间排查驱动锁问题必备。ftraceRT补丁时代已经比较成熟可以追踪调度点、唤醒延迟和中段线程的运行轨迹。配置CONFIG_FUNCTION_TRACER之后用trace-cmd record -p function_graph -l irq* record这样抓取分析中断线程调度的实际情况。perf sched查看调度统计和延迟分布可以用来对比RT内核和普通内核在调度维度上的差异生成调度延迟的时间轴图。日志取证方面除了dmesg之外我强烈建议在启动参数里加上loglevel7观察RT补丁的详细信息同时打开CONFIG_SCHEDSTATS这样/proc/pid/schedstat里才能看到每个线程的调度延迟和排队时间方便定位具体哪个线程在延迟敏感链路上拖后腿。6. 项目里的经验沉淀写到这差不多就是移植的主线收尾了。这次3.0.101的RT-Preempt移植前前后后从环境准备到数据达标大概花了三周时间真正写代码的时间其实很少大部分时间都用在了版本匹配、配置检查、驱动适配和延迟数据反复测试上。我对这套老内核实时化路线的体会是RT-Preempt的价值下限很高它能在不改应用、不换硬件的前提下把调度延迟这个核心指标压到百微秒级对绝大多数工业控制场景已经够用。但它的价值上限也受限明显要接受内核内存占用增加、驱动并发语义变化带来的一系列适配工作。实时系统从来没有免费午餐RT-Preempt把复杂度从应用层挪到了内核层而内核层的复杂度就需要用扎实的编译、配置和调试基本功去消化。最后留一个比较容易被初学者忽略的提醒RT补丁打上之后内核里任何新加的驱动代码都要带着实时意识去写尤其是中断回调里绝对不能出现睡眠调用锁的选择上能上rtmutex就别再抱着自旋锁不撒手。实时性是整个系统从头到尾的行为约束不是某一个配置开关开了就一了百了的。