Linux模塊編程框架


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)
all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	$(RM) .tmp_versions Module.symvers modules.order .tmp_versions .*.cmd *.o *.ko *.mod.c
endif

這個簡單的Makfile是利用ubuntu主機的源碼Makefile來編譯模塊,學習模塊編程的開始階段在主機進行編譯調試更方便一點,下面我解釋一下這個Makefile,首先,我們的思路還是通過內核的Makefile來准備我們的模塊,而內核的Makefile一旦執行,就會給KERNELRELEASE這個變量賦值,所以第一次進入我們這個Makfile的時候,這個變量還是空,所以執行else的部分——給相關的變量賦值,make默認編譯第一個目標allmake -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=2insmod mydemo.ko i=10insmod 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");


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM