一 概述
傳統的應用編寫時,每添加一個模塊,都需要在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>>>>>>
至此功能已實現。。。。本篇完