4. Linux 内核裁剪基础:源码结构、Kconfig 与 Makefile 机制、内核配置方法

说到内核裁剪,很多人一上来就开干——直接 make menuconfig,然后一顿乱选。结果呢?要么编译不过,要么跑起来各种崩溃。我早年也干过这种蠢事,后来才明白:不懂内核的骨架,裁剪就是瞎蒙。

这一章,咱们把内核的底裤扒开看看。你搞清楚了三件事——源码怎么组织的、Kconfig 怎么玩的、Makefile 怎么编译的——后面裁剪才能有的放矢。

4.1 内核源码结构:先认路,再动手

Linux 内核源码虽然大(几万个文件),但目录结构其实很清晰。我习惯把它分成三大块:架构相关、驱动、核心子系统。

下面这张图,是我每次带新人必画的。你把它记住,找文件至少快一倍。

Linux 内核源码顶层目录结构(裁剪视角) linux-6.x/ arch/(架构相关) drivers/(设备驱动) 核心子系统/ arm/ arm64/ x86/ riscv/ mips/ boot/(启动代码) include/(架构头文件) i2c/ spi/ gpio/ mmc/ net/ usb/ video/ input/ tty/ pwm/ dma/ clk/ regulator/ pinctrl/ kernel/(调度、IPC) mm/(内存管理) fs/(文件系统) net/(网络协议栈) ipc/ init/ block/ lib/ 💡 裁剪重点 arch/ 决定你能跑什么 CPU;drivers/ 是裁剪大头(占 50%+ 代码);核心子系统尽量少动

几个关键目录,我单独拎出来说说:

  • arch/:你用的 ARM、RISC-V、x86,都在这里。裁剪时先确定架构,然后进去把不用的 SoC 平台、不用的板级文件删掉。我做过一个项目,光 arch/arm/mach-* 就砍掉了 80% 的板级支持。
  • drivers/:这是裁剪的主战场。内核 70% 的代码量都在这里。你想想看,你的板子上只有 I2C、SPI、GPIO、MMC,那什么 FireWire、ISDN、红外遥控,统统可以关掉。
  • fs/:文件系统。嵌入式常用 ext4、squashfs、ubifs、fat。NFS 调试时可以开,产品阶段关掉。至于什么 XFS、Btrfs、NTFS,基本用不上。
  • net/:网络协议栈。如果你不需要 IPv6、不需要蓝牙、不需要 802.11,果断关掉。我见过有人把整个 net/ 关掉,内核小了 30%。
我的习惯:拿到一个新内核,第一件事不是配置,而是 ls arch/ drivers/ fs/ net/ 扫一遍目录。心里有个数,哪些目录是“肯定要的”,哪些是“肯定不要的”。这比直接进 menuconfig 瞎翻快得多。

4.2 Kconfig 机制:内核的“菜单系统”

Kconfig 是什么?说白了,它就是内核的菜单配置文件。每个功能模块都有一个 Kconfig 文件,里面定义了:这个功能叫什么、依赖什么、有没有子选项。

你执行 make menuconfig 时看到的那些树形菜单,就是内核把成千上万个 Kconfig 文件解析后拼出来的。

4.2.1 Kconfig 语法速览

我不打算把 Kconfig 语法全讲一遍(那够写一本书了),但几个核心关键字你必须认识:

关键字 作用 示例
config 定义一个配置选项 config CONFIG_FOO
tristate 三态:不编译/编译进内核/编译为模块 tristate "Foo support"
bool 二态:编译或不编译 bool "Enable Foo feature"
depends on 依赖另一个选项 depends on CONFIG_I2C
select 反向选择(自动开启依赖项) select CONFIG_CRC32
help 帮助信息 help\n Say Y here if you need Foo.
if / endif 条件块 if CONFIG_FOO\n ... \nendif

举个例子,你打开 drivers/i2c/Kconfig,会看到类似这样的内容:

config I2C
    tristate "I2C support"
    select CRC32
    help
      I2C is a low-speed bus used by many sensors and EEPROMs.

if I2C
    config I2C_BOARDINFO
        bool "Provide device info from board files"
        default y

    config I2C_CHARDEV
        tristate "I2C device interface"
        help
          Say Y here to use /dev/i2c-N interface.
endif

看到没?I2C 是 tristate,可以编成模块。I2C_BOARDINFO 是 bool,要么编要么不编。而且 I2C_CHARDEV 只有在 I2C 开启后才可见——这就是依赖链。

我曾经踩过的坑:裁剪时关掉了一个驱动,结果编译报错说某个符号未定义。查了半天,发现是另一个驱动 select 了这个被关掉的驱动。记住:select 是强制依赖,你关不掉。遇到这种情况,要么把依赖它的驱动也关掉,要么保留。

4.2.2 配置选项的三种状态

在 menuconfig 里,你看到的选项有三种状态:

  • Y:编译进内核(built-in)。内核启动时直接加载,速度快,但体积大。
  • M:编译为模块(module)。按需加载,体积小,但需要文件系统和 modprobe 支持。
  • N:不编译。彻底砍掉。

裁剪时我的原则是:能 M 不 Y,能 N 不 M。但要注意,有些功能(比如根文件系统驱动、内存管理)必须 Y,不能 M。你想想看,内核都还没挂载文件系统,怎么加载模块?

4.3 Makefile 机制:内核怎么编译的

