一 概述
传统的应用编写时,每添加一个模块,都需要在main中添加新模块的初始化。也就是说增加的一个不能算是真正的独立模块,得在main中修改代码才能集成这模块功能。有没有什么办法可以实现main跟其他模块之间隔离呢?main不再关心有什么模块,模块的删减也不需要修改main?
二 liunx内核模块初始化
如果你对liunx模块有一定了解,你应该知道liunx模块都是独立加载,加载模块,不需要修改main代码。甚至不需要重新编译代码。那么内核是如何实现的呢?
模块初始化都会调用 module_init ,那么这函数做了哪些东西呢?我们着入点就从这个module_init 函数开始
// module_init定义在<include/linux/module.h>
#define module_init(x) __initcall(x);
// __initcall 定义在<include/linux/init.h>
#define __initcall(fn) device_initcall(fn)
#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;
// 例如 初始化一个iptables 模块 module_init(ip_tables_init) 其实就等效于
// static initcall_t _initcall_ip_tables_init_6 __attribute__((unused, section(".initcall6.init"))) = ip_tables_init ;
__ attribute__ ((section(”name“)))是gcc编译器支持的一个编译特性(arm编译器也支持此特性),实现在编译时把某个函数/数据放到name的数据段中。原理如下
- 模块通过__ attribute__((section(“name”))) ,会构建初始化函数表。放到你命名的name数据段中
- 而默认链接脚本缺少自定义的数据段的声明,需要在链接脚本添加你定义的数据段的声明
- 而main在执行初始化时,只需要把name数据段中的所有初始化接口执行一遍即可.
那么这里有两个问题 :
- 如何再链接脚本中添加自己定义的数据段的声明呢?
- main是如何将放入数据段的,模块接口都执行了一遍了呢?
内核是根据不同的架构,调用内核自己写的对应的链接脚本。而这部分本人也没有完全理解,这里就也深入内核讨论这块。
ld链接命令有两个关键的选项如下
ld -T <script>:指定链接时的链接脚本
ld --verbose:打印出默认的链接脚本
内核最终其实用了 ld -T arch/$(SRCARCH)/kernel/vmlinux.lds 指定架构对应的链接脚本。我们以”ARCH=arm“ 为例,查看链接脚本:arch/arm/kernel/vmlinux.lds
可以发现其实就在.bss 数据段前添加了 自己定义数据段的声明,如下
_initcall_start = .;
_initcall6_start =.; *(.initcall6.init)
_initcall_end = .;
// 当然内核初始化的数据段不止module_init 中要初始的 还有好几个不同的初始数据段,这里代码只是简化的列出 module_init 中的数据段。
typedef int (*initcall_t)(void);
static initcall_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_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)
{
......
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)
{
......
if (initcall_debug)
ret = do_one_initcall_debug(fn);
else
ret = fn();
......
}
......
按0-7的初始化级别,依次调用各个级别的初始化函数表,而驱动module_init的初始化级别为6。在“for (fn = initcall_levels[level]; fn <initcall_levels[level+1]; fn++)”的for循环调用中,实现了遍历当前初始化级别的所有初始化函数。
通过上述的代码追踪,我们发现module_init的实现有以下关键步骤:
- 通过module_init的宏,在编译时,把初始化函数放到了数据段:.initcall6.init
- 在内核自定义的链接,申明了.initcall6.init的数据段存放的位置,以及指向数据段地址的变量:_initcall6_start
- 在init/main.c中的for循环,通过_initcall6_start的指针,调用了所有注册的驱动模块的初始化接口
- 最后通过Kconfig/Makefile选择编译的驱动,实现只要编译了驱动代码,则自动把驱动的初始化函数构建到统一的驱动初始化函数表
三 自己实现动态模块初始化
分析了内核使用__ attribute__((section(“name”)))构建的驱动初始化函数表。自己就可以把这部分动态初始化应用到自己的项目了。
简单一个例子: 今天的行程 你可能会有多个安排,比如休闲的时候去图书馆看会书 晚上又想去打个篮球运动一下 。因此你需要加载两个行程安排。
ldsdefine.h
#ifndef LDSDEFINE_H_
#define LDSDEFINE_H_
typedef void (*init_call)(void); // 初始化函数指针函数
#define _self_init __attribute__((unused,section(".myinit")))
#define DECLAER_INIT(func) static init_call _fn_##func _self_init = func // 表示初始化了函数放到 .myinit 数据段中
#endif /* LDSDEFINE_H_ */
加载看书,打球行程安排
// move_fuction.c 运动模块
#include <unistd.h>
#include <stdio.h>
#include "ldsdefine.h"
static void move_init(void)
{
printf("move module >>>>> palying basketball\n");
}
DECLAER_INIT(move_init);
//arder_fuction.c 休闲模块
#include <unistd.h>
#include <stdio.h>
#include "ldsdefine.h"
static void arder_init(void)
{
printf("arder module >>>>> reading book\n");
}
DECLAER_INIT(arder_init);
调用了DECLAER_INIT 表示休闲模块,运动模块的初始化函数都放入到了数据段myinit中了。而链接脚本中还没有声明定义的数据段。
上面提到链接脚本 ld链接命令有两个关键的选项
通过命令”ld --verbose”获取默认链接脚本:
GNU ld version 2.20.51.0.2-15.fc13 20091009
Supported emulations:
elf_i386
i386linux
elf_x86_64
elf_l1om
using internal linker script:
==================================================
XXXXXXXX (缺省链接脚本)
==================================================
我们截取分割线”=======“之间的链接脚本保存为:ldsmodule.lds
在.bss的数据段前添加了自定义的数据段:
_init_start = .;
.myinit : { *(.myinit) }
_init_end = .;
表示声明了自定义数据段myinit , _init_start 指向自定义数据段 myinit 开始位置,_init_end 指向自定义数据段 myinit 结束位置。
你知道了数据段放入的开始结束位置。那么你在main.c 中就可以动态加载里面初始化的函数了。通过extern引用链接脚本声明的数据段,就能获取到数据段内所有的函数。
mian.c
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include "ldsdefine.h"
extern init_call _init_start;
extern init_call _init_end;
int main(void)
{
init_call *init_ptr = &_init_start;
printf("staring loading today schedule>>>>>>\n");
for (;init_ptr < &_init_end; init_ptr++) {
(*init_ptr)();
}
printf("ending loading today schedule>>>>>>\n");
return 0;
}
但是编译你会发现编译不成功,会显示如下错误
./src/main.o: In function `main':
/home/song/workspace/module_init_test/Debug/../src/main.c:20: undefined reference to `_init_start'
/home/song/workspace/module_init_test/Debug/../src/main.c:22: undefined reference to `_init_end'
那是因为你用的链接脚本是默认的,默认脚本并不知道你定义的数据段。你得链接你刚保存的ldsmodule.lds 脚本。
需要在IDE自动生成的 makefile 中
原本是 gcc -o"module_init_test" $(OBJS) $(USER_OBJS) $(LIBS)
添加 -T src/ldsmodule.lds 如下
# Tool invocations
module_init_test: $(OBJS) $(USER_OBJS)
@echo 'Building target: $@'
@echo 'Invoking: GCC C Linker'
gcc -T src/ldsmodule.lds -o"module_init_test" $(OBJS) $(USER_OBJS) $(LIBS)
@echo 'Finished building target: $@'
@echo '
最后运行结果如下,可以看到休闲(arder) ,运动(move)模块都加载了。
[root@wus Debug]# ./module_init_test
staring loading today schedule>>>>>>
arder module >>>>> reading book
move module >>>>> palying basketball
ending loading today schedule>>>>>>
至此功能已实现。。。。本篇完