☰
Linux内核配置:defconfig与.config区别及生成机制解析
2026/10/1 11:34:16 网站建设 项目流程

搞内核移植、驱动适配、板级配置的人,几乎都有过这种经历:在arch/arm64/configs/下面的某个xxx_defconfig里老老实实加了一行CONFIG_MY_DRIVER=y,保存、编译、烧写,结果驱动该加载不起来还是加载不起来,去设备上查/proc/config.gz也翻不到这个选项。回头打开源码树根目录的.config一看,那一行压根就不在里面——这时候才反应过来,defconfig和.config根本不是同一个东西,中间隔着一整套配置生成机制。

这就是我想展开聊的话题。defconfig和.config是 Linux 内核构建体系里最容易混淆、也最容易耽误时间的一对文件。前者是给人看、往仓库里提交的"种子配置",后者是编译时真正被读取的"最终答案",两者之间由 Kconfig 语言和scripts/kconfig/下的那套工具连接起来。把这条链路摸清楚,能省下大量"改了不生效"的排查时间;做板级适配、合并厂商内核分支、处理配置碎片的时候,也能少踩几个坑。

不管你是在跑第一遍make menuconfig的新手,还是已经在维护厂商内核配置碎片的老手,这条链路都值得从头捋一遍。下面我把原理、命令、坑点和完整实操流程都摊开讲,每个配置怎么写、为什么这么写、写错了会怎样,尽量说得明白点。

1. 这两个文件到底谁管谁

1.1 先看一个真实的翻车现场

我拿一块基于 ARM64 的板子举例。板子的 I2C 上挂了一颗传感器,驱动源码在drivers/iio/...下面,已经随内核源码一起放进来了。你要做的第一步,显然是让内核在编译时把这个驱动编进去。

第一反应是在arch/arm64/configs/里找到对应板子的配置文件,比如myboard_defconfig,在末尾加上:

CONFIG_MY_SENSOR=y

然后make ARCH=arm64 myboard_defconfig再编译。编译过程很顺利,日志里也没有报错,镜像照样出来了。烧进去开机,dmesg | grep my_sensor什么都没有,/sys/bus/i2c/devices里也找不到那颗传感器。

这时候你去源码树根目录grep CONFIG_MY_SENSOR .config,发现结果是空的。也就是说,编译时真正被读的那个文件里,压根没有你要的这行。加在defconfig里的那行字,被"吃掉"了。

为什么会这样?因为myboard_defconfig在生成.config的时候,要经过 Kconfig 依赖求解。如果CONFIG_MY_SENSOR在它所在的 Kconfig 文件里写着depends on IIO,而你的配置里CONFIG_IIO是关的,那么这个符号就不会出现在.config里——即使你手动在 defconfig 里写了=y。这条规则看似不近人情,其实是为了保证配置自洽,理解它之后,绝大部分"配置不生效"的困惑都能自己解开。

1.2 Kconfig 才是真正的规则制定者

很多人以为defconfig就是配置的全部,其实它只是个索引。真正定义"有哪些配置项、取值范围是什么、依赖谁、默认值是什么"的,是散布在整个源码树里的Kconfig文件。每个有配置选项的目录下都有一份,比如drivers/iio/Kconfig、drivers/net/Kconfig、fs/Kconfig,它们通过source语句层层串联,从顶层Kconfig一路引下去,最终形成一棵完整的配置选项树。

一个典型的条目长这样:

config MY_SENSOR tristate "My I2C sensor driver" depends on I2C select REGMAP_I2C help Say Y here to enable support for the My I2C sensor. To compile this driver as a module, choose M here.

这几行里,config MY_SENSOR定义了符号名,最终在.config里就是CONFIG_MY_SENSOR;tristate说明它是三态值,可以编进内核也可以编成模块;depends on I2C是硬性门槛,I2C 子系统没开,这个选项在menuconfig里都不会出现;select REGMAP_I2C表示一旦打开这个选项,就顺带把 REGMAP_I2C 拉起来。

