在正式進行驅動開發前,需要了解有關模塊編程和內核編程的一些基本概念。在本節中將會構造幾個完整的(但絕對沒啥功用的)模塊。設置測試系統一般的發行版本都會裝好內核代碼樹的,用過的Red Hat Enterprise Linux 5.x, Fedora 15/16, CentOS5.x系統中,其在/usr/src/kernels/$(shelluname -r)/目錄下.你最好使用的內核源碼樹是和運行時的內核一個版本,起碼初學時不會遇到微妙的問題。
2.6版本的 Linux 內核提供了用戶工具來管理模塊:lsmod: 查看mod
modprobe:insmod 和 rmmod 的包裝
depmod:用於創建模塊依賴項
modinfo:用於爲模塊宏查找值,查看模塊信息
讓我們從老熟人:Hello World 模塊說起:
/* hello.c */
#include <linux/init.h>
#include <linux/module.h>
MODULE_AUTHOR(" anonyWoo <[email protected]>");
MODULE_DESCRIPTION("To say Hello to the word");
MODULE_LICENSE("Dual BSD/GPL");
static __init int hello_init(void)
{
printk(KERN_ALERT "Hello, world!\n");
return 0;
}
static __exit void hello_exit(void)
{
printk(KERN_ALERT "Seeyou than, cruel world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
在module.h中定義了大量的MODULE_前綴宏,當然也包含了用於聲明模塊初始化及模塊註銷時清除函數的宏:module_init();
module_exit();
模塊初始化函數:
static __init int hello_init(void)
其中的__init就是告訴內核該函數只在初始化時使用一次,隨後其佔據的內存就被內核釋放了。所以如果你的初始化函數還爲其他的功能模塊提供功能,那就別加上__init了。
與其相似的,__exit就是當模塊移除時,內核會調用來移除模塊。如果模塊被編譯進了內核或者模塊不允許移移除,內核2.6用宏CONFIG_MODULE_UNLOAD來配置模塊不允許被移除。那麼__exit函數只是誒簡單的丟棄。如果一個模塊未定義一個清除函數,則模塊時不可移除的。
關於__init還得說倆句,內核時怎麼知道當hello_init函數初始化完成後,在哪釋放她佔的內存呢?這就有必要了解一下ELF(Executable and Linking Format, 可執行可鏈接格式)。
圖 1
看見上面的objdump輸出了吧:其中的.init.text和.exit.text代碼段。任何加了__init和__exit前綴的函數都會被鏈接到相應的段中去,到時內核會自動銷燬他們。
另外,你也可以進入/sys/module/hello/ sections 目錄中,也會看到各個節。
且看init.h中的:
/* These macros are used to mark some functions or
* initialized data (doesn't apply to uninitialized data)
* as `initialization' functions. The kernel can take this
* as hint that the function is used only during the initialization
* phase and free up usedmemory resources after
*
* Usage:
* For functions:
*
* You should add __init immediately before the functionname, like:
*
* static void __init initme(int x, int y)
* {
* extern int z; z= x * y;
* }
#define __init __attribute__ ((__section__((".init.text")))
__attribute__是GNU C的一個擴展,主要用來聲明一些特殊的屬性,而section就是其中之一。上面的意思是由__init修飾的代碼會被鏈接到到.init.text段中(section, 節)在init.h中你會看到如下定義:
#define __define_initcall(level,fn,id) \
static initcall_t__initcall_##fn##id__used\
__attribute__((__section__(".initcall" level ".init"))) = fn
#define device_initcall(fn) __define_initcall("6",fn,6)
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x)
static __init int hello_init(void)module_init(hello_init) /* 就是將hello_init指針放到到 .initcall6.init 子節中了
* module_init會被do_initcall調用來初始化 */
模塊到底是如何裝載與卸載的呢?
我們在用戶空間中:
[wang2@wuhz hello_world]$ insmod hello.ko
insmod 會調用用戶空間的系統調用init_module將hello.ko二進制文件複製到內核空間中去,接着交給內核;然後進入內核到達內核函數sys_init_module(2.6.32中在linux/sysycalls.h中)。這是加載模塊的主要函數,它利用非常多的其他函數完成工作。
rmmod則會使delete_module執行system call調用,而delete_module最終會進入內核,並調用sys_delete_module將hello.ko模塊從內核刪除。
裝載細節:
init_module->sys_init_module->capable(CAP_SYS_MODULE)
->load_module->layout_and_allocate->copy_and_check
->setup_load_info
->check_modinfo
現在,我們看看加載模塊時的內部函數。
當調用內核函數 sys_init_module 時,會開始一個許可檢查,只有特權用戶纔可以執行這個操作(通過 capable 函數完成)。
然後,調用 load_module 函數,這個函數首先會爲將模塊分配ELF塊內存,接着調用copy_from_user將hello.ko模塊加載到內核並執行必要的檢查。圖片1中的各個節(section或叫段)共同組成了ELF;
接着會進行ELF類型type檢查,架構arch檢查,size檢查,etc.
然後創建一些快捷變量(Convenience variables),方便隨後的訪問,同時檢查ELF的區段頭部,ELF的start address 0x00000000,這樣就容易定位各個節的地址了;這期間還檢查了是否開啓了CONFIG_MODULE_UNLOAD標誌(文章開始說過了);
還會檢查version;進行版本檢查,需要三個節的配合:
6.modinfo
9.__versions
11.gnu.linkonce.this_module
接受模塊參數,並且更新模塊狀態正在加載:
mod->state= MODULE_STATE_COMING 。如果需要 per-CPU 數據(這在檢查區段頭時確定),那麼就分配 per-CPU 塊;
前面load_module分配的ELF內存時臨時的,現在知道誰可以留誰得走了,就爲模塊分配最終的位置,並移動相應的節;
然後執行另一個分配,大小是模塊必要區段所需的大小。迭代臨時 ELF 塊中的每個區段,並將需要執行的區段複製到新的塊中。接下來要進行一些額外的維護。同時還進行符號解析,可以解析位於內核中的符號(被編譯成內核映象),或臨時的符號(從其他模塊導出);
然後爲每個剩餘的區段迭代新的模塊並執行重新定位(do relocations);這個步驟與架構有關,因此依賴於爲架構(./linux/arch/<arch>/kernel/module.c)定義的 helper 函數;
最後,刷新指令緩存(因爲使用了臨時.text 區段),執行一些額外的維護(釋放臨時模塊內存,設置系統文件,是/sys/module/hello入口項,通過mod_sysfs_init(mod);來創建的,這回在設備模型章節中我們好好學習學習),返回mod。
卸載細節:
當調用內核函數 sys_delete_module(將要刪除的模塊的名稱作爲參數傳入)之後,第一步同樣是確保調用方具有權限;
接下來會檢查一個列表,查看是否存在依賴於這個模塊的其他模塊。這裏有一個名爲 modules_which_use_me 的列表,它包含每個依賴模塊的一個元素。如果這個列表爲空,就不存在任何模塊依賴項,因此這個模塊就是要刪除的模塊(否則會返回一個錯誤);
接下來還要測試模塊是否加載。用戶可以在當前安裝的模塊上調用 rmmod,因此這個檢查確保模塊已經加載;
在幾個維護檢查之後,倒數第二個步驟是調用模塊的exit 函數(模塊內部自帶);
最後,調用 free_module 函數。
調用 free_module 函數之後,您將發現模塊將被安全刪除。該模塊不存在依賴項,因此可以開始模塊的內核清理過程。
首先,從安裝期間添加的各種列表中(系統文件、模塊列表等)刪除模塊;
其次,調用一個與架構相關的清理例程(可以在 ./linux/arch/<arch>/kernel/module.c 中找到);
然後,迭代具有依賴性的模塊,並將這個模塊從這些列表中刪除;
最後,從內核的角度而言,清理已經完成,爲模塊分配的各種內存已被釋放,包括參數內存、per-CPU 內存和模塊的 ELF 內存(core 和 init)。
版本依賴:
如果出現下面情況:
[root@wuhz hello_world]# insmod hello.ko
Error inserting './hello.ko': -1 Invalidmodule format
可以使用命令
#dmesg | tail -n 20
查看一下具體的原因,一般是構建hello.ko模塊的環境(內核源碼樹)和當前的kernel不一致造成的。
內核符號表:
你應該知道,linux系統中有非常多的優秀的簡潔的小工具,一個優秀的管理元可以通過整合這些個小工具來完成超複雜的工作!就如上面的命令:
#dmesg | tail -n 20
tail接受dmesg的輸出,並顯示最後20行的信息!同樣的各個模塊之間也可以共享各自的功能,輸出接口就好了,準確點是符號(symbols)。insmod會使用公共內核符號表來解析模塊中未定義的符號。公共內核符號表中包含了所有的全局內核項(func和variables)的地址,這使得模塊化成爲可能。當然,當你的模塊裝載到內核的時候,他所導出的符號也會成爲內核符號表的一部分供其他模塊使用!
linux爲我們提供一些宏方便我們導出符號以供她用:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);/* 顯然由他導出的符號只能供GPL授權的模塊使用 */
他們只能在模塊的全局部分導出,不能在函數中導出,因爲上面兩個宏會被擴展成一個特殊變量的申明,而改變量必須是全局的。
用objdump看一下會多出倆個節__ksymtab_gpl和__kcrctab_gpl(checksum of somesymbols).在裝載時,內核從這個節(section或叫段)中來尋找模塊導出的變量。詳見<linux/module.h>文件。
預備知識:
所有的模塊代碼中都包含下面兩行代碼:
#include <linux/module.h> /* 其包含了模塊需要的大量符號和函數定義*/
#include <linux/init.h> /* 制定初始化和清除函數 */
也可能含有:
#include <linux/moduleparam.h> /* 可參數化裝載模塊 */
指定模塊使用的許可證:
MODULE_LICENSE("GPL");
/* "GPL" [GNU Public License v2 or later]
* "GPL v2" [GNU Public License v2]
* "GPL and additional rights" [GNU Public License v2 rights and more]
* "Dual BSD/GPL" [GNU Public License v2
* or BSD license choice]
* "Dual MIT/GPL" [GNU Public License v2
* or MITlicense choice]
* "Dual MPL/GPL" [GNU Public License v2
* orMozilla license choice]
*
* Thefollowing other idents are available
*
* "Proprietary" [Non free products]
*/
也有些其他以MODULE_爲前綴的宏()定義在<linux/module.h>:
MODULE_AUTHOR("anonyWoo");
MODULE_DESCRIPTION("helloa.kois to say to world: Hello!");
MODULE_VERSION(_version);
模塊參數:
/*helloa.c 英語註釋的比較差,見諒哈 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/stat.h> /* this header contains the accesspermission,e.g. S_IRUGO as showed bellow */
#include <linux/kernel.h>
#define MAXSIZE 5
MODULE_LICENSE("Dual BSD/GPL"); /*to state the lincense */
static int array[MAXSIZE] = {1, 2, 3, 4};
static char *whom = "anonyWoo";
static int howmany = 1;
/** #insmod helloa.ko howmany=25
* */
module_param(howmany, int, S_IRUGO);
/* #insmod helloa.kowhom="Miss.wong" */
module_param(whom, charp, S_IRUGO);
/** passing the parameter from the commandline has this form:
#insmod ./helloa.ko array=4,5,6 */
module_param_array(array, int, NULL,S_IRUGO);
/*static __init int hello_init(void)*/
static int hello_init(void)
{
int i, j;
for(i = 0; i < howmany; i++)
printk(KERN_ALERT "Hello,world %d time(s), hello %s\n", i + 1, whom);
for(j = 0; j <MAXSIZE; j++ )
printk(KERN_ALERT "This is%d time(s) output\n", *(array + j));
return 0;
}
static __exit void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world!\n");
}
EXPORT_SYMBOL_GPL(hello_init);
module_init(hello_init);
module_exit(hello_exit);
Makefile
#if we had define the KERNELRELEASE, for itcalls from the kernel tree,
#that we can use the buildin statement asbellow.
ifneq ($(KERNELRELEASE),)
obj-m := helloa.o
#else, call from the command line directly,
#in this case, we have to call the kernel tree
else
#這裏按照ldd3的似乎不行,遂改成如下形式了
KERNELDIR ?= /usr/src/kernels/$(shell uname -r)/
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
endif
#make
#insmod helloa.ko howmany=5 whom=”Miss.wong”array=7,8,9
#dmesg | tail -n 10
...
Hello, world 1 time(s), hello Miss.wong
Hello, world 2 time(s), hello Miss.wong
Hello, world 3 time(s), hello Miss.wong
Hello, world 4 time(s), hello Miss.wong
Hello, world 5 time(s), hello Miss.wong
This is 7 time(s) output
This is 8 time(s) output
This is 9 time(s) output
This is 4 time(s) output
#lsmod | grep helloa
helloa 1314 0
#rmmod helloa
Goodbye, cruel world!
關於module_param(variable,type, perm);
/* type 可以是bool, charp, int, invbool(inverse bool), long, short, ushort, uint,ulong, or intarray。 */
module_param_array(array_name, array_item_type, array_nump, perm );
array_nump會是用戶提供的參數個數,且模塊會拒絕接受超過數組大小的值,由sizeof(array[0]) , 零數組長度,動態獲得其長度,指定NULL就可以了。
perm在<linux/stat.h>中定義,用於sysfs中入口項的訪問掩碼:eg. umask=0700,file: rwx------則相減得:---------
結束:
Ch02 is over...
To be continued...