注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

内核构建系统  

2011-10-17 22:37:05|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

一、基本思路和问题

基本思路和目的就是尽可能的减少上层对下层的依赖。这也就是说,当下层添加、修改一个模块之后,上层的代码不需要修改,通缩的说这就叫做“框架”。但是,如果上层向完全摆脱对下层的依赖是不可能的,就像自由是可以的,但是要在某个限制范围之内。

首先连接上层和底层的一个重要约定就是

obj-XXX

变量,其中的XXX可以为y、m和n,这也就是kconfig中经典的三状态变量的三个可能值,分别对应直接编入内核builtin、编辑为模块和禁制该功能。这个obj-xxx就是上层和下层约定的第一个变量,如果下层希望添加一个新的模块或者文件,它就向者三个变量中追加自己的信息即可,从而让上层知道自己的存在和模式,这样上层就相当于为某个服务提供了一个队列,所有需要服务的模块都按照自己的需要来挂入各自的队列。

但是这里引申出来的问题是

1、易用性。虽然我们可以在Makefile中简单的添加需要内核编译的中间文件,但是缺点就是不直观,结构松散,并且一些依赖性和一致性无法保证。所有的这些就需要引入一个Makefile输入系统的前端,由这个前端来协助进行这些中间文件的使能、禁止、模块化等。这样就可以把可变、易变的配置信息独立为一个配置文件,而Makefile真正的功能代码就可以保持不变。这也是一种 可变/不变 或者说 数据/代码 隔离的思想。

这样就引入了Makefile的配置前端,Kconfig系统,这个系统事实上虽然看起来比较简单,但事实上它和所有的脚本语言一样,有自己的此法分析器、语法分析器及对应的语义功能。相对的解析文件就在linux-2.6.37.1\scripts\kconfig文件夹下。如果有了语法和语义,就可以对输入进行更强的合法性检测,例如,当功能A使能的时候,功能B是必须要被是能的,也就是说B是A的必要条件,这个依赖就可以在脚本中进行说明,由脚本语言自动来保证这个必要条件(对应的kconfig语法就是 select ,例如menuconfig SOUND_PRIME select SOUND_OSS_CORE)。还有一些功能是互斥的,也就是A、B、C等若干个功能是不能同时使能的,这样就需要使用到choice语法,从而让用户完成一个多选一的游戏。当然还有一些只能Y或者N、字符串配置、数值配置、三态配置等配置,这些就是配置类型的关系了。

这里可以引申出来一个结论,就是menuconfg只是Makefile的一个(一种)前端,而这个前端的目的就是生成一个Makefile的输入,那么我们采用最原始的方法手动修改其中的内容也是可以的,只要这个修改和其它地方没有必然联系。例如测试的时候需要在内核中强制、临时添加一个功能,就可以直接修改Makefile就可以了,没必要非要往config上凑。或者我们可以直接在make的命令行上对变量赋值也是可以的,这是最环保无污染的修改方式了。

2、可用性。大部分情况下,我们添加的中间文件都是一个平凡的文件,也就是它对编译系统来说只是一个普通的文件,该文件的编译选项和其它文件的编译选项可能没有什么不同。当然其它的选项可能也没有特别之处,例如依赖的生成、输出位置等,所以当我们添加一个新的中间文件的时候很可能就是使用默认构建的。但是作为一个框架,不能说大部分情况就是如此而让极少数异端没有生存机会,这样就以为框架要提供方法,当用户系统修改某个特定中间文件的构建方法的时候就真的可以修改。我们看一下linux-2.6.37.1\scripts\Makefile.lib文件

orig_c_flags   = $(KBUILD_CPPFLAGS) $(KBUILD_CFLAGS) $(KBUILD_SUBDIR_CCFLAGS) \
                 $(ccflags-y) $(CFLAGS_$(basetarget).o)
_c_flags       = $(filter-out $(CFLAGS_REMOVE_$(basetarget).o), $(orig_c_flags))

