kbuild:the linux kernel build system

来源:互联网 发布:7天不归还淘宝极速退款 编辑:程序博客网 时间:2024/06/05 18:00

翻译仅供学习交流使用,版权归原作者所有,原文链接:http://www.linuxjournal.com/content/kbuild-linux-kernel-build-system

linux最为让人惊叹的事情之一就是它的同一套代码在从巨型计算机到嵌入式设备各种不同硬件系统上的适用性,稍微花费一点时间思考一下的话,linux应该是唯一的拥有统一代码的一个操作系统。例如微软和苹果都是为桌面系统和移动设备使用不同的操作系统内核。有如下两个原因使得linux能做到统一:一是linux内核中做了很多非常好的抽象处理,二是linux的内核编译系统能根据需要编译出高度定制的二进制文件。

linux内核是一个整体式设计的架构,所有的内核代码运行在同一个可以共享的地址空间中。证因为得益于这样的设计,所以你可以在编译时就能选择哪些需要的功能。但是从技术上来说linux内核并不是一个真正的整体式的设计,因为它可以在运行时通过加载模块进行功能扩展。被加载的模块必须在编译时将符号包含进内核中,否则会因为找不到依赖关系而加载失败。模块的作用是可以对指定的功能进行编译或者运行的延缓执行。一旦内核被加载之后,只是所有共享相同地址空间的代码在编译之后的部分功能被加载了。虽然linux内核支持模块扩展功能,但是你必须要在编译时对大部分特性进行配置,将其编译到内核映像中,这样才能支持在内核运行时动态加载其他功能模块。

基于此,在编译linux内核时选择哪些代码进行编译就显得尤为重要。linux内核采用条件编译的方式来实现。linux内核代码中有大量的配置选项来保证哪些功能被包含,这些配置选项能指定到具体的C文件、代码片段或者数据结构。所以需要有一个方便有效的方式来管理所有的这些编译配置选项。Kernel build system正是这样的系统。

这里不在详细介绍kbuid系统的细节内容,他们都能从linux内核的说明文档行中找到。相反的,我们在这里探讨一下怎样使用kbuild来构建你自己在linux内核代码中增加的类似设备驱动程序之类的代码。

kbuild系统由四个主要部分组成:
config symbols:决定哪些代码或者源文件能被编译进内核的编译选项

kconfig files:编译选项配置文件,编译选择菜单基于这些配置文件生成

.config files:存储编译选项的具体值,可以手动修改这些文件中存储的值,也可以通过make 选择菜单自动生成

Makefiles:普通的GNU mekefiles,执行命令,编译源文件以及生成内核镜像文件和模块文件

下面详细分析这些组成部分。

Compilation Options:Configuration Symbols

Configuration Symbols 决定了哪些特性能被编译进内核中,包括了两种类型的symbols,布尔值和三态值。这两种类型所代表的意义不仅仅是在于所能表示的数值不同而已。布尔值表示两种可能真或者假,三态值能表示的值包括:支持、不支持或者模块方式。
linux内核中并不是所有的功能都能支持以模块的形式进行编译,许多特性都是必须要在内核编译时决定内核是否要支持的。例如不能在运行时增加SMP或者抢占式的内核功能,所以这些功能需要用布尔值在编译时就确定好。大部分特性都是支持模块方式或者是编译进内核的,所以这些特性可以用三态值进行选择,编译进内核、不编进内核或者独立模块的方式。当然除了这两种symbol类型之外还有strings和hex两种类型,不过它们不是用来进行编译条件选择的,这里就不在细说。linux内核文档中可以找到关于config symbols、types和使用方法的完整说明。

Defining Configuration Symbols:Kconfig Files
Configuration Symbols的值通过Kconfig files来定义。每个Kconfig file 能定任意数量的symbols,而且能包含其他的kconfig files。编译系统累世make menuconfig读取这些文件生成树形菜单选项。内核代码中的每个目录都有一个kconfig并且包含子目录下的kconfig files,内核代码最顶层的kconfig是所有kconfig files的起点,编译脚本都是从最顶层的kconfig file开始访问,并根据每个目录下的kconfig file中的包含关系一次遍历各子目录的kconfig files。

Storing Symbol Values:.config file
所有的config symbol的值都是存储在.config file中的,每次通过make生成的菜单选项选择的值都会被存储到.config file中去,既可以改变已有的选项值,也可以新增之前没有的选项。因为.config file是一纯文本文件方式存储的,所以也可以通过手动方式自行修改其中的选项值,手动方式可以非常方便的修改或者恢复之前的内核编译选项。

Compiling the kernel :Makefiles
最后要说的就是编译脚本makefile了,这些脚本文件时用来编译内核镜像和模块文件的根本,和kconfig files一样,每个目录下都有一个makefile,该makefile只能编译本目录下的文件,整个编译过程通过编译调用每个子文件夹实现所有源文件的编译以及内核镜像和模块文件的生成。

