手写一个简易的自底向上注册初始化的代码

概述

        之前阅读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_initmodule_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)();
    }
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值