Linux内核初始化调用,第2部分:深入研究实现

2020-09-27 00:59:12

在这篇关于Linux内核initcall的博客文章系列的第一部分中,我们研究了它们的用途、用法以及调试它们的方法(使用initcall_debug或ftrace)。在这第二部分中,我们将更深入地研究initcall的实现,看看__device_initcall()宏、rootfs initcall以及如何执行模块。

Initcall定义导致最终的__define_initcall()。这就是我们现在要关注的问题。

如果我们使用我们的伪示例,postcore_initcall()将导致第一个__Define_initcall()(ID为2),从而导致另一个_Define_initcall()具有3个参数。以下是我们上一篇文章的状态摘要:

所有这些参数都将用于创建一个initcall_t条目,该条目将根据给定的参数命名。在我们的示例中,__initcall_mypostcore_init2。

使用关键字ATTRIBUTE和SECTION将允许我们将对象文件命名为SECTION。在postcore initcall的情况下,它将是.initcall2.init。它对于所有postcore-initcall都是相同的,所有这些调用都分组在.initcall2.init部分中。

使用objdump可以确认这一点。可以查看新内核对象文件并搜索我们的函数名称:

我们有一个.initcall2.init部分,它引用我们的条目__initcall_postcore_init2,指向我们的postcore伪示例。总而言之,__define_initcall函数将创建特定于所使用的initcall的对象文件部分(这要归功于它的id),指向所创建的函数。

如果我们查看所有现有的initcall2(即postcore initcall),我们可以看到每个函数指针的地址紧随其后:

$objdump-t vmlinux.o|grep.initcall2.init 00000000 l O.initcall2.init 00000004__initcall_ATOM_POOL_INIT2 00000004 l O.initcall2.init 00000004__initcall_mvebu_soc_device2 00000008 l O.initcall2.init 00000004__initcall_Coherency_Late_Init2 0000000c l O.initcall2.init 00000004__initcall_imx_mmdc_init2 00000010 l O.initcall2.init 00000004__initcall_omap_hwmod_setup_all2 [.] 0000007c l O.initcall2.init 00000004__initcall_mypostcore_init2 00000080 l O.initcall2.init 00000004__initcall_RockChip_grf_init2 [.]。

此initcall2.init部分包含已注册的所有postcore的initcall的函数地址。该顺序在编译时执行,具体取决于Makefile中的顺序。

让<;pà>;执行一个示例,以证明一个级别的所有初始化调用之间的排序是通过Makefile中的排序执行的,而不是按照任何其他方式(字母顺序,...)。

我们创建两个虚拟示例作为postcore initcall:包含mydriver_func()initcall的mydriver.c和包含mytherdriver_func()initcall的mytherdriver.c。让我们将这两个驱动程序放在RTC子系统中(当然,它可以放在其他任何地方): $cat Drivers/rtc/mydriver.c #include<;linux/init.h>; 静态int__init mydriver_func(Void) { 返回0; } Postcore_initcall(Mydriver_Func); $cat Drivers/rtc/mytherdriver.c #include<;linux/init.h>; 静态int__init mytherdriver_func(Void) { 返回0; } Postcore_initcall(Mytherdriver_Func);

我们将把mydriver放在第一个编译的位置,然后放入mytherdriver: $git diff驱动程序/RTC/Makefile [.] -rtc-core-y:=class.o接口。o +rtc-core-y:=class.o接口.o mydriver.o mytherdriver.o。

编译之后,让我们看看目标文件: $objdump-t vmlinux.o|grep";driver_func"; 0008c3c8 l F.init.text 00000008 mydriver_func 000000c8 l O.initcall2.init 00000004__initcall_mydriver_func2 0008c3d0 l F.init.text 00000008 mytherdriver_func 000000cc l O.initcall2.init 00000004__initcall_mytherdriver_func2。

如您所见,该部分的地址因函数名称而异:__initcall_mydriver_func2为000000c8,__initcall_mytherdriver_func2为000000cc。__initcall_mydriver_func2的地址在__initcall_mytherdriver_func2的地址之前。

最后,让我们使用FTrace:检查执行顺序: #cat/sys/kernel/debug/tracting/trace|grep driver_func SWAPPER/0-1[000]....。0.059546:INITCALL_START:FUNC=MYDRIVER_FUNC+0x0/0x8 SWAPPER/0-1[000]....。0.059556:INITCALL_FINISH:FUNC=MYDRIVER_FUNC+0x0/0x8 ret=0 SWAPPER/0-1[000]....。0.059571:INITCALL_START:FUNC=mytherdriver_FUNC+0x0/0x8 SWAPPER/0-1[000]....。0.059581:INITCALL_FINISH:FUNC=mytherDRIVER_FUNC+0x0/0x8 ret=0

现在,让我们仅在Makefile中颠倒顺序,并重现完全相同的测试: $git diff驱动程序/RTC/Makefile [.] -rtc-core-y:=class.o接口。o +rtc-core-y:=class.o接口.o mytherdriver.o mydriver.o。

