一.LINUX Kernel Module
LINUX Kernel是組件模式的,所謂組件模式是指:LINUX Kernel在運行時,允許“代碼”動態的插入或者移出Kernel。
所謂模塊是指:相關的一些子程序,數據、入口點和出口點共同組合成的一個單一的二進制映像,也就是一個可裝載的Kernel目標文件。
模塊的支持,使得系統可以擁有一個最小的內核映像,並且通過模塊的方式支持一些可選的特徵和驅動程序。
模塊可動態的插入Kernel和從Kernel中移除,提供了一種調試內核程序的簡便方法。模塊的加載方式分爲兩種:靜態加載和動態加載。
靜態加載是將模塊直接編譯入內核,若模塊需要修改和升級,我們就得重新編譯整個內核,並且必須重新燒寫內核,工作量加大。
動態加載是需要時,加載入內核,不需要時,從內核卸載。不需對內核進行編譯和燒寫,就可方便的對模塊進行修改和升級,大大減小了工作量,也省去了我們很多的麻煩.
二.實踐
1>.Hello World:
模塊的開發就像寫一個應用程序,它有自己的入口點,出口點,生命週期.
/*
*hello.c Hello,World! As a Kernel Module
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
/*
*hello_init the init function , called when the module is loaded.
*Return zero if successfully loaded, nozero otherwise.
*/
static int hello_init(void)
{
printk(KERN_ALERT" Hello,World!\n");
return 0;
}
/*
*hello_exit the exit function ,called when the module is removed.
*/
static void hello_exit(void)
{
printk(KERN_ALERT"Good,Bye!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("HuG");
註釋:
module_init()和module_exit()都是宏。
module_init()把hello_init()函數註冊爲這個模塊的入口點。當模塊被裝載時,內核調用hello_init()函數。
module_init()的任務是把它的唯一參數作爲相應模塊的初始化函數。
初始化函數必須有如下的形式:int my_init(void)
由於初始化函數不會被外部的代碼直接調用,所以,不必export這個函數。所以將初始化函數標誌爲static會更加的合理。
初始化函數的返回值:如果初始化成功,返回0;否則返回非零。
此處的初始化函數僅僅是打印出一句話。實際開發的模塊中,初始化函數一般完成的工作是:註冊資源,爲數據結構分配內存等等。
同理,module_exit()是把hello_exit()函數註冊爲這個模塊的出口點。當模塊從內核中移除時,內核調用這個函數。
退出函數在返回之前,必須清除模塊所佔的資源,確保硬件處於一致狀態等等。
退出函數必須有如下形式:void my_exit(void);同上,將函數標誌爲static會更合理。
注意:若採取靜態加載的方式,將模塊編譯進內核,則內核啓動時,調用static int my_init(void);但是退出函數static void my_exit(void);不會包含在內核的映像內,它也不會被調用。因爲靜態的加載方式,是將模塊當作內核的一部分編譯進內核中,所以代碼永遠不會從內核中刪除。
宏MODULE_LICENSE()用於指定這個文件的版權許可。
MODULE_AUTHOR()用於指定本文件的作者。宏的值完全是爲了提供說明信息。
2>.building Modules
在編譯模塊,讓模塊開始工作之前,我們必須確定模塊的源碼放置位置:
有兩種方式:
一、把模塊的源碼增加到內核源碼的一個合適的地方,即可以把文件作爲一個"patch”,最終也可以將代碼合到官方的源碼樹內。
二、在源碼樹之外維護和編譯模塊。
2.1>.在源碼樹內添加
我們通常的選擇是將模塊放入源碼樹之內,作爲LINUX的一部分。這樣模塊可以生存在內核的源碼樹內。
如果,我們的模塊是一個和USB相關的一個驅動,我們可將其放入drivers/usb/目錄下。我們進入drivers/usb/gadget/目錄下,我們發現gadget/目錄下有很多驅動程序,都是些和USB相關的驅動。因此,我們將模塊放在此目錄下。
新建目錄drivers/usb/gadget/hhtest/,將模塊的所有文件放於此目錄下(test.c+Makefile+Kconfig)
A>.test.c見hello world程序;
B>.Makefile中需寫入:
obj-$(CONFIG_USB_GADGET_TEST) += test.o
C>.Kconfig寫入:
config USB_GADGET_TEST
tristate "Hello Driver added by hh"
//在menuconfig界面顯示的配置選項名稱
default n
help
test for adding driver to menuconfig.
/×××××××××××××××××××××格式說明××××××××××××××××××××××××××××××
config USB_GADGET_TEST
tristate "Gadget Netmeeting support"
default n
help
If you say Y here,netmeeting driver will be compiles into the kernel.You can also say M here and the driver will be built as a module named netmeeting.ko
If unsure , say N.
第一行定義了配置選項。事先已經假設存在有CONFIG_前綴,因此用不着我們寫。
第二行說明了這個配置選項是三態的,即有三種選擇方式。第一種是選擇Y,表示相對應的程序編譯到Kernel之內;第二種選擇是M,表示相對應的程序編譯成模塊;第三種選擇是N,表示不編譯相對應的程序。
如果沒有編譯成模塊這個選項,可以用bool代替tristate。指令tristate後帶引號的文本是配置選項名,用於各種配置實用程序的選項顯示。
第三行爲這個配置選項指定一個默認值,這裏的默認值是選擇n。即:不編譯相對應的程序。
第四行help指令表示其後面是幫助文本。有助於用戶和開發人員理解相應的程序和建立自己的內核。
還有一些其它的指令。
//Kconfig文件用於銜接整個Kernel源碼樹;
×××××××××××××××××××××××××××××××××××××××××××××××××××/
D>.在Makefile中添加:obj-$(CONFIG_USB_GADGET_TEST) += hhtest/
//添加目的:使build系統能夠沿着源碼樹往下找到hhtest/子目錄
E>.其次要在一個已經存在的Kconfig文件中添加如下一行(一般在新建目錄的父目錄的Kconfig添加):
source "drivers/usb/gadget/hhtest/Kconfig"
source命令的作用是,讓後面帶的文件或者目錄下的文件生效。相當於,讓文件執行一下,讓修改生效。省去了重啓電腦的麻煩。
由於我們的Kconfig是新建的,所以,它並沒有生效。
所以,我們在一個已經存在的Kconfig文件中,調用source命令 , 讓我們新建的Kconfig文件生效。
==================================================================================================================
源碼樹內添加方法{多文件}:
如果新加模塊包含多個文件,則可以將Makefile(gadget/hhtest/下)寫爲:
obj-$(CONFIG_USB_GADGET_ONLINE) += test.o
test-objs :=one.o two.o three.o four.o
這樣,test.ko由one.c,two.c,three.c,four.c四個文件編譯鏈接而成。
如果,想要爲這些文件指定額外的gcc編譯選項,在Makefile文件中添加類似如下的一行:
EXTRA_CFLAGS += -ONLY_TEST
如果我們將模塊的所有文件放置在目錄gadget/下,則將hhtest/目錄下的Makefile中的內容,添加到目錄gadget/目錄下的Makefile中即可;
===================================================================================================================
2.2>.置於源碼樹之外
如果將模塊源碼置於源碼樹之外,那麼在自己的源碼目錄下創建Makefile文件,並且添加:
obj-m := test.o
這樣會把netmeeting.c編譯成netmeeting.ko。如果有多個源文件,那麼在Makefile文件中添加:
obj-m := test.o
test-objs :=one.o two.o three.o four.o
放置於源碼樹之外和放置於源碼樹之內,主要區別在於build過程。
放置於源碼樹之外,編譯模塊時,需要使用命令make去找到內核源碼文件和基本的Makefile文件。如下所示:
make -C /kernel/source/location SUBDIRS=$PWD modules
/keinel/source/location是已經配置過的內核源碼樹位置。
3、安裝模塊(installing Modules)
編譯後的模塊要放在目錄/lib/modules/version/kernel/下。一般是在Makefile中添加:
modules_install:
cp netmeeting.ko /lib/modules/version/kernel/
.PHONY:
modules_install
此步操作,需要root權限。
4.生成模塊依賴(Generating Modules Dependencies)
LINUX模塊實用工具能夠理解模塊間的依賴性。
也就是說如果模塊chen依賴於模塊sbb,那麼在裝載模塊chen時,模塊sbb會自動的裝載。
也就是LINUX下常說的依賴性編譯:文件A的編譯依賴於B,B的編譯依賴於文件C。
在root權限下,運行如下命令來建立模塊間的依賴信息。
sudo depmod 或 sudo depmod -n //-n:show process
----------------------------------------------------------------------------------------------------------------------------------------
該命令詳細的使用方法和規則,可通過下面命令來查看幫助:
depmod --help
----------------------------------------------------------------------------------------------------------------------------------------
模塊間的依賴信息存放在文件/lib/modules/version/modules.dep中.
----------------------------------------------------------------------------------------------------------------------------------------
5、裝載模塊(loading Modules)
用命令insmod裝載模塊是最簡單的一種方法。它請求內核裝載指定的模塊。
命令insmod不會檢查模塊間的依賴關係,也不會執行是否有錯誤的檢查。
加載模塊:
insmod (模塊名)
卸載模塊:
rmmod (模塊名)
這兩個工具很簡單,實用,但是缺乏智能性。
××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××
而實用工具modprobe提供了依賴性關係的解決方案,智能的錯誤檢查和報告等。裝載模塊時,是我們的首選。
root權限下,執行命令:
加載模塊:
modprobe (模塊名) (模塊參數) //模塊參數見下一節6.
modprobe命令,不僅試圖裝載寫在其後的模塊,還試圖裝載它依賴的所有模塊。因此,它是首選。
卸載模塊:
modprobe r (模塊名)
這裏的modprobe可以移除多個模塊,還可以移除它依賴的並且不在使用中的其它模塊。
6、模塊參數(Module Parameters)
對於如何向模塊傳遞參數,LINUX Kernel提供了一個簡單的框架。其允許驅動程序聲明參數,並且用戶在系統啓動或模塊裝載時爲參數指定相應值,在驅動程序中,參數的用法如同全局變量。這些模塊參數也能夠在sysfs中顯示出來(sysfs暫不清楚是什麼)。結果,有許多辦法來創建和管理模塊參數。
通過宏module_param()定義一個模塊參數:
module_param(name,type,perm);
參數解釋:
name:既是用戶看到的參數名,也是模塊內接受參數的變量。
type:表示參數的數據類型,是下列之一:byte,short,ushort,int,uint,long,ulong,charp,bool,invbool。
參數類型分別是:a byte, a short integer, an unsigned short integer, an integer, an unsigned integer,
a long integer, an unsigned long integer, a pointer to a char, a Boolean, a Boolean whose value is
inverted from what the user specifies.
The byte type is stored in a single char and the Boolean types are stored in variables of type int.
The rest are stored in the corresponding primitive C types.
perm:指定了在sysfs中相應文件的訪問權限。
訪問權限用通常的八進制格式來表示或者通常的S_Ifoo定義。
八進制格式的使用和操作系統下是一致的。
0755:表示所有制是讀寫、執行的權限。所在組是讀和執行的權限。其他用戶是讀和執行的權限。
S_Ifoo下:例如:
S_IRUGO|S_IWUSR(表示其它用戶具有讀權限,用戶具有寫權限)。用0表示完全關閉在sysfs中相對應的項。
因爲宏是不能聲明變量的,所以,在使用宏之前,必須先聲明變量。典型的用法如下:
static unsigned int use_acm = 0;
module_param(use_acm,uint,S_IRUGO);
這些變量的聲明是放在模塊源文件的開頭部分。即use_acm是全局變量。
我們可以使用宏module_param_named()使模塊源文件內部的變量名與外部的參數名有不同的名字。
可以理解爲,給變量名加了個引用。
module_param_named(name,variable,type,perm);
name:外部可見的參數名
variable:源文件內部的全局變量
例如:
static unsigned int max_test = 9;
module_param_named(maximum,max_test,int,0);
如果模塊參數是一個字符串時,通常使用charp類型定義這個模塊參數。內核複製用戶提供的字符串到內存,並且相對應的變量指向這個字符串。例如:
static char *name;
module_param(name,charp,0);
另一種方法是通過宏
module_param_string()讓內核把字符串直接複製到程序中的字符數組內。
module_param_string(name,string,len,perm);
name:外部的參數名
string:內部的變量名
len:以string命名的buffer大小(len可以小於buffer的大小,但是沒有意義)
perm:sysfs的訪問權限(或者perm爲零,表示完全關閉相對應的sysfs項)。
以上的都是隻能傳遞一個參數給模塊。如果要給模塊傳遞多個參數,可以通過宏
module_param_array(name,type,nump,perm);
name:既是外部模塊的參數名又是程序內部的變量名。name數組必須靜態分配。
type:是數據類型。
perm:sysfs的訪問權限。
nump:是一個指針。其值表示有多少個參數存放在數組name中。
例如:
static int finish[MAX_FISH];
static int nr_fish;
module_param_array(fish,int,&nr_fish,0444);
我們可以通過宏module_param_array_named(name,array,type,nump,perm);
參數的意義和宏module_param_named()是一樣的。
最後,用宏MODULE_PARM_DESC()對參數進行說明:
static unsigned short size = 1;
module_param(size,ushort,0644);
MODULE_PARM_DESC(size,"The size in inches of the fishing pole"\
"connected to this computer");
使用這些宏,需要包含頭文件:<linux/moduleparam.h>
===================================================================================================================
7.輸出符號(Exported Symbols)
當裝載模塊的時候,模塊是動態的鏈接入內核之中。
然而,動態鏈接的二進制代碼只能調用外部函數,所以外部函數必須明確的輸出,才能被模塊調用。
在內核中,通過EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()來達到目的。
輸出的函數,可以被其它的模塊調用。
沒有輸出的函數,不能被其它模塊調用。
模塊比核心內核映像代碼具有更嚴格的鏈接和調用規則。因爲所有核心源文件鏈接成一個單一的作爲基礎的映像,因此在內核中核心代碼可以調用任何非靜態的接口。
當然,輸出符號也必須是非靜態屬性。
一套輸出的內核符號稱之爲輸出的內核接口,也稱之爲Kernel API。
當函數聲明時,用EXPORT_SYMBOL()把函數輸出。
例如:
int usb_gadget_register_driver(struct usb_gadget_driver *driver)
{
.......
}
EXPORT_SYMBOL(usb_gadget_register_driver);
這樣,任何模塊都可以調用函數usb_gadget_register_driver(),只要在源文件中包含了聲明這個函數的頭文件,或者extern這個函數的聲明(這點同C語言)。
若你希望你的接口讓只遵守GPL的模塊調用。那麼通過MODULE_LICENSE()的使用,內核鏈接器能夠保證做到這一點。
EXPORT_SYMBOL_GPL(usb_gadget_register_driver);只允許標有GPL許可證的模塊訪問函數usb_gadget_register_driver()。
如果你的代碼配置爲模塊方式,那麼必須確保:源文件中使用的所有接口必須是已經輸出的符號,否則導致在裝載時鏈接錯誤。