help段的文字不要小看,很多驱动作者会把硬件连接注意事项、模块名、依赖的外部固件写在里面,遇到不认识的选项,先按?看帮助文本,比到处搜快得多。

1.3 defconfig 是种子,.config 是果实

现在来解释为什么两个文件的行数差那么多。一个完整的.config动辄两三千行甚至更多,而arch/arm64/configs/下的defconfig通常只有几百行。原因在于defconfig里保存的是"相对默认值的差异",凡是没有特殊要求的选项,都不写;.config里保存的是"求解之后的全部结果",每一个符号都有明确的 y、m、n 或者具体数值。

.config的第一行通常写着这样一句:

# # Automatically generated file; DO NOT EDIT. #

这不是摆设。它明确告诉你,这个文件是生成物。你在.config里手动改的东西,下一次执行make xxx_defconfig或make olddefconfig时可能就被覆盖了。正确的做法是改defconfig,然后让它重新生成.config。只有在临时调试、验证某个选项效果的时候,才适合直接动.config,验证完立刻用savedefconfig把有效改动回写到defconfig里。

说得形象一点:defconfig是你写给构建系统的一份"需求清单",.config是构建系统把这份清单跟整本 Kconfig 规则书对照之后,填出来的"最终执行方案"。清单上写了但规则不允许的,会被划掉;清单上没写但规则要求默认开启的,会被补上。两者内容不一致,是常态,不是 bug。

2. 配置系统的内部逻辑

2.1 从 defconfig 到 .config 的三步链路

把这条链路拆开看,大致分三步走。第一步是读入种子。执行make ARCH=arm64 myboard_defconfig的时候,构建系统调用scripts/kconfig/conf这个程序,把arch/arm64/configs/myboard_defconfig当作初始赋值表读进来,同时把整棵 Kconfig 树也解析一遍,搞清楚每个符号的类型、依赖和默认值。

第二步是依赖求解。这一步是整个流程里最关键的。程序按符号之间的依赖关系算出最终结果:显式在 defconfig 里指定过的、而且依赖也满足的,按指定的来;没指定但依赖满足的,取 Kconfig 里写的default值;依赖不满足的,一律强制成 n。这就是为什么你写在 defconfig 里的行有时会消失——不是被忽略了,是在这一步被判为无效。

第三步是写出。求解完成后,生成根目录的.config,同时把它转成两个供编译使用的文件:include/config/auto.conf给 Makefile 用,include/generated/autoconf.h给 C 代码里的条件编译用。你在源码里写的#ifdef CONFIG_MY_SENSOR,依据的就是后一个文件。

三步走完,才算完成一次完整的配置生成。理解了这三步,后面排查问题的思路就清晰了:要么是种子写错了,要么是依赖没满足,要么是编译没重新读配置。

2.2 三态值的含义与依赖求解

Kconfig 支持的类型不多,但每种都有明确的用途。把常见的几种列出来:

类型取值范围menuconfig 里的显示典型用途
booly / n[ ]或[*]纯功能开关,不适合做成模块
tristatey / m / n< >、<M>、<*>绝大多数驱动、文件系统
int整数(100)缓冲区大小、数量上限
hex十六进制(0x1000)地址、掩码、对齐值
string字符串"..."路径、参数名

tristate为什么值得单独说?因为它的三个取值对应三种构建行为:y是编进内核镜像,m是编成独立的.ko模块,n是不编译。很多驱动同时支持这两种方式,你在menuconfig里按m就能把它变成模块,加载和卸载都方便,调试阶段尤其好用。反过来,如果某个符号类型是bool,那是作者明确判断它"不能做成模块",比如跟启动流程强相关的代码,这时候就不要想着用m了。

依赖求解的规则可以概括成几句话:依赖不满足的符号一律为 n;依赖满足且没显式指定的取默认值;显式指定但依赖不满足的,指定无效。另外像int、hex这类还有range限制,你填的数字如果超出范围,会被悄悄夹到边界值上。这类问题在配置界面里看不出来,只有对比.config才能发现,所以涉及数值参数的改动,改完一定要grep一下确认。

