4. 内核编译与部署:获取源码、配置内核(menuconfig)、编译优化(CCACHE、LTO)、生成deb包与烧录

说实话,内核编译这件事,很多工程师觉得「不就是 ./configure && make 吗?」。但车载场景下,事情远没那么简单。你想想看,一个跑在车上的 Linux 内核,既要实时响应,又要稳定可靠,还得考虑安全启动和 OTA 升级。编译这一步要是没做好,后面全是坑。

我个人习惯把内核编译分成四个阶段:源码获取、配置、编译优化、打包部署。咱们一个一个来聊。

4.1 获取内核源码

车载项目里,我强烈建议你别直接从 kernel.org 拉主线代码。为什么?因为车规级芯片(比如 NXP i.MX8、TI TDA4、瑞萨 R-Car)都有自己维护的 BSP 内核。我曾经在一个项目里直接用了主线 5.10,结果发现 GPU 驱动和 VPU 编解码模块全都不支持,折腾了两周才切回厂商 BSP。

获取源码的几种方式:

  • 芯片厂商 BSP 包:最推荐。比如 NXP 的 Yocto BSP,TI 的 Processor SDK。里面包含了完整的内核源码、驱动补丁、设备树文件。
  • Git 仓库克隆:如果厂商有公开的 Git 仓库,直接 git clone 下来。注意要拉取对应的 release tag,别用 master 分支。
  • 主线内核 + 厂商补丁:适合有经验的团队。先下载主线源码,再打上厂商提供的补丁集。

重要提醒:拿到源码后,第一件事是检查 Makefile 顶部的版本号。确认它和你芯片的硬件版本匹配。我见过有人把 i.MX8QM 的内核烧到 i.MX8MM 上,结果网口、显示全部不工作。

4.2 内核配置(menuconfig)

配置内核,说白了就是告诉内核:「哪些功能我要,哪些功能我不要」。车载场景下,配置的原则是「最小化 + 实时化」。

我一般这样操作:

  1. 先加载厂商提供的默认配置:make ARCH=arm64 defconfig 或者 make ARCH=arm64 xxx_defconfig
  2. 然后运行 make menuconfig 进行微调

menuconfig 里,有几个关键选项必须关注:

配置项 路径 说明
CONFIG_PREEMPT_RT Kernel Features → Preemption Model 实时抢占内核,车载必备
CONFIG_HZ_1000 Kernel Features → Timer frequency 1000Hz 时钟中断,提升响应精度
CONFIG_CPU_FREQ_GOVERNOR CPU Power Management → CPU Frequency scaling 选 performance 或 userspace,避免调频抖动
CONFIG_NO_HZ_FULL Kernel Features → Timer tick handling 全无滴答模式,减少中断干扰

我的小技巧:配置完成后,执行 make savedefconfig 生成一个精简的 defconfig 文件。下次编译时直接加载这个文件,省得每次都要进 menuconfig 重新选。我在项目里就是这么干的,团队里所有人都用同一份配置,避免「你编译的和我编译的不一样」这种尴尬。

4.3 编译优化(CCACHE、LTO)

编译内核是个体力活。一个完整的内核编译,ARM64 架构下大概要 10-20 分钟。如果每次改个驱动都要等这么久,那效率太低了。

4.3.1 CCACHE:编译缓存

CCACHE 的原理很简单:把编译过的 .o 文件缓存起来。下次编译时,如果源文件没变,直接拿缓存结果,跳过编译。

启用方式:

# 安装 ccache
sudo apt install ccache

# 设置环境变量
export CCACHE_DIR=/path/to/ccache_cache
export CC="ccache gcc"
export CXX="ccache g++"

# 或者直接修改 Makefile 中的 CC 变量
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- CC="ccache aarch64-linux-gnu-gcc" -j8

我第一次用 CCACHE 时,第二次编译只用了 3 分钟。嗯,那种感觉就像从绿皮火车换成了高铁。

注意:CCACHE 缓存目录建议放在 SSD 上,别放机械硬盘。另外,如果换了交叉编译工具链,记得清空缓存:ccache -C。我曾经因为没清缓存,用新工具链编译出来的内核在目标板上跑不起来,排查了半天才发现是缓存捣的鬼。

4.3.2 LTO:链接时优化

LTO(Link Time Optimization)能让编译器在链接阶段做全局优化。说白了,就是让编译器看到整个程序的「全貌」,而不是一个文件一个文件地优化。

启用方式:

# 在 menuconfig 中开启
General setup → Enable the LTO (Link Time Optimization)

# 或者直接在 .config 中设置
CONFIG_LTO_CLANG=y
# 或者
CONFIG_LTO_GCC=y

LTO 的好处:代码体积更小,运行效率更高。但代价是编译时间会变长 20%-30%。车载场景下,我建议在发布正式版本时开启 LTO,开发调试阶段可以关掉,省时间。

4.4 生成 deb 包与烧录

编译完成后,我们需要把内核打包成可部署的格式。deb 包是 Debian/Ubuntu 系统的标准格式,在车载 Linux 中很常见。

4.4.1 生成 deb 包

内核源码自带了一个打包脚本:

# 生成 deb 包
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8 deb-pkg

# 或者生成带 dtbs 的包
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8 bindeb-pkg

执行完后,会在源码目录的上一级生成几个 .deb 文件:

  • linux-image-*.deb:内核镜像
  • linux-headers-*.deb:内核头文件(用于编译驱动模块)
  • linux-dtb-*.deb:设备树文件

我个人习惯把这三个包都保留下来。尤其是 dtb 包,很多人会忽略它,结果烧录后设备树不匹配,外设全都不工作。

4.4.2 烧录到目标板

烧录方式取决于你的硬件平台。常见的有:

  • SD 卡烧录:把 deb 包拷贝到 SD 卡,然后在目标板上用 dpkg -i 安装
  • 网络烧录:通过 TFTP/NFS 启动,然后在线安装
  • fastboot 烧录:适用于 Android 或 AOSP 系统

我常用的 SD 卡烧录流程:

# 在宿主机上挂载 SD 卡
sudo mount /dev/sdb1 /mnt/sd

# 拷贝 deb 包
cp linux-image-*.deb linux-dtb-*.deb /mnt/sd/

# 卸载 SD 卡
sudo umount /mnt/sd

# 插入目标板,启动后执行
sudo dpkg -i /mnt/sd/linux-image-*.deb
sudo dpkg -i /mnt/sd/linux-dtb-*.deb
sudo reboot

核心要点:烧录前一定要备份当前内核。我习惯在烧录前先执行 dpkg -l | grep linux-image 查看当前内核版本,然后保留一份旧内核的 deb 包。万一新内核起不来,还能回滚。

4.5 避坑指南

最后,分享几个我在项目中踩过的坑:

  • 编译工具链版本不匹配:我曾经用 GCC 9 编译的内核,在 GCC 7 的环境下加载模块,结果报 version magic mismatch。解决方案:宿主机和目标板的工具链版本保持一致。
  • 设备树文件遗漏:有一次我编译了内核,但忘了更新 dtb 文件。结果以太网 PHY 地址变了,网络完全不通。排查了整整一天才发现是 dtb 没更新。
  • 内核模块签名:如果开启了 Secure Boot,内核模块需要签名。否则 insmod 时会报 Required key not available。记得在编译时生成并安装签名密钥。

嗯,内核编译与部署这部分就聊到这里。下一章咱们会深入讲解实时补丁的集成与调试,到时候见。