linux内核模块初始化
内核启动过程中需要完成各个部分的初始化,⽐如中端、页⾯管理、slab分配器、任务调度器、⽹络、PCI设备等等的初始化,这些初始化⼤概可以分为两种:
⼀种是关键的,必须完成的⽽且必须以特定的顺序来完成的初始化,这部分的代码往往是直接便如内核的⽽且是直接调⽤的
另⼀种是⾮关键的的⼦系统(或者说模块、功能)的初始化,这部分根据配置可以不加载,可以以built-in的⽅式编到内核的可执⾏⽂件中,也可以以模块的⽅式加载。但是对于这⼀类来说,它们也需要内核的关键⼦系统的⽀持,甚⾄在它们之间也存在某种依赖或者说顺序关系,因此它们的初始化需要以另⼀种⽅式来实现。
对于第⼆类的⼦系统,内核在do_initcalls完成它们的初始化,在这之前内核会完成第⼀类⼦系统的初始化。do_initcalls的调⽤时机为start_kernel->rest_init->kernel_init->do_basic_setup->do_initcalls
⼀、do_initcalls
1.1 实现⽅式
该函数的实现体现了linux对第⼆类⼦系统进⾏初始化的⽅式的设计思想。Linux提供了不同的“初始化等级”,linux会按照等级从⾼(level 0)到低(level 7)来进⾏初始化,也就是说会⾸先初始化等级⾼的,然后初始化等级低的。其代码逻辑很简单:
[cpp]
1. int level;
2.
3.
4. for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
5.    do_initcall_level(level);
因此,⼀个模块只需要将⾃⼰的初始化函数定义为某个等级,do_initcalls在进⾏该等级的初始化时,就会调⽤该模块的初始化函数。
为了⽅便使⽤,内核使⽤了如下的宏来定义各个初始化等级:
[cpp]
1. #define __define_initcall(fn, id) \
2.    static initcall_t __initcall_##fn##id __used \
3.    __attribute__((__section__(".initcall" #id ".init"))) = fn
4.
5.
6. /*
7.  * Early initcalls run before initializing SMP.
8.  *
9.  * Only for built-in code, not modules.
10.  */
11. #define early_initcall(fn)      __define_initcall(fn, early)
12.
13.
14. /*
15.  * A "pure" initcall has no dependencies on anything else, and purely
16.  * initializes variables that couldn't be statically initialized.
17.  *
18.  * This only exists for built-in code, not for modules.
19.  * Keep main.c:initcall_level_names[] in sync.
20.  */
21. #define pure_initcall(fn)      __define_initcall(fn, 0)
22.
23.
24. #define core_initcall(fn)      __define_initcall(fn, 1)
25. #define core_initcall_sync(fn)      __define_initcall(fn, 1s)
26. #define postcore_initcall(fn)      __define_initcall(fn, 2)
27. #define postcore_initcall_sync(fn)  __define_initcall(fn, 2s)
28. #define arch_initcall(fn)      __define_initcall(fn, 3)
29. #define arch_initcall_sync(fn)      __define_initcall(fn, 3s)
30. #define subsys_initcall(fn)    __define_initcall(fn, 4)
31. #define subsys_initcall_sync(fn)    __define_initcall(fn, 4s)
32. #define fs_initcall(fn)        __define_initcall(fn, 5)
33. #define fs_initcall_sync(fn)        __define_initcall(fn, 5s)
34. #define rootfs_initcall(fn)    __define_initcall(fn, rootfs)
35. #define device_initcall(fn)    __define_initcall(fn, 6)
36. #define device_initcall_sync(fn)    __define_initcall(fn, 6s)
37. #define late_initcall(fn)      __define_initcall(fn, 7)
38. #define late_initcall_sync(fn)      __define_initcall(fn, 7s)
39.
40.
41. #define __initcall(fn) device_initcall(fn)
上述定义针对的是⾮模块,即要编译到内核的影响中的⼦系统,如果是模块,则定义变成了:
[cpp]
1. #define early_initcall(fn)      module_init(fn)
2. #define core_initcall(fn)      module_init(fn)
3. #define postcore_initcall(fn)      module_init(fn)
4. #define arch_initcall(fn)      module_init(fn)
5. #define subsys_initcall(fn)    module_init(fn)
6. #define fs_initcall(fn)        module_init(fn)
7. #define device_initcall(fn)    module_init(fn)
8. #define late_initcall(fn)      module_init(fn)
9.
10.
11. #define security_initcall(fn)      module_init(fn)
12.
linux内核文件放在哪
13.
14. /* Each module must use one module_init(). */
15. #define module_init(initfn)                \
16.    static inline initcall_t __inittest(void)      \
17.    { return initfn; }                  \
18.    int init_module(void) __attribute__((alias(#initfn)));
顺便提⼀句,module_init在不⽀持模块的系统中会变成:
#define __initcall(fn) device_initcall(fn)
#define module_init(x)  __initcall(x);
1.2 内存安排
do_initcalls是从特定的内存区域取出初始化函数的指针,然后来调⽤它的,因⽽上边定义的各个等级的初始化函数都应该被放在特定的内存区域,每个等级的初始化函数都被放在⾃⼰特定的初始化区域中。
Linux提供了⼀个头⽂件“vmlinux.lds.h”,该⽂件定义了⼀些宏⽤于辅助写连接脚本,从其中可以看到最终会出现在连接脚本中的各个内存section以及它们的相对位置,定义各个“初始化等级”所在的内存section的相关代码如下:
[cpp]
1. #define INIT_DATA_SECTION(initsetup_align)              \
2.    .init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {      \
3.        INIT_DATA                      \
4.        INIT_SETUP(initsetup_align)            \
5.        INIT_CALLS                      \
6.        CON_INITCALL                        \
7.        SECURITY_INITCALL                  \
8.        INIT_RAM_FS                    \
9.    }
该宏定义了初始化数据段,其中就包括了INIT_CALLS段。
1. #define INIT_CALLS_LEVEL(level)                    \
2.        VMLINUX_SYMBOL(__initcall##level##_start) = .;      \
3.        *(.initcall##level##.init)              \
4.        *(.initcall##level##s.init)            \
5.
6.
7. #define INIT_CALLS                          \
8.        VMLINUX_SYMBOL(__initcall_start) = .;          \
9.        *(.initcallearly.init)                  \
10.        INIT_CALLS_LEVEL(0)                \
11.        INIT_CALLS_LEVEL(1)                \
12.        INIT_CALLS_LEVEL(2)                \
13.        INIT_CALLS_LEVEL(3)                \
14.        INIT_CALLS_LEVEL(4)                \
15.        INIT_CALLS_LEVEL(5)                \
16.        INIT_CALLS_LEVEL(rootfs)                \
17.        INIT_CALLS_LEVEL(6)                \
18.        INIT_CALLS_LEVEL(7)                \
19.        VMLINUX_SYMBOL(__initcall_end) = .;
该宏即定义了各个初始化等级所在的section,⽐如INIT_CALLS_LEVEL(5)会被展开为:
  VMLINUX_SYMBOL(__initcall5_start) =.;
  *(.initcall5.init)
*(.initcall5s .init)
__initcall5_start就是会被do_initcalls使⽤的⼀个变量,它被do_initcalls⽤于定位初始化等级5的起始地址,进⽽到定义为初始化等级5的所有初始化函数,并调⽤它们。
同时从INIT_CALLS中也可以看出,所有等级的初始化函数都被放在从__initcall_start到__initall_end之间的内存区域中。
其它的内存区域的定义也可以在头⽂件“vmlinux.lds.h”中到,如果感兴趣的话可以查看该⽂件。
⼆、初始化参数
2.1 初始化参数的定义和初始化时机
Linux允许⽤户在启动时指定⼀些内核选项(也就是启动参数),这些参数会被传递给相应的处理函数。
有三种⽅式来定义内核选项:
1. 使⽤宏early_param(str, fn),对于模块,该宏被定义为空
2. 使⽤宏__setup(str, fn),对于模块,该宏被定义为空
3. 使⽤宏module_param(name, type, perm)
其中前两种⽅式中,str为要定义的选项的名字,其值(即“=”后的部分)将会被传递给fn来处理。第三种⽅式没有函数选项,这是因为在新的内核中,内核为每⼀种数据类型提供了⼀个kernel_param_ops结构,该结构保存了对应该数据类型的set/get/free函数,因⽽不需要每个模块提供⾃⼰的函数,如果需要定义⼀个内核选项,只需要提供名字,类型以及期望的访问权限即可。
采⽤第⼀种⽅式定义的内核选项会在内核启动的开始阶段被parse_early_param处理。⽽其它两种⽅式则会由下边的调⽤处理:
1. parse_args("Booting kernel", static_command_line, __start___param,
2.            __stop___param - __start___param,
3.            -1, -1, &unknown_bootoption);
parse_args的参数的含义:
第⼀个参数:⼀个字符串,⽤于提⽰正在解析的内核选项
第⼆个参数:命令⾏参数
第三个参数:要解析的参数的⾸地址。要解析的参数就是通过module_param定义的参数,它们会被存放在__param段中,下⾯会给出其相关定义
第四个参数:要解析多少个参数
第五个参数和第六个参数:参数的最⼩等级和最⼤等级,只有落在这个等级范围内的参数才会被解析
最后⼀个参数是⼀个函数:⽤于指定如果不能识别某个参数则应该由谁进⾏处理,这⾥使⽤了unknown_bootoption,它会处理使⽤__setup(str, fn)定义的参数
实际上parse_early_param最终也会调⽤parse_args,但在它的调⽤中,对于不能识别的参数使⽤的处
理函数是do_early_param。module_param最终会通过下⾯的宏定义;
[cpp]
1. #define __module_param_call(prefix, name, ops, arg, perm, level)    \
2.    /* Default value instead of permissions? */        \
3.    static int __param_perm_check_##name __attribute__((unused)) =  \
4.    BUILD_BUG_ON_ZERO((perm) < 0 || (perm) > 0777 || ((perm) & 2))    \
5.    + BUILD_BUG_ON_ZERO(sizeof(""prefix) > MAX_PARAM_PREFIX_LEN);    \
6.    static const char __param_str_##name[] = prefix #name;      \
7.    static struct kernel_param __moduleparam_const __param_##name  \
8.    __used                              \
9.    __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \
10.    = { __param_str_##name, ops, perm, level, { arg } }
2.2 初始化参数的内存安排
通过early_param和__setup定义的初始化参数会被放在.init.setup段中,具体的代码可以查看头⽂件"init.h"。.init.setup段的定义也在头⽂件“vmlinux.lds.h”中,如下所⽰
[cpp]
1. #define INIT_SETUP(initsetup_align)                \
2.        . = ALIGN(initsetup_align);            \
3.        VMLINUX_SYMBOL(__setup_start) = .;          \
4.        *(.init.setup)                      \
5.        VMLINUX_SYMBOL(__setup_end) = .;
从上述内存段的定义可以看出,该宏定义的内存区域起始于__setup_start,结束于__setup_end。
通过module_param定义的参数会被放在.para段中,该段同样可以在头⽂件“vmlinux.lds.h”中到,如下所⽰:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。