首页 / 资讯中心 / 文章详情

老内核实时化改造:RT-Preempt移植全流程与延迟优化实测

老内核实时化改造:RT-Preempt移植全流程与延迟优化实测 ★ FEATURED ARTICLE
嵌入式圈子有个老生常谈的问题BSP锁在了一个五六年甚至十年前的内核上业务还要上实时控制怎么办我年前正好接手了这么一摊事把Linux 3.0.101内核移植了RT-Preempt补丁到一块ARM Cortex-A9开发板上前后折腾了小两周。这篇就是把整个过程的选型逻辑、配置细节、编译部署和实测数据整理出来给同样被老内核绑定、又想做实时性改造的兄弟们一条可以直接抄的路径。先说结论在老内核上做RT-Preempt移植技术难度并没有想象中那么大真正的坑集中在三个方面——补丁版本匹配、驱动兼容性和功耗/中断行为变化。把这三点拿住后面基本就是顺水推舟。1. 为什么放着新内核不用偏要在3.0.101上做实时1.1 被BSP绑定的现实谁都想要新内核但嵌入式产品不是你想升就能升的。我手上这块板子的BSP是厂家基于3.0.101定制的里面包含了自己改过的显示驱动、电源管理框架、以及一堆板级初始化代码。真要升级到4.4或者5.x意味着所有底层驱动都得重写验证周期少说两个月。而客户给的功能交付周期只有三周。这种情况下在现有内核上打RT补丁是性价比最高的方案。RT-Preempt全称PREEMPT_RT本身就是对主线内核的一组增量补丁它不改变内核的功能框架只是把调度、中断和锁机制替换成具有确定性的实现。对驱动和BSP代码的侵入很小大部分驱动只要能正常编译通过行为基本不变。1.2 3.0.101这个版本的特殊性3.0.101是3.0.x系列生命周期末尾的一个稳定版本维护周期覆盖到了2013年之后。在它的生命周期中RT补丁社区同步维护了对应的PREEMPT_RT分支这就保证了当时能拿到经过验证的补丁包。如果你用的是某个特别冷门的版本号比如3.0.87虽然也能找到对应的rt补丁但维护力度明显不如长期稳定分支。另外3.0.x时代ARM平台还没有强制全面设备树化很多板卡走的是板级文件arch/arm/mach-xxx加静态平台设备的方式。这对移植来说反而是个好消息少了一层设备树状态转换的问题编译出来的镜像和原来的启动流程完全兼容。提示动手之前先确认你板子的启动方式。如果本来就是设备树引导那内核配置里CONFIG_ARM_APPENDED_DTB之类的选项要小心如果走的是传统ATAGS启动3.0.101的RT补丁基本不碰这块风险很低。2. 动手之前先搞懂RT补丁改了什么我觉得这个环节比补丁本身更重要。很多人打完补丁不会配配了不会验证就是因为不知道RT补丁到底动了内核哪几根筋。这里用最直白的话讲清楚三件事。2.1 中断线程化让硬中断变成可调度的任务RT补丁最核心的改变是把几乎所有的硬中断处理放到内核线程里执行。原来一个外设中断进来CPU可能要立刻跳到中断处理函数RT内核则只做最关键的动作——比如给中断控制器应答一下——然后把这个中断的工作打包成一个实时线程去调度。这意味着什么意味着某个中断处理即使执行时间很长也不会直接打断当前正在运行的高优先级实时任务。只要那个实时任务优先级更高它就能在中断线程被调度之前继续跑。这是系统延迟变得稳定的最根本来源。2.2 锁机制替换spinlock变成了可睡眠锁这是另一个容易被误解的点。在上游内核里自旋锁spinlock的临界区是禁止抢占的同一个CPU上别的任务压根不能跑。RT补丁把大部分spinlock替换成了基于rtmutex实现的可睡眠锁。这样改之后持锁的线程是允许被更高优先级的任务抢走的前提是那个任务没有去抢同一把锁里面被保护的数据。而rtmutex自带优先级继承机制低优先级线程拿着锁时如果挡住了高优先级线程系统会临时提高低优先级线程的优先级让它赶紧跑完释放锁。这套机制下来系统的最坏情况延迟上限有数学保证而不是靠运气。2.3 高精度时钟和优先级继承的联动实时调度没有精准的时钟等于白搭。RT补丁正常会启用高精度定时器hrtimer配合1000Hz的节拍频率把内核线程的唤醒精度提升到微秒级别。同时RT补丁里中断线程、定时器线程、软中断处理线程都明确了各自的调度策略并支持SCHED_FIFO和SCHED_RR这两种实时调度策略配合优先级继承保证了从中断产生到实时任务真正跑起来这个链条里的每一环都不会被无界等待。这三条理解了后面看menuconfig里的选项就不会一头雾水。每个配置项背后都对应这里讲的某一项机制。3. 补丁获取与版本匹配这一步错后面全白搭3.1 找对补丁的命名规律RT补丁的发布命名非常规律patch-内核版本-rt编号.patch。比如linux-3.0.101内核对应的补丁就是patch-3.0.101-rt110.patch这样的格式。注意这里有两个版本号前面的3.0.101必须和你的内核版本完全一致后面的rt编号则代表PREEMPT_RT补丁的迭代次数rt编号越大说明修的问题越多。下载的时候别只看内核版本还要确认这个补丁是对应哪个维护阶段的。RT补丁维护者会同时维护多个内核分支的rt补丁你必须在和内核小版本完全匹配的目录下去找。举个例子3.0.101和3.0.102的代码差异虽然很小但直接用3.0.102的rt补丁打到3.0.101上几乎必定失败因为补丁里的上下文行号对不上。3.2 patch命令的正确操作姿势拿到补丁之后进内核源码根目录用patch命令打进去。这里有一个细节补丁发布时常常是压缩格式需要先解压再用标准输入送入patchcd linux-3.0.101 xzcat ../patch-3.0.101-rt110.patch.xz | patch -p1-p1这个参数的意思是剥离补丁路径里第一层目录前缀。绝大多数内核补丁都要求-p1如果加错了或者不加patch会直接报错说找不到文件。打完补丁建议马上做一次确认grep CONFIG_PREEMPT_RT init/Kconfig能看到PREEMPT_RT相关的Kconfig条目说明补丁确实进去了。另外补丁目录下还会多出localversion-rt之类的文件这个文件最终会影响内核版本号显示后面检查内核信息时能看到带rt后缀的版本。3.3 版本匹配失败的特征与应对最常见的情况是patch执行到一半报Hunk #N FAILED at ...。遇到这种不用慌多数是因为内核源码不干净——之前改过东西或者没有完全恢复。处理办法是先git checkout .或者从tar包重新解压一遍再打补丁。如果确认源码是干净的还是失败那基本可以断定补丁版本不对。此时不要硬着头皮用--fuzz参数强行打就算打上了那些没打对的地方会在编译时暴露出一堆莫名其妙的错误极难排查。宁可多花十分钟找到正确的补丁版本重来一遍。注意打补丁之前严禁对源码做任何手动修改包括换行符转换、脚本批量替换这类看似无害的操作。RT补丁对上下文非常敏感一点点差异都会导致失败或者打错位置。4. 内核配置菜单里必须盯紧的开关4.1 抢占模型的选择打上RT补丁后内核的抢占配置会多出新的选项。在3.0.101的RT补丁下CONFIG_PREEMPT_RT_FULL就是我们要选的完全实时抢占模式。这个选项有几个前提选项必须同时打开比如CONFIG_PREEMPT、CONFIG_PREEMPT_RT等menuconfig里一般会根据依赖关系自动处理。这里提醒一下如果之前的内核配置里选了CONFIG_PREEMPT_VOLUNTARY或者CONFIG_PREEMPT_NONE必须在抢占模型菜单里改成ULL_PREEMPT_RT_FULL实际上菜单结构是Preemption Model下面选择Fully Preemptible Kernel (RT)。这个选项选错了补丁等于白打实时性不会有任何改善。4.2 必须一起打开的配置项我整理的这组配置是RT改造的标配组合缺哪个实时性都会打折CONFIG_HIGH_RES_TIMERSy高精度定时器RT内核唤醒精度的基础CONFIG_HZ_1000y内核节拍频率设为1000Hz降低调度粒度CONFIG_IRQFORCE_THREADINGy强制所有中断线程化。这个一般会自动打开但如果被关了某些后续出现的request_irq会直接报错需要特别注意CONFIG_PREEMPT_RTyRT补丁的基础开关CONFIG_CPUMASK_OFFSTACKy有些ARM平台要求用于CPU亲和性向量分配以我用的A9平台为例菜单里我把System Type的默认配置保留不变只改实时性相关的条目。然后用以下命令把配置导出到文件方便走查make ARCHarm menuconfig # 手动修改配置后保存 cp .config my_rt_defconfig建议把RT相关的配置单独做个顶层注释比如在.config文件开头写一段说明标明哪些配置是这次移植加入的哪些是BSP原有的。后面升级调试时这份记录能替你省下大量翻菜单的时间。4.3 与此冲突、需要关闭的配置有开就有关。RT内核里有两个方向的东西必须处理干净否则要么编译不过要么运行期出怪事。第一个是调试选项。CONFIG_DEBUG_SPINLOCK、CONFIG_DEBUG_ATOMIC_SLEEP在普通内核里斩很好的调试工具但在RT内核下会频繁报BUG: sleeping function called from invalid context——这不是内核真出问题了而是spinlock变成可睡眠锁之后原本原子上下文里不允许睡眠的旧检查逻辑和新的锁语义冲突了。这批检查在RT里基本都建议关掉不然内核日志会被刷屏甚至导致系统卡死。第二个是内核里某些驱动自己实现了不可中断的关中断临界区典型如CONFIG_CPU_IDLE里的cpuidle governor。这个后面坑的部分会详细讲。在配置阶段建议先把CONFIG_CPU_IDLE关掉先保证系统能跑起来再逐步开启验证。还有些平台相关的选项比如老内核里常见的CONFIG_MTD_CFI探测时序、DMA引擎的CONFIG_DMADEVICES如果发现编译错误优先排查是不是这些驱动里用了旧式的local_irq_save/local_irq_disable并且和RT补丁的检查冲突。判断标准很简单RT补丁会主动把不兼容的驱动列入该驱动基于非实时内核编写请自行验证的范畴Kconfig里通常有提示遇到就单独评估。5. 交叉编译与部署从源码到开发板跑起来5.1 编译环境的准备这块板子用的是armv7-a架构工具链选择arm-linux-gnueabihf-系列属于常规操作。需要注意的是工具链版本别太新3.0.x时代的内核用太新的GCC有时会遇到__aeabi_unwind_cpp_pr0这类链接错误或者头文件兼容性告警。我最后用的是gcc 4.9.4的工具链稳定没有坑。环境变量统一在编译目录顶部设置export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- export INSTALL_MOD_PATH/opt/rootfs5.2 编译命令与常见错误配置好之后编译内核和模块的命令很常规make ARCHarm uImage make ARCHarm modules make ARCHarm INSTALL_MOD_PATH/opt/rootfs modules_install第一步编译内核镜像时如果BSP里原来有U-Boot的旧式image格式需求需要确认CONFIG_ARM_ATAG_DTB_COMPAT这类的设置否则出来的uImage在U-Boot下启动会卡在Starting kernel。编译过程里我最常遇到的错误是驱动文件里的spin_lock相关API变化。RT补丁把spin_lock底层改成rtmutex后有些代码虽然在普通内核下能过在RT下却报出类型不匹配或者函数参数不对。这类问题没有捷径只能逐个看编译告警把使用了raw_spin_lock与spin_lock混用的代码按照RT语义调整。经验之谈驱动里真正需要保证不可抢的极端临界区本来就该用raw_spin_lockRT内核里它的语义和普通内核完全一致反而是原来的spin_lock需要细看。5.3 uImage、设备树与rootfs部署细节我的板卡使用传统ATAGS启动所以不需要设备树编译出来uImage直接替换boot分区里的旧镜像就行。如果你是设备树启动记得确认DTB里的中断控制器节点和RT补丁的CONFIG_IRQCHIP配置是否匹配个别老平台的中断控制器驱动在RT下有优先级翻转问题需要在设备树里调整interrupt-controller的默认触发方式。rootfs方面我直接在NFS挂载的rootfs上调试省去反复刷SD卡的等待。模块安装到/opt/rootfs之后通过NFS能实时看到驱动加载情况。等到系统稳定了再集成为ext4镜像烧写。启动后的第一件事不是跑测延迟而是先确认内核版本号uname -a # Linux (none) 3.0.101-rt110 #1 SMP PREEMPT RT ...看到PREEMPT RT字样说明补丁生效系统已经以实时抢占模式运行。这一步确认好再继续下面的测试。6. 用数据说话实时性到底改善了没有6.1 cyclictest的编译与用法实时性验证业界几乎统一用cyclictest它来自rt-tests工具集。这套工具很小直接拿源码交叉编译即可make CROSS_COMPILEarm-linux-gnueabihf- cyclictest把编译出来的cyclictest放进rootfs开发板上执行./cyclictest -t 1 -p 80 -n -i 1000 -l 100000这个命令的意思是启动1个测量线程实时优先级80使用绝对时钟-n就是clock_nanosleep方式采样间隔1000微秒循环10万次。最终会输出最小值、平均值、最大值三组延迟数据单位微秒。这个max值就是判断实时性最关键的数字。虽然我的场景对多核隔离没有要求但如果你用的是SMP多核ARM建议加-a 1参数把测试线程固定在某个核上避免CPU迁移带来的测量毛刺。6.2 实测数据解读我在这块Cortex-A9上测到的结果大致如下数值已按脱敏处理量级真实可参考配置状态平均延迟最大延迟普通内核CONFIG_PREEMPTy80-120微秒500-800微秒RT补丁优先级8030-50微秒180-220微秒RT补丁优先级9020-30微秒120-150微秒看数据有两点值得说。第一RT补丁的提升大头不在平均值而是最大延迟。普通内核下max值动不动飙到几百微秒这在控制周期是1ms的伺服系统里直接就是灾难。RT之后max值能压到相对稳定的区间这才是实时内核的立身之本。第二测试线程优先级对结果的物理影响超出很多人预期。优先级从80提高到90最大延迟几乎砍掉一半。原因在于高优先级实时线程可以更早抢占内核里的软中断线程和中断线程。实际项目里要根据系统的总线程数量和负载情况给控制任务留出足够的优先级余量。6.3 影响延迟的隐藏因素测出稳定延迟不代表完事大吉有几个隐藏因素会偷偷毁掉你的实时性。最典型的是内核里仍然存在的关中断长临界区典型来源是DMA驱动、NAND控制器驱动。这些代码用raw_local_irq_disable直接关中断RT内核也没办法强制抢占它们。我当初就发现在跑NAND读写的同时做cyclictestmax值从150微秒直接跳到600微秒。排查手段是打开CONFIG_WAKEUP_LATENCY相关的trace工具或者在板级文件里临时把相关驱动的中断优先级调低看看延迟是否回落。另一个因素是中断风暴。某个设备如果产生高频中断比如网卡收包即使中断线程化了中断线程本身也会频繁被唤醒挤压实时任务。这种情况可以在线程化之后用chrt -r提高实时任务的优先级同时用中断亲和性把高频中断绑到另一个核上。7. 我这轮移植踩过的坑替你们排一遍7.1 驱动在RT内核下编译不过第一个大坑来自某平台自身的显示驱动。这个驱动里重度使用了spin_lock_irqsave并且假设锁的持有期间CPU不会被打断。在RT内核下spin_lock语义改变造成显示缓冲区的DMA操作与buffer update之间存在竞态编译本身没问题但运行时会偶发花屏。处理办法是严格区分临界区性质只有直接操作硬件寄存器、且不允许被任何中断打断的短小操作才用raw_spin_lock_irqsave涉及DMA描述符链表更新这类操作改为使用rt_mutex加等待队列的机制让等待DMA完成的代码从忙等变成可睡眠等待。这是一个中等规模的驱动改动需要反复验证。如果你只是想让驱动先跑起来一个相对省事的做法是把驱动编译成模块在RT内核启动后再insmod加载。这样即使驱动里有不兼容的行为系统整体实时性不会因为初始化阶段卡在某个中断里而被拖垮。7.2 中断线程化导致外设异常这个坑非常隐蔽体现在UART上就是串口丢数据。原因倒不复杂UART的RX中断线程化之后中断线程的调度优先级低于发送数据线程当串口接收FIFO很小且baud率很高时线程还没被调度到FIFO已经溢出了。解决思路有两个方向。一个是在设备树或板级平台数据里把UART中断线程的优先级调高优先保证它被及时调度。另一个是在实测后发现实在无法满足波特率要求时给UART驱动打桩把RX路径改回硬中断处理request_irq时使用IRQF_NO_THREAD标志。这个标志能让指定中断绕过线程化机制保留原始语义。IRQF_NO_THREAD不是随便用的每多一个非线程化中断系统的最大延迟上限就可能被拉高。我最终只给调试串口保留了这个特例对外通信串口还是走线程化方案代价是用更大的DMA缓冲吸收FIFO溢出。7.3 电源管理配置与实时性的冲突第三个坑来自cpuidle。3.0.101的老cpuidle驱动会在CPU进入WFI/idle状态时执行较长的阻塞操作期间中断延迟会被拉高。RT内核社区很早就提到过深度idle状态和实时性天然冲突通常建议评估时把cpuidle关掉只保留WFI至少保证系统在大部分测试场景下没有idle引入的额外延迟。我在实测中确认开启cpuidle后系统空载时cyclictest结果偶尔出现300微秒以上的尖峰关闭cpuidle尖峰消失。如果你的项目对功耗没有那么极端的要求直接CONFIG_CPU_IDLEn是最省事的方案。同样需要警惕的是老内核里的CONFIG_CPU_FREQ调频调压驱动在触发频率切换时会短暂阻塞当前CPU上的调度。实时控制任务往往对频率切换造成的延迟非常敏感建议评估测试阶段先把CPU调度器设置为performance模式避免变频干扰测试数据。7.4 一个绕不开的排查工具中断延迟trace老内核的RT补丁自带/proc/latency_stats和/sys/kernel/debug/tracing接口。在排查为什么最大延迟突然变大时先刷一下latency_stats能看到到底是什么内核线程、在哪个函数附近产生了微秒级的延迟峰。这个列表优先看那些irq/设备名结尾的线程它们通常是中断线程化的直接产物。用起来很简单cat /proc/latency_stats输出里的每一行标注了一个内核线程的活跃最高延迟试着把irq/xxx这类线程的优先级调低一点或者把对应设备的中断绑到空闲核上往往就能把干扰源压下去。这个过程就像排查程序瓶颈只是对象从用户态线程换成了内核中断线程而已。最后再分享一个小技巧RT补丁移植完之后别急着把所有功能的验证都做一遍先跑至少4小时的stress-ng加cyclictest组合测试。我这次的板子在连续跑了6小时后暴露了一个只有长时间运行才会出现的DMA超时问题而短时间测试完全看不出来。实时性这种特性问题往往出在低频偶发路径上压测时间线拉长问题才愿意露头。
阅读完成 · 觉得有帮助?
咨询建站