4. Linux内核编译与配置:内核源码获取、内核配置系统(Kconfig/Makefile)、内核编译与烧写
好,咱们今天聊聊Linux内核的编译与配置。说实话,这是嵌入式Linux开发中最基础、也最容易踩坑的一环。我当年刚入行时,第一次编译内核就折腾了整整两天——不是缺工具链,就是配置选项选错了,最后烧进去系统起不来。嗯,这些教训让我后来养成了不少好习惯,今天一并分享给你。
4.1 内核源码获取
先说说源码从哪来。你可能会想,这还不简单?去kernel.org下载呗。对,但嵌入式开发没那么简单。
我个人习惯的做法是:
- 官方主线内核:从
kernel.org获取,适合学习或通用平台 - 芯片厂商提供的BSP内核:比如NXP、TI、Rockchip等厂商会维护自己的内核分支,里面包含了板级支持包和驱动
- SoC厂商的Git仓库:很多厂商在GitHub或Gitee上维护了内核仓库,比如
torvalds/linux的某个分支
获取源码的命令很简单:
# 从主线仓库克隆(以5.10 LTS为例)
git clone --depth=1 -b v5.10 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
# 或者下载压缩包
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.tar.xz
tar -xf linux-5.10.tar.xz
为什么要加 --depth=1?说白了,就是只拉取最新的一次提交,省时间省空间。完整的历史记录动辄几个GB,你想想看,嵌入式开发板子的存储空间本来就紧张,没必要。
4.2 内核配置系统:Kconfig与Makefile
内核的配置系统,说白了就是一套「搭积木」的机制。Kconfig定义「有哪些积木块」,Makefile定义「怎么把这些积木块搭起来」。
4.2.1 Kconfig:配置选项的定义
Kconfig文件分布在各个子目录中,比如 drivers/i2c/Kconfig 里定义了I2C相关的配置选项。它的语法其实不难理解:
config I2C_CHARDEV
tristate "I2C /dev entries support"
depends on I2C
help
Say Y here to enable I2C device interface.
This provides a /dev/i2c-N interface.
这里 tristate 表示三种状态:Y(编译进内核)、M(编译成模块)、N(不编译)。depends on 是依赖关系——你选了I2C字符设备,前提是I2C核心已经被选中。
关键点:Kconfig的依赖关系是链式的。我曾经在配置一个音频驱动时,选了某个编解码器,结果它依赖I2C、GPIO、DMA三个子系统。漏掉任何一个,编译就报错。所以配置时一定要看仔细了。
4.2.2 Makefile:编译规则
Makefile决定了哪些文件被编译,以及怎么编译。举个例子:
# drivers/i2c/Makefile
obj-$(CONFIG_I2C_CHARDEV) += i2c-dev.o
obj-$(CONFIG_I2C_ALGOBIT) += i2c-algo-bit.o
这里的 obj-$(CONFIG_I2C_CHARDEV) 会被展开成 obj-y 或 obj-m 或空。如果配置成Y,就编译进内核;配置成M,就编译成模块;没配置,就不编译。
你想想看,这套机制是不是很巧妙?Kconfig负责「你要什么」,Makefile负责「怎么给」。两者配合得天衣无缝。
4.2.3 配置方法
内核提供了多种配置界面:
| 命令 | 说明 | 适用场景 |
|---|---|---|
make menuconfig |
基于ncurses的文本菜单 | 最常用,SSH远程也适用 |
make xconfig |
基于Qt的图形界面 | 桌面环境,操作直观 |
make defconfig |
使用默认配置 | 快速开始,再手动微调 |
make savedefconfig |
生成精简的配置文件 | 保存最小化配置 |
我个人最常用的是 make menuconfig。为什么?因为嵌入式开发经常在服务器或虚拟机上进行,没有图形界面。而且menuconfig支持搜索(按 / 键),找某个选项特别方便。
小技巧:配置完成后,配置信息保存在 .config 文件中。我建议你把这个文件备份一份,命名为 config_myboard。下次重新编译时,直接 cp config_myboard .config,省得从头配。
4.3 内核编译
配置好了,接下来就是编译。嗯,这一步其实最「无脑」,但也是最容易出问题的。
4.3.1 设置交叉编译工具链
嵌入式开发嘛,肯定是在PC上编译,在开发板上运行。所以需要指定交叉编译器:
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
make menuconfig
make -j4
ARCH 指定目标架构,CROSS_COMPILE 指定交叉编译器的前缀。这里要注意,你的工具链路径要加到 PATH 环境变量里。
注意:我曾经犯过一个低级错误——忘了设置 CROSS_COMPILE,结果编译出来的内核是x86架构的,烧到ARM板上当然起不来。折腾了半天才发现,真是欲哭无泪。
4.3.2 编译产物
编译完成后,主要生成以下文件:
arch/arm/boot/zImage:压缩后的内核镜像arch/arm/boot/uImage:带U-Boot头部的镜像(如果配置了)arch/arm/boot/dts/:设备树文件(.dtb)vmlinux:未压缩的内核,用于调试
对于大多数嵌入式平台,我们最终需要的是 zImage 或 uImage 加上对应的 .dtb 文件。
4.3.3 编译内核模块
如果有些驱动配置成了模块(M),还需要单独编译和安装模块:
make modules
make modules_install INSTALL_MOD_PATH=/path/to/rootfs
INSTALL_MOD_PATH 指定模块安装到根文件系统的路径。模块会按 /lib/modules/$(KERNEL_VERSION)/ 的目录结构安装。
4.4 内核烧写
编译好了,怎么烧到板子上?这取决于你的硬件平台和启动方式。
4.4.1 通过U-Boot烧写
大多数嵌入式Linux板子都用U-Boot作为bootloader。烧写步骤一般是:
- 将内核镜像和设备树文件放到TFTP服务器或SD卡上
- 在U-Boot命令行中设置启动参数
- 使用
tftp或fatload命令加载到内存 - 使用
bootm或bootz启动
举个例子:
# 从TFTP加载内核
tftp 0x82000000 zImage
tftp 0x83000000 myboard.dtb
# 启动内核
bootz 0x82000000 - 0x83000000
4.4.2 通过烧录工具烧写
有些平台(比如全志、瑞芯微)有专门的烧录工具,通过USB将镜像烧写到NAND Flash或eMMC中。这类工具通常需要把内核镜像打包成特定的格式。
经验之谈:我建议你在开发阶段用TFTP或NFS启动,这样修改内核后不用反复烧写,直接重启就能看到效果。等内核稳定了,再烧写到板载存储中。这个习惯能帮你节省大量时间。
4.4.3 烧写后的验证
烧写完成后,上电启动。如果系统能正常启动到shell,恭喜你,内核编译和烧写成功了。如果启动卡住了,别慌,看看串口输出的内核日志,通常能定位到问题。
常见的启动失败原因:
- 设备树与硬件不匹配
- 根文件系统路径或类型配置错误
- 内核缺少关键驱动(比如存储控制器驱动)
- 启动参数中的内存地址不对
4.5 小结
内核编译与配置,说白了就是三步:拿源码、配选项、编译烧写。但每一步都有不少细节。我建议你从一块成熟的开发板开始,比如BeagleBone Black或树莓派,它们的社区支持好,资料多。等熟悉了流程,再挑战自己画板子的项目。
嗯,今天就聊到这儿。下一章咱们会深入设备树,这可是嵌入式Linux开发的「硬骨头」,到时候咱们好好啃一啃。