ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux内核配置:defconfig与.config区别及生成机制解析

Linux内核配置:defconfig与.config区别及生成机制解析 搞内核移植、驱动适配、板级配置的人几乎都有过这种经历在arch/arm64/configs/下面的某个xxx_defconfig里老老实实加了一行CONFIG_MY_DRIVERy保存、编译、烧写结果驱动该加载不起来还是加载不起来去设备上查/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_SENSORy然后make ARCHarm64 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_SENSORtristate说明它是三态值可以编进内核也可以编成模块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 ARCHarm64 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_I2Cy再重新生成配置。多数情况下警告会自己消失。如果补了依赖还有警告那可能是源码树里存在循环依赖这就属于上游配置本身的缺陷只能绕开或者给上游提补丁。和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 目标不少名字又长得像很容易混。我把实际用得最多的几个整理成表命令作用是否改写 .configmake xxx_defconfig用指定种子生成配置会整体覆盖是make menuconfig基于当前 .config 做图形化修改是make oldconfig保留已有值只对新增符号逐个提问是make olddefconfig保留已有值新增符号直接取默认是make savedefconfig导出精简后的种子文件否生成 defconfigmake 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 ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- defconfig跑完之后根目录就有.config了。接着打开图形界面做裁剪make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- menuconfig界面里第一件要做的事是关掉明显用不上的子系统。比如板子上没有声卡就在 Device Drivers 里把 Sound card support 关掉没有无线网卡把 Network device support 下的 Wireless LAN 关掉不跑桌面把 Graphics support 里的一堆显示驱动关掉。每关一个大项都可以按/搜索符号名确认它在哪一层搜到之后按数字直接跳过去比一层层翻菜单快很多。裁剪的时候有个原则值得记一下不确定的先留着确定不要的再关。因为很多驱动之间存在隐式依赖一刀切下去可能牵出一堆编译错误回头再找原因反而更费时间。判断某个驱动是否用得上可以看它对应硬件的连接方式I2C、SPI 上的器件一般可以通过设备树描述来确认也可以用lspci、lsusb这类信息反推。裁剪满意后把.config导出成种子make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- savedefconfig生成的defconfig就在源码树根目录行数通常只有完整.config的几分之一。把它放到板子对应的位置cp defconfig arch/arm64/configs/myboard_defconfig make ARCHarm64 CROSS_COMPILEaarch64-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 --includeKconfig .第一条查最终结果第二条查它在 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 ARCHarm64 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_DEBUGn但中间某个通用碎片里写了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_SENSORy CONFIG_IIOm顶层 Makefile 和各级 Kbuild 都会把它包含进来用$(CONFIG_MY_SENSOR)这样的方式判断。第二个是include/generated/autoconf.h内容是 C 预处理宏#define CONFIG_MY_SENSOR 1源码里那些#ifdef CONFIG_MY_SENSOR判断的就是它。三态值在头文件里的表达方式有区别y会被定义成1m会被定义成1但同时还有CONFIG_MY_SENSOR_MODULEn则完全不定义。所以驱动里常见的写法是#ifdef CONFIG_MY_SENSOR ... #endif或者用IS_ENABLED(CONFIG_MY_SENSOR)这种更安全的方式能同时兼容 y 和 m。提示改了.config但发现autoconf.h没更新那说明配置同步没跑。手动执行make ARCHarm64 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 ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- olddefconfig make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- prepareprepare会重新生成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改动要回写 defconfigfragment 里的值被改掉合并顺序不对把自己的碎片放到最后这套表能覆盖八成的现场问题。剩下的两成多半是版本差异老代码里的符号在新版本里被拆分或重命名了写死了旧名字自然不生效。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 ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- olddefconfig grep -n CONFIG_MY_SENSOR .config scripts/diffconfig .config.bak .config三条命令不到十秒配置对不对一清二楚。另外维护一份自己的配置变更记录很值。什么时候加了什么符号、为什么加、对应哪个硬件版本随手记在提交信息里。过半年回来看你会庆幸自己当时多写了那两行字。配置文件这个领域最容易出问题的地方从来不是语法而是当初为什么这么配没人记得。后续如果要把这套流程接到自动化构建里可以把merge_config.sh加olddefconfig加diffconfig做成一个校验脚本在 CI 里跑一遍合并出问题当场就能发现比等到烧板子的时候再查省事得多。
返回列表