TX2平台RT-Linux实时内核移植实战:从补丁到确定性调度
一开始并不打算碰实时内核手里的TX2模块跑官方BSP自带的Linux做视觉定位工位也一直没出过大问题。真正让我下决心移植RT-Linux也就是PREEMPT_RT的是一次被甲方拿示波器当场打脸的联调位置闭环周期设了1ms任务调度器的实际响应却时常跳到3ms以上高速运动中重复定位精度直接崩。从那时我才意识到在TX2这类嵌入式平台上缺的不是算力而是确定性的调度能力。把RT-Linux移植进去不是为了跑分是为了让每一个中断和任务调度都变得可预期。这篇文章就把我在这块TX2模块上移植RT-Linux的完整过程写出来涉及内核源码、实时补丁、设备树、根文件系统部署、cyclictest延迟验证以及那些文档里不会写清楚的坑。1. 移植RT-Linux前先弄清楚TX2缺的是什么1.1 一次抖动超标带来的反思那次联调用的是一款基于TX2的图像脉冲输出设备工作逻辑其实很简单相机触发信号进来系统响应后计算出补偿量再通过GPIO输出脉冲控制运动平台。真正的问题在“实时”两个字上。普通内核虽然也能在1ms内完成绝大多数任务但它是尽力而为无法保证最坏情况。一旦系统里出现网卡中断、内存紧张、其他进程抢占调度延迟就会无规则地拉长。我当时的测试方法是直接把示波器接在GPIO上触发后测输出翻转的时延跑了半小时均值只有二百多微秒看起来不错。可把最大延迟拉出来看超过2.8ms的数据点一抓一大把。这个“看起来不错”的系统其实根本扛不住硬实时需求。要解决这个问题就把内核换成PREEMPT_RT。1.2 PREEMPT_RT与默认内核的本质差异在Linux未打RT补丁时内核里大量临界区是禁止抢占的。一个普通进程正在执行系统调用或者内核线程在操作链表、自旋锁这时即使RT任务优先级很高也必须在临界区外等待。PREEMPT_RT补丁的核心思路是把很多自旋锁替换为可睡眠的互斥锁让临界区变成可抢占点再配合强制中断线程化让绝大多数中断不能无限期打断RT任务。用个生活化的类比默认内核像一个人在不允许插队的柜台办事不管后面排的队伍多重要必须等当前窗口结束PREEMPT_RT则把每个大窗口拆成很多个小窗口优先级的含义从“排队顺序”变成了“随时可以插队”高优先级任务几乎可以在任何代码路径上抢到CPU。TX2默认BSP内核的配置通常是CONFIG_PREEMPT_VOLUNTARY或CONFIG_PREEMPT__LL这类档次进程上下文可以做一定程度的抢占但内核临界区依然不能。移植成RT后CONFIG_PREEMPT_RT_FULLy才真正把内核的“全局不允许抢占”逻辑拆除。1.3 移植目标把“最坏情况”变成可承诺指标实时系统最看重的指标是“最大延迟”而不是“平均延迟”。我给自己定下的目标是GPIO软件响应最坏延迟不超过500微秒cyclictest在指定核心上长时间运行最大延迟不超过150微秒。这个目标不能靠感觉必须有一串可复现的测量数据支撑。这个目标也决定了后面的所有取舍哪些内核功能该关掉、哪些配置该打开、CPU频率策略怎么调、中断绑到哪个核、RT线程应该放哪个核上跑都围绕这一条做。2. 版本对齐与构建环境准备2.1 确认板卡发布包和当前内核版本移植第一步不是急着下载补丁而是确认手头板子运行的BSP版本。TX2的不同发布包对应不同的内核基线常见的是4.9分支也有4.14分支。查版本的命令很直接# 在板子上查看发布包版本 head -n 1 /etc/nv_tegra_release # 查看运行中内核版本和编译信息 uname -a # 查看内核当前抢占配置 zcat /proc/config.gz | grep PREEMPT如果/proc/config.gz不存在说明内核没有开CONFIG_IKCONFIG_PROC那就得在源码目录里找构建时用的config文件。记录下完整的uname -r比如某个小版本后续找实时补丁就靠它对齐。2.2 获取匹配的实时补丁PREEMPT_RT补丁在Linux RT项目里是按内核小版本发布的比如针对4.9.xxx系列会有对应的patch-4.9.xxx-rtXXX.patch.gz。理论上补丁必须打到和它同一小版本的干净内核上。TX2官方BSP内核对4.9主线会有大量下游改动直接拿官方BSP源码打RT补丁有较大概率出现补丁冲突。这里我的做法是拿到当前BSP匹配的内核源码包看清楚它的基线版本例如某版BSP自带的是4.9.140。尽量选择与这个基线最接近的RT补丁版本。如果冲突太多宁可在BSP源码基础上手动调整也不要强行换个与BSP完全无关的主线内核否则NVIDIA的板级驱动、设备树、电源管理代码全都对不上。实际中很多开发板常见的RT移植方法是把官方内核回退到纯主线4.9后再打RT补丁但TX2上我不建议一上来就这么干因为TX2的很多东西依赖BSP的板级补丁脱离之后可能连基本外设都起不来。我的方案更保守基于BSP源码树应用RT补丁再手工处理冲突。2.3 建立交叉编译环境在主机上准备AArch64交叉编译工具链。Ubuntu系统直接安装sudo apt install gcc-aarch64-linux-gnu make bc bison flex libssl-dev libncurses-dev导出环境变量时注意大小写export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu-编译主机内存最好16GB以上磁盘预留30GB左右。TX2内核源码解压后不算大但编译过程会生成大量临时文件特别是模块多的时候磁盘不够会造成不明不白的“No space left on device”。2.4 内核源码的最小清理源码包拿到后先不要急着配置做一次彻底清理避免上次编译残留影响make distclean然后确认源码根目录的Makefile里VERSION、PATCHLEVEL、SUBLEVEL、EXTRAVERSION的值确认基线版本。比如值为4.9.140那你的目标就是找一个接近4.9.140的RT补丁。如果你手里源码的SUBLEVEL和RT补丁版本差一两个小版本可以先给源码单独打上对应的主线小版本补丁但这一步需要谨慎。通常我会优先找一个完全匹配版本的patch省得手动解决几十个冲突。3. 打补丁和内核配置90%的问题都出在这里3.1 patch过程中遇到的三种状况打补丁的命令很简单zcat patch-4.9.140-rt1.patch.gz | patch -p1 --dry-run先dry-run一次绝不直接应用。输出会有三种情况clean补丁干净应用最理想。offset补丁能打上但有偏移。通常没问题但要看偏移量是不是很大。如果偏移了十几行以上就要警惕是不是基线已经有较大差异。FAILED有拒绝的hunk最麻烦。这时不能不管先看一下.rej文件里的具体内容手动修改。RT补丁绝大多数冲突集中在kernel/目录和arch/arm64相关的文件里手动合并时至少要理解补丁在做什么不能无脑强行塞进去。我踩过的坑某个版本的补丁已经包含了部分printk修改而BSP源码在那段代码上做过定制直接应用会把BSP里的调试功能覆盖掉编译倒是能过但运行日志行为怪怪的。所以打完补丁后用git diff --stat配合git diff逐一确认关键文件的改动范围很有必要。3.2 需要重点关注的内核配置项RT补丁应用成功后进入配置阶段。先在BSP默认配置基础上生成初始配置make tegra18_defconfig如果没有这个目标就到arch/arm64/configs/下找类似tegra_defconfig的文件用make 那个文件名加载。然后打开图形配置界面make menuconfig需要重点确认以下几项CONFIG_PREEMPT_RT_FULLy这是RT内核的主开关对应菜单路径Kernel Features - Preemption Model - Fully Preemptible Kernel (RT)。CONFIG_HZ_1000y内核时钟频率调到1000Hz。实时任务通常希望时间粒度为1ms如果内核还在100Hz很多延迟指标天然吃亏。CONFIG_HIGH_RES_TIMERSy高精度定时器必须打开。CONFIG_IRQ_FORCED_THREADINGy强制中断线程化。这样中断处理程序不再直接抢占RT任务的上下文而是排队到内核线程里执行。CONFIG_CPUMASK_OFFSTACKy如果支持建议打开便于更灵活地设置CPU亲和性。CONFIG_NO_HZ_FULLy可选但如果做CPU隔离调优很有用。同时要检查一些会拖后腿的调试选项CONFIG_LOCKDEP锁依赖检查调试用生产环境必须关否则RT路径里每次锁操作都带额外检查延迟显著上升。CONFIG_PROVE_LOCKING同样会拖慢锁路径。CONFIG_DEBUG_PREEMPTRT下会加大开销建议关。CONFIG_SCHED_DEBUG倒不影响延迟但会导致数据量大建议保持关闭。配置完成后把配置保存下来make savedefconfig这样能生成一个精简的defconfig后续多次构建或调整时效率高。3.3 编译Image、设备树和模块配置完成后编译。TX2是64位ARM最终目标是Image而不是zImagemake Image dtbs modules -j$(nproc)编译过程中常见错误包括缺少libssl-dev导致的证书生成失败以及某些BSP私有头文件没被RT补丁同步修改导致的语法错误。遇到后者优先到include/linux和include/trace目录里找补丁回退记录。编译产物集中在这几个位置内核镜像arch/arm64/boot/Image设备树arch/arm64/boot/dts/下TX2常见命名含tegra186-quill-p3310字样不同载板对应不同DTB。可加载模块分散在源码树里后续通过modules_install统一安装。为了后续部署方便先把模块安装到一个临时目录make modules_install INSTALL_MOD_PATH./rootfs_stage这样能在不污染主机的情况下把模块单独打包出来后续拷到板子上的rootfs里。3.4 碰到编译错误时的处理思路编译错误别一上来就搜报错字符串先判断问题出在哪一类如果是arch/arm64/kernel下的某个文件报错大概率是CPU特性宏定义冲突检查CONFIG_ARM64_PMEM这类选项是否因为RT补丁被意外打开。如果是drivers/cpufreq或drivers/clk报错说明BSP的电源管理代码和RT补丁对锁的修改冲突多半是某个自旋锁改成了mutex驱动代码没跟上。这种问题通常手动调整驱动里的锁调用即可。如果是mm/目录报错一般和CONFIG_DEBUG_PAGEALLOC配置有关实时内核不建议开这个选项。我遇到的实际情况是BSP内核对cpuidle部分做过定制RT补丁改了CPUidle的锁机制编译时报了一堆函数签名不匹配。后来我把CONFIG_CPU_IDLE先关掉编译过了再逐步开回来对比发现只要避开某个驱动设备节点问题就不会复现。这种排查思路比盲目改代码省时间。4. 部署到TX2不碰运气只走流程4.1 做个可回滚的启动环境直接在TX2内部eMMC上覆盖内核风险很大万一起不来整个系统会变得很难救。我的建议是从第一台板子开始就养成“外部启动”的习惯准备一张质量可靠的SD卡或一块USB硬盘把现有rootfs完整克隆过去所有内核替换都在这个外部存储上操作。引导方式通过板载bootloader选择不需要动内部固化内容出了问题直接把设备切回原存储就能恢复。克隆rootfs时注意保留权限和特殊文件sudo rsync -aHAXx / /media/backup_rootfs/这条命令将当前系统整体同步到备份分区遇到已挂载的虚拟文件系统可以通过--exclude参数排除掉/proc、/sys、/dev、/run。4.2 装载内核镜像、DTB和可加载模块把编译产物拷贝到外部rootfs里假设外部rootfs挂载在/media/backup_rootfssudo cp arch/arm64/boot/Image /media/backup_rootfs/boot/ sudo cp arch/arm64/boot/dts/tegra186-quill-p3310-1000-c03-00-base.dtb /media/backup_rootfs/boot/模块部分用之前INSTALL_MOD_PATH./rootfs_stage生成的目录直接同步覆盖sudo rsync -a ./rootfs_stage/lib/modules/ /media/backup_rootfs/lib/modules/注意模块目录命名lib/modules/4.9.140-rt1这种名称必须和uname -r输出的完全一致否则内核启动后找不到模块。4.3 修改启动配置并验证启动TX2使用U-Boot引导启动入口一般是/boot/extlinux/extlinux.conf。典型内容类似LABEL primary KERNEL /boot/Image FDT /boot/tegra186-quill-p3310-1000-c03-00-base.dtb APPEND consolettyS0,115200n8 root/dev/mmcblk0p1 rw rootwait这一版的root参数必须和实际rootfs分区对应。如果是SD卡启动就要写/dev/mmcblk1p1或其他实际节点不要照抄别人教程里的参数。有条件的接一个USB转串口调试线观察U-Boot和内核输出。没有串口时就接HDMI显示器但缺点是无法看到早期串口阶段的完整日志。我烧录后习惯先在U-Boot命令行里手动启动一次而不是直接改默认配置fatload mmc 1:1 ${kernel_addr_r} /boot/Image fatload mmc 1:1 ${fdt_addr_r} /boot/xxx.dtb booti ${kernel_addr_r} - ${fdt_addr_r}能正常起来后再确认extlinux.conf这样排错范围会小很多。手打命令虽然繁琐但能把“引导配置错误”和“内核本身问题”立刻分开。4.4 一键部署脚本多次反复部署后我写了一个脚本把编译、打包、同步到SD卡的过程收敛成一条命令。脚本核心逻辑如下#!/bin/bash set -e export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- REMOTE_ROOTFS/media/backup_rootfs make Image dtbs modules -j$(nproc) make modules_install INSTALL_MOD_PATH./rootfs_stage sudo cp arch/arm64/boot/Image $REMOTE_ROOTFS/boot/ sudo cp arch/arm64/boot/dts/tegra186-quill-p3310-1000-c03-00-base.dtb $REMOTE_ROOTFS/boot/ sudo rsync -a ./rootfs_stage/lib/modules/ $REMOTE_ROOTFS/lib/modules/脚本简练但每次执行前要确认REMOTE_ROOTFS真的挂载了否则会把文件写到一个空目录里浪费半小时排查。5. 用cyclictest验证实时性5.1 交叉编译rt-tests验证实时性最常用的工具是rt-tests里的cyclictest。可以在板子上直接编译也可以在主机上交叉编译。我个人习惯交叉编译速度快且不用给板子装一堆编译工具链。git clone https://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git cd rt-tests make CROSS_COMPILEaarch64-linux-gnu- cyclictest编译产物是cyclictest二进制直接拷贝到TX2上放到/usr/local/bin/sudo cp cyclictest /usr/local/bin/交叉编译时如果报rt-utils.o相关的头文件缺失通常是主机缺少numactl相关开发库安装libnuma-dev即可。5.2 合理选择cyclictest参数cyclictest参数很多我不建议一上来就抄一堆看不懂的长参数。最常用的一个验证命令sudo cyclictest -t 1 -p 99 -n -m -i 1000 -l 1000000 -a 0简单解释-t 1只开一个测试线程。-p 99线程优先级设为99最高RT优先级。-n使用时钟CLOCK_MONOTONIC。-m锁定内存防止页面换出造成抖动。-i 1000线程间隔1000微秒也就是1ms。-l 1000000循环100万次。-a 0将测试线程绑定到CPU0。如果板子上有多个CPU我还习惯加-q静默模式把逐条数据关掉只保留统计结果。输出会像这样# /dev/cpu_dma_latency set to 0us T: 0 ( 1234) P: 99 I:1000 C: 1000000 Min: 15 Act: 21 Avg: 32 Max: 107这里最关键的就是Max这一列。只要Max稳定在你的设计约束之内实时性就算达标。5.3 RT内核与默认内核对比移植完成后最直观的证明是做一个对比实验先用官方默认内核跑同一组cyclictest参数再用RT内核跑。我常用的对比方式是各跑一百万次线程间隔1ms测试绑到同一个核心避免CPU特性差异影响结论。实验结果一般是这样默认内核平均延迟三四十微秒最大延迟经常跳到一两毫秒。RT内核平均延迟十几微秒最大延迟通常在几十到一百多微秒之间。导致最大延迟差异的多数是中断处理和内核临界区抢占。默认内核里某个网卡中断或调度器tick打断测试线程时测试线程只能等一整段临界区走完RT内核里这些临界区被分割成可抢占点线程的等待时间大幅缩短。这里要提醒一句不要在板子满载时直接拿这个结果当“验收数据”。更严谨的做法是同时运行一个高CPU负载任务例如stress-ng --cpu 4再测一轮cyclictest看最大延迟是否还在可控范围内。这个数据才是系统真实能承诺的最坏情况。5.4 长时运行时的测量陷阱曾有一次我跑cyclictest跑了12小时发现最大延迟的尖峰总是集中在前20分钟。后来排查发现是某个内核线程在启动阶段反复做热迁移大概二十分钟后稳定了。要让数据真实至少连续跑一小时以上且每十分钟记录一次统计。还有一个容易被忽略的点CYCLICTEST线程如果被调度到Denver核上延迟表现和A57核差异明显。Denver核的乱序执行深度大应用层面的吞吐更好但对实时中断的确定性反而不如A57核。所以我在对比实验时把测试线程和中断都绑定到A57核心上让数据具备可比性。6. 进一步压榨确定性中断、调频与核隔离6.1 中断亲和性与CPU隔离RT补丁解决的是内核抢占问题但系统里还有大量中断。就算中断线程化了线程本身也是在某个CPU上排队执行的如果这个CPU恰好是RT测试线程的CPU延迟一样会变大。常用的手段是把RT任务钉在一个核上把所有可能打扰它的中断移到其他核上。比如创建一个专门跑RT任务的CPU比如CPU2然后# 把网卡中断迁移到CPU0 echo 1 /proc/irq/irq好/smp_affinity # 查看各中断号 cat /proc/interrupts中断号每次启动可能变化最好写脚本在启动时动态设置。还有一个更整颗粒度的手段是给内核传isolcpus2,3参数把CPU2和CPU3从普通调度器的负载均衡中隔离出去让普通进程很难自动跑上去。Tegra平台的媒体、GPU等驱动会注册大量中断随便隔离可能造成驱动启动失败。保守做法是先隔离CPU2把测试线程放CPU2其余中断尽量多留在CPU0和CPU1上。6.2 CPU调频和电源状态的影响TX2默认的cpufreq调控器通常是interactive或schedutil频率会随负载变化。但调频过程本身会导致一段时间的执行不确定性频率切换的窗口里中断延迟和任务执行时间都会波动。实时系统里我一般把频率固定在最高档# 查看可用调控器 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors # 切换到用户空间调控器 echo userspace /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 固定在最高频率 echo max_freq /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed同时CPUidle带来的深睡眠也会引入唤醒延迟。实时场景下如果延迟预算很低可以直接在内核cmdline里加cpuidle.off1禁掉CPUidle逻辑。代价是功耗上升发热增加。若产品对功耗敏感可以保留浅睡眠状态逐项测试最坏延迟后再确定。6.3 实际验证过的调优组合我这套TX2平台上经过多轮实测比较稳定的一组是项目设置CPU隔离isolcpus2,3测试线程所在CPUCPU2CPU调频固定最高频率userspace调控器CPUidlecpuidle.off1功耗不敏感场景中断绑定网卡、USB、SD等中断尽量避开CPU2实时线程调度策略SCHED_FIFO优先级95以上内存锁定关键线程调用mlockall(MCL_CURRENT这组配置下cyclictest最大延迟能稳定在90微秒以内相比默认内核有数量级的改善。但这不代表所有TX2都能复现毕竟散热、功率模式、外设负载都会影响结果。6.4 怎样长期监控而不干扰系统实时系统上线后不能只靠“当下测一次”建议在板子上挂一个长期监控脚本每30秒记录一次平均最大延迟存入日志文件#!/bin/bash while true; do echo $(date %s) $(cyclictest -t 1 -p 95 -n -m -i 1000 -l 10000 -q -a 2 | awk /^#/ {print $0}) /var/log/rt_monitor.log sleep 30 done注意监控本身也会消耗一点点CPU把它绑到不跑RT任务的核心上优先级放低对系统的影响可以忽略。7. 复盘这些坑版本错位、配置冲突、启动失败7.1 内核小版本对不上补丁最容易踩的坑就是版本错位。官方BSP源码里的内核可能显示为4.9.140但实际包含了许多非主线提交直接拿主线4.9.140的RT补丁打上去会有一批冲突。有些冲突正好发生在调度器核心文件里手动修一个是两个修到最后你会发现RT补丁的语义已经变味了测试结果自然也不理想。正确姿势是先把源码切换到一个接近主线的状态。如果BSP在内核子版本上做了大量定制可以先检查git log里是否有Merge branch linux-4.9.y这类历史找一个没有被板级改动污染的基线再做RT补丁。手头板子实在没有干净基线那就选RT补丁版本比当前基线低一个的小版本冲突面通常更小。7.2 启动到一半panic的常见原因移植后启动如果卡在SATA、USB或时钟驱动初始化阶段大概率不是RT补丁本身的问题而是RT补丁改变了中断线程化时机导致某些驱动在初始化阶段假设中断能立即打断当前上下文。这类驱动常见于老旧的MIPI CSI、GPIO扩展器、看门狗等。遇到这种情况我的排查路径是先看panic日志里最后一个初始化的子系统然后到该驱动的probe函数里找request_irq、request_threaded_irq调用看是否依赖irq_set_affinity立即可用。如果RT下中断在线程化后被延迟调度驱动初始化顺序就得调整把关键中断改为在IRQF_NO_THREAD模式下注册让中断不走线程化路径优先执行。这类问题没有通用补丁妥妥靠板级调试经验。每次改完都先跑一遍启动压力测试确保不是偶发时序问题。7.3 RT内核跑多媒体业务的性能折损RT内核不是说免费午餐。同一个TX2RT内核跑纯CPU计算任务时吞吐量可能比默认内核低5%到10%这是因为锁实现变更、中断线程化、调试选项被关掉后某些并行路径不再像以前那样顺畅。对以视觉和图像处理为主要任务的TX2应用这个折损需要提前评估。我在某次移植后跑了一个相机采集加AI推理的demo帧率从默认内核的30FPS掉到27FPS左右虽然实时性提升明显但如果业务对帧率极度敏感就要考虑把视觉处理放到单独一个处理器或专用硬件上把实时控制线程留在Linux RT侧。还有一点要注意NVIDIA的多媒体库如果以专有二进制形式发布可能依赖某些未被RT补丁修改的内核接口。好在我遇到的这些库主要是用户态封装没有直接依赖内核结构体大小所以RT内核下还能正常运行。但如果你在移植后遇到ioctl失败或API返回奇怪错误码的情况优先怀疑内核头文件版本和多媒体的用户态库版本不匹配。7.4 实时任务的“最后一公里”应用层调度内核实时性再强应用层稀烂照样白搭。RT线程必须用pthread_setschedparam显式设置SCHED_FIFO策略和优先级不能只靠nice -n -20。优先级建议按系统负载情况分层设计比如硬实时控制线程优先级90-99。数据采集线程优先级70-80。日志、统计等非实时线程默认优先级。还有RT线程进入循环前必须把内存锁住避免执行过程中页面缺失触发I/O等待。锁内存的方式是mlockall(MCL_CURRENT | MCL_FUTURE)。此外默认的glibc和某些驱动库会在后台执行futex唤醒之外的附加操作建议RT线程里不要频繁调用printf、malloc、sleep等可能进入内核阻塞的接口。我更习惯的做法是RT线程只用无锁环形缓冲区与普通线程通信控制线程自身不碰任何动态内存分配整个运行周期里所有资源都是启动时预分配好的。配合RT内核才能在示波器上看到一条稳定、可复现的低延迟响应曲线。移植RT-Linux这件事说难也难说容易也容易。往后包括GPIO脉冲触发、PWM波形输出、激光振镜控制这类对时序敏感的工位我都尽量劝客户直接在TX2上跑到RT内核量化数据一放出来大家自然就信了。如果你也在折腾TX2的实时改造在版本对齐和配置这块稳住慢一点比什么都重要。等你的板子能连续跑一整天cyclictest最大延迟始终在预算里你会有种很踏实的感觉。