2.3 select 与 depends on 的坑

depends on和select是 Kconfig 里两个方向相反的机制。depends on是"我需要别人先存在",是自我约束;select是"我一旦打开,就强制把别人打开",是对别人的干预。后者的副作用更大,也更容易出问题。

最典型的现象是编译时冒出一串警告:

warning: (MY_SENSOR && OTHER_DRIVER) selects REGMAP_I2C which has unmet direct dependencies (I2C)

这句话的意思是:MY_SENSOR 通过 select 想把 REGMAP_I2C 拉起来,但 REGMAP_I2C 自己声明了依赖 I2C,而 I2C 是关的,所以这个 select 无法生效。警告不是致命错误,编译可能照样过,但最终.config里CONFIG_REGMAP_I2C是 n,你依赖的代码在链接阶段就会报符号找不到。

处理这类问题有个固定套路:先把上游的依赖补上。既然 REGMAP_I2C 需要 I2C,那就先确认CONFIG_I2C=y,再重新生成配置。多数情况下警告会自己消失。如果补了依赖还有警告,那可能是源码树里存在循环依赖,这就属于上游配置本身的缺陷,只能绕开或者给上游提补丁。

和select对应的还有个imply,语义是"建议性打开",可以被用户或其他关系覆盖,优先级更低。有些较新的驱动改用imply来避免强制拉取,遇到的时候注意区别对待。

提示:select引起的依赖警告一定要处理干净。它往往不是一个孤立的警告,而是配置链路断裂的第一个信号。

3. 实操:从零给一块板子做配置裁剪

3.1 环境准备与源码目录认知

先准备一个能编译的源码目录。假设你手上是某个版本的内核源码压缩包,解压出来:

mkdir -p ~/work/kernel && cd ~/work/kernel tar -xf linux-6.x.tar.xz cd linux-6.x

进到源码树以后,先弄清楚几个关键路径:

ls arch/arm64/configs/ | head -20 # 各板子的 defconfig 汇总在这里 ls scripts/kconfig/ # 配置系统的核心脚本 ls kernel/configs/ # 通用配置碎片常放这里

arch/arm64/configs/是板级配置的存放地,命名一般跟开发板或产品代号对应,一个名字就是一个make xxx_defconfig的目标。scripts/kconfig/下面是配置工具的实现,其中merge_config.sh和diffconfig这两个脚本在后续会反复用到。kernel/configs/里放的是一些跨平台通用的配置碎片,厂商内核和通用内核项目经常在这里叠加。

交叉编译工具链也要提前配好,把aarch64-linux-gnu-前缀对应的gcc、ld、objcopy都放进 PATH。如果工具链没配,make defconfig这一步通常还能过,但到make prepare或者真正编译的时候就会报找不到交叉编译器,白等一轮。

3.2 常用配置命令对照

配置相关的 make 目标不少,名字又长得像,很容易混。我把实际用得最多的几个整理成表:

命令作用是否改写 .config
make xxx_defconfig用指定种子生成配置,会整体覆盖是
make menuconfig基于当前 .config 做图形化修改是
make oldconfig保留已有值,只对新增符号逐个提问是
make olddefconfig保留已有值,新增符号直接取默认是
make savedefconfig导出精简后的种子文件否,生成 defconfig
make listnewconfig只列出新增但未配置的符号否
make localmodconfig参考当前已加载模块裁剪配置是
make tinyconfig生成最小化配置是
make randconfig随机生成配置,测试用是

这里有两个命令值得单独强调。一个是olddefconfig,它在脚本化流程里几乎是标配:修改了.config或者合并了配置碎片之后,一定要跟一条olddefconfig,让依赖关系重新求解一遍,否则.config里会残留互相矛盾的项。另一个是savedefconfig,它能把当前.config里"与默认值不同的部分"抽出来,生成一份干净、行数少的种子文件,这才是应该提交到仓库的东西。

