为NVIDIA AGX平台编译实时内核:从PREEMPT_RT原理到工程实践
1. 项目概述为什么AGX需要实时补丁如果你正在用NVIDIA Jetson AGX Xavier或者Orin系列做机器人、自动驾驶或者工业控制大概率遇到过一个问题系统在满负荷运行时偶尔会出现几毫秒甚至几十毫秒的“卡顿”。对于普通PC这或许无关紧要但对于一个要求确定性响应的实时系统比如高速移动的机械臂或者正在避障的无人车这种非预期的延迟足以导致任务失败甚至发生危险。这正是我们今天要解决的核心痛点——为NVIDIA AGX平台打上实时补丁R35.3.1将其从一个高性能的嵌入式AI计算平台转变为一个兼具强大算力和硬实时Hard Real-Time能力的可靠边缘节点。简单来说标准的Linux内核包括NVIDIA JetPack SDK提供的是一个通用操作系统其调度策略以“公平”和“高吞吐量”为目标。这意味着所有进程包括你的关键控制线程都需要和系统后台任务、内存管理、磁盘I/O等“争抢”CPU时间。实时补丁PREEMPT_RT的核心使命就是彻底改造内核的调度、中断和锁机制最大限度地减少这种“争抢”带来的不确定性确保高优先级任务能够在严格的时间限制内得到执行。我选择R35.3.1这个特定版本是因为它对应着JetPack 5.1.2/5.1.3所基于的L4T 35.3.1内核版本。这是一个经过社区和工业界长期验证的相对稳定的组合。网上很多教程要么版本过旧要么步骤跳跃太大忽略了AGX平台特有的交叉编译环境和设备树配置。接下来我会把我从源码准备、环境配置、内核编译、到设备树修改和系统烧录的完整过程以及其中踩过的每一个坑毫无保留地分享出来。整个过程需要一定的Linux和嵌入式开发基础但只要你跟着步骤走即使是第一次接触内核编译也能成功。2. 前期准备工具链与源码获取给AGX打实时补丁本质上是一次针对特定硬件的内核定制化编译。这意味着我们不能在AGX设备本身上完成所有工作需要一个x86_64架构的宿主机进行交叉编译。整个工作流可以概括为在Ubuntu宿主机上使用NVIDIA提供的专用工具链编译生成适用于ARM64架构AGX设备的内核镜像和设备树。2.1 宿主机环境搭建首先你需要一台运行Ubuntu 20.04或22.04的x86_64电脑作为编译主机。我强烈推荐使用物理机或配置足够的虚拟机至少分配8核CPU、16GB内存和100GB硬盘空间因为内核编译极其消耗资源。在宿主机上安装必要的依赖包sudo apt-get update sudo apt-get install -y build-essential bc kmod cpio flex libncurses5-dev libelf-dev libssl-dev dwarves bison rsync git这些工具是编译内核的基础bc用于计算libncurses5-dev用于make menuconfig时的图形化配置界面libssl-dev和dwarves是新版本内核编译所必需的。2.2 获取NVIDIA官方源码与工具链这是最关键的一步必须确保源码、工具链和补丁的版本严格对应。我们的目标是L4T 35.3.1内核。下载Linux for Tegra (L4T) 驱动包前往NVIDIA开发者网站找到JetPack 5.1.2或5.1.3的页面下载“BSP Sources”或“Driver Package”。通常是一个名为Jetson_Linux_R35.3.1_aarch64.tbz2的文件。这个压缩包包含了内核源码、模块和公共头文件。# 假设下载到 ~/Downloads 目录 cd ~ tar -xjf ~/Downloads/Jetson_Linux_R35.3.1_aarch64.tbz2解压后会得到一个Linux_for_Tegra/目录其子目录source/public/里存放着内核源码kernel_src.tbz2。解压内核源码cd ~/Linux_for_Tegra/source/public tar -xjf kernel_src.tbz2此时内核源码位于~/Linux_for_Tegra/source/public/kernel/kernel-5.10。获取交叉编译工具链同样在NVIDIA开发者网站找到“Toolchains”部分下载适用于ARM64的Linaro GCC交叉编译工具链。例如gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz。cd ~ tar -xf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz将工具链路径加入环境变量方便后续使用export CROSS_COMPILE~/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu- export ARCHarm642.3 获取实时内核补丁实时补丁并非NVIDIA提供我们需要从Linux内核官方社区获取。访问 https://mirrors.edge.kernel.org/pub/linux/kernel/projects/rt/5.10/找到与我们的内核版本5.10匹配的补丁系列。对于L4T 35.3.1其内核版本基于5.10.104我们需要找到最接近的rt补丁。我使用的是patch-5.10.148-rt66.patch.gz。注意补丁版本不一定完全一致小版本差异通常可以应用但需要测试。cd ~ wget https://mirrors.edge.kernel.org/pub/linux/kernel/projects/rt/5.10/patch-5.10.148-rt66.patch.gz gunzip patch-5.10.148-rt66.patch.gz注意版本匹配是成功的生命线。不匹配的补丁会导致成千上万的编译错误。如果找不到完全匹配的应选择比内核版本稍旧的rt补丁成功率更高。例如内核是5.10.104可以尝试5.10.100左右的rt补丁。应用补丁的命令是patch -p1 ../patch-5.10.148-rt66.patch在源码根目录执行。如果出现大量“Hunk FAILED”说明版本不兼容需要更换补丁。3. 内核配置与实时补丁应用准备好所有材料后我们进入内核配置的核心环节。这一步决定了最终内核的功能、性能和实时性能力。3.1 应用实时补丁首先进入内核源码目录并应用补丁cd ~/Linux_for_Tegra/source/public/kernel/kernel-5.10 patch -p1 ~/patch-5.10.148-rt66.patch应用过程中终端会滚动显示大量信息。你需要密切关注是否有“Hunk FAILED”的错误。如果只有少数几个个位数失败并且涉及的是一些非核心的驱动文件比如某个特定硬件的驱动有时可以手动检查并忽略。但如果涉及核心调度文件如kernel/sched/下的文件失败就必须更换补丁版本。3.2 导入NVIDIA默认配置NVIDIA为AGX设备提供了默认的内核配置文件。我们以此为基础进行修改能最大程度保证硬件兼容性。# 清理之前可能存在的配置 make mrproper # 导入NVIDIA为Jetson AGX Xavier/Orin准备的默认配置 # 这里以Xavier为例Orin设备配置名可能不同请查阅NVIDIA文档 make ARCHarm64 CROSS_COMPILE${CROSS_COMPILE} tegra_defconfigtegra_defconfig这个目标会载入arch/arm64/configs/tegra_defconfig文件它包含了使内核能在Tegra/Orin芯片上正常运行的所有必要驱动和选项。3.3 启用实时内核特性现在我们启动内核配置图形界面开启实时功能make ARCHarm64 CROSS_COMPILE${CROSS_COMPILE} menuconfig你会看到一个基于ncurses的文本图形界面。使用方向键导航回车键进入子菜单或选择选项空格键切换选项状态[*]表示编译进内核[M]表示编译为模块[ ]表示不编译。需要修改的关键配置项位于以下路径General setup - Preemption Model进入General setup。找到Preemption Model按回车。这里是最关键的一步选择Fully Preemptible Kernel (Real-Time)。这会将CONFIG_PREEMPT_RT设置为y是启用实时补丁的核心。处理器类型与特性进入Processor type and features。确保Timer frequency设置为1000 HZ。更高的定时器频率可以提供更精细的时钟粒度有利于降低调度延迟但会略微增加CPU开销。对于实时系统1000Hz是一个常用值。检查High Resolution Timer Support是否启用应该是默认启用的。内核调试与锁机制实时内核会改变锁的行为。进入Kernel hacking-Lock Debugging (spinlocks, mutexes, etc.)。考虑关闭Lock debugging: prove locking correctness。这个调试功能在开发时有用但会引入额外的性能开销在生产实时内核中可以关闭。保留NVIDIA特定驱动在配置过程中不要随意取消任何与Tegra、GPU、NVMe、相机如VI/CSI、视频编解码器相关的驱动。特别是Device Drivers-Graphics support下的NVIDIA Tegra Graphics Support以及相关显示、GPU驱动必须保留。配置完成后选择 Save 使用默认的.config文件名保存。退出menuconfig。实操心得在menuconfig中你可以按/键搜索配置项。例如搜索PREEMPT_RT可以快速定位到实时选项。保存配置后强烈建议备份.config文件cp .config .config.rt_backup。这样如果后续编译出错或配置混乱可以快速回滚。4. 内核编译与设备树生成配置保存后就进入了漫长的编译阶段。编译过程会生成内核镜像Image、内核模块.ko文件以及设备树二进制文件.dtb。4.1 启动编译过程使用多线程编译以大幅缩短时间-j后面的数字根据你宿主机的CPU核心数设定通常为核心数的1到2倍# 编译内核镜像和设备树 make ARCHarm64 CROSS_COMPILE${CROSS_COMPILE} -j16 Image dtbs modules这条命令会Image编译生成压缩的内核镜像文件arch/arm64/boot/Image。dtbs编译生成设备树二进制文件位于arch/arm64/boot/dts/nvidia/目录下例如tegra194-p2888-0001-p2822-0000.dtbAGX Xavier或tegra234-p3701-0000-p3737-0000.dtbOrin NX/Orin Nano。modules编译所有配置为模块[M]的内核驱动。编译过程可能需要30分钟到2小时取决于宿主机的性能。如果中途出现错误最常见的根源是缺少依赖包根据错误信息安装对应的开发包。补丁冲突如果错误指向某个源文件语法错误很可能是实时补丁应用失败。需要检查补丁版本或手动解决冲突仅建议高级用户尝试。配置冲突某些驱动选项与实时特性冲突。可以尝试在menuconfig中禁用一些不必要或可疑的调试选项。4.2 安装内核模块到临时目录编译完成后我们需要将模块安装到一个临时目录而不是宿主机的系统目录。# 创建临时模块目录 mkdir -p ~/rt_kernel_modules # 安装模块到该目录 make ARCHarm64 CROSS_COMPILE${CROSS_COMPILE} INSTALL_MOD_PATH~/rt_kernel_modules modules_install执行后所有内核模块.ko文件及其依赖关系会被组织到~/rt_kernel_modules/lib/modules/5.10.148-rt66这样的路径下。4.3 准备刷机文件现在我们需要将编译产物替换到L4T文件系统的对应位置以便后续刷机。替换内核镜像cd ~/Linux_for_Tegra # 备份原始内核镜像 sudo cp kernel/Image kernel/Image.original # 用新编译的实时内核镜像替换 sudo cp ~/Linux_for_Tegra/source/public/kernel/kernel-5.10/arch/arm64/boot/Image ./kernel/替换设备树文件# 首先确认你的AGX设备对应的设备树文件名。 # 对于AGX Xavier: 通常是 tegra194-p2888-0001-p2822-0000.dtb # 对于Orin系列需要根据具体载板查询例如 tegra234-p3701-0000-p3737-0000.dtb # 备份原始dtb sudo cp kernel/dtb/tegra194-p2888-0001-p2822-0000.dtb kernel/dtb/tegra194-p2888-0001-p2822-0000.dtb.original # 替换为新编译的dtb sudo cp ~/Linux_for_Tegra/source/public/kernel/kernel-5.10/arch/arm64/boot/dts/nvidia/tegra194-p2888-0001-p2822-0000.dtb ./kernel/dtb/重要必须确保设备树文件名与刷机时使用的完全一致。你可以查看Linux_for_Tegra/bootloader/t186ref/或t234ref/目录下的配置文件如board_config.t186.xml来确认。替换内核模块 这是最容易出错的一步。不能简单复制文件因为模块之间存在依赖关系。正确做法是先将原始文件系统中的模块备份然后用我们编译安装的整套模块替换。# 进入根文件系统目录如果已经解压 # 假设根文件系统在 ~/Linux_for_Tegra/rootfs cd ~/Linux_for_Tegra/rootfs # 备份原模块目录 sudo mv lib/modules/5.10.104-tegra lib/modules/5.10.104-tegra.backup # 将新编译的模块目录复制过来 sudo cp -r ~/rt_kernel_modules/lib/modules/5.10.148-rt66 ./lib/modules/ # 非常重要更新模块依赖关系 sudo chroot . /bin/bash -c depmod -a 5.10.148-rt66注意chroot命令是在根文件系统的上下文环境中执行depmod确保生成的modules.dep等文件路径正确。如果遇到chroot无法执行bash的问题可以尝试先进入rootfs目录再通过sudo执行depmod但需指定内核版本sudo depmod -a -b $(pwd) 5.10.148-rt66。5. 刷机与实时性验证所有文件准备就绪后就可以将定制的实时系统刷写到AGX设备上了。5.1 进入恢复模式与刷机将AGX设备通过Micro-USB线连接到宿主机。按住AGX上的“Force Recovery”按钮通常是一个小孔然后轻按一下“Power”按钮保持“Force Recovery”按住约2秒后松开。在宿主机上运行lsusb命令应该能看到一个NVIDIA Corp. APX设备表示已进入恢复模式。执行刷机命令cd ~/Linux_for_Tegra # 如果是全新刷机包含根文件系统 sudo ./flash.sh jetson-agx-orin-devkit mmcblk0p1 # 如果只想更新内核和内核模块保留原有用户数据可以使用 --no-flash 参数只生成镜像然后单独刷写特定分区更高级此处不展开flash.sh脚本会自动识别设备类型并烧写所有必要的分区。这个过程会擦除设备上原有的所有数据。5.2 系统启动与基础检查刷机完成后AGX设备会自动重启。首次启动可能会比平时慢一些。登录系统后进行以下检查确认内核版本uname -r输出应该显示5.10.148-rt66或类似包含rt字样表明实时内核已成功运行。检查实时补丁状态cat /sys/kernel/realtime如果输出1则表示实时补丁已生效。如果文件不存在或输出0则实时补丁未成功启用需要回溯检查配置和编译步骤。测试基本功能运行nvidia-smi检查GPU驱动是否正常加载。测试相机、USB、网络等外设是否工作正常。因为实时内核可能改变了某些驱动的中断处理方式极少数驱动可能需要重新调整配置。5.3 实时性性能测试安装实时性测试工具进行量化评估sudo apt-get update sudo apt-get install rt-tests循环延迟测试cyclictest 这是最常用的实时延迟测试工具。它创建一个高优先级实时线程定期唤醒并测量实际唤醒时间与预期时间的偏差延迟。# 运行一个简单的测试运行10分钟 sudo cyclictest -t -p 80 -n -i 1000 -l 600000-t: 使用时钟CLOCK_MONOTONIC。-p 80: 设置线程优先级为80数字越大优先级越高范围1-99。-n: 使用nanosleep。-i 1000: 线程间隔为1000微秒1毫秒。-l 600000: 循环600000次10分钟。 测试结束后关注输出的Max Latency最大延迟值。在标准内核下这个值可能在几百微秒到几毫秒。在打上RT补丁并正确调优后理想情况下最大延迟应稳定在几十微秒以内。我优化后的AGX Xavier在系统负载较轻时最大延迟可以控制在15-30微秒。压力测试下的延迟 单独测试意义不大需要结合压力测试。打开另一个终端运行stress工具制造CPU、内存、IO压力。sudo apt-get install stress stress --cpu 4 --io 2 --vm 1 --vm-bytes 1G --timeout 600s同时在第一个终端运行cyclictest。观察在系统压力下最大延迟 (Max Latency) 和平均延迟 (Avg Latency) 的增长情况。一个健壮的实时系统即使在压力下延迟也应保持相对稳定不会出现数量级的飙升例如从几十微秒跳到几毫秒。6. 系统调优与稳定性加固仅仅刷入实时内核往往还不能达到最优的实时性能。Linux系统中有许多后台任务和中断处理程序会干扰实时线程。需要进行一系列系统调优。6.1 隔离CPU核心将特定的CPU核心专门分配给实时任务避免其他内核线程和用户态进程的干扰。例如假设AGX有8个CPU核心0-7我们将核心7隔离出来专供实时任务使用。修改内核启动参数 编辑AGX设备上的/boot/extlinux/extlinux.conf文件。sudo vi /boot/extlinux/extlinux.conf在APPEND那一行的末尾添加isolcpus7。修改后可能类似APPEND ${cbootargs} quiet root/dev/mmcblk0p1 rw rootwait rootfstypeext4 consolettyTCU0,115200n8 consoletty0 fbconmap:0 isolcpus7重启生效。将实时任务绑定到隔离核心 在运行cyclictest或你自己的实时应用时使用taskset命令将其绑定到隔离的核心上。sudo taskset -c 7 cyclictest -t -p 90 -n -i 1000 -l 1000000同时你也可以通过cset工具进行更复杂的cgroup CPU隔离管理。6.2 调整内核调度参数禁用看门狗和NMI中断 某些非关键的中断可能会引起延迟。可以尝试在启动参数中禁用它们需测试稳定性。 在/boot/extlinux/extlinux.conf的APPEND行添加nowatchdog nmi_watchdog0调整CPU频率调控器 动态频率调整DVFS会引入不确定性。对于隔离的核心将其调控器设置为performance模式使其始终运行在最高频率。# 查看所有CPU的调控器 cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor # 将cpu7设置为performance模式 echo performance | sudo tee /sys/devices/system/cpu/cpu7/cpufreq/scaling_governor为了使设置永久生效可以安装cpufrequtils并配置或创建systemd服务。6.3 内存与IO优化禁用透明大页 透明大页Transparent Huge Pages在运行时合并内存页可能引起不可预测的延迟。echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled限制swap使用 对于实时任务应尽量避免发生内存页交换swap这会导致极高的延迟。可以将swappiness设置为0。echo 0 | sudo tee /proc/sys/vm/swappiness使用实时调度策略 在你的实时应用程序中使用sched_setscheduler()系统调用将线程的调度策略设置为SCHED_FIFO或SCHED_RR并赋予高优先级80。这能确保你的线程在就绪时能抢占任何非实时线程。7. 常见问题排查与解决实录在实战中你几乎一定会遇到下面这些问题。我把我的排查经验和解决方案记录下来。7.1 刷机后无法启动卡在开机Logo这是最令人紧张的情况。可能的原因和解决步骤设备树不匹配这是最常见的原因。你编译和替换的设备树文件.dtb与你的硬件载板不匹配。解决方案重新进入恢复模式使用原始的、未修改的Linux_for_Tegra目录或使用备份的原始dtb文件重新刷机。确认设备能正常启动后再次核对你的设备型号与设备树文件名的对应关系。AGX Xavier、Orin NX、Orin Nano的设备树都不同。内核镜像损坏或配置错误内核编译过程中出现未捕获的错误或者关键驱动如存储控制器驱动被错误禁用。解决方案检查编译日志的最后部分是否有error。确保在menuconfig中Device Drivers-MMC/SD/SDIO card support下的Tegra相关MMC/SD主机控制器驱动是启用的[*]或[M]。模块依赖问题根文件系统中的内核模块版本与正在运行的内核版本不匹配导致关键驱动如GPU、显示无法加载。解决方案确保lib/modules下的目录名称与uname -r的输出完全一致并且正确执行了depmod。排查工具连接串口调试线到AGX的调试串口通常是40-pin连接器上的特定引脚可以在启动早期看到内核的详细打印信息这对于定位启动卡在哪一步至关重要。7.2 实时测试延迟依然很高100us即使打了补丁延迟也可能不理想。按以下顺序排查检查隔离是否生效运行cyclictest时同时用htop或top命令按1显示所有CPU观察是否有其他进程在隔离的核心如cpu7上运行。系统内核线程名字以[xxx]表示可能仍然会被调度到隔离核心。更严格的隔离需要使用cset shield或isolcpus结合nohz_full、rcu_nocbs参数。尝试在启动参数中增加isolcpus7 nohz_full7 rcu_nocbs7。中断干扰使用cat /proc/interrupts命令观察隔离核心cpu7是否在处理大量中断。理想情况下它的中断计数应该增长非常缓慢或为0。解决方案设置中断亲和性IRQ affinity将大部分中断绑定到非隔离核心。例如将网络中断绑定到cpu0-cpu6# 找到网络设备的中断号比如eth0 grep eth0 /proc/interrupts | awk {print $1} | cut -d: -f1 # 假设中断号是200将其亲和性设置为0x7F二进制01111111即cpu0-6 echo 7f | sudo tee /proc/irq/200/smp_affinity这是一个复杂且需要针对每个中断逐一调整的过程。有脚本可以自动化但需谨慎操作。电源管理干扰CPU的C-states休眠状态进入和退出会带来延迟。对于隔离核心可以禁用深度C-states。解决方案在启动参数中添加processor.max_cstate1 intel_idle.max_cstate0Intel的参数ARM平台可能不同AGX上可能需要研究Tegra/Orin特定的电源管理参数或在内核配置中关闭CONFIG_CPU_IDLE和CONFIG_ARM_TEGRA_CPUIDLE注意这需要深入测试可能影响功耗和散热。7.3 NVIDIA特定功能异常如nvidia-smi报错、GPU无法使用实时内核可能修改了内存分配、中断或DMA相关的底层机制与NVIDIA闭源驱动nvidia.ko产生兼容性问题。症状nvidia-smi提示 “Failed to initialize NVML: Driver/library version mismatch” 或直接报通信错误。原因内核模块版本不匹配或者NVIDIA用户态库由JetPack安装与当前运行的内核不兼容。解决方案确保模块匹配如前所述正确安装你编译出的内核模块。重新安装用户态CUDA/驱动如果模块没问题可能需要重新安装NVIDIA用户态驱动包。这很麻烦因为需要从NVIDIA获取与你编译的内核版本对应的驱动包。更可行的方法是在标准内核下通过JetPack或SDK Manager完整安装驱动和CUDA然后在编译实时内核时确保kernel/kernel-5.10/nvidia目录下的驱动源码被正确编译并替换。NVIDIA内核驱动源码通常包含在kernel_src.tbz2中。妥协方案如果实时性要求不是极端苛刻可以尝试不将GPU驱动编译进内核而是作为模块加载并使用标准内核提供的NVIDIA模块但这可能带来轻微的非确定性。7.4 系统运行一段时间后出现卡死或内核恐慌这可能是实时补丁与某个特定硬件驱动或内核子系统存在深层次冲突。收集崩溃信息如果系统还能响应查看内核日志dmesg。如果完全卡死重启后检查/var/log/kern.log或通过串口日志查看崩溃前的最后信息。常见嫌疑点锁的调试选项尝试在menuconfig中彻底关闭Kernel hacking-Lock Debugging下的所有选项然后重新编译。特定设备驱动如果崩溃日志指向某个驱动如i2c-tegrapcie-tegra尝试在内核配置中将其禁用[ ]或编译为模块[M]进行测试。内存管理实时补丁对内存分配路径有修改。可以尝试在启动参数中增加slub_debug-来关闭SLUB分配器的调试或使用memtest进行长时间内存测试排除硬件问题。最后的建议实时补丁的稳定性需要长时间的压力测试。建议在你实际的应用场景下进行至少72小时的不同断循环测试监控系统延迟和功能是否正常。实时性优化是一个权衡的艺术需要在性能、功耗、功能和稳定性之间找到最佳平衡点。对于AGX这样的复杂异构平台完全达到极致的硬实时性能非常困难但通过上述步骤将其关键任务的延迟从毫秒级降低到百微秒甚至十微秒级对于绝大多数边缘AI与控制系统来说已经是质的飞跃。