kbuild-the Linux Kernel Build System
Linux Kernel Build System
Linux的一个惊人的地方就是仅仅使用同一个代码库就可以应用在无数的计算系统,从超级计算机到嵌入式设备。它可能是目前唯一一个使用同一代码库的操作系统软件。像微软和苹果,他们针对桌面和移动平台都有各自不同的内核(windowns NT/windows CE 和OS X/IOS)。Linux能够使用统一代码库主要归因于两点,第一是丰富的抽象层和间接层,第二点是扩展性极强的编译系统。
Linux 内核是一体化架构,这就意味着内核代码运行在内核空间并且共享相同的地址空间。正因如此,你应该编译内核的时候就要确定所需要的features。严格的说,Linux也不完全是一体化架构,这是因为Linux允许在运行时插入模块来进行扩展。内核必须包含模块使用到的所有内核符号才能载入模块。如果内核在编译时不包含这些符号,那么模块会因为缺失依赖而无法载入。模块是一种推迟编译(或执行)某个内核feture的一种手段。一旦模块被载入到内核,它就成为了一体化架构内核的一部分,共享内核运行的地址空间。即使Linux支持模块插入的功能,但是你还是最好在编译内核的时候就确认好需要编译进内核的特性。
正因如此, 可以选择哪些代码编译进内核尤为重要。在Linux中使用选择性编译来实现该功能。用来选择Linux特性的配置选项非常多,他们最终会决定内核编译会包含哪些C文件,代码或者数据结构。
所以,一种简单高效的编译管理应运而生,它就是内核编译系统,即Kbuild。
Kbuild包含4个主要组成部分:
- Config symbols:配置符号就是编译选项,用来选择源码中需要编译的代码,并且确定编译进内核的对象。
- Kconfig files:用来定义Config symbols的文件,其中包含类型、描述、依赖等属性。menuconfig可以读取Kconfig生成图形化配置菜单。
- .config file:保存config symbols所选值的文件,你可以手动进行修改,或者使用menuconfig来进行修改更新。
- Makefiles:Makefile表述各个源码之间的关系, 主要用于生成目标,也就是编译内核。
Compilation Options:Configuration Symbols
配置符号决定了哪些特性哪些模块会包含在最终的内核镜像内。kbuild提供两种配置符号,分别是布尔和三态。他们的区别仅仅只是可选值的数量不同,但是这样的区别比看起来更重要。布尔符号可以配置为true和false,两者选其一。三态符号可以选择三种类型值,yes,no,或者module。
不是所有代码都能被编译成模块。许多特性非常重要且关键以至于必须在内核编译时就需要确定是否包含。比如我们无法在一个运行的内核插入多对称处理器功能或者内核抢占功能。所以我们需要用布尔配置符号来明确这些特性。大多数的特性是可以被滞后编译为模块,当然也可以在内核编译时增加。所以三态符号就是用来决定你编译的特性是内建(y),模块(m)或者都不是(n)。
除了布尔和三态,还有很多其他的配置符号,比如字符串和十六进制。但是由于他们并不是用来选择编译,所以这里就不多述。参考Linux Kernel文档了解更多。
Defining Configuration Symbols:Kconfig Files
配置符号被定义在名为Kconfig的文件中。每个Kconfig文件能描述任意多的配置选项,并且可以引用其他Kconfig文件。make menuconfig 指令读取所有的Kconfig文件,来构造图形化的内核配置菜单。在内核源码树中的每个目录都会有一个Kconfig文件,这个Kconfig文件中会包含子所有子目录下的Kconfig。内核源码树根目录的Kconfig就是配置选项的树根。menuconfig(scripts/kconfig/mconf),gconfig(scripts/kconfig/gconf)以及其他的配置编译脚本都会从根目录的Kconfig开始,递归读取所有的Kconfig文件,以此建立配置菜单。哪些目录编译会用到都定义在Kconfig中,同时也要根据用户配置选项来确定。
Storing Symbol Values:.config File
所有配置选项的值都保存在名为.config的文件中。每次想要改变内核编译配置,你可以执行make menuconfig指令。它会读取Kconfig文件来创建菜单,并且根据.config文件的定义来更新配置选项。另外这个工具也会按照你设置的新配置来更新.config文件。当然如果.config文件不存在,它也会新创建一个。
由于.config文件本身就是文本文件,你也可以直接修改它而不需要使用特殊工具。.config文件的保存和恢复也就相当简单,只需要复制粘贴即可。
Compiling the Kernel:Makefiles
Kbuild中的最后一个组件是Makefile文件。它是用来编译内核镜像和模块的。同Kconfig文件一样,每一个子目录都有一个Makefile文件,用来编译当前目录下的源文件。整个编译过程是递归的,根目录的Makefile文件会调用并执行子目录的Makefile,各个子目录的源代码都被编译成二进制对象。所有这些二进制对象被链接生成最终的内核镜像文件。
Sample: Adding Coin driver
Linux内核的kbuild系统已经基本了解,现在我们通过向内核增加一个设备驱动来举个例子。这个例子是一个简单的字符串驱动设备,名叫coin。他的功能是模拟硬币的翻转,并反馈正面(head)或者反面(tail)的其中一个值。这个驱动还提供一个可选特性,就是通过一个虚拟文件可以读取之前硬币翻转的统计结果。
以下代码展示coin设备的使用方法。
1 root@localhost:~# cat /dev/coin 2 tail 3 root@localhost:~# cat /dev/coin 4 head 5 root@sauron:/# cat /sys/kernel/debug/coin/stats 6 head=6 tail=4
向Linux内核增加特性需要三步:
1.首先需要将源码保存在正确目录下,比如将wifi设备保存到drivers/net/wireless
2.然后修改源码所在目录的Kconfig文件,添加新增特性的配置选项
3.最后修改源码所在目录的Makefile文件,添加需要编译的目标对象
coin驱动是字符设备驱动,所以应把源码coin.c保存到drivers/char
下一步是编写用户进行coin驱动的编译配置选项。我们需要在driver/char/Kconfig文件添加2个配置符号。一个符号用来选择是否将驱动加入到内核,另一个符号决定是否提供统计功能。
和大多数驱动一样,coin驱动可以直接编译进内核,或者编译成一个模块,或者什么都不是。所以第一个配置符号名为COIN,是一个三态符号(y/n/m)。第二个符号名为COIN_STAT用来决定是否提供统计信息,显然这是个二值决定,所以就是布尔符号(y/n)。如果你并没有选择coin驱动,也就是COIN为n,那么COIN_STAT的配置就没有任何意义。这是内核中常见的概念,比如,如果你没有先使能块设备功能,那么基于块设备的文件系统(ext3 或者 fat32)就无法添加到内核。显然,在配置符号之间存在依赖关系。在Kconfig文件中,我们可以使用depends关键词来实现配置符号之间的依赖关系。当我们使用make menuconfig构建编译配置菜单时,那些依赖没有满足的配置项都会被隐藏起来。depends是Kconfig文件中众多关键词的其中之一。想要完成的Kconfig语法介绍,可以参考内核源码树中的kbuild/kconfig-languate.txt
以下代码片段展示drivers/char/Kconfig文件中添加的的coin驱动部分
# # Character device configuration # menu "Character devices" config COIN tristate "Coin char device support" help Say Y here if you want to add support for the coin char device. If unsure, say N. To compile this driver as a module, choose M here: the module will called coin. config COIN_STAT bool "flipping statistics" depends on COIN help Say Y here if you want to enable statistics about the coin char device.
我们要如何查看刚刚添加的配置选项呢?
正如之前所提到的,使用make menuconfig来构建一个图形化的配置菜单,然后我们就可以选择哪些编译进内核,哪些编译成模块。
命令行工具scripts/kconfig/mconf 执行会读取所有Kconfig文件来构造图形化配置菜单接口。我们通过这个工具来更新COIN 和COIN_STAT的编译选项。下图展示了图形化的配置菜单界面。
一旦你结束了编译配置并准备退出菜单,如果你修改了不同的配置,那么配置工具会询问是否要保存新的配置。保存的话就会把配置选项更新并保存到.config文件中。针对每个配置项,固定的前缀CONFIG_被添加。比如,一个布尔配置项如果被选择上,那么在.config文件中会被保存如下
CONFIG_COIN_STA==y
如果不选择上,那么保存如下
# CONFIG_COIN_STAT is not set
三态的配置项yes或者no的保存结果和布尔的一样,但是,请记住三态的另外一种配置是将特性编译为模块。举个例子,如果你选择将COIN驱动配置为模块,在.config文件中的结果如下
CONFIG_COIN=m
下面的片段展示coin驱动的配置情况之一,coin被编译为模块,使能翻转统计功能
CONFIG_COIN=m CONFIG_COIN_STAT=y
如果我们想直接编译进内核并且屏蔽翻转统计功能,我们会看到如下配置
CONFIG_COIN=y #CONFIG_COIN_STAT is not set
一旦我们得到配置好的.config文件,我们就可以开始编译了。当我们执行编译目标进行内核编译时,首先kbuild会执行一个二进制软件读取所有Kconfig文件和.config文件,如下所示
scripts/kconfig/conf Kconfig
这个二进制软件会更新(或者创建)一个C头文件,这个头文件包含我们之前设置的所有配置项和它的值。这个头文件保存在include/generated/autoconf.h。每个GCC编译指令都包含了这个头文件,所以在内核源码中可以引用这些配置符号。
这个头文件是由几千个#define的宏定义组成,这些宏定义描述每一个配置选项和对应的值。我们来看看具体如何定义的。
为TRUE的布尔类型配置和为Yes的三态类型配置是一样的,举个例子,布尔类型的CONFIG_COIN_STAT为TRUE和三态类型的CONFIG_COIN为Yes的时候,头文件的结果如下
#define CONFIG_COIN_STAT 1 #define CONFIG_COIN 1
为FALSE的布尔类型配置和为No的三态类型配置是一样的,举个例子
#define CONFIG_COIN_STAT 0 #define CONFIG_COIN 0
对于三态类型,如果COIN被配置为模块,则在头文件中保存如下
#define CONFIG_COIN_MODULE 1
第三也是最后一步需要更新对应源码所在目录的Makefile文件,然后kbuild就能编译你选择的驱动。
但是,我们要如何命令kbuild选择性编译我们的源码呢?
内核编译系统包含两个主要任务:创建内核二进制镜像和内核模块。为了实现这两个主要任务,makefile需要维护两个列表对象:obj-y和obj-m。前一个列表对象是用来内建内核镜像,后者是用来编译为模块。
.config文件中的配置符号和autoconf.h头文件中的宏定义被用来扩展需要编译的对象列表。Kbuild会递归进入每一个目录,然后编译各个目录下makefile所列举的对象。更多makefile的用法和信息,请参考Document/kbuild/makefiles.txt.
对于coin驱动,我们只需要在drivers/char/Makefile中添加如下语句。
obj-$(CONFIG_COIN) += coin.o
这条语句会告诉kbuild去创建一个coin.c源码的编译对象,并且把它加入到对象列表。coin.o对象会被增加到obj-y还是obj-m还是要取决于CONFIG_COIN的值。如果CONFIG_COIN配置符号没有定义,那么coin.o根本就不会被编译。
好了, 现在我们知道如何选择性编译源文件了。接下来就要知道如何选择性编译代码段。这其实很简单,只需要使用autoconf.h头文件中的宏定义即可。
下面代码展示完整coin字符设备驱动
1 #include <linux/kernel.h> 2 #include <linux/module.h> 3 #include <linux/fs.h> 4 #include <linux/uaccess.h> 5 #include <linux/device.h> 6 #include <linux/random.h> 7 #include <linux/debugfs.h> 8 9 #define DEVNAME "coin" 10 #define LEN 20 11 enum values {HEAD, TAIL}; 12 13 struct dentry *dir, *file; 14 int file_value; 15 int stats[2] = {0, 0}; 16 char *msg[2] = {"head\n", "tail\n"}; 17 18 static int major; 19 static struct class *class_coin; 20 static struct device *dev_coin; 21 22 static ssize_t r_coin(struct file *f, char __user *b, 23 size_t cnt, loff_t *lf) 24 { 25 char *ret; 26 u32 value = random32() % 2; 27 ret = msg[value]; 28 stats[value]++; 29 return simple_read_from_buffer(b, cnt, 30 lf, ret, 31 strlen(ret)); 32 } 33 34 static struct file_operations fops = { .read = r_coin }; 35 36 #ifdef CONFIG_COIN_STAT 37 static ssize_t r_stat(struct file *f, char __user *b, 38 size_t cnt, loff_t *lf) 39 { 40 char buf[LEN]; 41 snprintf(buf, LEN, "head=%d tail=%d\n", 42 stats[HEAD], stats[TAIL]); 43 return simple_read_from_buffer(b, cnt, 44 lf, buf, 45 strlen(buf)); 46 } 47 48 static struct file_operations fstat = { .read = r_stat }; 49 #endif 50 51 int init_module(void) 52 { 53 void *ptr_err; 54 major = register_chrdev(0, DEVNAME, &fops); 55 if (major < 0) 56 return major; 57 58 class_coin = class_create(THIS_MODULE, 59 DEVNAME); 60 if (IS_ERR(class_coin)) { 61 ptr_err = class_coin; 62 goto err_class; 63 } 64 65 dev_coin = device_create(class_coin, NULL, 66 MKDEV(major, 0), 67 NULL, DEVNAME); 68 if (IS_ERR(dev_coin)) 69 goto err_dev; 70 71 #ifdef CONFIG_COIN_STAT 72 dir = debugfs_create_dir("coin", NULL); 73 file = debugfs_create_file("stats", 0644, 74 dir, &file_value, 75 &fstat); 76 #endif 77 78 return 0; 79 err_dev: 80 ptr_err = class_coin; 81 class_destroy(class_coin); 82 err_class: 83 unregister_chrdev(major, DEVNAME); 84 return PTR_ERR(ptr_err); 85 } 86 87 void cleanup_module(void) 88 { 89 #ifdef CONFIG_COIN_STAT 90 debugfs_remove(file); 91 debugfs_remove(dir); 92 #endif 93 94 device_destroy(class_coin, MKDEV(major, 0)); 95 class_destroy(class_coin); 96 return unregister_chrdev(major, DEVNAME); 97 }
Conclusion
Linux, despite being a monolithic kernel, is highly modular and customizable. You can use the same kernel in a varied range of devices from high-performance clusters to desktops all the way to mobile phones. This makes the kernel a very big and complex piece of software. But, even when the kernel has millions of lines of code, its build system allows you to extend it with new features easily. In the past, to have access to an operating system's source code, you had to work for a big company and sign large NDA agreements. Nowadays, the source of probably the most modern operating system is publicly available. You can use it, study its internals and modify it in any creative way you want. The best part is that you even can share your work and get feedback from an active community. Happy hacking!
Articles by Javier Martinez Canillas
Translated by Joey