还有个细节:make defconfig不带名字时,找的是arch/$ARCH/configs/defconfig。如果不指定ARCH,$ARCH默认是宿主机架构,在 x86 机器上折腾 ARM64 内核时,很容易误操作到宿主的配置上。养成习惯,命令里写全ARCH=和CROSS_COMPILE=。

3.3 一步步裁剪并生成自己的 defconfig

下面走一遍完整流程。先基于一个现成的板子配置起步:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig

跑完之后根目录就有.config了。接着打开图形界面做裁剪:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig

界面里第一件要做的事是关掉明显用不上的子系统。比如板子上没有声卡,就在 Device Drivers 里把 Sound card support 关掉;没有无线网卡,把 Network device support 下的 Wireless LAN 关掉;不跑桌面,把 Graphics support 里的一堆显示驱动关掉。每关一个大项,都可以按/搜索符号名确认它在哪一层,搜到之后按数字直接跳过去,比一层层翻菜单快很多。

裁剪的时候有个原则值得记一下:不确定的先留着,确定不要的再关。因为很多驱动之间存在隐式依赖,一刀切下去可能牵出一堆编译错误,回头再找原因反而更费时间。判断某个驱动是否用得上,可以看它对应硬件的连接方式,I2C、SPI 上的器件一般可以通过设备树描述来确认;也可以用lspci、lsusb这类信息反推。

裁剪满意后,把.config导出成种子:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- savedefconfig

生成的defconfig就在源码树根目录,行数通常只有完整.config的几分之一。把它放到板子对应的位置:

cp defconfig arch/arm64/configs/myboard_defconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- myboard_defconfig

第二次生成.config,再用diff跟刚才的比一下,确认两者实质一致。到这一步,你的板级配置就算成型了。以后所有配置改动都改myboard_defconfig,改完重新make myboard_defconfig,别人拉代码也能复现同样的配置。

注意:savedefconfig生成的defconfig是"相对默认值"的结果。如果哪天上游修改了某个符号的默认值,你这份种子文件里的行为可能跟着变。升级内核版本时,一定要重新跑一遍配置对比,别直接照搬。

3.4 配置差异对比与回滚

配置一多,光靠眼睛看.config是看不出变化的。内核自带了一个diffconfig脚本,专门干这个:

scripts/diffconfig .config.old .config

它会用+和-标出两项配置之间的差异,只显示变化的部分,比手工 diff 干净得多。常见的用法是在改配置前先备份:

cp .config .config.bak make menuconfig scripts/diffconfig .config.bak .config

还有一个开关-m,作用是合并显示成一行,适合配置项很多、差异很碎的场景。另外如果只是想知道某个符号到底落在了哪,直接grep最快:

grep -n "CONFIG_MY_SENSOR" .config grep -rn "config MY_SENSOR" --include=Kconfig .

第一条查最终结果,第二条查它在 Kconfig 里的定义位置。两条一起用,基本能定位所有"这个符号为什么没生效"的问题。

4. 厂商内核与 GKI 的配置分层

4.1 fragment 机制是怎么回事

做手机、车机这类产品的时候,内核配置的维护方式跟开发板完全不一样。上游确定的通用内核有一份基线配置,各家厂商在此基础上叠加自己平台相关的选项。如果每家的改动都直接往那份基线配置里塞,维护会立刻失控。于是就有了配置碎片机制,英文叫 fragment。

fragment 本质上就是一个个小的配置文件,内容格式跟defconfig一样,但只写自己关心的那几十行。基线配置保持干净,各平台、各产品线各自维护自己的碎片,构建时按顺序合并。这样做的好处很直接:别人升级基线,你的碎片不受影响;同一份基线,配上不同碎片就能产出不同产品的配置。

看一个典型的目录结构,基线配置放在arch/arm64/configs/gki_defconfig,通用碎片放在kernel/configs/下面,命名往往是android-base.config、android-recommended.config这种。厂商自己的碎片通常放在产品目录里,跟平台代码放一起。