Kconfig 决定了“编译什么”,Makefile 决定了“怎么编译”。每个目录下都有一个 Makefile,里面用 obj-y、obj-m、obj-$(CONFIG_XXX) 来控制哪些文件参与编译。

举个例子,drivers/i2c/Makefile:

obj-y           += i2c-core.o i2c-dev.o
obj-$(CONFIG_I2C_CHARDEV) += i2c-dev.o
obj-$(CONFIG_I2C_GPIO)    += i2c-gpio.o
obj-$(CONFIG_I2C_S3C2410) += i2c-s3c2410.o

这里的关键是:

  • obj-y:无条件编译。比如 i2c-core.o,只要 I2C 子系统开启,就必须编译。
  • obj-$(CONFIG_XXX):条件编译。如果 CONFIG_I2C_GPIO=y,就编译 i2c-gpio.o 并链接进内核;如果是 =m,就编译成独立的 .ko 模块。

裁剪时,你不需要手动改 Makefile——那是 Kconfig 的事。但理解这个机制,能帮你快速定位:某个功能对应的源文件在哪里,依赖链是怎样的。

一个小技巧:如果你不确定某个配置选项对应哪些源文件,可以这样查:
grep -r "CONFIG_I2C_GPIO" drivers/
结果会告诉你,哪些 Makefile 引用了这个配置,哪些源文件有条件编译。我在调试裁剪问题时,这招屡试不爽。

4.4 内核配置方法:menuconfig 实战

好了,理论讲完了,咱们上手。配置内核的方法有好几种,但嵌入式开发最常用的就是 menuconfig。

4.4.1 启动 menuconfig

在源码根目录下执行:

make ARCH=arm menuconfig

如果你是交叉编译,记得指定 CROSS_COMPILE:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

界面出来后,你会看到一个蓝底灰字的菜单。上下键移动,回车进入子菜单,空格切换选项状态(Y/M/N),? 查看帮助,/ 搜索。

4.4.2 搜索功能:救命用的

内核配置项成千上万,你不可能一个个翻。我每次裁剪,用得最多的就是 / 搜索。比如我想找 GPIO 相关的配置:

  1. 按 /,输入 GPIO,回车。
  2. 结果会列出所有包含 "GPIO" 的配置项,以及它们的路径、当前值、依赖关系。
  3. 按数字键可以直接跳转到对应位置。

举个例子,搜索 I2C_GPIO 的结果可能是:

Symbol: I2C_GPIO [=n]
Type  : tristate
Prompt: GPIO-based bitbanging I2C
  Location:
    -> Device Drivers
      -> I2C support (I2C [=y])
        -> I2C Hardware Bus support
  Defined at drivers/i2c/busses/Kconfig:123
  Depends on: I2C [=y] && GPIOLIB [=y]

看到没?它告诉你这个选项在哪个 Kconfig 文件的哪一行,依赖什么。这对排查“为什么这个选项灰了”特别有用。

4.4.3 裁剪实战:一个最小配置的诞生

我带你走一遍典型的裁剪流程。假设我们有一个 ARM Cortex-A7 的板子,只需要跑 Linux,挂载 squashfs 根文件系统,支持 I2C、SPI、GPIO、MMC、串口。

步骤是这样的:

  1. 加载默认配置:make ARCH=arm defconfig(或者用厂商提供的 xxx_defconfig)。
  2. 进入 menuconfig:make ARCH=arm menuconfig。
  3. 关闭不需要的架构:General setup → 关掉不用的架构支持。
  4. 裁剪驱动:进入 Device Drivers,把下面这些统统设为 N:
    • Graphics support(没有显示)
    • Sound card support(没有音频)
    • USB support(没有 USB)
    • MMC/SD/SDIO card support(如果有就保留)
    • Network device support(如果不需要网卡就关掉)
  5. 裁剪文件系统:File systems → 只保留 squashfs、proc、sysfs、devtmpfs,其他全关。
  6. 裁剪网络:Networking support → 如果不需要网络,直接关掉 Networking support 顶层选项。
  7. 保存退出:按 Exit 直到提示保存,选 Yes。

这样一轮下来,内核能从默认的十几 MB 瘦身到 2-3 MB(压缩后)。

我的经验:第一次裁剪别追求极致。先保证能跑起来,再逐步关掉不需要的功能。每关一批,编译一次,测试一次。我曾经一次性关掉太多,结果内核启动到一半就 panic,排查了整整两天——最后发现是把 GPIOLIB 关了,而 MMC 驱动依赖它。

4.4.4 保存与复用配置

配置完成后,内核会自动保存到 .config 文件。你可以把它备份起来:

cp .config my_board_defconfig

下次编译时,直接加载:

make ARCH=arm my_board_defconfig

或者,你也可以把它放到 arch/arm/configs/ 目录下,这样就能用 make ARCH=arm my_board_defconfig 直接加载了。我习惯每个项目都保留一份 defconfig,方便后续复用和版本管理。

注意:.config 文件里有很多自动生成的选项(比如 CONFIG_GCC_VERSION),不同内核版本之间不要直接复制。正确的做法是用 make savedefconfig 生成最小化的 defconfig,然后再复制。

4.5 本章小结

这一章的内容,说白了就是三件事:

  • 源码结构:知道东西在哪,才能知道砍什么。
  • Kconfig 与 Makefile:理解配置和编译的机制,才能避免“关了这个那个报错”的尴尬。
  • menuconfig 操作:工具用得溜,裁剪效率翻倍。