2. Linux内核源码结构与构建系统入门

内核配置系统,说白了就是 Linux 内核的「开关面板」。你想想看,一个内核支持几千种硬件、几百种文件系统、几十种网络协议,总不能一股脑全编译进去吧?Kconfig 就是用来管理这些开关的。

我刚开始接触内核时,觉得 Kconfig 就是个简单的菜单描述语言。直到有一次,我写了一个驱动模块,depends on 写错了依赖关系,结果配置界面里我的选项死活不出现……嗯,从那以后我才认真啃了一遍 Kconfig 语法。

2.1 Kconfig 的核心元素

Kconfig 文件通常放在每个驱动目录下,文件名就叫 Kconfig。它由一系列配置条目组成,每个条目控制一个内核配置选项。

2.1.1 config:最基本的配置项

一个 config 条目定义一个开关变量,最终会生成 .config 文件里的 CONFIG_XXX=y 或 CONFIG_XXX=m。

config MY_DRIVER
    tristate "My custom driver support"
    default n
    help
      This is a custom driver for demonstration.
      Say Y here to enable it, M to build as module.

这里 tristate 表示三态:Y(编译进内核)、M(编译为模块)、N(不编译)。如果是 bool,就只有 Y 和 N 两个选项。

个人习惯:我写驱动时,如果功能可以独立加载/卸载,一定用 tristate。调试阶段用 M 编译成模块,省得每次改代码都要重编整个内核。

2.1.2 menuconfig:带子菜单的配置项

menuconfig 和 config 的区别在于:前者可以展开一个子菜单,里面放更多配置项。

menuconfig MY_SUBSYSTEM
    bool "My subsystem support"
    default n

if MY_SUBSYSTEM

config MY_FEATURE_A
    bool "Feature A"
    default y

config MY_FEATURE_B
    tristate "Feature B"
    default m

endif

只有 MY_SUBSYSTEM 被选中时,MY_FEATURE_A 和 MY_FEATURE_B 才会出现在配置界面里。这个机制在管理复杂驱动时特别有用。

2.1.3 choice:单选组

当一组选项只能选一个时,用 choice。比如选择 CPU 调度算法、选择 I/O 调度器。

choice
    prompt "I/O Scheduler"
    default DEFAULT_DEADLINE

config DEFAULT_DEADLINE
    bool "Deadline"
    select MQ_IOSCHED_DEADLINE

config DEFAULT_CFQ
    bool "CFQ"
    select MQ_IOSCHED_CFQ

endchoice

这里 prompt 是给用户看的提示文字。default 指定默认选中项。注意:choice 里的选项必须是 bool 类型。

我曾经踩过的坑:在 choice 里用了 tristate,结果配置界面直接报语法错误。内核的 Kconfig 解析器对类型检查很严格,choice 只接受 bool。

2.1.4 source:引入子目录的 Kconfig

内核的 Kconfig 是树状结构的。顶层 Kconfig 通过 source 引入各个子目录的配置。

source "drivers/my_driver/Kconfig"
source "net/my_protocol/Kconfig"

路径是相对于内核源码根目录的。我习惯在每个驱动目录下放一个独立的 Kconfig,然后在父目录里用 source 引入,这样模块化清晰,也方便别人维护。

2.2 依赖与选择:depends on 和 select

这两个关键字是 Kconfig 里最容易搞混的,我当年就吃过亏。

关键字 含义 方向 典型场景
depends on 依赖某个选项被启用 被动等待 驱动依赖某个总线或框架
select 强制启用另一个选项 主动拉取 驱动需要某个底层库
config MY_USB_DRIVER
    tristate "My USB driver"
    depends on USB
    select MY_HELPER_LIB
    help
      This driver requires USB support and
      automatically selects MY_HELPER_LIB.

这里 depends on USB 表示:如果 CONFIG_USB 没开,我的驱动在配置界面里根本不会出现。select MY_HELPER_LIB 表示:只要用户选了 MY_USB_DRIVER,MY_HELPER_LIB 就会被自动设为 Y 或 M。

关键区别:depends on 是「你不开我就不出现」,select 是「你选我就带上它」。滥用 select 可能导致依赖循环,内核的 Kconfig 解析器会报错。

2.3 default 和 help

default 设置默认值,help 提供帮助信息。这两个看似简单,但用好了能省很多事。

config MY_DRIVER_DEBUG
    bool "Enable debug output for my driver"
    depends on MY_DRIVER
    default n
    help
      Say Y here to enable verbose debug messages.
      This will slow down the driver.
      If unsure, say N.

我个人习惯:新驱动默认 n,除非是平台必须的驱动(比如串口、时钟控制器)才默认 y。调试选项一律默认 n,避免影响性能。

避坑指南:我曾经把调试选项默认设为 y,结果客户反馈驱动跑起来特别慢。查了半天才发现是调试日志一直在刷。从那以后,所有调试相关的配置我都默认 n。

2.4 Makefile 与内核编译的关联

Kconfig 定义「要不要编译」,Makefile 定义「怎么编译」。两者通过 CONFIG_XXX 变量连接。

# Kconfig
config MY_DRIVER
    tristate "My driver"
    default n

# Makefile
obj-$(CONFIG_MY_DRIVER) += my_driver.o

当 CONFIG_MY_DRIVER=y 时,obj-y 会把 my_driver.o 编进内核;当 CONFIG_MY_DRIVER=m 时,obj-m 会把它编成 .ko 模块;当 n 时,整行被忽略。

如果驱动由多个源文件组成:

obj-$(CONFIG_MY_DRIVER) += my_driver.o
my_driver-objs := main.o helper.o init.o

或者用更简洁的写法:

obj-$(CONFIG_MY_DRIVER) := my_driver.o
my_driver-y := main.o helper.o init.o
注意:Makefile 里的变量名必须和 Kconfig 里的 CONFIG_ 前缀完全一致。大小写敏感,少写一个字母,编译时就找不到文件。我见过有人把 CONFIG_MY_DRIVER 写成 CONFIG_my_driver,结果编出来的 .ko 文件是空的。

2.5 知识体系总览

下面这张图总结了 Kconfig 和 Makefile 的协作关系:

Kconfig 与 Makefile 协作流程 Kconfig 配置定义 config MY_DRIVER tristate "My driver" depends on USB select HELPER_LIB default n 生成 .config 文件 CONFIG_MY_DRIVER=m 传递给 Makefile 编译规则 obj-$(CONFIG_MY_DRIVER) += my_driver.o my_driver-objs := main.o helper.o 编译输出 my_driver.ko 模块 关键点 Kconfig 定义「要不要编」 Makefile 定义「怎么编」

2.6 实战建议

写 Kconfig 时,我总结了几条经验:

  • 命名规范:驱动选项用 CONFIG_<DRIVER_NAME>,调试选项加 _DEBUG 后缀,一目了然。
  • 依赖最小化:能用 depends on 就别用 select,避免引入不必要的依赖链。
  • 帮助信息写清楚:至少说明这个驱动是干什么的、依赖什么硬件、选 Y 还是 M 的区别。我见过太多 Kconfig 的 help 就写一行 "Say Y here",等于没写。
  • 测试配置:写完 Kconfig 后,运行 make menuconfig 看看你的选项是否出现在正确的位置,依赖关系是否生效。
一句话总结:Kconfig 是内核的「菜单」,Makefile 是「厨房」。菜单决定了做什么菜,厨房决定了怎么做。两者配合好了,你的驱动才能被正确地编译进内核。