3. 内核配置基础:配置系统组成

说实话,我刚接触内核配置那会儿,看着 make menuconfig 出来的界面,心里是有点懵的。那么多选项,有的打 Y,有的打 M,有的干脆空着。到底谁管着谁?

后来我才明白,这一切的背后,是两套东西在起作用:Kconfig 和 Makefile。一个管“有什么选项”,一个管“怎么编译”。今天我们就把它拆开看看。

3.1 配置系统的“骨架”:Kconfig 语法

Kconfig 文件,说白了就是内核配置菜单的“剧本”。它定义了每个选项的名字、类型、默认值,以及谁依赖谁。你看到的 menuconfig 界面,就是解析这些文件渲染出来的。

一个典型的 Kconfig 条目长这样:

config CONFIG_EXAMPLE
    bool "Enable example feature"
    default y
    depends on CONFIG_OTHER_FEATURE
    help
      Say Y here if you want to enable the example feature.
      If unsure, say N.

这里有几个关键字段:

  • config:声明一个配置选项,后面跟的是宏名字(通常带 CONFIG_ 前缀)。
  • bool / tristate / string / hex / int:选项的类型,决定了你能选什么值。
  • default:默认值。不写的话,bool 默认是 n,tristate 默认也是 n。
  • depends on:依赖关系。这个选项只有在依赖项被选中时才会出现。
  • help:帮助信息,给开发者看的。
我的习惯:每次写 Kconfig 时,我都会把 help 写清楚。别偷懒,三个月后你自己回来看,可能都忘了这个选项是干嘛的。

3.2 配置选项的五种类型

这是最基础,也最容易搞混的地方。我见过不少新手把 bool 和 tristate 用错,导致驱动只能编进内核,不能作为模块加载。

类型 可选值 典型用途
bool y / n 开关选项,要么编进内核,要么不编
tristate y / m / n 驱动选项,可编进内核、可编成模块、可不编
string 任意字符串 路径、名称、版本号等文本信息
hex 十六进制数 地址、掩码、寄存器值
int 十进制整数 数量、大小、超时值等

举个例子,你写一个网卡驱动:

config MY_NET_DRIVER
    tristate "My network driver"
    default m
    help
      This is a tristate driver. You can build it as a module.

config MY_NET_DEBUG
    bool "Enable debug output"
    depends on MY_NET_DRIVER
    default n
    help
      Say Y to enable verbose debug messages.

config MY_NET_TX_QUEUE_SIZE
    int "TX queue size (16-1024)"
    range 16 1024
    default 256
    help
      Number of descriptors in the TX ring.

config MY_NET_BASE_ADDR
    hex "Base address (e.g. 0x10000000)"
    default 0x10000000
    help
      Memory-mapped base address of the device.
我曾经踩过的坑:有一次我把一个驱动写成了 bool,结果客户想把它编成模块加载,死活不行。后来查了半天才发现是类型写错了。所以,驱动选项一律用 tristate,除非你确定它必须编进内核。

3.3 依赖关系与选择逻辑

配置选项不是孤立的。它们之间有依赖、有互斥、有反向选择。理解这些关系,才能避免配出“死锁”或“缺胳膊少腿”的内核。

3.3.1 depends on:正向依赖

最常用的依赖关系。A 依赖 B,那么只有 B 被选中时,A 才会出现在菜单里。

config A
    bool "Feature A"
    depends on B

如果 B 没选,A 就不可见。这很直观。

3.3.2 select:反向依赖

有时候,你选了 A,就自动把 B 也选上。这叫 select。

config A
    bool "Feature A"
    select B

选了 A,B 自动变成 y。注意,select 会绕过 depends on 检查,容易造成依赖冲突。我一般只在“A 必须依赖 B 才能工作”时才用,而且会加注释说明。

经验之谈:能用 depends on 就别用 select。select 是“强拉”,容易把依赖关系搞乱。我在一个项目里见过 select 链导致循环依赖,编译直接报错。

3.3.3 choice:互斥选择

一组选项里只能选一个。比如 CPU 调度策略、内存分配算法等。

choice
    prompt "Scheduling policy"
    default SCHED_CFS

config SCHED_CFS
    bool "Completely Fair Scheduler"

config SCHED_RT
    bool "Real-time Scheduler"

config SCHED_IDLE
    bool "Idle Scheduler"

endchoice