其中basetarget定义于linux-2.6.37.1\scripts\Kbuild.include

# filename of target with directory and extension stripped
basetarget = $(basename $(notdir $@))

也就是一个目标删除掉目录和扩展名的形式。

其中有两项,一个是CFLAGS_$(basetarget).o,这个是一个目标需要添加的特有标志;反过来,如果说想要删除构建包办的某个选项中的一个选项,那就可以使用CFLAGS_REMOVE_来删除,这个删除使用了makefile的filter-out命令来完成,所以也算是巧妙。例如,在早期的至少是在2.6.21中,如果内核不使用优化(也就是O0编译),那么会出现连接错误,因为代码里有这样的内容

#if defined(__GNUC__) && defined(__OPTIMIZE__)
所以出现不优化就连接不过的情况,所以此时就可以找到那些包含了这个头文件的.c文件,然后在Makefile里添加CFLAGS_ERR.o=-O1,也就是给这个文件开小灶,让它优化编译从而避免警告。

3、易用性。有些模块并不是由一个目标文件组成的,而是多个文件共同实现一个功能,这个功能可以直接编入内核,或者可以编译为模块,所以就需要有一种方式来确定一个模块的所有的组成模块(composite objs)。这里内核采用的是另一层结构,就是在obj-m的基础上再次向下划分。由于obj-m中的每个单词都代表一个模块,例如obj-m=snd.o  ipv6.o 等模块,而snd.o又是代表一个声音模块,它由多个文件对应的目标文件一起构成,此时就在这个模块名的基础上再次扩充一个规则,就是 mod-objs变量中的各个变量是mod模块的组成部分,这些变量链接组成一个mod.o。例如

\linux-2.6.37.1\sound\core\Makefile中

#
# Makefile for ALSA
# Copyright (c) 1999,2001 by Jaroslav Kysela <perex@perex.cz>
#

snd-y     := sound.o init.o memory.o info.o control.o misc.o device.o 这些是snd功能必须的基本模块
snd-$(CONFIG_ISA_DMA_API) += isadma.o这些是snd使能之后该模块的一些可选子模块,也就是说,即使snd选择为y或者m,这些作为snd的子模块依然可以选择为n或者y,表示snd的这些子模块不被使能。这点对一会模块输入目标的计算的理解有帮助
snd-$(CONFIG_SND_OSSEMUL) += sound_oss.o info_oss.o
snd-$(CONFIG_SND_VMASTER) += vmaster.o
snd-$(CONFIG_SND_JACK)   += jack.o

snd-pcm-objs := pcm.o pcm_native.o pcm_lib.o pcm_timer.o pcm_misc.o \
  pcm_memory.o

snd-page-alloc-y := memalloc.o
snd-page-alloc-$(CONFIG_SND_DMA_SGBUF) += sgbuf.o

snd-rawmidi-objs  := rawmidi.o
snd-timer-objs    := timer.o
snd-hrtimer-objs  := hrtimer.o
snd-rtctimer-objs := rtctimer.o 这里一个逻辑模块snd_rtctimer_objs就是逻辑模块snd_rtctimer的一个输入文件
snd-hwdep-objs    := hwdep.o

obj-$(CONFIG_SND)   += snd.o
obj-$(CONFIG_SND_HWDEP)  += snd-hwdep.o
obj-$(CONFIG_SND_TIMER)  += snd-timer.o
obj-$(CONFIG_SND_HRTIMER) += snd-hrtimer.o
obj-$(CONFIG_SND_RTCTIMER) += snd-rtctimer.o
obj-$(CONFIG_SND_PCM)  += snd-pcm.o snd-page-alloc.o
obj-$(CONFIG_SND_RAWMIDI) += snd-rawmidi.o

obj-$(CONFIG_SND_OSSEMUL) += oss/
obj-$(CONFIG_SND_SEQUENCER) += seq/