4.2 merge_config.sh 的合并顺序

合并靠的是内核自带的脚本:

scripts/kconfig/merge_config.sh -m \ arch/arm64/configs/gki_defconfig \ kernel/configs/android-base.config \ kernel/configs/vendor-myplatform.config

-m表示只合并,不立刻跑olddefconfig。合并完成之后再手动补一条:

make ARCH=arm64 olddefconfig

顺序很关键:越靠后的文件优先级越高,会覆盖前面同名符号的值。所以基线放最前,通用配置在中间,产品专有配置放最后。反过来排,产品配置就可能被通用配置覆盖掉,出现"改了没反应"的情况。

合并过程本身也会打印信息,主要看两类。一类是Value of CONFIG_XXX is redefined by fragment ...,这是正常的覆盖提示,说明后面的文件改了前面的值;另一类是Value of CONFIG_XXX is redefined by fragment ... but this fragment is not applied或者依赖相关的警告,这就说明覆盖没生效,得去查那个符号的依赖是不是没满足。

4.3 冲突与覆盖的处理

碎片一多,冲突是免不了的。最常见的场景是:你的产品碎片里写了CONFIG_PM_DEBUG=n,但中间某个通用碎片里写了=y,而你的碎片排在它前面,结果就是关不掉。解决办法有两个,要么调整合并顺序把自己的碎片放最后,要么干脆在自己的碎片里也把它显式写一遍。

再一种情况是两个碎片互相 select,把某个符号的状态改得跟预期不符。这种要靠olddefconfig之后的.config来核实:合并完不要急着编译,先grep几个关键符号确认状态。我自己的习惯是维护一份"关键符号清单",每次合并后批量grep一遍,几秒钟的事,能挡住不少低级失误。

还有一种容易被忽略的情况:碎片里写了某个符号,但该符号在当前的 Kconfig 树里根本不存在。这时候合并脚本通常不会报错,那一行就被静默丢弃了。板子换了、平台代码升级了,碎片里的符号名可能已经变了,定期用grep -rn "config SYMBOL"核实一下它还在不在,是有必要的。

5. 编译期配置是如何生效的

5.1 auto.conf 与 autoconf.h

.config生成之后,构建系统还会把它转成两个文件。第一个是include/config/auto.conf,内容是 Makefile 风格的变量赋值,比如:

CONFIG_MY_SENSOR=y CONFIG_IIO=m

顶层 Makefile 和各级 Kbuild 都会把它包含进来,用$(CONFIG_MY_SENSOR)这样的方式判断。第二个是include/generated/autoconf.h,内容是 C 预处理宏:

#define CONFIG_MY_SENSOR 1

源码里那些#ifdef CONFIG_MY_SENSOR判断的就是它。三态值在头文件里的表达方式有区别:y会被定义成1,m会被定义成1但同时还有CONFIG_MY_SENSOR_MODULE,n则完全不定义。所以驱动里常见的写法是:

#ifdef CONFIG_MY_SENSOR ... #endif

或者用IS_ENABLED(CONFIG_MY_SENSOR)这种更安全的方式,能同时兼容 y 和 m。

提示:改了.config但发现autoconf.h没更新,那说明配置同步没跑。手动执行make ARCH=arm64 syncconfig可以强制刷新。

5.2 Makefile 与 Kbuild 怎么读配置

驱动要参与编译,得在对应目录的 Makefile 里写一行:

obj-$(CONFIG_MY_SENSOR) += my_sensor.o

这行的意思是:如果CONFIG_MY_SENSOR的值是y,就把目标编进内核;值是m,就编成模块;值是n,就什么都不做。这一行写错了,或者符号名对不上,配置再对驱动也不会被编进去。这是排查"配置生效了但驱动没进镜像"时首先要看的地方。

目录里如果还有Kconfig文件,别忘了在父目录的Kconfig里加一行source "drivers/xxx/Kconfig",否则你的配置项在menuconfig里根本不会出现——很多人加完驱动、加完 Makefile 却找不到菜单项,就是漏了这一步。