练习:增加coin driver
现在我们已经清楚了kbuild系统的一些基本知识,来做一下练习,在内核中增加一个设备驱动。改设备驱动是一个实现了模拟掷硬币结果的简单字符驱动设备,除了得到head或者tail的结果之外,还通过debugfs虚拟文件系统统计了之前出现过的结果。下面列出了和coin device 交互的结果显示:
root@localhost:~#cat /dev/coin
tail
root@localhost:~#cat /dev/coin
head
root@sauron:/# cat /sys/kernel/debug/coin/stats
head=6 tail=4

在linux内核中增加一个设备驱动程序,需要如下三个步骤:
1.在合理的路径下增加源文件,例如在drivers/net/wireless下面增加wifi驱动,在fs下面增加新文件系统驱动;
2.修改增加了源文件目录下的kconfig文件,增加新的config symbols;
3.修改新源文件目录下的makefile文件,使得新增加的代码能被编译到;

因为coin 设备驱动是一个字符设备驱动,所以将coin.c源文件防盗drivers/char目录下,然后在该目录下的kconfig文件中增加一个是否增加coin设备驱动到内核的选项,和统计功能是否生效的选项。和大多数其他驱动一样,coin设备驱动可以被编译进内核,或者编译成独立的模块,或者根本就不支持。所以增加的第一个编译选项COIN以三态值形式定义,y/n/m,第二个编译选项COIN_STAT用来决定是否显示coin设备的统计结果,所以用布尔值定义即可,y/n。当然如果COIN定义成n时,表示内核不支持coin设备时,统计结果显示是没有意义的,这种情况在内核中普遍存在,例如,如果不支持块设备时,增加对ext3,ext2或者fat32这样的块设备文件系统是没有意义的。显然,各种功能之间存在一定的依赖关系,我们必须遵循这样的结构模型。这样的约束可以通过在kconfig文件中定义config symbols之间的关系实现,当定义了好了依赖关系之后,通过make menuconfig生成的选择菜单时,会自动隐藏不满足依赖关系的选项。关于依赖关系的描述关键字只是kconfig文件的特性之一,更完整的描述在内核文档的kbuild/kconfig-language.txt中有描述。

kconfig entries for the coin driver

#
#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, sar N.
                  To compile this driver as a module, choose M here:
                  the module will be called coin.
config COIN_STAT
              bool "
flipping statistics'
              depends on COIN
             help 
              say Y here is you want to enable statistics about the coin char device.

那么接下来如何使用这些新增加的config symbols呢,kbuild系统会通过读取kconfig文件生成可以供选择的编译选项,例如当执行了make menuconfig之后,scripts/kconfig/mconf脚本会读取所有的kconfig文件并且生成一个菜单界面,然后就能通过菜单界面定义COIN和CONI_STAT的值了,图一显示了生成的COIN菜单界面。
图一 编译选项菜单界面


当通过菜单界面完成了编译选项值得定义之后,会将所有的值保存到.config文件中,对于每一个编译选项都会在.config文件中存储,并增加一个CONFIG的前缀,例如COIN_STAT的存储形式是
CONFIG_COIN_STAT=y

如果没有为某个编译选项赋值,那么在.config文件中记录如下:
#CONFIG_COIN_STAT is not set

以三态值定义的COIN也类似,只不过定义成独立模块时的值是这样的:
CONFIG_COIN=m

当在.config文件中定义成如下形式时:
CONFIG_COIN=m
CONFIG_COIN_STAT=y
就是告诉了kbuild系统,我们需要将coin设备编译成独立的模块,并且启用统计功能,如果想将coin设备编译进内核中,并且不启用统计功能的话,需要定义成如下形式:
CONFIG_COIN=y
#CONFIG_COIN_STAT is not set
当完成了.config文件的定义之后,就能构建内核和驱动模块了,当执行编译时,首先调用的是scripts/kconfig/conf Kconfig,这个二进制文件会创建或者更新一个定义了symbols的C头文件,该文件存储在include/generated/autoconf.h,gcc编译系统会自动包含这个头文件,所以这些symbols就能在其他内核代码中被使用了。该头文件的内容是由大量的宏定义组成,下面来了解一下这个宏定义的规则。
布尔值的ture和三态值的y被定义成相同的值。这两种类型都会定义三种类型的宏。例如当CONFIG_COIN_STAT定义为true,CONFIG_COIN定义成yes时,会生成如下的宏定义:
#define __enabled_CONFIG_COIN_STAT 1
#define __enabled_ CONFIG_COIN_STAT_MODULE 0
#define CONFIG_COIN_STAT 1

#define __enabled_CONFIG_COIN 1
#define __enabled_CONFIG_COIN_MODULE 0
#define CONFIG_COIN 1

CONFIG_COIN_STAT 定义成false,CONFIG_COIN定义成no时生成的宏定义如下:
#define __enabled_CONFIG_COIN_STAT 0
#define __enabled_CONFIG_COIN_STAT_MODULE 0

#define __enabled_CONFIG_COIN 0
#define __enabled_CONFIG_COIN_MODULE 0

    CONFIG_COIN定义成module时,生成的宏定义如下:
