概述
之前阅读Linux代码时,经常会发现这样的初始化方式;
static int __init my_module_init(void); // 用户的初始化函数
static int __init my_module_exit(void); // 用户的卸载/清理函数
module_init(my_module_init);
module_exit(my_module_exit);
其中module_init和module_exit是常见的注册和卸载模块到内核的宏,而my_module_init和my_module_exit则是用户所需的初始化模块和执行清理函数,事实上,除了module(属于device_init),还有其它类型的初始化,Linux将这些初始化分为七个等级,按照时间顺序/重要程度划分,在内核初始化阶段按先后顺序执行这些注册函数。
/*
* A "pure" initcall has no dependencies on anything else, and purely
* initializes variables that couldn't be statically initialized.
*
* This only exists for built-in code, not for modules.
* Keep main.c:initcall_level_names[] in sync.
*/
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
所有通过initcall注册在内核的函数,不需要在用户的代码中进行显示的调用(也无法被调用,因为内核和用户空间是隔断的),从而避免了传统自顶向下注册过程中,需要大量堆积显示调用初始化代码的弊端,使代码可读性变差(尤其是大型系统)。而是以一种自底向上传递形式,将初始化函数的函数指针存放在特定的内核空间中,在上电后,cpu会找到这些内存空间进行顺序调用初始化。
接下来,我将使用keil,试着手写一份简易的自底向上注册代码。
Flash区域划分
首先要确认我们的初始化函数需要存放在哪里,它最好是一个固定的位置,或者至少让程序能知道这个段落的位置。该行为通常在链接阶段进行确定,因为在编译阶段,编译器仅将程序打包成二进制可执行的代码,但如何存放到flash中需要由链接器指定,而链接器的默认行为又是由链接文件指定的:
对于keil,该文件通常是.sct文件(scatter);
对于iar,则是.icf文件;
对于gcc编译,则是.ld文件。
由于使用keil进行编译,这里以.sct文件做示范,定义了四个初始化区域,分别为等级0、1、2、3,后续将按照优先级顺序(数字从小到大)在上电后依次被初始化。
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x00000000 0x00040000 { ; load region,片上flash范围
ER_IROM1 0x00000000 0x00015000 { ; execution region,可执行代码区域
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
INIT_0 0x00014000 { ; 初始化等级0的区域
*(.init_0) ; 初始化等级0的区域标识,代码需要用该标识查找该区域
}
INIT_1 0x00014200 {
*(.init_1)
}
INIT_2 0x00014400 {
*(.init_2)
}
INIT_3 0x00014600 {
*(.init_3)
}
RW_IRAM1 0x20000000 0x00008000 { ; 运行内存区域
.ANY (+RW +ZI)
}
}
获取段落
按照上述方式划分flash段落后,keil会自动生成一些变量来标记段落的起始和结束位置。如在划分INIT_0段落后,keil就会生成两个数组,其数组名分别是INIT_0的起始和结束地址:
// init.h
#define SECTION_START(_section) extern uint32_t Load$$##_section##$$Base[]
#define SECTION_END(_section) extern uint32_t Load$$##_section##$$Limit[]
进一步,就可以声明以下变量,来获取不同目标段落的地址信息:
//init.c
SECTION_START(INIT_0);
SECTION_START(INIT_1);
SECTION_START(INIT_2);
SECTION_START(INIT_3);
SECTION_END(INIT_0);
SECTION_END(INIT_1);
SECTION_END(INIT_2);
SECTION_END(INIT_3);
或许会有疑问:为什么要这样获取INIT_0的地址呢?上面不是定义了INIT_0的地址为0x14000吗?直接使用0x14000不就行了吗?答案是也可以,但一方面是定义了段落后,就可以告诉链接器把某些代码放到该段落里,另一方面如果修改了段落地址后可以自动进行索引,不需要手动寻找并修改。
使用修饰器将指针存放到段落
编译器可以识别一些特殊的修饰指令,用于影响编译/链接的行为模式,如__attribute__()和#pragma,不同的编译器对这些指令支持的程度不同,在这里,我们用__attribute__指令创建一个宏,用于将初始化函数的指针存放到前面划分的flash区域:
// init.h
typedef void (*init_fn_t)(void);
#define INIT_EXPORT(fn, level) \
const init_fn_t _init_##fn \
__attribute__((used, section(".init_" #level))) = (init_fn_t)fn
其中used标识防止被编译器优化(可加可不加),section标识用于添加到对应段落,这里就和前面的段落划分相对应了,使用到.init_0 ~ .init_3这些段落,当我们想将某个初始化函数存储到某个段落时,就可以调用这个INIT_EXPORT宏,将该函数的指针存放到对应段落里。
为了进一步将不同段落的意义区分开,可以将该宏扩展为以下这些宏定义:
// init.h
#define core_initCall(fn) INIT_EXPORT(fn, 0) // 内核初始化
#define device_initCall(fn) INIT_EXPORT(fn, 1) // 设备初始化,主要是硬件部分
#define fs_initCall(fn) INIT_EXPORT(fn, 2) // 文件系统加载
#define app_initCall(fn) INIT_EXPORT(fn, 3) // 应用初始化
这样用户在编写模块时就可以根据模块的用途,调用不同的宏进行注册。
遍历获取函数指针
在做好所有的准备工作后,接下来只需要在上电时,获取这些段落中的初始化函数指针进行调用了。前面不是通过extern关键字获取了段落的起始和结束地址吗?现在就可以用这些地址信息遍历段落了。注意由于我们希望不同段落的初始化具有优先级之分,因此优先遍历的INIT_0段落的函数,最后再遍历INIT_3段落。
// init.c
void system_init(void)
{
const init_fn_t *fn;
for (fn = (init_fn_t *)Load$$INIT_0$$Base;
fn < (init_fn_t *)Load$$INIT_0$$Limit; fn++)
{
if (*fn) (*fn)();
}
for (fn = (init_fn_t *)Load$$INIT_1$$Base;
fn < (init_fn_t *)Load$$INIT_1$$Limit; fn++)
{
if (*fn) (*fn)();
}
for (fn = (init_fn_t *)Load$$INIT_2$$Base;
fn < (init_fn_t *)Load$$INIT_2$$Limit; fn++)
{
if (*fn) (*fn)();
}
for (fn = (init_fn_t *)Load$$INIT_3$$Base;
fn < (init_fn_t *)Load$$INIT_3$$Limit; fn++)
{
if (*fn) (*fn)();
}
}

被折叠的 条评论
为什么被折叠?



