当前位置: 首页 > 科技观察

Linux内核如何巧妙的初始化各个模块

时间:2023-03-13 00:05:22 科技观察

相信很多在研究Linux内核源码的同学经常会发现有些模块的初始化函数找不到调用者,比如下面网络模块的初始化函数://net/ipv4/af_inet.cstaticint__initinet_init(void){.../**SettheIPmoduleup*/ip_init();/*SetupTCPslabcacheforopenrequests.*/tcp_init();/*SetupUDPmemorythreshold*/udp_init();...}fs_initcall(inet_init);即便是搜索整个内核代码,也找不到哪里调用了这个函数,那么这个函数是怎么调用的呢?秘密就在这个函数之后的代码行中:fs_initcall(inet_init);这行代码中,fs_initcall是一个宏,具体定义如下:".init")))=fn;...#define__define_initcall(fn,id)___define_initcall(fn,id,.initcall##id)...#definefs_initcall(fn)__define_initcall(fn,5)宏后展开,上述宏调用的结果大致如下:staticinitcall_t__initcall_inet_init5__attribute__((__section__(".initcall5.init")))=inet_init;fs_initcall宏最后定义了一个静态变量,其类型为initcall_t,其值为宏参数所代表的函数地址。initcall_t类型定义如下:typedefint(*initcall_t)(void);从上面可以看出,initcall_t是一个函数指针类型,它定义的变量会指向一个参数必须为空,返回类型必须为int的函数。我们可以再看看上面的inet_init方法,它确实满足了这些要求。综上所述,fs_initcall宏定义了一个变量__initcall_inet_init5,其类型为initcall_t,其值为inet_init函数的地址。说到这里,相信很多同学都会想到Linux内核调用inet_init函数一定要用这个变量吧?对与错。是的,因为内核确实是通过变量指向的内存获取inet_init方法的地址,调用该方法。是错误的,因为内核并没有通过上面的__initcall_inet_init5变量来访问这块内存。那么如果没有这个变量,是否可以通过其他方式访问这块内存呢?当然,这是Linux内核设计的巧妙之处。我们再看一下上述宏展开后静态变量__initcall_inet_init5的定义。在这个定义中,有一些代码如下:__attribute__((__section__(".initcall5.init")))这部分代码不属于c语言标准,而是gcc对c语言的扩展。它的作用是声明该变量属于.initcall5.init段。所谓段,可以简单理解为对程序占用的内存区域的一种布局和规划。例如,我们的公共部分有.text来存储我们的代码,以及.data或.bss来存储我们的变量。通过这些段的定义,我们可以把程序中相关的函数放在同一个内存区域,方便内存管理。除了这些默认的section,我们还可以通过gcc的属性自定义section,这样我们就可以把相关的函数或者变量放在同一个section中。例如,上面的__initcall_inet_init5变量属于自定义部分.initcall5.init。在定义了这些段之后,我们可以在链接脚本中告诉链接器这些段在内存中的位置和布局是什么样的。对于x86平台,内核的链接脚本为:arch/x86/kernel/vmlinux.lds.S在这个脚本中,定义了.initcall5.init这些段,具体逻辑如下://include/asm-generic/vmlinux.lds.h#defineINIT_CALLS_LEVEL(level)\__initcall##level##_start=.;\KEEP(*(.initcall##level##.init))\KEEP(*(.initcall##level##s.init))\#defineINIT_CALLS\__initcall_start=.;\KEEP(*(.initcallearly.init))\INIT_CALLS_LEVEL(0)\INIT_CALLS_LEVEL(1)\INIT_CALLS_LEVEL(2)\INIT_CALLS_LEVEL(3)\INIT_CALLS_LEVEL(4)\INIT_CALLS_LEVEL(5)\INIT_CALLS_LEVEL(rootfs)\INIT_CALLS_LEVEL(6)\INIT_CALLS_LEVEL(7)\__initcall_end=.;从上面可以看出,initcall相关的部分有很多。上面示例中的.initcall5.init只是其中之一。此外,还有.initcall0.init、.initcall1.init等段,这些段通过宏INIT_CALLS_LEVEL定义了它们的处理规则。同一层的段放在同一个内存区,不同层段的内存区按照层的大小依次连接在一起。上面的__initcall_inet_init5变量,它的section是.initcall5.init,level是5。假设我们还有其他方法调用了宏fs_initcall,那么这个宏为这个方法定义的静态变量所属的section也是。initcall5.init,级别也是5。由于这个变量和__initcall_inet_init5变量所属的initcall在同一级别,所以他们连续放在同一个内存区。也就是说,这些5级静态变量占用的内存区域是连续的,而且由于这些变量的类型都是initcall_t,所以正好组成了一个initcall_t类型的数组,而数组的起始地址也定义在INIT_CALLS_LEVEL宏,它是__initcall5_start。如果我们要调用这些级别为5的initcalls,只需要先获取__initcall5_start地址,将其作为元素类型为initcall_t的数组的起始地址,然后遍历数组中的元素,得到函数元素对应的指针,则可以通过这个指针调用对应的函数。来看下具体代码://init/main.cexterninitcall_entry_t__initcall_start[];externinitcall_entry_t__initcall0_start[];externinitcall_entry_t__initcall1_start[];externinitcall_entry_t__initcall2_start[];externinitcall_entry_t__initcall3_start[];externinitcall_entry_t__initcall4_start[];externinitcall_entry_t__initcall5_start[];externinitcall_entry_t__initcall6_start[];externinitcall_entry_t__initcall7_start[];externinitcall_entry_t__initcall_end[];staticinitcall_entry_t*initcall_levels[]__initdata={__initcall0_start,__initcall1_start,__initcall2_start,__initcall3_start,__initcall4_start,__initcall5_start,__initcall6_start,__initcall7_start,__initcall_end,};staticvoid__initdo_initcall_level(intlevel){initcall_ent=in...forlevelt;fn