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)
配置内核,说白了就是告诉内核:「哪些功能我要,哪些功能我不要」。车载场景下,配置的原则是「最小化 + 实时化」。
我一般这样操作:
- 先加载厂商提供的默认配置:
make ARCH=arm64 defconfig或者make ARCH=arm64 xxx_defconfig - 然后运行
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。记得在编译时生成并安装签名密钥。
嗯,内核编译与部署这部分就聊到这里。下一章咱们会深入讲解实时补丁的集成与调试,到时候见。