Kbuild:Linux 内核构建系统 | Linux 杂志
Linux 的一个惊人之处在于, 相同的代码库可用于不同范围的计算系统, 从超级计算机到非常小的嵌入式设备. 如果您停下来想一想, Linux 可能是唯一拥有统一代码库的操作系统. 例如, Microsoft 和 Apple 为其桌面和移动操作系统版本 (Windows NT/Windows CE 和 OS X/iOS) 使用不同的内核. 这在 Linux 上成为可能的两个原因是内核具有许多抽象层和间接级别, 并且因为其构建系统允许创建高度定制的内核二进制映像.
Linux 内核采用单体架构, 即整个内核代码运行在内核空间, 共享同一个地址空间. 由于这种体系结构, 您必须选择内核在编译时将包含的功能. 从技术上讲, Linux 不是一个纯粹的单片内核, 因为它可以在运行时使用可加载的内核模块进行扩展. 要加载模块, 内核必须包含模块中使用的所有内核符号. 如果这些符号在编译时未包含在内核中, 则由于缺少依赖项, 模块将不会被加载. 模块只是延迟编译 (或执行) 特定内核功能的一种方式. 一旦内核模块被加载, 它就是整体内核的一部分, 并与内核编译时包含的代码共享相同的地址空间. 即使 Linux 支持模块,
因此, 能够选择要在 Linux 内核中编译 (或不编译) 哪些代码非常重要. 实现这一目标的方法是使用条件编译. 有大量配置选项可用于选择是否包含特定功能. 这转化为决定特定的 C 文件, 代码段或数据结构是否将包含在内核映像及其模块中.
因此, 需要一种简单有效的方法来管理所有这些编译选项. 管理它的基础设施——构建内核映像及其模块——被称为内核构建系统 (kbuild).
我在这里不对 kbuild 基础设施做太多详细的解释, 因为 Linux 内核文档提供了很好的解释 (Documentation/kbuild). 相反, 我讨论了 kbuild 基础知识并展示了如何使用它在 Linux 内核树中包含您自己的代码, 例如设备驱动程序.
Linux 内核构建系统有四个主要组件:
- 配置符号: 编译选项, 可用于有条件地编译源文件中的代码, 并决定将哪些对象包含在内核映像或其模块中.
- Kconfig 文件: 定义每个配置符号及其属性, 例如其类型, 描述和依赖项. 生成选项菜单树的程序 (例如,
make menuconfig
) 从这些文件中读取菜单条目. - .config 文件: 存储每个配置符号的选定值. 您可以手动编辑此文件或使用许多
make
配置目标之一, 例如 menuconfig 和 xconfig, 它们调用专门的程序来构建树状菜单并自动为您更新 (和创建).config 文件. - Makefiles: 普通的 GNU makefiles, 描述了源文件和生成每个 make 目标所需的命令之间的关系, 例如内核映像和模块.
现在, 让我们更详细地了解这些组件中的每一个.
编译选项: 配置符号
配置符号用于决定哪些功能将包含在最终的 Linux 内核映像中. 两种符号用于条件编译:布尔和三态. 它们的区别仅在于每个值可以采用的值的数量. 但是, 这种差异比看起来更重要. 布尔符号 (毫不奇怪) 可以采用两个值之一:true 或 false. 另一方面, 三态符号可以采用三个不同的值: 是, 否或模块.
并非内核中的所有内容都可以编译为模块. 许多特性是如此具有侵入性, 以至于您必须在编译时决定内核是否支持它们. 例如, 您不能向正在运行的内核添加对称多处理 (SMP) 或内核抢占支持. 因此, 使用布尔配置符号对这些类型的功能很有意义. 大多数可以编译为模块的功能也可以在编译时添加到内核中. 这就是存在三态符号的原因——决定您是否要编译内置功能 (y), 作为模块 (m) 或根本不编译 (n).
除了这两个符号之外, 还有其他配置符号类型, 例如字符串和十六进制. 但是, 因为它们不用于条件编译, 所以我不在这里介绍它们. 阅读 Linux 内核文档以获得配置符号, 类型和用途的完整讨论.
定义配置符号:Kconfig 文件
配置符号在称为 Kconfig 文件的文件中定义. 每个 Kconfig 文件都可以描述任意数量的符号, 还可以包含 (源) 其他 Kconfig 文件. 构建内核编译选项配置菜单的编译目标, 例如 make menuconfig, 读取这些文件来构建树状结构. 内核中的每个目录都有一个 Kconfig, 其中包括其子目录的 Kconfig 文件. 在内核源代码目录的顶部, 有一个 Kconfig 文件, 它是选项树的根目录.menuconfig (scripts/kconfig/mconf), gconfig (scripts/kconfig/gconf) 和其他编译目标调用从根 Kconfig 开始的程序, 并递归读取位于每个子目录中的 Kconfig 文件以构建它们的菜单. 访问哪个子目录也在每个 Kconfig 文件中定义, 并且还取决于用户选择的配置符号值.
存储符号值:.config 文件
所有配置符号值都保存在一个名为 .config 的特殊文件中. 每次要更改内核编译配置时, 都需要执行 make 目标, 例如 menuconfig 或 xconfig. 它们读取 Kconfig 文件以创建菜单并使用 .config 文件中定义的值更新配置符号的值. 此外, 这些工具会使用您选择的新选项更新 .config 文件, 如果它之前不存在, 也可以生成一个.
由于 .config 文件是纯文本, 您也可以在不需要任何专门工具的情况下对其进行更改. 保存和恢复以前的内核编译配置也非常方便.
编译内核:Makefile
kbuild 系统的最后一个组成部分是 Makefiles. 这些用于构建内核映像和模块. 与 Kconfig 文件一样, 每个子目录都有一个 Makefile, 它只编译其目录中的文件. 整个构建是递归完成的——顶层 Makefile 进入其子目录并执行每个子目录的 Makefile 以生成该目录中文件的二进制对象. 然后, 这些对象用于生成模块和 Linux 内核映像.
把它们放在一起: 添加硬币驱动程序
现在您对 kbuild 系统基础有了更多的了解, 让我们考虑一个实际的例子——将设备驱动程序添加到 Linux 内核树中. 示例驱动程序用于称为硬币的非常简单的字符设备. 驱动程序的功能是模仿抛硬币并在每次读取时返回两个值之一: 正面或反面. 该驱动程序有一个可选功能, 可以使用特殊的 debugfs 虚拟文件公开以前的翻转统计信息. 清单 1 显示了与硬币设备交互的示例.
清单 1. 硬币字符设备语义
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 内核添加功能 (例如硬币驱动程序), 您需要做三件事:
将源文件放在有意义的地方, 例如 drivers/net/wireless
用于 Wi-Fi 设备或 fs 用于新文件系统.
为放置文件的子目录 (或多个子目录) 更新 Kconfig, 配置符号允许您选择包含该功能.
更新放置文件的子目录的 Makefile, 以便构建系统可以有条件地编译您的代码.
因为此驱动程序是针对字符设备的, 所以将 coin.c 源文件放在 drivers/char 中.
下一步是为用户提供编译硬币驱动程序的选项. 为此, 您需要在 drivers/char/Kconfig 文件中添加两个配置符号: 一个选择将驱动程序添加到内核, 第二个决定驱动程序统计信息是否可用.
像大多数驱动程序一样, coin 可以构建在内核中, 作为模块包含或根本不包含. 因此, 第一个配置符号称为 COIN, 属于三态 (y/n/m) 类型. 第二个符号 , COIN_STAT 用于决定是否要公开统计信息. 显然这是一个二元决策, 所以符号类型是 bool (y/n). 此外, 如果您选择不包含硬币驱动程序本身, 那么将硬币统计信息添加到内核中也没有意义. 这种行为在内核中很常见——例如, 如果您没有首先启用块层, 则无法添加基于块的文件系统, 例如 ext3 或 fat32. 显然, 符号之间存在某种依赖关系, 您应该对此建模. 幸运的是, 您可以使用 depends on 关键字在 Kconfig 文件中描述配置符号的关系. 例如, 当 make menuconfigtarget 生成编译选项菜单树, 它隐藏所有不满足符号依赖关系的选项. 这只是可用于描述 Kconfig 文件中的符号的众多关键字之一. 有关 Kconfig 语言的完整说明, 请参阅 Linux 内核文档目录中的 kbuild/kconfig-language.txt.
清单 2 显示了 drivers/char/Kconfig 文件的一部分, 其中添加了硬币驱动程序的符号.
清单 2. Coin 驱动程序的 Kconfig 条目
#
# 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 be 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, 构建包含所有编译选项的树形菜单的目标使用此配置符号, 因此您可以选择要在内核及其模块中编译的内容. 例如, 当你执行:
$ make menuconfig
命令行实用程序 scripts/kconfig/mconf 将启动并读取所有 Kconfig 文件以构建基于菜单的界面. 然后您使用这些程序来更新您的 COIN 和 COIN_STAT 编译选项的值. 图 1 显示了导航到设备驱动程序→字符设备时菜单的外观; 查看如何设置投币驱动器的选项.
图 1. 菜单示例
完成编译选项配置后, 退出程序, 如果您进行了一些更改, 系统将要求您保存新配置. 这会将配置选项保存到 .config 文件中. 对于每个符号,.config 文件中都会附加一个 CONFIG_ 前缀. 例如, 如果符号是布尔类型并且您选择了它, 在 .config 文件中, 符号将这样保存:
CONFIG_COIN_STAT=y
另一方面, 如果您没有选择该符号, 则不会在 .config 文件中设置它, 您将看到如下内容:
CONFIG_COIN_STAT is not set
无论是否选择, 三态符号都与 bool 类型具有相同的行为. 但是, 请记住, tristate 还有第三种选择, 即将该功能编译为模块. 例如, 您可以选择将 COIN 驱动程序编译为模块, 并在 .config 文件中包含如下内容:
CONFIG_COIN=m
以下是 .config 文件的一部分, 显示了为投币器符号选择的值:
CONFIG_COIN=m
CONFIG_COIN_STAT=y
在这里你告诉 kbuild 你想将硬币驱动程序编译为一个模块并激活翻转统计. 如果您选择编译内置驱动程序并且没有翻转统计信息, 您将得到如下内容:
CONFIG_COIN=y
# CONFIG_COIN_STAT is not set
一旦有了 .config 文件, 就可以编译内核及其模块了. 当您执行一个编译目标来编译内核或模块时, 它首先执行一个读取所有 Kconfig 文件和 .config 的二进制文件:
$ scripts/kconfig/conf Kconfig
该二进制文件使用您为所有配置符号选择的值更新 (或创建) 一个 C 头文件. 这个文件是 include/generated/autoconf.h, 每条 gcc 编译指令都包含它, 所以内核中的任何源文件都可以使用这些符号.
该文件由数千个 #define 宏组成, 这些宏描述了每个符号的状态. 让我们看看宏的约定.
值为 true 的布尔符号和值为 yes 的三态符号被同等对待. 对于它们两者, 定义了三个宏.
例如, CONFIG_COIN_STAT 值为 true 的 bool 符号和 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
同样, 值为 false 的布尔符号和值为 no 的三态符号具有相同的语义. 对于它们两者, 定义了两个宏. 例如, 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 带有值的模块将生成以下内容:
#define __enabled_CONFIG_COIN 0
#define __enabled_CONFIG_COIN_MODULE 1
#define CONFIG_COIN_MODULE 1
好奇的读者可能会问为什么需要那些 __enabled_option
宏?CONFIG_option 仅仅有 and 还不够 CONFIG_option_MODULE
吗? 而且, 为什么 _MODULE
即使是 bool 类型的符号也要声明?
好吧,__enabled_
常量由三个宏使用:
#define IS_ENABLED(option) \
(__enabled_ ## option || __enabled_ ## option ## _MODULE)
#define IS_BUILTIN(option) __enabled_ ## option
#define IS_MODULE(option) __enabled_ ## option ## _MODULE
因此,__enabled_option
和 __enabled_option_MODULE
总是被定义, 即使对于 bool 符号也是如此, 以确保该宏适用于任何配置选项.
第三步也是最后一步是更新放置源文件的子目录的 Makefile, 这样 kbuild 就可以编译您的驱动程序 (如果您选择的话).
但是, 您如何指示 kbuild 有条件地编译您的代码?
内核构建系统有两个主要任务: 创建内核二进制映像和内核模块. 为此, 它维护了两个对象列表: 分别是 obj-y 和 obj-m. 前者是将在内核映像中构建的所有对象的列表, 后者是将被编译为模块的对象的列表.
来自 .config 的配置符号和来自 autoconf.h 的宏与一些 GNU make 语法扩展一起使用来填充这些列表.Kbuild 递归地进入每个目录并构建列表, 添加在每个子目录的 Makefile 中定义的对象. 有关 GNU make 扩展和对象列表的更多信息, 请阅读 Documentation/kbuild/makefiles.txt.
对于硬币驱动程序, 您唯一需要做的就是在 drivers/char/Makefile 中添加一行:
obj-$(CONFIG_COIN) += coin.o
这告诉 kbuild 从源文件 coin.c 创建一个对象并将其添加到对象列表中. 因为 CONFIG_COIN 的值可以是 y 或 m, coin.o 对象将根据符号值添加到 obj-y 或 obj-m 列表中. 然后它将构建在内核中或作为一个模块. 如果你没有选择这个 CONFIG_COIN 选项, 符号是未定义的, coin.o 根本不会被编译.
现在您知道如何有条件地包含源文件了. 难题的最后一部分是如何有条件地编译源代码段. 这可以通过使用 autoconf.h 中定义的宏轻松完成. 清单 3 显示了完整的硬币字符设备驱动程序.
清单 3. 硬币字符设备驱动程序示例
#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] = {0, 0};
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);
}
在清单 3 中, 您可以看到 CONFIG_COIN_STAT 配置选项用于注册 (或不注册) 一个特殊的 debugfs 文件, 该文件向用户空间公开抛硬币统计信息.
图 2 总结了内核构建过程, git diff --stat
命令的输出显示了您为包含驱动程序而修改的文件:
drivers/char/Kconfig | 16 +++++++++
drivers/char/Makefile | 1 +
drivers/char/coin.c | 89 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 106 insertions(+), 0 deletions(-)
图 2. 内核构建过程
结论
尽管 Linux 是一个整体内核, 但它是高度模块化和可定制的. 您可以在从高性能集群到台式机一直到手机的各种设备中使用相同的内核. 这使得内核成为一个非常庞大和复杂的软件. 但是, 即使内核有数百万行代码, 它的构建系统也允许您轻松地用新功能扩展它. 过去, 要访问操作系统的源代码, 您必须在大公司工作并签署大型 NDA 协议. 如今, 可能是最现代的操作系统的源代码是公开的. 您可以使用它, 研究它的内部结构, 并以您想要的任何创造性方式对其进行修改. 最好的部分是您甚至可以分享您的工作并从活跃的社区获得反馈. 快乐黑客!
文章评论