目录
- 1、module_init宏
- 1.1 展开
- 1.2 解释以下几个标识
- 1.2.1 fn
- 1.2.2 id
- 1.2.3 类型 initcall_t :
- 1.2.4 __used
- 1.2.5 __init
- 1.2.6 __attribute__
- 1.3 实例说明
- 2、 驱动启动机制
- 2.1 initcall_t 类型的数组
- 2.2.1 __initcallx_start数组
- 2.2.2 initcall_levels[]数组
- 2.3 编译器向数组填入初始化函数指针
- 3、总结
1、module_init宏
头文件在include/linux/init.h
在一个驱动模块中,常会写下如下的代码:
int __init lcd_init(void)
{
/*做一些工作*/
return 0;
}
module_init(lcd_init);
常说用module_init()宏来标识初始化函数lcd_init(),函数lcd_init是这个驱动模块的一个入口。这里就来解释,这个入口是如何实现的。
1.1 展开
首先,把宏module_init()一层层展开,来看这个宏到底做了什么。
#define module_init(x) __initcall(x);
参数:
x:在内核启动时或模块插入时运行的函数
*module_init()将在do_initcalls()期间(如果是内置的)或在模块插入时(如果是模块)调用。每个模块只能有一个。
这个宏是如何在内核初始化时被调用的,或是在模块被插入时(insmod命令)被调用的。带着这个疑问,把宏展开:
#define module_init(x) __initcall(x);
->
\qquad
\qquad
\qquad
#define __initcall(fn) device_initcall(fn)
->
\qquad
\qquad
\qquad
\qquad
\qquad
\qquad
#define device_initcall(fn) __define_initcall(fn, 6)
最后,如下:
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
1.2 解释以下几个标识
1.2.1 fn
\qquad fn是初始化函数的名称
1.2.2 id
\qquad id是一个整数值,用于标识初始化函数所属的组别。
1.2.3 类型 initcall_t :
typedef int (*initcall_t)(void);
这是一个函数指针类型,这个指针指向的函数型如: int fun(void);
1.2.4 __used
__used宏在Linux内核中的定义如下:
#ifdef __GNUC__
#define __used __attribute__((__used__))
#else
#define __used
#endif
\qquad
在这个定义中,首先通过#ifdef GNUC__判断是否使用的是GNU C编译器(gcc)。如果是,则使用__attribute((used))来标记变量或函数为“已使用”。
\qquad
这个特殊的属性告诉编译器和链接器,这些标记为未使用的代码或数据是有意的,不应该被删除或警告。
如果不是使用的GNU C编译器,则直接定义__used为空,即不做任何操作。
1、防止被一些编译器或工具误删:有时候,编译器或其他代码优化工具会删除被认为未使用的代码或数据,这可能导致一些意外的行为或错误。通过使用__used宏,可以确保这些代码或数据不会被误删。
2、用于强制链接:在某些情况下,可能需要强制链接某些未使用的模块或函数。通过使用__used宏,可以告诉链接器,这些标记为未使用的代码或数据也需要被链接进最终的可执行文件或模块中。
\qquad 需要注意的是,使用__used宏标记的代码或数据应该是确切知道未使用的,否则可能会导致不必要的内存消耗或其他潜在问题。 总而言之,__used宏用于在特定情况下标记某些代码或数据为有意的未使用,以防止编译器或链接器对其进行删除或警告。
1.2.5 __init
头文件 include / linux/init.h
宏定义
#define __init __section(.init.text) __cold notrace
\qquad
这段宏定义用于在Linux内核中标记函数为初始化函数。让我们逐个解释这个宏的各个部分:
1. 功能上__init这是一个宏,用于告诉编译器将函数放置在特定的代码段中。在内核中,.init.text是一个特殊的代码段,用于存放初始化函数。通过使用这个宏,可以将函数放置在这个代码段中。
2. __section(.init.text): 用于将函数放置在.init.text代码段中。.init.text代码段在内核启动时会被加载和执行。
3. __cold: 这是一个函数属性,用于告诉编译器这个函数很少被执行,因此可以进行一些优化。__cold属性通常用于初始化函数,因为这些函数在内核启动后只会被执行一次。
4. notrace: 这是一个函数属性,用于告诉编译器不要在这个函数中插入任何跟踪代码。跟踪代码通常用于调试和性能分析,但对于初始化函数来说,通常不需要进行跟踪。
\qquad
综上所述,这个宏定义的作用是将函数标记为初始化函数,并将其放置在特定的代码段中。同时,通过使用__cold和notrace属性,可以告诉编译器进行一些优化,并避免在这个函数中插入跟踪代码。
1.2.6 attribute
attribute((section(“.initcall” #id “.init”))): 这是一个属性声明,用于将初始化函数放置在特定的代码段中。#id是一个预处理器的字符串化操作,将id参数转换为字符串。这样,初始化函数就会被放置在名为.initcallX.init的代码段中,其中X是id的值。
1.3 实例说明
如果我们用一个实例来说明,比较清楚的知道展开后是什么样的
module_init(lcd_init);
-> __define_initcall(lcd_init , 6);
展开后就成如下的样子
static initcall_t __initcall_lcd_init6 __used
__attribute__((__section__(".initcall6.init"))) = lcd_init;
展开后的结果是定义了一个名为__initcall_lcd_init6的静态变量,该变量的类型是initcall_t(函数指针类型),并将其初始化为lcd_init函数的地址。这个变量被放置在名为.initcall6.init的代码段中。
2、 驱动启动机制
\qquad 因此,可以看到,在Linux内核中,module_init()宏用于定义内核模块初始化时需要调用的函数。该宏的展开后,会创建一个静态的初始化调用函数指针变量,这个变量用于保存需要在内核模块加载时执行的函数。
\qquad 这个函数指针变量的目的是为了在内核启动过程中的初始化阶段,通过调用这个函数进行模块的初始化工作。这个函数指针变量会被添加到内核的初始化调用链表中,并在适当的时候被调用。 实际上,当内核加载一个模块时,会遍历这个初始化调用链表,并按照函数指针的顺序依次调用这些初始化函数。
2.1 initcall_t 类型的数组
2.2.1 __initcallx_start数组
\qquad
在Linux3.14的内核中,初始化函数是通过一系列不同组别的initcall_t
类型的数组组成的。如下:
extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];
数组名后带有数字分组标识。这正好与前面讲过的module_init()展开后也会生成一个带组别的变量就对应起来了,(截图说明):
2.2.2 initcall_levels[]数组
初始化调用链表来管理的。这个链表是一个全局变量数组,称为__initcall_levles
。这个变量定义在init/main.c
文件中。
数组元素是上面提到的__initcallx_start数组。
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
2.3 编译器向数组填入初始化函数指针
\qquad 初始化函数被添加到初始化调用链表的过程是在内核编译阶段完成的。
\qquad
这些初始化函数的地址是在编译期间通过特定的代码生成工具(如scripts/link-vmlinux.sh
)生成的。这些工具会扫描内核源代码中对__define_initcall宏的调用,并将生成的初始化函数地址放入对应的数组中。
因此,__initcall_start[]等数组的值是由编译过程中的代码生成工具填入的。在Linux 3.14中,这些数组存储了所有需要在内核初始化过程中调用的初始化函数的地址。
\qquad
这些宏定义的初始化函数会在运行时被调用,完成相应的初始化工作。
在init/main.c文件中,有一个函数do_initcalls()
,它会遍历初始化调用链表,并按照优先级依次调用其中的初始化函数。
\qquad
这个函数的定义如下:
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
static void __init do_initcall_level(int level)
{
extern const struct kernel_param __start___param[], __stop___param[];
initcall_t *fn;
strcpy(initcall_command_line, saved_command_line);
parse_args(initcall_level_names[level],
initcall_command_line, __start___param,
__stop___param - __start___param,
level, level,
&repair_env_string);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}
int __init_or_module do_one_initcall(initcall_t fn)
{
int count = preempt_count();
int ret;
char msgbuf[64];
if (initcall_debug)
ret = do_one_initcall_debug(fn);
else
ret = fn();
msgbuf[0] = 0;
if (preempt_count() != count) {
sprintf(msgbuf, "preemption imbalance ");
preempt_count_set(count);
}
if (irqs_disabled()) {
strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
local_irq_enable();
}
WARN(msgbuf[0], "initcall %pF returned with %s\n", fn, msgbuf);
return ret;
}
\qquad
do_initcalls()
函数会遍历链表中的每个元素,检查初始化函数是否存在并且没有被列入黑名单。然后,它会调用do_one_initcall()
函数来执行初始化函数。这个函数会将初始化函数作为参数传递,并执行它。
3、总结
\qquad
总结起来,初始化调用链表是一个全局变量,用于管理初始化函数。链表的元素是initcall_t类型的结构体,包含初始化函数的指针和优先级值。在init/main.c文件中的do_initcalls()
函数会遍历链表,并按照优先级依次调用其中的初始化函数。