5.3 改了配置却没重编

有一种情况很气人:.config明明改对了,grep也能查到,编译却还是老样子。这通常是构建系统的增量机制在作怪。内核用文件时间戳判断哪些东西需要重编,配置变更的传播依赖include/config/下一个叫auto.conf.cmd之类的中间文件。如果时间戳乱了,或者你用工具手动改了文件但没更新 mtime,构建系统就可能认为"什么都没变"。

稳妥的解决办法是强制刷新配置依赖:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- prepare

prepare会重新生成autoconf.h和auto.conf相关的内容。如果还不行,直接清掉include/config/和include/generated/两个目录再编。代价是重编范围变大,但能排除掉所有缓存问题。做配置调试的时候,我一般会先make clean一次,虽然费点时间,但省得跟构建系统斗智斗勇。

6. 常见问题速查与避坑清单

6.1 配置不生效类

这一类问题的表现是:.config里没有你想要的符号,或者值是 n。排查顺序我整理成表:

现象可能原因处理方式
defconfig 里写了但 .config 没有依赖没满足查 Kconfig 的 depends on,补上上游符号
符号存在但被强制成 n被别的符号 select 覆盖grep选它的符号,检查冲突
menuconfig 里找不到选项Kconfig 没被 source在父目录 Kconfig 里补 source 行
改了 .config 重启后失效又跑了一次 defconfig改动要回写 defconfig
fragment 里的值被改掉合并顺序不对把自己的碎片放到最后

这套表能覆盖八成的现场问题。剩下的两成多半是版本差异:老代码里的符号在新版本里被拆分或重命名了,写死了旧名字自然不生效。

6.2 编译报错类

配置相关的编译错误,最常见的是符号未定义和依赖警告。前者比如undefined reference to regmap_i2c_init,意思是代码引用了某个子系统,但这个子系统没被配置进来。解决办法是找到该子系统对应的符号,在配置里打开它。

后者就是前面说的select警告。警告本身不阻断编译,但它预示着后面可能出问题,不要因为"能编过"就忽略。还有一种报错是*** Configuration file ".config" not found!,说明你还没生成配置就直接编译了,补一条make xxx_defconfig即可。

如果报错提示某个头文件找不到,先检查是不是make prepare没跑。缺少include/generated/下的文件时,编译几乎必然失败,而这通常只是配置同步没完成。

6.3 提交与协作类

最后聊聊工程协作。往仓库提交配置改动时,永远提交savedefconfig生成的种子文件,不要提交完整的.config。原因有两个:一是完整配置几千行,diff 噪音极大,review 的人根本看不出你改了哪几项;二是完整配置里包含大量版本相关的默认值,换个内核版本就全变了,合并时冲突满天飞。

提交前跑一遍scripts/diffconfig,确认改动就是你想改的那几项,没有连带影响。如果你在碎片机制下工作,还要确认改动放在了正确的碎片里,而不是随手丢进了基线配置。

7. 我在实际维护中的几点体会

最后一个经验:把"配置改动"和"配置验证"分成两个动作,中间一定要插一次olddefconfig。我见过太多次直接改完就编译、结果配置被依赖求解改回去的情况,白等一轮编译时间。现在的习惯是,任何配置改动之后都执行这么一小段:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig grep -n "CONFIG_MY_SENSOR" .config scripts/diffconfig .config.bak .config

三条命令,不到十秒,配置对不对一清二楚。

另外,维护一份自己的"配置变更记录"很值。什么时候加了什么符号、为什么加、对应哪个硬件版本,随手记在提交信息里。过半年回来看,你会庆幸自己当时多写了那两行字。配置文件这个领域,最容易出问题的地方从来不是语法,而是"当初为什么这么配"没人记得。

后续如果要把这套流程接到自动化构建里,可以把merge_config.sh加olddefconfig加diffconfig做成一个校验脚本,在 CI 里跑一遍,合并出问题当场就能发现,比等到烧板子的时候再查省事得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询