#define __enabled_CONFIG_COIN 0
#define __enabled_CONFIG_COIN_MODULE 1
#define CONFIG_COIN_MODULE 1

好奇的读者或许会问为什么要定义这些带__enabled前缀的宏?如果直接定义CONFIG_option宏不是更加有效吗?而且为什么连布尔类型的值也还要定义带_MODULE后缀的宏呢?
这些修饰前缀或者后缀会被如下三个宏使用:
#define IS_ENABLED(option) (__enabled_##option || __enabled_##option##_MODULE)
#define IS_BUILTIN(option) __enabled_##option
#define IS_MODULE(option) __enabled_##option##MODULE
所以这些带前缀或者后缀修饰符的宏必须要被定义。

最后就是修改源代码所在目录的makefile了,使得kbuild系统能够编译到新增加的代码。但是如何才能指示kbuild系统完成编译呢?
kbuild系统完成两个主要的功能,创建内核镜像和独立模块文件。obj-y列表定义了所有需要被编译进内核中的对象,obj-m列表定义了所有需要编译成独立模块的对象。通过GNU make的语法扩展使用.config文件和autoconf.h中的符号和宏定义来扩展这些列表,kbuild会遍历所有子文件夹中的makefile,读取makefile中定义的对象。更多关于GNU make的语法扩展和对象列表相关信息,参见内核文档kbuild/makefiles.txt。
对于coin设备,只需要在drivers/char/Makefile中增加如下一行语句即可:
obj-$(CONFIG_COIN) +=coin.o
kbuild会根据这行语句自动寻找coin.c源文件进行编译,并将之加入到对应的对象列表中,根据CONFIG_COIN的值,coin.o将会被加入到obj-y或者obj-m中去,而如果CONFIG_COIN没有定义的话,那么coin.o则不会被编译。
coin.c的源代码:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/random.h>
#include <linux/debugfs.h>

#define DEVNAME "coin"
#define LEN  20
enum values {HEAD, TAIL};

struct dentry *dir, *file;
int file_value;
int stats[2= {00};
char *msg[2= {"head\n""tail\n"};

static int major;
static struct class *class_coin;
static struct device *dev_coin;

static ssize_t r_coin(struct file *f, char __user *b,
                      size_t cnt, loff_t *lf)
{
        char *ret;
        u32 value = random32() % 2;
        ret = msg[value];
        stats[value]++;
        return simple_read_from_buffer(b, cnt,
                                       lf, ret,
                                       strlen(ret));
}

static struct file_operations fops = { .read = r_coin };

#ifdef CONFIG_COIN_STAT
static ssize_t r_stat(struct file *f, char __user *b,
                         size_t cnt, loff_t *lf)
{
        char buf[LEN];
        snprintf(buf, LEN, "head=%d tail=%d\n",
                 stats[HEAD], stats[TAIL]);
        return simple_read_from_buffer(b, cnt,
                                       lf, buf,
                                       strlen(buf));
}

static struct file_operations fstat = { .read = r_stat };
#endif

int init_module(void)
{
        void *ptr_err;
        major = register_chrdev(0, DEVNAME, &fops);
        if (major < 0)
                return major;

        class_coin = class_create(THIS_MODULE,
                                  DEVNAME);
        if (IS_ERR(class_coin)) {
                ptr_err = class_coin;
                goto err_class;
        }

        dev_coin = device_create(class_coin, NULL,
                                 MKDEV(major, 0),
                                 NULL, DEVNAME);
        if (IS_ERR(dev_coin))
                goto err_dev;

#ifdef CONFIG_COIN_STAT
        dir = debugfs_create_dir("coin", NULL);
        file = debugfs_create_file("stats"0644,
                                   dir, &file_value,
                                   &fstat);
#endif

        return 0;
err_dev:
        ptr_err = class_coin;
        class_destroy(class_coin);
err_class:
        unregister_chrdev(major, DEVNAME);
        return PTR_ERR(ptr_err);
}

void cleanup_module(void)
{
#ifdef CONFIG_COIN_STAT
        debugfs_remove(file);
        debugfs_remove(dir);
#endif

        device_destroy(class_coin, MKDEV(major, 0));
        class_destroy(class_coin);
        return unregister_chrdev(major, DEVNAME);
}

图2kbuild系统的编译处理过程:


尾声
linux内核,作为一个整体性设计非常好的系统,同时也具备了高度的模块化和可定制性。我们能将同一份内核运用到从高性能的大型计算机到移动设备的各种设备上,这样就使得该系统成为了一个非常复杂的大型软件系统。但是,虽然内核中包含了上百万行的代码,它的编译构架系统非常方便使用,我们能方便的进行扩展。在旧时代,为了得到一份操作系统的代码,我们必须要到一些大公司工作才有可能,而且还得签署一大堆的保密协议。现在,可能是最现代话得操作系统源代码已经得到公开,我们能方便的,学习,修改和使用它。最棒的是,你能通过开发者论坛共享你的成果和得到大家的反馈。Happy hacking!


0 0
原创粉丝点击