Vmlinux.o中的部分也是颠倒的: $objdump-t vmlinux.o|grep";driver_func"; 0008c3c8 l F.init.text 00000008 mytherDRIVER_FUNC 000000c8 l O.initcall2.init 00000004__initcall_mytherdriver_func2 0008c3d0 l F.init.text 00000008 mydriver_func 000000cc l O.initcall2.init 00000004__initcall_mydriver_func2。

以及功能的执行: #cat/sys/kernel/debug/tracting/trace|grep driver_func SWAPPER/0-1[000]....。0.059520:INITCALL_START:FUNC=mytherdriver_FUNC+0x0/0x8 SWAPPER/0-1[000]....。0.059530:INITCALL_FINISH:FUNC=mytherDRIVER_FUNC+0x0/0x8 ret=0 SWAPPER/0-1[000]....。0.059545:INITCALL_START:FUNC=MYDRIVER_FUNC+0x0/0x8 SWAPPER/0-1[000]....。0.059555:INITCALL_FINISH:FUNC=MYDRIVER_FUNC+0x0/0x8 ret=0。

到目前为止,我们知道将函数创建为initcall将在每个驱动程序中创建特定于initcall级别的部分(postcore_initcall=>;.initcall2.init),并且此特定级别的每个initcall将根据Makefile顺序在最终内核映像中排序。

但是内核是如何对它们之间的所有initcall级别进行排序的呢?相对于其他initcall,postcore initcall是在什么时候执行的?它是怎么处理的?让我们来找出..。

如果您还记得,每种类型的initcall都有一个ID。这是排序的关键。在上述部分之后,我们知道每种类型的initcall将根据其ID具有不同的区段名称:.initcall1.init、.initcall2.init等。

Initcall排序的主要实现在init/main.c中完成。是的,真的,您看到的是Linux内核代码中的init/main.c!

Initcall_level是一个数组,其中每个条目都是该特定级别的指针。Initcall_level[]包含不同的__initcall<;n>;_start。

Extern initcall_entry_t__initcall_start[]; Extern initcall_entry_t__initcall0_start[]; Extern initcall_entry_t__initcall1_start[]; Extern initcall_entry_t__initcall2_start[]; Extern initcall_entry_t__initcall3_start[]; Extern initcall_entry_t__initcall4_start[]; Extern initcall_entry_t__initcall5_start[]; Extern initcall_entry_t__initcall6_start[]; Extern initcall_entry_t__initcall7_start[]; Extern initcall_entry_t__initcall_end[]; 静态initcall_entry_t*initcall_level[]__initdata={ __initcall0_start, __initcall1_start, __initcall2_start, __initcall3_start, __initcall4_start, __initcall5_start, __initcall6_start, __initcall7_start, __initcall_end, };

我们已经知道initcall是一种将所选函数放在特定目标文件部分的机制。这些将在引导时迭代。要做到这一点,内核必须以某种方式知道它们的实际位置。这是通过链接器使用脚本实现的,该脚本创建__initcall<;n>;_start符号(include/asm-Generic/vmlinux.lds.h):

.init.data:at(ADDR(.init.data)-0) __initcall_start=.;Keep(*(.initcallearly.init)) __initcall0_start=.;Keep(*(.initcall0.init)) __initcall1_start=.;Keep(*(.initcall1.init)) __initcall2_start=.;Keep(*(.initcall2.init)) __initcall3_start=.;Keep(*(.initcall3.init)) __initcall4_start=.;Keep(*(.initcall4.init)) __initcall5_start=.;Keep(*(.initcall5.init)) __initcallrootfs_start=.;Keep(*(.initcallrootfs.init)) __initcall6_start=.;Keep(*(.initcall6.init)) __initcall7_start=.;Keep(*(.initcall7.init)) __initcall_end=。

如果不是链接器脚本专家,我们可以假设__initcall2_start条目指向目标文件中.initcall2.init节的第一个地址。

将处理所有可能的initcall级别的主要函数称为do_initcall(),可在init/main.c中找到:

静态void__init do_basic_setup(Void) { [.] Do_initcall(); } 静态void__init do_initcall(Void) { INT级别; [.] For(level=0;level<;array_size(Initcall_Level)-1;level++){ [.] Do_initcall_level(级别,命令行); } }。

此函数处理此数组中的所有级别。简单介绍COMMAND_LINE参数,它只是普通命令行的副本,可以包含模块的参数。此函数调用另一个函数do_initcall_level,其中代码(简化)如下:

静态void__init do_initcall_level(int level,char*command_line) { Initcall_entry_t*fn; [.] FOR(fn=initcall_level[level];fn<;initcall_level[level+1];fn++) Do_one_initcall(initcall_from_entry(Fn)); }

由于函数do_one_initcall,上面的函数(Do_Initcall_Level)调用特定级别的所有initcall。由于initcall_entry_t上的for循环,它将通过do_one_initcall函数执行包含顺序存储的函数指针的所述部分的地址。换句话说,在此for循环期间,fn的第一个值是由__initcall2_start提供的地址(对应于找到的第一个.initcall2.init部分)。所有部分都根据其在Makefile中的顺序进行组织。此for循环将迭代所有地址(fn++)。此代码在迭代所有initcall2.init部分之后传递所有地址的参数:

