Linux是單內核系統,可通用計算平臺的外圍設備是頻繁變化的,不可能將所有的(包括將來即將出現的)設備的驅動程式都一次性編譯進內核,為瞭解決這個問題,Linux提出了可載入內核模塊(Loadable Kernel Module,LKM)的概念,允許一個設備驅動通過模塊載入的方式,在內核運行起來之後" ...
Linux是單內核系統,可通用計算平臺的外圍設備是頻繁變化的,不可能將所有的(包括將來即將出現的)設備的驅動程式都一次性編譯進內核,為瞭解決這個問題,Linux提出了可載入內核模塊(Loadable Kernel Module,LKM)的概念,允許一個設備驅動通過模塊載入的方式,在內核運行起來之後"融入"內核,載入進內核的模塊和本身就編譯進內核的模塊一模一樣。
一個程式在編譯的地址的相對關係就已經確定了,運行的時候只是進行簡單的偏移,為了使模塊載入進內核後能夠被放置在正確的地址,並正確的調用系統的運行的導出符號表,編譯模塊的時候必須要使用系統的編譯地址,並調用系統編譯出得靜態的導出符號表。即模塊必須使用系統的配置環境:Makefile+.config,一旦這兩個文件任意一個發生了變化,都很可能導致模塊的編譯地址與系統的編譯地址不匹配,造成運行時的錯誤甚至宕機。
導出符號表
從提供系統運行效率的角度,一個模塊不是也不應該是完全獨立的,即一個模塊往往會調用其他模塊提供的功能來實現自己的功能,這樣做能更好實現系統的分工並提高效率。Linux為了實現模塊間的相互調用,設計了導出符號表,每個模塊都可以將自己的一個私有的標號導出到系統層級,以使該標號對其他模塊可見,系統在編譯一個模塊的時候會自動導出這個模塊的導出符號表到modules.syms文件(如果沒有導出任何符號,可以為空),併在載入一個模塊的時候會自動將該模塊的導出符號表與系統自身的導出符號表合併。一個系統的源碼的導出符號表一般在源碼頂層目錄的modules.syms文件中,查看正在運行的系統導出符號表使用cat /proc/kallsyms。註意,正如前面解釋的,我們的模塊之所以能夠正常運行,一個重要原因就是編譯我們模塊使用的符號地址就是編譯內核時使用的符號地址,所以運行起來雖然地址會有偏移,但是模塊中相關的符號的地址也會和內核地址一起偏移,也就還能找得到。基於這種思想,我們也可以直接查看系統當前運行的地址,將地址賦值給一個函數指針並使用,也是沒有問題的,當然,這隻是闡述原理,並不建議這麼寫模塊。
下麵這個例子可以看出編譯出的地址和運行時的地址是不一樣的:
導出符號表可以大大的提高系統的運行效率,這也是只有開源系統才能提供的一個強大的功能,但是,導出符號表的引入會導致一個小小的麻煩--模塊的依賴,當我們使用lsmod的時候,就可以查看系統當前的模塊,其最後兩列分別是該模塊被引用的次數以及引用該模塊的內核模塊,當一個模塊被其他模塊引用時,我們是不能進行卸載的,同樣,如果模塊A依賴於模塊B,那麼如果模塊B不載入的時候模塊A也載入不了。在編寫多模塊的時候尤其要註意這個問題,可以寫一個腳本管理多個依賴模塊。Linux內核使用兩個巨集來導出一個模塊的符號
EXPORT_SYMBOL(符號名)
EXPORT_SYMBOL_GPL(符號名)
模塊編譯方法
藉助內核的Makefile,編譯出的XXX.ko(Kernel Object)就是可載入到該內核的外部模塊,為了利用內核的Makefile,我們可以將編譯外部模塊的Makefile寫成如下的格式:
ifneq ($(KERNELRELEASE),)
export-objs = demo.o
obj-m = extern.o
else
KERNELDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
endif
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
.tmp_versions Module.symvers modules.order .tmp_versions .*.cmd *.o *.ko *.mod.c
這個簡單的Makfile是利用ubuntu主機的源碼Makefile來編譯模塊,學習模塊編程的開始階段在主機進行編譯調試更方便一點,下麵我解釋一下這個Makefile,首先,我們的思路還是通過內核的Makefile來準備我們的模塊,而內核的Makefile一旦執行,就會給KERNELRELEASE
這個變數賦值,所以第一次進入我們這個Makfile的時候,這個變數還是空,所以執行else
的部分——給相關的變數賦值,make預設編譯第一個目標all
,make -C $(KERNELRELEASE)
就是進入到KERNELRELEASE
指定的目錄並執行裡面的Makefile,顯然,這就是我們內核源碼的頂層Makefile,接下來的選項M=$(PWD) modules
都是傳入這個頂層Makefile的參數,表示我要編譯一個模塊,這個模塊位於M
指定的目錄,所以內核會進行相關的配置並最終進入到"這個模塊所在的目錄",此時,我們的這個Makefile會再被進入一次,這一次是從內核Makefile中跳入這裡的,,KERNELRELEASE
已經被定義過,內核Makefile想要的就是obj-m
後面指定的要編譯的目標文件,所以內核Makfile就會找到我們寫的模塊源文件進行編譯。如此我們就得到了能在ubuntu下執行的xxx.ko文件,如果需要在開發板上運行,只需要將內核路徑改成開發板運行系統的源碼路徑即可,同時記得要導出相關的環境變數( ARCH, CROSS_COMPILE )
註冊/註銷模塊
Linux為每個模塊都預留了相應的地址,註冊模塊即讓該模塊對內核可見,這也是模塊工作的先決條件。註冊之後,我們就可以通過查看內核輸出信息dmesg命令來查看模塊的運行情況。經常使用內核函數printk()
來輸出系統信息進行列印調試。使用insmod XXX.ko載入一個模塊,使用rmmod XXX.ko卸載一個模塊,使用lsmod查看當前系統中的模塊及其引用情況
insmod使用的是init_module()
系統調用,這個系統調用的實現是sys_init_module()
rmmod使用delete_module()
系統調用,這個系統調用的實現是sys_delete_module()
模塊的程式框架
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
/* 構造/析構函數 */
static int __init mydemo_init(void)
{
//構造設備/驅動對象
//初始化設備/驅動對象
//註冊設備/驅動對象
//必要的硬體初始化
}
static void __exit mydemo_exit(void)
{
//回收資源
//註銷設備/驅動對象
}
/* 載入/卸載模塊 */
module_init(mydemo_init);
module_exit(mydemo_exit);
/* 授權 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XJ");
MODULE_DESCRIPTIPON("mymydemo");
/* 導出符號 */
EXPORT_SYMBOL(data);
註意這裡的授權是必須的,如果一個模塊沒有授權,那麼很多需要該授權的函數甚至都不能使用,同理,不合適的授權也會導致模塊運行或載入的錯誤,所以初學者一定不要忽視這個授權,相關授權的選項在"linux/module.h"中,這裡我把相關的說明貼出來供大家參考
/*
* The following license idents are currently accepted as indicating free
* software modules
*
* "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 MIT license choice]
* "Dual MPL/GPL" [GNU Public License v2
* or Mozilla license choice]
*
* The following other idents are available
*
* "Proprietary" [Non free products]
*/
另一個細節是Linux內核源碼的預設頭文件路徑是頂層目錄的include目錄,所以包含頭文件的時候include可以省略,
第一個Linux模塊
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
static int __init demo_init(void)
{
printk(KERN_INFO"demo_init:%s,%s,%d"__FILE__,__func__,__LINE__);
return 0;
}
static void __exit demo_exit(void)
{
printk(KERN_INFO"demo_exit:%s,%s,%d"__FILE__,__func__,__LINE__);
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
執行insmod xjDemo.ko,查看執行結果
模塊傳參
我們編寫的模塊還可以在insmod的時候傳入參數,Linux提供了幾個巨集(函數)用於接收外部的參數。模塊內部使用這些函數,只需執行insmod xjDemo.ko num=2,insmod mydemo.ko i=10,insmod mydemo.ko extstr="hello" 等命令就可以將參數傳入模塊
module_param(num,type,perm); //接收一個傳入的int數據
module_param(num,type,perm); //接收一個傳入的charp數據
module_param_array(num,type,nump,perm); //接收一個數組
module_param_string(name,string,len,perm); //接收一個字元串
MODULE_PARAM_DESC("parameter description");