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

系统 Linux
相信很多在研究linux内核源码的同学,经常会发现一些模块的初始化函数找不到调用者,比如下面的网络模块的初始化函数。一起来看看吧。

 [[378352]]

相信很多在研究linux内核源码的同学,经常会发现一些模块的初始化函数找不到调用者,比如下面的网络模块的初始化函数: 

// net/ipv4/af_inet.c  
static int __init inet_init(void)  
 
        ...  
        /*  
         *      Set the IP module up  
         */  
        ip_init();  
        /* Setup TCP slab cache for open requests. */  
        tcp_init();  
        /* Setup UDP memory threshold */  
        udp_init();  
        ...  
 
fs_initcall(inet_init); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

即使你在整个内核代码中搜索,也找不到任何地方调用这个函数,那这个函数到底是怎么调用的呢?

秘密就在这个函数之后的一行代码里:

fs_initcall( inet_init); 
  • 1.

在该行代码中,fs_initcall是一个宏,具体定义如下: 

// include/linux/init.h  
#define ___define_initcall(fn, id, __sec) \  
        static initcall_t __initcall_##fn##id __used \  
                __attribute__((__section__(#__sec ".init"))) = fn;  
...  
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)  
...  
#define fs_initcall(fn)                 __define_initcall(fn, 5) 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

在该宏展开后,上面宏调用的结果,大致像下面这个样子:

static initcall_t __initcall_inet_init5 __attribute__((__section__(".initcall5.init"))) = inet_init; 
  • 1.

由上可见,fs_initcall宏最终是定义了一个静态变量,该变量的类型是initcall_t,值是宏参数表示的函数地址。

initcall_t类型的定义如下:

typedef int (*initcall_t)(void); 
  • 1.

由上可见,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"))) 
  • 1.

该部分代码并不属于c语言标准,而是gcc对c语言的扩展,它的作用是声明该变量属于 .initcall5.init这个section。

所谓section,我们可以简单的理解为对程序所占内存区域的一种布局和规划,比如我们常见的 section有 .text用来存放我们的代码,.data或.bss用来存放我们的变量。

通过这些section的定义,我们可以把程序中的相关功能放到同一块内存区域中,这样来方便内存管理。

除了这些默认的section之外,我们还可以通过gcc的attribute来自定义section,这样我们就可以把相关的函数或变量放到相同的section中了。

比如上面的__initcall_inet_init5变量就属于.initcall5.init这个自定义section。

在定义了这些section之后,我们可以在链接脚本中告诉linker,这些section在内存中的位置及布局是什么样子的。

对于x86平台来说,内核的链接脚本是:

arch/x86/kernel/vmlinux.lds.S 
  • 1.

在该脚本中,对.initcall5.init等这些section做了相关定义,具体逻辑如下: 

// include/asm-generic/vmlinux.lds.h  
#define INIT_CALLS_LEVEL(level)                                         \  
                __initcall##level##_start = .;                          \  
                KEEP(*(.initcall##level##.init))                        \  
                KEEP(*(.initcall##level##s.init))                       \  
#define INIT_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 = .; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

由上可见,initcall相关的section有很多,我们上面例子中的.initcall5.init只是其中一个,除此之外还有 .initcall0.init,.initcall1.init等等这些section。

这些section都是通过宏INIT_CALLS_LEVEL来定义其处理规则的,相同level的section被放到同一块内存区域,不同level的section的内存区域按level大小依次连接在一起。

对于上面的__initcall_inet_init5变量来说,它的section是.initcall5.init,它的level是5。

假设我们还有其他方法调用了宏fs_initcall,那该宏为该方法定义的静态变量所属的section也是.initcall5.init,level也是5。

由于该变量和__initcall_inet_init5变量所属的initcall的level都相同,所以它们被连续放在同一块内存区域里。

也就是说,这些level为5的静态变量所占的内存区域是连续的,又因为这些变量的类型都为initcall_t,所以它们正好构成了一个类型为initcall_t的数组,而数组的起始地址也在INIT_CALLS_LEVEL宏中定义了,就是__initcall5_start。

如果我们想要调用这些level为5的initcall,只要先拿到__initcall5_start地址,把其当成元素类型为initcall_t的数组的起始地址,然后遍历数组中的元素,获取该元素对应的函数指针,就可以通过该指针调用对应的函数了。

来看下具体代码: 

// init/main.c  
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[];  
static initcall_entry_t *initcall_levels[] __initdata = {  
        __initcall0_start,  
        __initcall1_start,  
        __initcall2_start,  
        __initcall3_start,  
        __initcall4_start,  
        __initcall5_start,  
        __initcall6_start,  
        __initcall7_start,  
        __initcall_end,  
};  
static void __init do_initcall_level(int level)  
 
        initcall_entry_t *fn;  
        ...  
        for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)  
                do_one_initcall(initcall_from_entry(fn));  
 
static void __init do_initcalls(void)  
 
        int level;  
        for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)  
                do_initcall_level(level);  

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

在上面的代码中,do_initcalls方法遍历了所有的合法level,对于每个level,do_initcall_level方法又调用了该level里所有函数指针指向的函数。

我们上面示例中的inet_init方法就属于level 5,也是在这里被调用到的。

linux内核就是通过这种方式来调用各个模块的初始化方法的,很巧妙吧。

最后我们再来总结下:

1. 在各模块的初始化方法之后,一般都会调用一个类似于fs_initcall(inet_init)的宏,该宏的参数是该模块的初始化方法的方法名。

2. 该宏展开后的结果是定义一个静态变量,该变量通过gcc的attribute来声明其所属的initcall level的section,比如inet_init方法对应的静态变量就属于.initcall5.init这个section。

3. 在linux的链接脚本里,通过INIT_CALLS_LEVEL宏告知linker,将属于同一level的所有静态变量放到连续的一块内存中,组成一个元素类型为initcall_t的数组,该数组的起始地址放在类似__initcall5_start的变量中。

4. 在内核的初始化过程中,会通过调用 do_initcalls方法,遍历各个level里的各个函数指针,然后调用该指针指向的方法,即各模块的初始化方法。

各个模块的初始化方法就是这样被调用的。

希望你喜欢。 

 

责任编辑:庞桂玉 来源: 良许Linux
相关推荐

2025-02-13 11:11:53

Redis哨兵代码

2025-01-03 08:53:59

2022-11-15 20:48:41

Linux

2017-09-13 14:28:02

Linux初始化系统运行级别

2011-03-16 10:52:20

2021-07-07 05:00:17

初始化源码

2012-03-13 13:38:42

Java

2009-06-10 16:17:00

Netbeans JT初始化

2011-03-31 16:46:10

LinuxMySQL

2023-11-12 23:08:17

C++初始化

2009-03-27 18:27:48

2020-11-23 14:22:17

代码Go存储

2012-05-23 12:46:53

JavaJava类

2010-09-08 14:49:09

蓝牙协议栈

2023-10-06 20:57:52

C++聚合成员

2009-09-02 16:52:55

C#数组初始化

2010-02-24 15:41:19

Linux Light

2013-01-18 11:05:25

Win 8操作系统

2013-03-04 11:10:03

JavaJVM

2022-08-03 11:00:20

Linux内核
点赞
收藏

51CTO技术栈公众号