当前位置: 首页 > 科技观察

探索Linux内核:Kconfig-kbuild的秘密

时间:2023-03-17 19:26:48 科技观察

深入了解Linux配置/构建系统的工作原理。自从Linux内核代码迁移到Git以来,Linux内核配置/构建系统(也称为Kconfig/kbuild)已经存在了很长时间。然而,作为配套基础设施,它很少受到关注;即使是在日常工作中使用它的内核开发人员也从未真正考虑过它。为了探索Linux内核是如何编译的,本文深入了解了Kconfig/kbuild的内部结构,解释了.config文件和vmlinux/bzImage文件是如何生成的,并介绍了一个简洁的依赖项跟踪技巧。Kconfig构建内核的第一步始终是配置。Kconfig有助于使Linux内核高度模块化和可定制。Kconfig为用户提供了一些配置对象:配置对象解释了config使用命令行程序nconfig更新当前配置使用基于菜单的ncurses程序menuconfig更新当前配置使用基于菜单的程序xconfig更新当前配置使用基于Qt的前端程序gconfig更新当前配置利用基于GTK+的前端oldconfig根据提供的.config更新当前配置localmodconfig更新当前配置,禁用未加载的模块localyesconfig更新当前配置,使用defconcig默认值将本地模块转换为核心defconfig从体系结构提供的新配置savedefconfig将当前配置保存为./defconfig(最小配置)allnoconfig新配置对所有选项回答否allyesconfig新配置对所有选项回答是configurealldefconfignewconfigwithallsymbols(options)settodefaultvaluesrandconfignewconfigwithalloptionsrandomlyselectedlistnewconfiglistnewoptionsolddefconfigsameasoldconfig但是设置新的symbols(options)为默认值而不询问kvmconfigenablesupportforKVM来宾内核模块的附加选项xenconfig启用具有xen支持的dom0来宾内核模块的附加选项tinyconfig配置尽可能小的内核我认为menuconfig是这些目标中最受欢迎的这些目标由不同的主程序主机程序处理提供者内核并在内核构建期间构建。一些目标有一个GUI(为了用户方便),大多数没有。Kconfig相关的工具和源代码主要位于scripts/kconfig/下的内核源代码中。从scripts/kconfig/Makefile可以看到,主要有几个程序,包括conf、mconf、nconf。除了conf,每个都负责基于GUI的配置目标,因此conf处理大多数目标。从逻辑上讲,Kconfig的基础结构有两部分:一部分实现了一种新的语言来定义配置项(参见内核源代码下的Kconfig文件),另一部分解析Kconfig语言并处理配置操作。大多数配置目标都有大致相同的内部流程(如下所示):注意所有配置项都有默认值。第一步读取源码根目录下的Kconfig文件,构建初始配置数据库;然后它根据以下优先级读取现有配置文件以更新初始数据库:.config/lib/modules/$(shell,uname-r)/.config/etc/kernel-config/boot/config-$(shell,uname-r)ARCH_DEFCONFIGarch/$(ARCH)/defconfig如果您通过menuconfig进行基于GUI的配置或通过oldconfig进行基于命令行的配置,则根据您的自定义更新数据库。最后,配置数据库被转储到一个.config文件中。但是.config文件不是内核构建的最终素材;这就是syncconfig目标存在的原因。syncconfig曾经是一个名为silentoldconfig的配置目标,但它并没有像它的旧名称那样做,所以它被重命名了。此外,由于它供内部使用(不供用户使用),因此已从上面的列表中删除。以下是syncconfig的作用:syncconfig将.config作为输入并输出许多其他文件,这些文件分为三类:auto.conf和tristate.conf用于makefile文本处理。例如,你可以在组件的makefile中看到这样的语句:obj-$(CONFIG_GENERIC_CALIBRATE_DELAY)+=calibrate.o。autoconf.hC语言的源文件。include/config/下的空头文件用于kbuild期间的配置依赖性跟踪。下面将进行说明。配置完成后,我们就会知道哪些文件和代码片段没有被编译。kbuild组件构建,称为递归make,是GNUmake管理大型项目的常用方式。kbuild是递归make的一个很好的例子。通过将源文件分成不同的模块/组件,每个组件都由自己的makefile管理。当您开始构建时,顶层makefile以正确的顺序调用每个组件的makefile,构建组件,并将它们收集到最终的可执行文件中。kbuild指向不同类型的makefile:Makefile位于源代码根目录中的顶级makefile。.config是内核配置文件。arch/$(ARCH)/Makefile是arch的makefile,用来补充顶层makefile。scripts/Makefile.*描述了所有kbuildmakefile的通用规则。最后,大约有500个kbuildmakefile。顶级makefile将包含体系结构makefile,读取.config文件,进入子目录,并在scripts/Makefile.*中定义的例程的帮助下对每个组件的makefile调用make以构建每个中间对象,并链接所有中间对象作为vmlinux。内核文档Documentation/kbuild/makefiles.txt描述了这些makefile的所有方面。举个例子,看看如何在x86-64上生成vmlinux:vmlinuxoverview(此图来源于RichardY.Steven的博客,经作者许可更新使用。)所有.o进入vmlinux的文件先进入自己的built-in.a,由变量KBUILD_VMLINUX_INIT、KBUILD_VMLINUX_MAIN、KBUILD_VMLINUX_LIBS表示,然后收集到vmlinux文件中。借助这个简化的makefile代码了解如何在Linux内核中实现递归make:-deps:=$(KBUILD_LDS)$(KBUILD_VMLINUX_INIT)$(KBUILD_VMLINUX_MAIN)$(KBUILD_VMLINUX_LIBS)exportKBUILD_VMLINUX_INIT:=$(head-y)$(init-y)exportKBUILD_VMLINUX_MAIN-y)$(corelibs-y2)$(驱动程序-y)$(net-y)$(virt-y)exportKBUILD_VMLINUX_LIBS:=$(libs-y1)exportKBUILD_LDS:=arch/$(SRCARCH)/kernel/vmlinux.ldsinit-y:=init/drivers-y:=drivers/sound/firmware/net-y:=net/libs-y:=lib/core-y:=usr/virt-y:=virt/#转换为对应的内置.ainit-y:=$(patsubst%/,%/built-in.a,$(init-y))core-y:=$(patsubst%/,%/built-in.a,$(core-y))drivers-y:=$(patsubst%/,%/built-in.a,$(drivers-y))net-y:=$(patsubst%/,%/built-in.a,$(net-y))libs-y1:=$(patsubst%/,%/lib.a,$(libs-y))libs-y2:=$(patsubst%/,%/built-in.a,$(filter-out%.a,$(libs-y)))virt-y:=$(patsubst%/,%/built-in.a,$(virt-y))#设置依赖。vmlinux-deps都是中间对象,vmlinux-dirs#是伪目标,所以每次遇到这个规则,vmlinux-dirs#的配方都会被执行。参考`infomake`的"4.6PhonyTargets"$(sort$(vmlinux-deps)):$(vmlinux-dirs);#变量vmlinux-dirs是每个built-in.avmlinux-dirs的目录部分:=$(patsubst%/,%,$(filter%/,$(init-y)$(init-m)\$(core-y)$(core-m)$(drivers-y)$(drivers-m)\$(net-y)$(net-m)$(libs-y)$(libs-m)$(virt-y)))#递归make的入口$(vmlinux-dirs):$(Q)$(MAKE)$(build)=$@need-builtin=1递归make的配方扩展如下:make-fscripts/Makefile.buildobj=initneed-builtin=1这意味着make将进入scripts/Makefile.build在scripts/link-vmlinux.sh的帮助下,继续构建每个built-in.avmlinux文件最终位于源根目录中。vmlinux与bzImage许多Linux内核开发人员可能不知道vmlinux和bzImage之间的关系。例如,它们在x86-64中的关系如下:源根目录中的vmlinux被剥离、压缩,放入piggy.S,然后与其他对等节点链接到arch/x86/boot/compressed/vmlinux。同时在arch/x86/boot下生成一个名为setup.bin的文件。根据CONFIG_X86_NEED_RELOCS的配置,可能存在带有重定位信息的可选第三个文件。内核提供的称为build的主机程序将这两个(或三个)部分构建到最终的bzImage文件中。依赖跟踪kbuild跟踪三种依赖关系:所有先决条件文件(*.c和*.h)所有先决条件文件中使用的CONFIG_选项来编译目标的命令行依赖关系第一种很容易理解,但是第一个呢?第二和第三?内核开发人员经常看到这样的代码:#ifdefCONFIG_SMP__boot_cpu_id=cpu;#endif当CONFIG_SMP改变时,这段代码应该重新编译。用于编译源文件的命令行也很重要,因为不同的命令行可能会产生不同的目标文件。当.c文件通过#include指令使用头文件时,您需要编写这样的规则:main.o:defs.hrecipe...在管理大型项目时,您需要大量这样的规则;把它们都写下来会很乏味和乏味。幸运的是,大多数现代C编译器都可以通过查看源文件中的#include行来为您编写这些规则。对于GNUCompilerCollection(GCC),只需添加一个命令行标志:-MDdepfile#Inscripts/Makefile.libc_flags=-Wp,-MD,$(depfile)$(NOSTDINC_FLAGS)$(LINUXINCLUDE)\-include$(srctree)/include/linux/compiler_types.h\$(__c_flags)$(modkern_cflags)\$(basename_flags)$(modname_flags)这将生成一个包含以下内容的.d文件:init_task.o:init/init_task.cinclude/linux/kconfig.h\include/generated/autoconf.hinclude/linux/init_task.h\include/linux/rcupdate.hinclude/linux/types.h\...然后主程序fixdep传递depfile文件和命令行作为输入来处理另外两个依赖,并输出一个makefile格式的..cmd文件,记录命令行和目标的所有先决条件(包括配置)。看起来是这样的:#Thecommandlineusedtocompilethetargetcmd_init/init_task.o:=gcc-Wp,-MD,init/.init_task.o.d-nostdinc......#Thedependencyfilesdeps_init/init_task.o:=\$(通配符include/config/posix/timers.h)\$(通配符include/config/arch/task/struct/on/stack.h)\$(通配符include/config/thread/info/in/task.h)\...include/uapi/linux/types.h\arch/x86/include/uapi/asm/types.h\include/uapi/asm-generic/types.h\...在递归make,将包含..cmd文件以提供任何依赖信息并帮助决定是否重建目标。这背后的秘密在于,fixdep会解析depfile(.d文件),然后解析里面的所有依赖文件,搜索所有CONFIG_字符串的文本,将其转换为对应的空头文件,添加到目标的先决条件中。每次配置更改时,相应的空头文件也会更新,因此kbuild可以检测到更改并重建依赖于它的目标。因为命令行也被记录下来,所以很容易比较上次和当前的编译参数。展望未来在2017年初新的维护者MasahiroYamada加入之前,Kconfig/kbuild很长一段时间都没有变化,现在kbuild再次处于积极开发中。如果您很快看到与本文不同的内容,请不要感到惊讶。