对技术执着
分类: LINUX
2015-03-14 17:25:21
原文地址:Linux内核构建系统之十 作者:kyok520
到目前为止,内核构建系统的大部分重要的地方都已讨论完毕,惟独有一个很关键的方面还没讨论完全,那就是依赖关系的处理。熟悉Linux内应用程序开发的人都知道,要想用 make 工具来自动化的管理他们的应用项目工程,就必须正确处理所要编译的目标和生成这些目标所需文件之间的依赖关系。举个例子,比方你要编译一个对象文件 hello.o,那么你就需要告诉 make 工具生成该对象文件所需要的依赖有哪些后,make 才能帮你正确的管理它的编译。这些所依赖的文件通常有可能是:对应的C程序文件 hello.c、你自己写的头文件、函数库内的头文件等等。
在实际应用项目开发的时候,我们开发者往往不会用手动的方式把这些所有依赖关系都列在 Makefile 中。更常见的方法是在 Makefile 中使用 gcc 的选项 -M(或者其他 -MD之类的),让它帮我们生成包含如下形式依赖性规则的 *.d 文件,然后在 Makefile 的后面用 -include 之类的来包含这个 *.d 文件。这样编译的时候,如果其中某个依赖文件被修改了,那对应的这个目标就一定会被make更新。
那对于 Linux kernel,其实也需要处理一系列的依赖关系。只不过,它将应用程所常用的那种依赖关系扩大化了。具体来说,内核构建系统在构建的时候,需要根据下列情况来决定一个目标是否需要被构建:
a) 如果存在有该目标对应的依赖文件被改动,或者当前还未被生成,那内核构建系统肯定要生成该目标;
b) 如果生成该目标的命令行与之前保存下来的命令行不一致,比方改了一下C编译器或汇编器的标志之类的,那该目标也要被重新生成;
c) 如果某个 CONFIG_XXX 配置选项被改了。比方原先是 =y 的,现在改成 =m;原先没设置过的,现在设置成 =m 之类的。那所有在源代码中使用过这个选项的,比方在 hello.c(也有可能是另外某个头文件) 里面用到了这个选项,那对应的目标 hello.o 就要被重新生成。
我们来看看这些是如何实现的,有些东西在之前的讨论中我们已经接触到过,这里只是总结性的提一下。也有些没讨论到的,这里会列出一些代码来分析。在内核构建系统的众多makefile中都会使用到字符串 "-MD,$(depfile)",比方是在像文件 .../scripts/Makefile.lib 中,将它赋给了 c_flags/a_flags/cpp_flags 等标志,这意味着构建系统在使用c编译器/汇编器/c预处理器等过程中,就会产生依赖规则文件。变量 depfile 定义在 .../scripts/Kbuild.include 文件中:
### # Name of target with a '.' as filename prefix. foo/bar.o => foo/.bar.o dot-target = $(dir $@).$(notdir $@) ### # The temporary file to save gcc -MD generated dependencies must not # contain a comma depfile = $(subst $(comma),_,$(dot-target).d)
另外,也会经常使用到字符串 "$(LINUXINCLUDE)",比方在同样的文件中赋给这些标志变量。而下面变量 LINUXINCLUDE 的定义中(定义在顶层Makefile 文件中)会使用 "-include include/linux/autoconf.h"。
# Use LINUXINCLUDE when you must reference the include/ directory. # Needed to be compatible with the O= option LINUXINCLUDE := -Iinclude \ $(if $(KBUILD_SRC),-Iinclude2 -I$(srctree)/include) \ -I$(srctree)/arch/$(hdr-arch)/include \ -include include/linux/autoconf.h
所以这意味着,在产生的依赖规则文件中,也会包含文件 .../include/linux/autoconf.h,并且这种包含是全局迷漫性的。也就是这个文件会被内核大多数目标所依赖。
这里会存在一个问题。假如我前后两次的内核编译过程中,区别只在于第一次配置选项 CONFIG_MY_DRIVER 被设置为 =m,而第二次被设置为 =y。那你想在这第二次编译过程中是不是因为更新了 .../include/linux/autoconf.h 文件而去全部编译依赖这个文件的所有目标呢?显然是这样的,因为 .../include/linux/autoconf.h 文件会因为配置选项的改变而变得更新,所以,自然而然,依赖它的所有目标会被重新更新。
这是没有必要的严重浪费。因为我只是改变了我自己所写内部模块对应的那个那个配置选项 CONFIG_MY_DEVICE_DRIVER。换句话讲,这个选项只影响到我自己写的那个内部模块,而其他任何内部模块或者基本内核代码都没有使用这个选项,所以在第二次编译过程中,没有必要去重新remake差不多全部目标,而只需要重新编译我自己写的那个内部模块即可。
为了避免这种不必要的额外负担,而达到只编译那些因为配置选项变更而确实受到影响的目标。内核构建系统使用 .../scripts/basic/fixdep 做了一个小动作。该动作修改依赖规则文件,从中删除对 .../include/linux/autoconf.h 文件的直接依赖,而代之以对 .../include/config/ 目录下的空的头文件的依赖。下面,我们慢慢来解释这是怎么做到的。
在前面我们讨论 kconfig 的时候说过,配置工具 .../scripts/kconfig/conf 在产生 auto.conf、auto.conf.cmd 和 autoconf.h 等三个文件。与此同时,它会在函数 conf_write_autoconf 中调用conf_split_config 函数。当时并没有说这个函数的确切工作过程,而只是留下一个伏笔说它会负责在 .../include/config 产生一系列的头文件。那这里我们详细来看看它内部是如何产生这些文件。先列出该函数的框架:
该函数先使用 conf_get_autoconfig_name 取得文件 ".../include/config/auto.conf" 的名称,然后用 for 循环处理每一个 CONFIG_XXX 的定义。比方针对我们之前的那个 CONFIG_MY_DEVICE_DRIVER 配置选项,处理的时候,它在字符数组 path 中存储这样的字符串:my/device/driver.h。接下来,它会在后面用 open 系统调用在 .../include/config 目录下去创建名为此字符串的空头文件。从这里可以看出,针对每个配置过的配置选项,都会有这样的头文件产生。
前面说过构建系统会将对 .../include/linux/autoconf.h 文件的依赖转换为对这里的这些头文件的依赖。这是怎样的一个机制?比方说,在我自己的内部模块中,因为使用到了配置选项 CONFIG_MY_DEVICE_DRIVER,所以 fixdep 会将我对 autoconf.h 文件的依赖转换成对 .../include/config/my/device/driver.h 头文件的依赖。而对其他内部模块或者基本内核的目标来说,在代码中没有使用到这个配置选项,所以 fixdep 对它们的处理,只是简单的从它们对应的依赖规则中删除对 autoconf.h 文件的依赖。
这个过程现在不明白不要紧,我们先来看看 fixdep 在构建系统代码中是如何被使用的。在这之后,我们来举例说明这个过程。在内核构建系统中,你在变量 if_changed_dep 以及宏 rule_cc_o_c 的定义中都能发现对 scripts/basic/fixdep 的调用:
# Execute the command and also postprocess generated .d dependencies file. if_changed_dep = $(if $(strip $(any-prereq) $(arg-check) ), \ @set -e; \ $(echo-cmd) $(cmd_$(1)); \ scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).tmp;\ rm -f $(depfile); \ mv -f $(dot-target).tmp $(dot-target).cmd)
define rule_cc_o_c $(call echo-cmd,checksrc) $(cmd_checksrc) \ $(call echo-cmd,cc_o_c) $(cmd_cc_o_c); \ $(cmd_modversions) \ $(cmd_record_mcount) \ scripts/basic/fixdep $(depfile) $@ '$(call make-cmd,cc_o_c)' > \ $(dot-target).tmp; \ rm -f $(depfile); \ mv -f $(dot-target).tmp $(dot-target).cmd endef
概括一下,其中对 fixdep 的调用语法是:fixdep 。fixdep 会读入依赖规则文件和命令行,并输出一些内容送向标准输出。那么前面这两段代码中对 fixdep 的调用就会将这些标准输出中输出的内容重重定向到文件 $(dot-target).tmp。最后此文件被重命名为 $(dot-target).cmd。
还是让我们举例来说明吧,现在假设我们要编译的目标是 .../drivers/mydriver.o,它是构成我们前面内部模块 .ko 的对应对象文件。那么根据前面我们讨论的记过,构建系统会调用 rule_cc_o_c 来生成 mydriver.o。假设前面针对 mydriver.o 生成出来的依赖规则文件 .mydriver.o.d 是这样的:
并且,生成 mydriver.o 的命令行是:
arm-linux-gcc -Wp,-MD,drivers/.mydriver.o.d -nostdinc ....
那么fixdep 在处理的时候,会首先向标准输出输出内容:
cmd_drivers/mydriver.o := arm-linux-gcc -Wp,-MD,drivers/.mydriver.o.d -nostdinc ...
然后,它遍历处理依赖规则中的所有依赖文件,用GREP检查这些文件中是否包含有对配置选项诸如 CONFIG_MY_DEVICE_DRIVER 之类的使用。如果有的话,它将 .../include/config/my/device/driver.h 文件也加到依赖文件列表中。注意在遍历处理的过程中,fixdep 会把 autoconf.h 从依赖列表中过滤掉。遍历完之后,它又会向标准输出输出下面这样的内容:
最后,所有的输出内容会被重定向到文件 .mydriver.o.tmp 中。接着该文件被重命名为 .mydriver.o.cmd。再最后这个命令文件会连同其它所有命令文件被包含进makefile规则链体系,这在前面已经看到过,这里不再论述。正是通过这样的小动作,内核构建系统移除了对 autoconf.h 的直接依赖,从而达到避免那种额外重新处理其他目标的负担的。