这里只能选一个,选了这个,其他的自动取消。

3.3.4 if / endif:条件块

当某个条件成立时,才展示一组选项。

if EXPERT

config DEBUG_OPTION
    bool "Enable debug"
    default n

endif

只有 EXPERT 被选中时,DEBUG_OPTION 才会出现。这常用于隐藏“危险”选项。

3.4 Makefile 规则:配置如何变成编译

Kconfig 定义了“有什么”,Makefile 则定义了“怎么编”。内核的 Makefile 通过 obj-$(CONFIG_xxx) 来根据配置决定是否编译某个文件。

# 如果 CONFIG_MY_DRIVER 是 y,就编译 my_driver.o 并链接进内核
# 如果是 m,就编译成 my_driver.ko 模块
# 如果是 n,就不编译
obj-$(CONFIG_MY_DRIVER) += my_driver.o

# 多个文件可以这样写
my_driver-objs := main.o helper.o
obj-$(CONFIG_MY_DRIVER) += my_driver.o

这里有个细节:obj-y 表示编进内核,obj-m 表示编成模块,obj-n 表示不编。而 $(CONFIG_MY_DRIVER) 的值就是 y、m 或 n,Makefile 自动匹配。

我建议:写 Makefile 时,尽量把 obj-$(CONFIG_xxx) 放在一起,别散落在文件各处。这样以后维护时,一眼就能看到哪些文件受哪个配置控制。

3.5 整体流程:从 Kconfig 到 .config

为了让你更直观地理解,我画了一张图,展示配置系统的完整流程:

内核配置系统流程 内核配置系统:从 Kconfig 到 .config Kconfig 文件 定义选项、类型、依赖 配置工具 menuconfig / xconfig .config 文件 CONFIG_xxx=y/m/n Makefile obj-$(CONFIG_xxx) += 编译输出 vmlinux / .ko 模块 说明: Kconfig 定义“有什么选项”,配置工具让你选择,.config 保存结果,Makefile 根据 .config 决定编译哪些文件。

3.6 实战:一个完整的配置示例

假设我们要添加一个虚拟字符设备驱动。完整的配置系统包括三部分:

1. Kconfig 文件(放在 drivers/char/Kconfig 里)

config VIRTUAL_CHAR
    tristate "Virtual character device driver"
    default n
    help
      A simple virtual character device for testing.
      Say M to build as a module, Y to build into kernel.

config VIRTUAL_CHAR_DEBUG
    bool "Virtual char device debug"
    depends on VIRTUAL_CHAR
    default n
    help
      Enable debug output for virtual char device.

2. Makefile 文件(放在 drivers/char/Makefile 里)

obj-$(CONFIG_VIRTUAL_CHAR) += virtual_char.o
virtual_char-$(CONFIG_VIRTUAL_CHAR_DEBUG) += debug.o

3. 生成的 .config 片段

CONFIG_VIRTUAL_CHAR=m
CONFIG_VIRTUAL_CHAR_DEBUG=y

这样,当你运行 make menuconfig 时,就能看到“Virtual character device driver”这个选项,选 M 后,virtual_char.c 会被编译成 virtual_char.ko 模块。如果同时选了 debug,debug.c 也会被编译进去。

注意:Makefile 里的 virtual_char-$(CONFIG_VIRTUAL_CHAR_DEBUG) 这种写法,是“条件编译子文件”的惯用技巧。但前提是 virtual_char.o 本身已经被 obj-$(CONFIG_VIRTUAL_CHAR) 选中了,否则这个条件分支不会生效。

3.7 避坑指南:我踩过的几个坑

  • 依赖循环:A select B,B select A,编译直接报错。解决办法:用 depends on 替代 select,或者重新设计依赖关系。
  • 类型不匹配:在 Makefile 里用 obj-$(CONFIG_xxx),但 Kconfig 里写的是 bool。bool 只有 y/n,没有 m,所以模块编译会失败。记住:驱动用 tristate,开关用 bool。
  • 默认值陷阱:Kconfig 的 default 只影响“第一次配置”时的值。如果你已经有一个 .config 文件,修改 Kconfig 的默认值不会自动更新它。需要 make olddefconfig 才能生效。
  • help 信息缺失:我见过一些内核模块,Kconfig 里 help 是空的。结果别人想用这个模块,根本不知道它是干嘛的。写 help 不是形式主义,是给未来的自己留条后路。