然后看一下构建框架对这个obj-m或者obj-y模块子模块的判断和计算。linux-2.6.37.1\scripts\Makefile.lib

# if $(foo-objs) exists, foo.o is a composite object
multi-used-y := $(sort $(foreach m,$(obj-y), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y))), $(m))))
multi-used-m := $(sort $(foreach m,$(obj-m), $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y))), $(m))))
multi-used   := $(multi-used-y) $(multi-used-m)
single-used-m := $(sort $(filter-out $(multi-used-m),$(obj-m)))

上面的sort主要是为了删除重复单词,然后对于obj-m中的每个单词,查看它对应的xx-objs或者xx-y两个变量是否为空,如果非空,则表示这个模块使用的是一个组合文件,也就是有一个或者多个.o文件链接而成的.o文件。例如 obj-m中包含了snd.o(注意,这个变量最后还是有一个o后缀的)然后$(m:.o=.-objs)也就是判断$(snd-objs)变量是否为空,对于-y也是这个模式,所以这样就判断除了一个模块是否需要其他的.o文件链接生成。对于obj-m,还是刚才说的,一个模块可能还有子功能,而这个子功能也可以选择为n,例如,如果上面的CONFIG_SND_JACK选择为n,那么snd-$(CONFIG_SND_JACK)   += jack.o就展开为snd-n += jack.o,所以通过snd-y就无法搜索到这个变量,从而snd模块就没有jack这个.o文件。

二、更新的动态计算

在Kbuild.include中有一个filechk宏,这个宏是用来读取文件内容,然后用文件内核和另一个文件通过cmp程序比较,看一个文件的编译命令是否变化,例如一个功能从使能变为去使能,一个优化级别从O2变化为O0等变化。但是这个是早期的一个版本,如果我们知道一个构建的完整命令,那么可以把这整个命令以变量的形式保存在一个文件里,然后下次编译的时候从这个文件里读取这个变量,然后和新的编译命令比较看是否有变化,如果有变化则重新构建一个文件。这一点主要是由于内核的很多配置选项是可以随时配置的,而这些配置在依赖的头文件关系中又无法体现,所以要在考虑文件依赖的同事还要考虑编译选项的变化以及配置的变化。

其实对于配置的变化是通过伪文件来体现的,就是对应于每个CONFIG_XX_YY,build自动生成一个空的xx/yy.h文件,这些文件位于include/config文件夹下,然后在gcc的标准输出中再加上这个空文件作为一个.o的依赖。然后config将会计算本次配置和上次配置是否有变化,如果某个选项有变化,则更新这个头文件的时间戳,从而让make判断出文件依赖有变化,从而重新生成这个.o文件。那么build是如何知道一个.o文件依赖或者说使用了哪些CONFIG呢,这个还真是没有好的办法,build就是拦截了gcc -MM输出的依赖,这些依赖都是文件,然后build就一次打开这些文件,在浙西文件中搜索CONFIG_这样的关键字,搜索到之后就把CONFIG_之后的字符串转换为头文件(如果CONFIG_之后有多个下划线,则下划线之间的内容转换为文件夹,例如CONFIG_SND_HRTIMER将会对应snd/hrtimer.h文件)。这个功能在linux-2.6.37.1\scripts\basic\fixdep.c中完成,有兴趣的同学可以看看其中的代码实现。

然后是一个构建命令是否变化,因为典型的makefile只会有文件之间的依赖,而事实上选项的变化必然会导致生成文件的变化,所以当选项变化的时候也需要来重新构建。所以,在每个.o文件相同的文件夹有一个.cmd后缀的文件,这个文件是makefile语法的文件,其中列出了一个.o文件所有的依赖文件(包括CONFIG_生成的伪头文件),然后还有一个定义为该.o文件构建命令的变量,它们分别对应dpes_xx.o和cmd_xx.o,通过这两个保存在文件中的命令就可以来确定下次构建的时候是否需要真的构建这个.o文件的构建。

