Linux是單塊內核(monolithic)的操作系統,整個系統內核都運行與一個單獨的包含域中。Linux內核是模塊化組成的,它允許內核在運行時動態地向其中插入或從中刪除代碼。這些代碼包括相關的子例程,數據,函數入口和函數出口,被一併組合在一個單獨的二進制鏡像中,即所謂的可裝載內核模塊中,或簡稱爲模塊。
支持模塊的好處就是基本內核鏡像可以儘可能的小,因爲可選的功能和驅動程序可以利用模塊形式再提供。模塊允許我們方便的刪除和重新載入內核代碼,也方便了調試工作。而且當熱插拔新設備時,可通過命令載入新的驅動程序。
- Hello world!
- /*
- *hello.c--hello world 簡單內核模塊程序
- */
- #include<linux/init.h>
- #include<linux/module.h>
- #include<linux/kernel.h>
- static int hello_init()
- {
- printk(KERN_ALERT"I bear a charmed life./n");
- return 0;
- }
- static void hello_exit(void)
- {
- printk(KERN_ALERT"out,out,brief candle!/n");
- }
- module_init(hello_init);
- module_exit(hello_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Shakespeare");
與大多數內核子系統不同,模塊開發更接近編寫新的應用系統,因爲至少要在模塊文件中具有入口點和出口點。
模塊的初始化函數(入口點)必須符合下面的形式:
- int my_init(void); //如果初始化順利完成,返回0,否則返回非零
因爲init函數通常不會被外部函數直接調用,所以不必導出該函數,它可以被標記爲static類型。
模塊的初始化函數通過module_init()例程註冊到系統中,在模塊裝載時被調用。調用module_init()實際上是一個宏調用,它惟一的參數是模塊的初始化函數。
模塊的出口函數偶module_exit()例程註冊到系統。在模塊從內存卸載時,內核便會調用模塊的出口函數。該函數可能
在返回前負責清理資源,以保證硬件處於一致狀態,或者其他別的操作。在退出函數返回後,模塊就被卸載了。退出函數的形式如下:
- void my_exit(void);
如果上述文件被靜態地編譯到內核映像中,那麼退出函數將不被包含,而且用於都不會被調用(如果不是編譯成模塊的話,代碼就永遠不需要從內核中卸載)。
MODULE_LICENSE()宏用於指定模塊的版權。如果載入非GPL模塊到系統內存,則會在內核中設置被污染標誌,這個標誌起到記錄信息的作用。不過如果開發者提交的bug報告中包含有被污染的標誌,那麼報告的信用會被減低。非GPL模塊不能調用GPL-only符號。
MODULE_AUTHOR()宏指定了代碼作者,它完全是用於信息記錄。
- 構建模塊
在2.6內核中,採用了新的“kbuild”構建系統,構建的第一步是決定在哪裏管理模塊源碼。可以把模塊源碼加入到內核源代碼樹中,或者作爲一個補丁,或者最終把代碼把代碼最終合併到正式的內核代碼樹中;另一種可行的方式是在內核源代碼樹之外維護和構建模塊源碼。
1. 放在內核源碼樹中
最理想的情況是你的模塊正式稱爲Linux內核的一部分,這樣就會被存放在內核源代碼樹中。
設備驅動程序存放在內核源碼樹根目錄deriver/的子目錄下,在其內部設備驅動文件被進一步按照類別、類型和特殊驅動程序得呢個更有序的組織起來。字符設備存在與drivers/char/目錄下,塊設備存放在drivers/block/目錄下,USB設備則存放在drivers/usb/目錄下。
假設我們的驅動程序名爲Foo XL,如果在目錄drivers/char/下建立了自己的代碼子目錄foo,要做的是向drivers/char/目錄中的Makefile文件添加代碼,不同的代碼產生不同的編譯效果:
- obj-m += foo/ //這行編譯指令告訴模塊構建系統在編譯模塊時需要進入foo/目錄中
- obj-$(CONFIG_FOO) += foo/ //如果驅動程序的編譯取決於一個特殊的配置選項,比如CONFIG_FISHING_POLE,這時要用這條編譯指令
- /*驅動程序智能化,可以自動檢測其他文件,如foo-xxx.c,這樣foo-main.c和foo-xxx.c就一起被編譯和連接到foo.ko模塊內*/
- obj-$(CONFIG_FOO) += foo.o
- foo-objs :=foo-main.o foo-xxx.o
- //注意,如果需要在構建文件時需要額外的編譯標記,添加如下指令
- EXTRA_CFLAGS += -DTITANIUM_POLE
如果把源文件置於drivers/char/目錄下,不建立新目錄,就將原來處於drivers/char/foo/下你寫的makefile中的代碼都加入到drivers/char/下的Makefile中。
編譯,運行內核構建過程。如果模塊編譯取決於配置選項,比如有CONFIG_FOO約束,那麼在編譯前首先要確保選項被允許。
2. 放在內核代碼外
如要要脫離內核源代碼以此來維護和構建自己的模塊,要做的就是在自己的源代碼目錄中建立一個Makefile文件,它只需要一行指令:
- obj-m := foo.o
就可以把foo.c編譯成foo.ko。如果有多個源文件,那麼就用下列兩行 :
- obj-m := foo.o
- foo-objs := foo-main.o foo-xxx.o
這樣foo-main.c和foo-xxx.c就一起被編譯和連接到foo.ko模塊內。
模塊在內核內或內核外構建的最大區別在於構建過程,當模塊在內核源碼外圍時,要告訴make如何找到內核源碼文件和基礎Makefile文件:
- /* /kernel source location是已配置的內核源代碼樹。
- *不要把要處理的內核源代碼樹放在/usr/src/linux下,而要移到你的home目錄下的某個方便訪問的地方
- */
- make -c /kernel source location SUBDIRS-$PWD modules
- 安裝模塊
編譯後的模塊將被裝入到目錄/lib/modules/version/kernel/下。比如,使用2.6.10版本內核,模塊的源代碼直接放在drivers/char/下,編譯後的驅動程序的存放路徑是:
/lib/modules/2.6.10/kernel/drivers/char/foo.ko
- //下面的構建命令用來安裝編譯的模塊到合適的目錄下,要以root權限運行
- make modules_install
- 產生模塊依賴性
Linux模塊之間存在依賴性。A模塊依賴於B模塊,那麼當載入A模塊時,B模塊也會被自動載入。這裏需要的依賴信息必須事先生成。若想產生內核依賴關係的信息,root用戶可以運行命令depmod
爲了執行更快的更新操作,可以只爲新模塊生成依賴信息,而不是生成所有的依賴關係,這時root用戶可以運行命令:
depmod -A
模塊依賴關係信息存放在/lib/modules/version/modules.dep文件中。
- 載入模塊
載入模塊最簡單的方法是通過insmod命令。這是個功能很有限的命令,它的作用是請求內核載入指定的模塊。insmod程序不執行任何依賴性分析或進一步的錯誤檢查。以root身份運行命令:
insmod foo
卸載一個模塊:
rmmod foo
系統提供了更先進的工具modprobe,它提供了模塊依賴性分析,錯誤智能檢查,錯誤報告以及其他功能和選項:
modprobe modulename [module parameter]
參數將在模塊加載時傳入內核。
modprobe命令不但會加載指定的模塊,還會自動加載任何它所依賴的有關模塊。它還可以用來從內核中卸載模塊,以root身份運行:
modprobe -r modulename
與rmmod命令不同,modprobe也會卸載給定模塊所依賴的相關模塊。其前提是這些相關模塊沒有被使用。
- 管理配置選項
前面設置了CONFIG_FOO配置選項,foo模塊將被自動編譯,若要添加一個新的配置選項還可以怎麼做呢?
向Kconfig文件中添加一項,用以對應內核源碼樹。
對驅動程序而言,Kconfig一般與源代碼處於同一目錄,比如,foo程序在目錄drivers/char/下,那麼就存在drivers/char/Kconfig。如果新建了子目錄存放foo,而且系統Kconfit文件存在於該目錄,那麼必須在一個已經存在的Kconfig文件(即drivers/char/Kconfig)中將它引入:
source "drivers/char/foo/Kconfig"
我們以Foo XL模塊添加選項爲例,在Kconfig文件中加入一個配置選項如下:
- config FOO
- tristate "Foo XL support"
- default n
- help
- If you say Y here, support for the Foo XL with computer interface will
- be complied into the kernel and accessible via device node. You can also
- say N here and the driver will be built as a module named foo.ko.
- If unsure, say N.
配置選項第1行定義了該選項所代表的配置目標。注意CONFIG_前綴不需要寫上。
第2行聲明選項類型爲tristate,也就是說可以被編譯進內核(Y),也可作爲模塊編譯(M),或者乾脆不編譯它(N)。如果編譯選項代表的是一個系統功能,而不是一個模塊,那麼編譯選項將用bool指令代替tristate,這說明它不允許被編譯成模塊。處於指令之後的引號內的文字爲該選項指定的名稱。
第3行指定了該選項的默認選擇,這裏默認爲不編譯它。
第4行help指令的目的是爲該選項提供幫助文檔。
除了上述選項,還有其他選項。
depends指令規定了在該選項被設置其,首先要設置的選項。假如依賴性不滿足,那麼該選項被禁止。比如:
- depends on FOO_DEPEND
加入到配置選項中,那麼就意味着CONFIG_FOO_DEPEND被選擇前,Foo XL模塊是不能被使用的。
select指令與depends指令類似,不同之處在於,select指定了誰,它就會強行將被指定的選項打開。不能濫用該指令,因爲它會自動的激活其他配置選項。比如:
- select BIT
意味着當CONFIG_FOO被激活時,配置選項CONFIG_BIT必然一起被激活。
如果select和depends同時指定多個選項,需要通過&&指令來進行多選。使用depends還可以利用歎號!前綴來指明禁止某個選項。比如:
- depends on DUMB_DREVERS && !NO_FOO_ALLOWED
bool選項和tristate往往會結合if指令一起使用,表示某個選項取決於另一個配置選項。如果條件不滿足,配置選項就會被禁止,甚至不會顯示在配置工具中。比如:
- bool “Deep sea mode”if OCEAN
意味着"Deep sea mode"只有在CONFIG_OCEAN選項打開時纔可見且有效。
If指令也可以與defualt指令結合使用,強制只有在條件滿足時default選項纔有效。
配置系統導出了一些元選項(meta-option)以簡化生成的配置文件。
CONFIG_EMBEDDED是用於關閉那些用戶想要禁止的關鍵功能。
CONFIG_BROKEN_ON_SMP用來表示驅動程序並非多處理器安全的。
CONFIG_EXPERIMENTAL用於說明某些功能尚在試驗或處於beta版本階段。
- 模塊參數
Linux語序驅動程序聲明參數,從而用戶可以在系統啓動或者模塊裝載時再指定參數值,這些參數對驅動程序來說屬於全局變量。模塊參數同時也將出現在sysfs文件系統中,這樣一來,無論是生成模塊參數,還是管理模塊參數的方式都變得靈活多樣了。
定義一個模塊參數可以通過宏module_param()完成:
- 在include/linux/moduleparam.h中
宏module_param()並沒有定義變量,必須在使用該宏前進行變量定義。例如:
- static int allow_live_bait = 1;
- module_param(allow_live_bait,bool,0644);
通常需要用一個charp類型來定義模塊參數(一個字符串),內核將用戶提供的這個字符串拷貝到內存,而且將你的變量指向該字符串。比如:
- static char *name;
- module_param(name,charp,0);
有可能模塊的外部參數名稱與對應的內部參數名稱,這時使用宏module_param_named()。比如:
- static unsigned int max_test = DEFAULT_MAX_LINE_TEST;
- module_para_named(maximum_line_test,max_test,int,0);
其他相關的宏:
- /* Actually copy string: maxlen param is usually sizeof(string). */
- #define module_param_string(name, string, len, perm) /
- static struct kparam_string __param_string_##name /
- = { len, string }; /
- module_param_call(name, param_set_copystring, param_get_string, /
- &__param_string_##name, perm); /
- __MODULE_PARM_TYPE(name, "string")
- /* Comma-separated array: *nump is set to number they actually specified. */
- #define module_param_array_named(name, array, type, nump, perm) /
- static struct kparam_array __param_arr_##name /
- = { ARRAY_SIZE(array), nump, param_set_##type, param_get_##type,/
- sizeof(array[0]), array }; /
- module_param_call(name, param_array_set, param_array_get, /
- &__param_arr_##name, perm); /
- __MODULE_PARM_TYPE(name, "array of " #type)
- #define module_param_array(name, type, nump, perm) /
- module_param_array_named(name, name, type, nump, perm)
-
導出符號表
模塊被載入後,就會動態連接到內核。注意,它與用戶空間中的動態連接庫類型,只有當被顯示導出後的外部函數,纔可以被動態庫調用。在內核中,導出內核函數需要使用特殊的指令:EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()。
導出的內核函數可以被模塊調用,而爲導出的函數模塊則無法被調用。模塊代碼的鏈接和調用規則相比核心內核鏡像中的代碼而言,更加嚴格。核心代碼在內核中可以調用任意非靜態接口,因爲所有的核心源碼文件都被鏈接成同一個鏡像。當然,被導出的符號表所包含的函數必須是非靜態的。
導出的內核符號表被看做是導出的內核接口,甚至稱爲API。
導出符號相對簡單,在聲明函數後,緊接着EXPORT_SYMBOL()指令就可以了。該函數如果定義在一個可訪問的頭文件中,那麼任何模塊現在都可以訪問它。
如果系統自己的接口僅僅對GPL兼容的模塊科技,那麼內核連接器使用MODULE_LICENCE()宏滿足這個要求,如果系統函數僅僅對標記爲GPL協議的模塊可見,那麼就要用EXPORT_SYMBOL_GPL()指令。
如果你的代碼被配置爲模塊,那麼就必須確保當它被編譯爲模塊時鎖用的全部接口都已經被導出,否則會產生鏈接錯誤,模塊不能成功編譯。