$objdump-t vmlinux.o|grep.initcall2.init 00000000 l O.initcall2.init 00000004__initcall_ATOM_POOL_INIT2 00000004 l O.initcall2.init 00000004__initcall_mvebu_soc_device2 00000008 l O.initcall2.init 00000004__initcall_Coherency_Late_Init2 0000000c l O.initcall2.init 00000004__initcall_imx_mmdc_init2 00000010 l O.initcall2.init 00000004__initcall_omap_hwmod_setup_all2 [.] 0000007c l O.initcall2.init 00000004__initcall_mypostcore_init2 00000080 l O.initcall2.init 00000004__initcall_RockChip_grf_init2 [.]。

Int_init_or_module do_one_initcall(Initcall_T Fn){ Int ret; [.] Do_trace_initcall_start(Fn); Ret=Fn(); Do_trace_initcall_Finish(fn,ret); [.] 返回RET; }。

开始/结束跟踪函数的使用(参见第一篇POST中关于initcall的调试部分)。

总之,initcall_level是一个数组,其中包含所有initcall级别的initcall<;n>;start列表。它们对应于第一个地址,即将用于每个级别的第一个.initcall<;n>;.init部分。再次以postcore_initcall为例。编译的第一个initcall2.init(取决于Makefile顺序)的地址将与initcall2_start指向的地址相同。在do_one_initcall()中,它将是第一个执行的函数。然后,使用do_initcall_level()的for循环,它将转到下一个函数指针的地址(多亏了fn++),依此类推,直到它到达所有initcall2的末尾。然后,多亏了do_initcall(),它将进入下一个级别,即initcall3。

如果您查看所有initcall定义,在postcore_initcall()的情况下,所有内容都基于一个ID。但是在rootfs_initcall()的情况下,该ID是一个字符串rootfs。让我们来看看这个特别的初始呼叫。

在init文件夹中,我们可以注意到它主要是从initramfs或块设备挂载rootfs。

根据我们之前看到的,我们将有一个带有相应函数指针的对象文件部分,这取决于内核的配置中是否启用了初始RAM文件系统支持。

$objdump-t vmlinux.o|grep.initcallrootfs 00000000 l%d.initcallrootfs.init 00000000.initcallrootfs.init 00000000 l.initcallrootfs.init 00000000$d 00000000 l O.initcallrootfs.init 00000004__initcall_panate_rootfsrootfs。

如果您还记得本博客文章系列的前一部分中关于initcall的内容,那么使用module_init()允许将模块作为device_initcall执行,以防它们被编译为内置的。在可加载模块的情况下,该函数将在模块插入时执行。代码如下:

#定义arly_initcall(Fn)module_init(Fn) #定义core_initcall(Fn)module_init(Fn) #定义postcore_initcall(Fn)module_init(Fn) #定义ARCH_initcall(Fn)module_init(Fn) #定义subsys_initcall(Fn)module_init(Fn) #定义fs_initcall(Fn)module_init(Fn) #定义rootfs_initcall(Fn)module_init(Fn) #定义device_initcall(Fn)module_init(Fn) 。#定义late_initcall(Fn)module_init(Fn) #定义console_initcall(Fn)module_init(Fn) /*每个模块必须使用一个module_init()。*/ #定义MODULE_INIT(Initfn)\ 静态内联initcall_t__可能_未使用__inittest(Void)\ {return initfn;}\ Int init_module(Void)__copy(Initfn)__attribute__((alias(#initfn);

我们已经看到了不可加载模块的情况(即第1部分中的#ifndef模块),所以让我们快速了解一下可以加载模块的情况。所有的initcall都被一个单独的定义所取代:module_init()。该宏正在创建init_module作为我们函数的别名。对于模块,添加了额外的代码部分,以将init_module别名添加到结构模块的.init字段。函数do_init_module()在插入时通过syscall调用。如果仔细观察,该函数正在使用我们已经讨论过的函数:

静态noinline int do_init_module(struct module*mod) { [.] /*启动模块*/ IF(mod->;init!=NULL) RET=do_one_initcall(mod-gt;init); [.]。

此函数使用前面的do_one_initcall()函数和mod->;init作为initcall的函数来执行!由于一些modpost脚本处理的额外代码,.init=init_module和init_module是我们函数的别名。

总而言之,当加载可加载模块时,初始化模块插入的syscall将调用作为initcall传入的module_init()函数。为了使其更通用,它使用别名(Init_Module)来指向这个特定的函数,并使用init字段来模块的结构。多亏了syscall机制,这意味着当您加载模块时,syscall将执行do_init_module(),该模块将使用现有的do_one_initcall()直接执行我们的函数。

为了避免再次编写我们已经看到的关于initcall实现的所有机制,我将用一张图来总结交互/实现。

而且,就是这样!我们在这两篇关于initcall的文章中看到了很多东西。我希望你喜欢读这篇文章,就像我喜欢写它一样。看看Linux内核的main.c文件太酷了,不是吗?!

请勾选此框以确认您已阅读并接受我们关于收集/存储和使用您的个人数据的隐私声明条款:*