Kbuild.include

# Find any prerequisites that is newer than target or that does not exist.
# PHONY targets skipped in both cases.
any-prereq = $(filter-out $(PHONY),$?) $(filter-out $(PHONY) $(wildcard $^),$^)所有比目标新的依赖

 

arg-check = $(strip $(filter-out $(cmd_$(1)), $(cmd_$@)) \
                    $(filter-out $(cmd_$@),   $(cmd_$(1))) ) 这两个filter-out用的是非常精髓的,它是Makefile中判断两个变量是否相等的办法,大家以后可以瞻仰、借鉴和学习一下。这里的cmd_$1就是刚才说的.cmd文件中cmd_target对应的构建命令变量,而cmd_$@则是当下完整的构建命令,所以可以完成比较结合下面的arg-check就可以完成运行时构建命令的比较

if_changed = $(if $(strip $(any-prereq) $(arg-check)),                       \
 @set -e;                                                             \
 $(echo-cmd) $(cmd_$(1));                                             \
 echo 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd)

这些文件是以句号开始的,所以在Linux桌面系统下默认是隐藏的,所以需要显示隐藏文件看到,为了让大家有一个感官的认识,下面是.mutex.o.cmd文件的内容

cmd_kernel/mutex.o := gcc -m32 -Wp,-MD,kernel/.mutex.o.d  -nostdinc -isystem /usr/lib/gcc/i686-redhat-linux/4.4.2/include -D__KERNEL__ -Iinclude  -include include/linux/autoconf.h -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common  -g   -pipe -msoft-float -mregparm=3 -freg-struct-return -mpreferred-stack-boundary=2  -march=i686 -mtune=pentium3 -mtune=generic -ffreestanding -maccumulate-outgoing-args -DCONFIG_AS_CFI=1 -DCONFIG_AS_CFI_SIGNAL_FRAME=1 -Iinclude/asm-i386/mach-generic -Iinclude/asm-i386/mach-default -fomit-frame-pointer  -fno-stack-protector -Wdeclaration-after-statement -Wno-pointer-sign    -D"KBUILD_STR(s)=\#s" -D"KBUILD_BASENAME=KBUILD_STR(mutex)"  -D"KBUILD_MODNAME=KBUILD_STR(mutex)" -c -o kernel/mutex.o kernel/mutex.c

deps_kernel/mutex.o := \
  kernel/mutex.c \
        $(wildcard include/config/debug/locking/api/selftests.h) \

………………
  kernel/mutex.h \
  include/asm/mutex.h \

kernel/mutex.o: $(deps_kernel/mutex.o)

$(deps_kernel/mutex.o):

三、quiet意义

我们可以看到,在makefile中,很多地方都定义了

quiet_cmd_XXX和cmd_XXX两个变量,其中的quiet就是我们直接make显示的输出,而使用make V=1 显示的则是后面的命令。

quiet_cmd_cc_o_c = CC $(quiet_modtag)  $@

          cmd_cc_o_c = $(CC) $(c_flags) -c -o $(@D)/.tmp_$(@F) $<

$(obj)/%.o: $(src)/%.c FORCE
 $(call cmd,force_checksrc)
 $(call if_changed_rule,cc_o_c)

if_changed_rule = $(if $(strip $(any-prereq) $(arg-check) ),                 \
 @set -e;                                                             \
 $(rule_$(1)))

所以计算完依赖之后执行rule_cc_o_c命令。

