Linux淘金记(一):module_init——初始化就该这么写
没头绪时,Linux源码必有解决方案。 ——伊曼努尔·康德
一、shi山般的初始化
写裸机时,初始化程序简单粗暴这么写:
#include "gpio.h"
#include "uart.h"
#include "led.h"
#include "spi.h"
...
int main(void)
{
gpio_init();
uart_init();
led_init();
spi_init();
...
}
优雅点这么写:
#include "gpio.h"
#include "uart.h"
#include "led.h"
#include "spi.h"
...
void bsp_init(void)
{
gpio_init();
uart_init();
led_init();
spi_init();
...
}
int main(void)
{
bsp_init();
...
}
真的优雅么?bsp_init就是一个fen坑!
问题一:
所有init函数调用都耦合到bsp_init()里,意味着每加一个驱动程序就得在bsp_init追加这个调用。一千个设备就得扩到上千行,增删改查恶心得要吐,还容易丢三落四,制造bug。自己的shi还好,别人的呢?
问题二:
对嵌入式项目,不同的定制方案需要不同的bsp_init,你需要在为每个项目重写一遍bsp_init,无聊又浪费精力。
问题三:
有没有这样的初始化办法,每个工程的main函数都调用一个相同的bsp_init,每个驱动的c文件无需将自己的xxx_init函数添加到bsp_init,bsp_init会自动调用它们?
对于问题三,像是做梦。但是回头想想,咱们能想到的问题,写内核的神级前辈们就没想到吗?来看看Linux内核的方案。
二、 释放函数名的本质
有这么个公式:
xxx_init == 函数名 == 指针 == 数据
那么c文件中的xxx_init函数就是数据,数据要放到内存中,内存又可寻址。
诶?灵感来了,要把猫赶到一个笼子里去。把所有bsp_init要调用的函数名字放到一个已知地址的内存段中,然后bsp_init去遍历这个内存段,并通过每个函数指针调用这个函数原型。当然这个灵感不是笔者想的,是写内核的神人想的。
Linux内核是这么做的:
第一步:把猫赶到一个笼子里,利用module_init
Linux几乎每个驱动模块的结尾都要使用module_init这个宏:
#define module_init(x) __initcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
#endif
展开:
#define module_init(x) static initcall_t __initcall_##x##6 __used \
__attribute__((__section__(".initcall6.init"))) = x;
例如module_init(led_init)就能展开为:
static initcall_t __initcall_led_initinit6 __used \
__attribute__((__section__(".initcall6.init"))) = led_init;
定义一个静态函数指针__initcall_led_initinit6指向初始化函数led_init,并且利用__attribute__把__initcall_led_initinit6链接到".initcall6.init"这个内存数据段里。简单来说就是把函数名丢到名为.initcall6.init的内存段里。如果每个驱动文件都这么做的话会出现这样:
第二步:遍历猫笼子,do_initcalls()
内核启动时会调用do_initcalls(),原型位于init/main.c:
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
循环调用do_initcall_level,原型(保留核心代码):
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));
}
最后do_one_initcall(保留核心代码):
int __init_or_module do_one_initcall(initcall_t fn)
{
/* ... */
ret = fn();
return ret;
/* ... */
}
遍历过程如图:
这样驱动文件中的xxx_init无需追加到do_initcalls中,只需在本文件使用module_init(xxx_init),然后在main函数调用do_initcalls。本质上这种做法是把在bsp_init函数中追加函数的工作交给了链接器。
三、在keil中实现module_init及万能初始化函数
第一步、修改链接脚本
1.找到MDK的链接脚本xxx.sct(xxx为工程名)的路径。注意这个文件需要编译工程后生成。
2.拷贝这个文件并重命名为module.sct。笔者这里把它放到了工程根目录下以免被误删。
3.链接器中选择module.sct,并打开编辑。
打开后如下:
4.追加自定义数据段_initcall6_init,这里模仿内核添加8个数据段:
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00100000 { ; load region size_region
ER_IROM1 0x08000000 0x00100000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00020000 { ; RW data
.ANY (+RW +ZI)
}
_initcall0_init +0 {
.ANY (_initcall0_init)
}
_initcall1_init +0 {
.ANY (_initcall1_init)
}
_initcall2_init +0 {
.ANY (_initcall2_init)
}
_initcall3_init +0 {
.ANY (_initcall3_init)
}
_initcall4_init +0 {
.ANY (_initcall4_init)
}
_initcall5_init +0 {
.ANY (_initcall5_init)
}
_initcall6_init +0 {
.ANY (_initcall6_init)
}
_initcall7_init +0 {
.ANY (_initcall7_init)
}
}
完成!
第二步、实现module_init
创建一个init.h文件,module_init的宏基本照抄Linux内核:
#ifndef __INIT_H
#define __INIT_H
typedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void);
#define __init
#define __exit
#define __used __attribute__((__used__))
#define __define_initcall(fn, id) __used \
static volatile initcall_t __initcall_##fn##id \
__attribute__((__section__("_initcall" #id "_init"))) = fn
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define device_initcall(fn) __define_initcall(fn, 6)
#define late_initcall(fn) __define_initcall(fn, 7)
#define __initcall(fn) device_initcall(fn)
#define __exitcall(fn) \
static volatile exitcall_t __exitcall_##fn__exit_call = fn
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
void __init do_initcalls(void);
#endif /* __INIT_H */
第三步、实现do_initcalls()
仿照内核,写init.c:
#include "init.h"
#if defined ( __CC_ARM )
extern unsigned int Image$$ER_IROM1$$Base;
extern unsigned int Image$$ER_IROM1$$Limit;
extern unsigned int Image$$ER_IROM1$$Length;
extern unsigned int Image$$RW_IRAM1$$Base;
extern unsigned int Image$$RW_IRAM1$$Limit;
extern unsigned int Image$$RW_IRAM1$$Length;
extern unsigned int Image$$RW_IRAM1$$ZI$$Base;
extern unsigned int Image$$RW_IRAM1$$ZI$$Limit;
extern unsigned int Image$$RW_IRAM1$$ZI$$Length;
extern unsigned int Image$$_initcall0_init$$Base;
extern unsigned int Image$$_initcall0_init$$Limit;
extern unsigned int Image$$_initcall0_init$$Length;
extern unsigned int Image$$_initcall1_init$$Base;
extern unsigned int Image$$_initcall1_init$$Limit;
extern unsigned int Image$$_initcall1_init$$Length;
extern unsigned int Image$$_initcall2_init$$Base;
extern unsigned int Image$$_initcall2_init$$Limit;
extern unsigned int Image$$_initcall2_init$$Length;
extern unsigned int Image$$_initcall3_init$$Base;
extern unsigned int Image$$_initcall3_init$$Limit;
extern unsigned int Image$$_initcall3_init$$Length;
extern unsigned int Image$$_initcall4_init$$Base;
extern unsigned int Image$$_initcall4_init$$Limit;
extern unsigned int Image$$_initcall4_init$$Length;
extern unsigned int Image$$_initcall5_init$$Base;
extern unsigned int Image$$_initcall5_init$$Limit;
extern unsigned int Image$$_initcall5_init$$Length;
extern unsigned int Image$$_initcall6_init$$Base;
extern unsigned int Image$$_initcall6_init$$Limit;
extern unsigned int Image$$_initcall6_init$$Length;
extern unsigned int Image$$_initcall7_init$$Base;
extern unsigned int Image$$_initcall7_init$$Limit;
extern unsigned int Image$$_initcall7_init$$Length;
#define __initcall0_start (initcall_t *)&Image$$_initcall0_init$$Base
#define __initcall1_start (initcall_t *)&Image$$_initcall1_init$$Base
#define __initcall2_start (initcall_t *)&Image$$_initcall2_init$$Base
#define __initcall3_start (initcall_t *)&Image$$_initcall3_init$$Base
#define __initcall4_start (initcall_t *)&Image$$_initcall4_init$$Base
#define __initcall5_start (initcall_t *)&Image$$_initcall5_init$$Base
#define __initcall6_start (initcall_t *)&Image$$_initcall6_init$$Base
#define __initcall7_start (initcall_t *)&Image$$_initcall7_init$$Base
#define __initcall_end (initcall_t *)&Image$$_initcall7_init$$Limit
#endif /* #if defined ( __CC_ARM ) */
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
static initcall_t *initcall_levels[] = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
static int do_one_initcall(initcall_t fn)
{
int ret;
ret = fn();
return ret;
}
static void do_initcall_level(int level)
{
initcall_t *fn;
for (fn = initcall_levels[level]; fn < initcall_levels[level + 1]; fn++)
do_one_initcall(*fn);
}
void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
注意:这里MDK使用自己的armcc编译工具链,获取数据段地址的语法于gcc不同,MDK内置了全局变量表示内存段的信息:
extern unsigned int Image$$name$$Base; //这个变量位于name段的起始
extern unsigned int Image$$name$$Limit; //位于name段的结束地址加1,也是下一段的开始
extern unsigned int Image$$name$$Length; //name段长度
其他函数提取核心代码。
四、在STM32测试一下
main函数无脑调用do_initcalls:
#include "stm32f4xx.h"
#include "stdio.h"
#include "console.h"
#include "init.h"
int main(void)
{
console_init();
printf("start test!\r\n");
do_initcalls();
while(1) {
}
}
添加两个测试模块文件module1.c和module2.c,利用module_init注册初始化函数:
#include "stdio.h"
#include "init.h"
int module1_init(void)
{
printf("module1 init!\r\n");
return 0;
}
module_init(module1_init);
#include "stdio.h"
#include "init.h"
int module2_init(void)
{
printf("module2 init!\r\n");
return 0;
}
module_init(module2_init);
编译,运行,完美!
五、优雅!
现在我们无需在main.c里include一堆头文件,然后bsp_init里加一堆init函数,只需要在main函数里调用do_initcalls,然后在每个需要初始化的.c文件内用module_init宏注册init函数。你甚至不用给这个.c文件做个.h文件,而且删除某个.c文件完全不会报错!最大的好处是我们可以在不同的项目上复用这个do_initcalls,只需更改链接脚本就可以。这就是松耦合的优雅。当然这块金子的价值远不止于此,期待日后的开发。
参考:
https://blog.csdn.net/weixin_39094034/article/details/123549216