define rule_cc_o_c
 $(call echo-cmd,checksrc) $(cmd_checksrc)     \
 $(call echo-cmd,cc_o_c) $(cmd_cc_o_c);      \
 $(cmd_modversions)        \
 $(call echo-cmd,record_mcount)       \
 $(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

# echo command.
# Short version is used, if $(quiet) equals `quiet_', otherwise full one.
echo-cmd = $(if $($(quiet)cmd_$(1)),\这里判断执行的是quiet_cmd_targe还是cmd_target,从而显示不同命令。也就是CC XXX.o
 echo '  $(call escsq,$($(quiet)cmd_$(1)))$(echo-why)';)

# printing commands
cmd = @$(echo-cmd) $(cmd_$(1))

四、ko与动态模块

ko文件的生成比较麻烦一些。在刚开始一个ko文件的配置和上面说的一样,只是在模块文件生成.o文件的同时(这个生成和obj-y的生成都是相同的),会额外生成一个.mod文件,这个文件记录了target.ko和这个target是由那些.o文件生成的,并且他们集中放在一个modver文件夹,从而可以供build通过grep .ko来搜索出这次构建所有的ko文件。找到所有ko文件的意义在于“依赖计算”。也就是说,一个ko文件使用的符号可能是在内核中直接定义,也有可能是在另一个ko文件中定义,而这个依赖需要在最终生成的ko文件中体现,所以需要知道系统中的所有ko文件。这个计算是在linux-2.6.37.1\scripts\mod\modpost.c中完成的。

static void add_depends(struct buffer *b, struct module *mod,
   struct module *modules)
{
 struct symbol *s;
 struct module *m;
 int first = 1;

 for (m = modules; m; m = m->next)
  m->seen = is_vmlinux(m->name);

 buf_printf(b, "\n");
 buf_printf(b, "static const char __module_depends[]\n");
 buf_printf(b, "__used\n");
 buf_printf(b, "__attribute__((section(\".modinfo\"))) =\n");
 buf_printf(b, "\"depends=");
 for (s = mod->unres; s; s = s->next) {
  const char *p;
  if (!s->module)
   continue;

  if (s->module->seen)
   continue;

  s->module->seen = 1;
  p = strrchr(s->module->name, '/');
  if (p)
   p++;
  else
   p = s->module->name;
  buf_printf(b, "%s%s", first ? "" : ",", p);
  first = 0;
 }
 buf_printf(b, "\";\n");
}
这里放入了modinfo节,要注意的是这个节里面是无结构的字符串,所以这里放入的是一个depends=mod1,mod2……的形式,这样便于使用简单的字符串处理。而代表这个模块最为重要的就是内置的一个struct module结构,该结构中有我们最为关心的init和exit接口,该功能是在

static void add_header(struct buffer *b, struct module *mod)
中完成,其中使用的代码为

static void add_header(struct buffer *b, struct module *mod)
{
 buf_printf(b, "#include <linux/module.h>\n");
 buf_printf(b, "#include <linux/vermagic.h>\n");
 buf_printf(b, "#include <linux/compiler.h>\n");
 buf_printf(b, "\n");
 buf_printf(b, "MODULE_INFO(vermagic, VERMAGIC_STRING);\n");
 buf_printf(b, "\n");
 buf_printf(b, "struct module __this_module\n");
 buf_printf(b, "__attribute__((section(\".gnu.linkonce.this_module\"))) = {\n");
 buf_printf(b, " .name = KBUILD_MODNAME,\n");
 if (mod->has_init)
  buf_printf(b, " .init = init_module,\n");
 if (mod->has_cleanup)
  buf_printf(b, "#ifdef CONFIG_MODULE_UNLOAD\n"
         " .exit = cleanup_module,\n"
         "#endif\n");
 buf_printf(b, " .arch = MODULE_ARCH_INIT,\n");
 buf_printf(b, "};\n");
}

我们可以在内核的linux-2.6.37.1\kernel\module.c:setup_load_info函数中找到该字符串,也就是这个节中只有一个简单的struct module结构。这里就是一个实现的思想:有类型的结构结构通过特殊的节名来标识的(例如__param节,__ksymtab节),如果是放入同一个节中的不同结构,那么使用字符串来表示,即XX=arg1,arg2形式(例如depends节)

 info->index.mod = find_sec(info, ".gnu.linkonce.this_module");

  评论这张
 
阅读(1536)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017