1. 內核簡單模塊的編寫
通過命令date可以獲取當前系統時間,如下面示例。
下面我們通過編寫一個簡單的內核模塊直接獲取當前系統時間。
1.1模塊源碼編寫
在Linux內核源碼中,定義了一個struct timeval結構體,結構體中有兩個成員變量tv_sec,tv_usec,分別保存當前系統時間的秒和毫秒,time_t和suseconds_t類型變量在x86架構中,均爲long型,變量類型定義在文件include/linux/time.h中。
00018: struct timeval {
00019: time_t tv_sec; / * seconds */
00020: suseconds_t tv_usec; / * microseconds */
00021: };
00022:
模塊源碼如下:
00001:
00002: #include <linux/ module.h>
00003: #include <linux/ time.h>
00004:
00005: static char modname[] = "time";
00006:
00007: extern struct timespec xtime;
00008:
00009: int init_module( void )
00010: {
00011: struct timeval tv;
00012: printk( "Installing %s module.", modname );
00013: do_gettimeofday(&tv);
00014: printk("\njiffies:%lu, tv.tv_sec:%lu, tv.tv_nsec:%lu ", jiffies, tv.tv_sec, tv.tv_usec);
00015:
00016: return 0;
00017: }
00018:
00019:
00020: void cleanup_module( void )
00021: {
00022: printk( "\nRemoving %s module.", modname );
00023: }
00024:
00025: MODULE_LICENSE("GPL");
00026:
1.2Makefile
創建一個Makefile,執行make,即可編譯生成內核模塊,生成後綴名爲.ko的文件。
Makefile內容:
注意:在default:後面的$(MAKE) … … 和rm –r … …兩行前面必須是Tab鍵,不能爲空格或其他字符,否則執行make時,會報告“Makefile:10: *** missing separator. Stop.”錯誤。
1.3模塊加載
執行make,編譯生成模塊.ko文件後,就可以通過insmod命令來加載模塊。
通過lsmod命令可以查看驅動是否成功加載到內核中。
通過insmod命令加載剛編譯成功的time.ko模塊後,似乎系統沒有反應,也沒看到打印信息。而事實上,內核模塊的打印信息一般不會打印在終端上。驅動的打印都在內核日誌中,我們可以使用dmesg命令查看內核日誌信息。
內核模塊time.ko獲取到的當前系統時間爲1289489871秒,與執行date ‘+%s’命令獲取到的值一致。
2內核模塊版本與符號表
在編寫和使用內核模塊過程中,會發現在某個內核版本上編譯的模塊只能在當前內核版本中使用。若模塊版本號與當前內核版本號不匹配導致就會無法加載,提示“insmod: error inserting 'time.ko': -1 Invalid module format”,內核會打印類似信息“time: version magic '2.6.32.12-0.7-default SMP mod_unload modversions ' should be '2.6.18-92.el5 SMP mod_unload gcc-4.1'”。
2.1內核模塊版本號
查看內核模塊版本信息的命令爲modinfo,如查看剛纔我們編寫的time.ko。
模塊的版本號在“vermagic“一項,當前系統中使用的模塊版本號都是相同的。
模塊版本號是哪裏決定的?我們是否可以更改?我們是否可以在當前系統中,編譯其他內核版本的模塊?
2.1.1模塊版本號的確定
在make編譯模塊時,通過-C參數制定內核源碼頭文件位置。前面我們編譯time模塊內核源碼頭文件位置爲/lib/modules/2.6.32.12-0.7-default/build。
make -C /lib/modules/2.6.32.12-0.7-default/build SUBDIRS=/root/programming modules
我們來分析模塊版本號的確定
vermagic: 2.6.32.12-0.7-default SMP mod_unload modversions
在RHEL5系統中,模塊版本號vermagic由include/linux/vermagic.h和include/linux/utsrelease.h兩個文件的內容來決定,即vermagic就爲VERMAGIC_STRING。
文件include/linux/utsrelease.h的內容如下:
文件include/linux/vermagic.h的內容如下:
而在SLES11.1內核2.6.32.12-0.7-default的模塊版本號VERMAGIC_STRING由scripts/mod/modpost可執行文件確定。
2.1.2模塊版本號的修改
前面分析了模塊版本號由VERMAGIC_STRING確定產生,若我們需要修改模塊版本號或希望在當前內核版本中編譯其他內核的模塊(注意gcc大版本和CPU架構i686/x86_64保持一致),只需要修改控制模塊的VERMAGIC_STRING值即可。
如我們將示例中的time模塊在RHEL5.2內核中編譯RHEL5.3內核模塊,然後在RHEL5.3系統上可以加載。
2.1.3編譯非當前內核版本模塊
在模塊版本號的修改介紹的方法中,僅適合內核版本(OS發行版本)差別不大的情況下,可以方便修改某塊版本。本小節介紹如何編譯非當前內核版本模塊。
步驟:
-
將待編譯特定內核源碼開發包拷貝到當前系統中某個目錄下
如我們打算在SLES11.1 x86_64系統中編譯RHEL5.5 x86_64內核模塊,應先將RHEL5.5 x86_64內核開發包拷貝到SLES11.1系統中。
-
修改Makefile,將KDIR指向待編譯內核開發包目錄
在Makefile中,設置KDIR變量爲指定內核源碼目錄位置。
3、執行make,編譯模塊
執行後,生成內核模塊。可以使用modinfo命令來查看新生成模塊的版本號。如
#modinfo /root/programming/time.ko
編譯非當前內核版本模塊後,加載再次提示“Invalid module format”時,通過dmesg命令查看加載失敗原因。
如上面的提示,我們直接include/linux/vermagic.h文件即可,將gcc版本信息值改爲固定值gcc-4.1即可。
2.2內核符號表及使用
加載模塊時,insmod使用公共內核符號表解析模塊中未定義的符號。公共符號表中包含了所有的全局內核項(即函數和變量)的地址,內核符號表的內容全部在文件/proc/kallsyms中,可以通過cat等命令查看。內核和模塊將函數、變量導出後,就成爲內核符號表的一部分。
在我們編寫的內核模塊中,可以使用內核或其他模塊定義的函數和變量,如本章示例的獲取時間模塊中,就調用了內核函數do_gettimeofday()。
內核中有兩個宏用來導出函數和變量:
EXPORT_SYMBOL(symbolname)
將函數或變量導出到所有模塊
EXPORT_SYMBOL_GPL(symbolname)
將函數或變量僅導出到GPL模塊
我們也可以在自己的模塊中導出部分函數或變量,這樣其他模塊就可以訪問這部分函數、變量。C語言用戶態程序編程中,我們常會使用在其他C文件或lib庫中定位的函數和變量,內核符號表和這有相似之處。
系統中所有內核和模塊導出的變量和函數,就成了內核符號表,在/proc/kallsyms文件中。
內核符號表中,第一列爲函數或變量的在內核中的地址,第二列爲符號的類型,第三列爲符號名,第四列爲符號所屬的模塊。若第四列爲空,則表示該符號屬於內核代碼。
內核符號屬性
符號屬性 |
含義 |
b |
符號在未初始化數據區(BSS) |
c |
普通符號,是未初始化區域 |
d |
符號在初始化數據區 |
g |
符號針對小object,在初始化數據區 |
i |
非直接引用其他符號的符號 |
n |
調試符號 |
r |
符號在只讀數據區 |
s |
符號針對小object,在未初始化數據區 |
t |
符號在代碼段 |
u |
符號未定義 |
若符號在內核中是全局性的,則屬性爲大寫字母,如T、U等。其他符號屬性含義,請參考命令nm的幫助信息。
00273: / * Only label it "global" if it is exported. */
00274: static void upcase_if_global(struct kallsym_iter *iter)
00275: {
00276: if (is_exported(iter- >name, iter- >owner))
00277: iter- >type += 'A' - 'a';
00278: }
00279:
若打算使用內核中的符號,在模塊中增加函數或變量說明即可。如:
00091: extern struct timespec xtime;
3模塊版本控制
Linux內核版本在不變升級,內核提供的API或符號可能也隨之變化。這對內核模塊開發來說,是一個比較麻煩的問題,通常要適應不同的內核版本,或者只針對具體某些內核版本開發。
內核爲了確保模塊的函數接口與內核藉口一致,採用了模塊版本控制。版本控制最簡單的辦法就是爲了內核和模塊都設置一個常量,該常量會隨着接口變化而不斷增加。加載模塊時,內核會檢查模塊提供的常量是否和內核版本常量相等,若不相等則拒絕加載。
採用常量的辦法進行版本控制,方法簡單,但不夠靈活。如內核部分接口變化後,版本常量就會增加。但若某模塊使用的這些接口並沒有變化,也會導致驅動無法加載。基於這個原因,最恰當的方法是將單個內核API的變化考慮進去。實際的模塊和內核實現無關,模塊和內核關係密切的是API接口。
3.1checksum方法
CRC checksum原理是使用函數的參數來計算校驗碼,若校驗碼不相等,加載模塊失敗。
我們來看一下內核執行模塊加載的函數load_module()(文件kernel/module.c)。1767行會調用check_modstruct_version()函數來檢查struct_module符號的CRC校驗碼。若校驗碼不相等,則提示“disagrees about version of symbol struct_module”。如
hwinc_kernel_driver: disagrees about version of symbol struct_module
Found checksum B6AF205C vs module F3D5F8AF
01600: static struct module *load_module(void __user *umod,
01601: unsigned long len,
01602: const char __user *uargs)
01603: {
01604: Elf_Ehdr *hdr;
01605: Elf_Shdr *sechdrs;
… …
01766: / * Check module struct version now, before we try to use module. */
01767: if (! check_modstruct_version(sechdrs, versindex, mod)) {
01768: err = - ENOEXEC;
01769: goto free_hdr;
01770: }
01771:
01772: modmagic = get_modinfo(sechdrs, infoindex, "vermagic");
01773: / * This is allowed: modprobe - - force will invalidate it. */
01774: if (! modmagic) {
01775: add_taint_module(mod,TAINT_FORCED_MODULE);
01776: printk(KERN_WARNING "%s: no version magic, tainting
kernel.\n",
01777: mod- >name);
01778: } else if (! same_magic(modmagic, vermagic )) {
01779: printk(KERN_ERR "%s: version magic '%s' should be '%s'\n",
01780: mod- >name, modmagic, vermagic );
01781: err = - ENOEXEC;
01782: goto free_hdr;
01783: }
在編譯內核模塊時,會生成*.mod.c文件,該文件中包含了模塊中各個符號的校驗碼。校驗碼的生成,由scripts/genksyms/genksyms計算生成。
注意:scripts/genksyms/genksyms文件是在內核源碼目錄或內核開發包目錄中,如/usr/src/linux-2.6.32.12-0.7-obj/x86_64/default/scripts/genksyms/genksyms
3.2vermagic
查看內核版本模塊信息時,會看到vermagic一項。模塊在裝載時,load_module()函數會比較(如前面代碼的1772行)當前運行內核的vermagic和當前要加載的模塊的vermagic比較,如果不同,則禁止加載模塊。
Vermagic的的確定請參考章節2.1.1。
3.3內核模塊版本控制使能與關閉
我們常遇到內核提示“disagrees about version of symbol struct_module”,而導致模塊無法加載的情況。
3.3.1內核中關閉/使能
若選擇關閉內核的模塊版本控制功能,則會避免出現這種情況。模塊版本控制選項在內核源碼配置文件.config中,註釋掉CONFIG_MODVERSIONS就取消了模塊版本控制。
CONFIG_MODVERSIONS=y
重新編譯內核,重啓即可。
3.3.2模塊中關閉/使能版本控制
如下面的實例。雖然模塊的vermagic和內核一致,但struct_module的版本號不一致。我們可以不修改當前內核,重新編譯模塊即可解決問題。
若去掉模塊版本控制後,加載驅動導致系統死機。建議解決辦法:使用待運行內核的.config配置文件覆蓋模塊編譯指向的內核開發包(源碼).config文件。
.config配置文件的獲取:(1)可以拷貝/proc/config.gz,然後解壓縮,拷貝爲.config;(2)若/proc/config.gz不存在,可以使用/boot/目錄下對應的內核配置文件;(3)或向內核提供者獲取.config配置文件。
此時我們可以修改內核開發包中的模塊版本控制選項,修改文件.config(在內核源碼或開發包根目錄下),註釋掉或刪除CONFIG_MODVERSIONS選項,重新編譯模塊即可去除模塊的版本控制。
CONFIG_MODULES=y
CONFIG_OBSOLETE_MODPARM=y
#CONFIG_MODVERSIONS=y
CONFIG_MODULE_SIG=y
4內核模塊參數
在用戶執行系統命令或其他程序時,可以使用參數。內核模塊也可以使用參數。
參數必須使用宏module_param()聲明,該宏定義在include/linux/moduleparam.h文件中。module_param()需要三個參數:參數名稱、類型、sysfs文件系統入口項的訪問權限掩碼。
模塊參數的定義必須放在任何函數之外。如本章獲取系統時間的模塊示例,我們增加province和population兩個參數(參數僅作示範,和系統時間無任何關係)。
00001: #include <linux/ module.h>
00002: #include <linux/ time.h>
00003: #include <linux/ moduleparam.h>
00004:
00005: static char modname[] = "time";
00006:
00007: static char *province = "Guangdong";
00008: module_param(province, charp, 0);
00009: static int population = 10000;
00010: module_param(population, int, 0);
00011:
00012: int init_module( void )
00013: {
00014: struct timeval tv;
00015: printk( "Installing %s module.", modname );
00016: do_gettimeofday(&tv);
00017: printk("\njiffies:%lu, tv.tv_sec:%lu, tv.tv_nsec:%lu ",
00018: jiffies, tv.tv_sec, tv.tv_usec);
00019:
00020: printk("\nProvince:%s, Population:%d \n", province , population );
00021:
00022: return 0;
00023: }
00024:
00025:
00026: void cleanup_module( void )
00027: {
00028: printk( "\nRemoving %s module.", modname );
00029: }
00030:
00031: MODULE_LICENSE("GPL");
00032:
加載模塊time後,內核打印信息:
內核模塊支持的參數類型如下:
bool
invbool
charp:字符串指針。內核會爲用戶提供的字符串自動分配內存。
int
long
short
uint:unsigned int
ulong:unsigned long
ushort:unsigned short
5模塊入口/出口函數及其他
每個內核模塊都要有初始化(入口)函數和清除(出口)函數,清除函數負責在模塊被移除前註銷接口並向系統返回所有資源。
在本章time模塊的示例中,並沒有像用戶態C程序一樣有main()入口函數。time模塊入口函數爲init_module(),而出口函數爲cleanup_module()。
在複雜的模塊中,我們可以指定模塊的入口/出口函數名稱。通過module_init()和module_exit()函數分別指定。如LSISAS1068E驅動mptsas中的入口/出口函數:
04828: module_init(mptsas_init);
04829: module_exit(mptsas_exit);
在模塊中,我們還可以添加作者信息、模塊描述、模塊版本等信息。
MODULE_AUTHOR():模塊作者信息
MODULE_DESCRIPTION():模塊描述
MODULE_LICENSE():模塊協議
MODULE_VERSION():模塊版本
如:
00070: #define my_NAME "Fusion MPT SCSI Host driver"
00071: #define my_VERSION MPT_LINUX_VERSION_COMMON
00072: #define MYNAM "mptscsih"
00073:
00074: MODULE_AUTHOR(MODULEAUTHOR);
00075: MODULE_DESCRIPTION(my_NAME);
00076: MODULE_LICENSE("GPL");
00077: MODULE_VERSION(my_VERSION);
6內核模塊與用戶程序區別
6.1用戶空間與內核空間
內核空間具有最高權限,可以訪問所有CPU寄存器和其他所有資源。
-
內核空間可以訪問所有的CPU指令和所有的內存空間、I/O空間。
-
用戶空間只能訪問有限的資源,若需要特殊權限,可以通過系統調用獲取相應的資源。
-
用戶空間允許頁面中斷,而內核空間則不允許。
-
用戶空間是0-3G的地址範圍,內核空間是3G-4G的地址範圍。
-
內核空間和用戶空間是針對線性地址空間的。
-
所有內核進(線)程共用一個地址空間,而用戶進程都有各自的地址空間。
Linux 32位系統用戶空間與內核空間
6.2內核模塊與應用程序的對比
-
內核模塊具有獨立的地址空間
模塊運行在內核空間中。應用程序運行在用戶空間中。系統軟件受到保護,不允許用戶程序訪問。內核空間和用戶空間有各自獨立的內存地址空間。
-
內核模塊具有更高的執行特權
運行在內核空間中的代碼要比運行在用戶空間中的代碼具有更大的特權。
-
內核模塊不按順序執行
用戶程序通常按順序執行並且從頭到尾地執行單獨的任務。內核模塊並不按順序執行,它註冊自己是爲了服務將來的請求。
-
內核模塊可以被中斷
在同一時刻,可能有許多進程同時向驅動程序發出請求。中斷程序可以在驅動程序正在響應系統調用時,向驅動程序發出請求。在對稱多處理器(SMP)系統中,驅動程序可能在多個 CPU 上併發地執行。
-
內核模塊必須是可搶佔的
-
內核模塊能夠共享數據
一個應用程序的不同線程常常不會共享數據。與之相對應的是,組成驅動程序的數據結構和例程被所有使用驅動程序的線程所共享。驅動程序必須能夠處理由多個請求導致的競爭問題。
-
錯誤處理
應用程序的錯誤導致Segmentation Fault,而內核模塊的錯誤影響整個系統,甚至使內核
7常見問題處理
1、頭文件引用
在用戶程序和內核模塊時,可能都會使用頭文件 #include <linux/time.h>,但兩者文件所在的位置是不同的。
用戶態用戶程序使用的time.h,在gcc庫文件中,一般位置是/usr/include/linux/time.h或/usr/include/sys/time.h。
內核模塊使用的time.h,在內核源碼頭文件中,一般位置是<內核版本>/include/linux/time.h,如/usr/src/kernels/2.6.18-128.el5-x86_64/include/linux/time.h。
2、提示內核build目錄不存在
在編寫好內核模塊後,執行make時,有的系統會提示類似“make: *** /lib/modules/2.6.18-128.el5xen/build: No such file or directory. Stop.”錯誤信息。原因在於內核源碼開發包沒有安裝。
解決辦法:安裝當前內核版本的源碼開發包。
3、模塊加載提示“Invalid Module Format”
解決步驟:
-
執行dmesg命令,查看模塊提示Invalid Module Format的詳細原因
-
根據提示信息,結合本章提到內核模塊版本號與修改一些,修復相應的錯誤。
4、模塊加載提示“Symbol not found”
解決步驟:
-
執行dmesg命令,查看模塊哪些符號在當前系統中不存在。
-
執行modinfo命令,查看當前模塊依賴關係,並檢查依賴的模塊是否已加載到系統中。
4、模塊加載提示“disagrees about version of symbol struct_module”
請參考“模塊版本控制”一節。
5、是否有辦法將模塊加載到非當前內核版本中,而不重新編譯模塊?
在內核版本相近和CPU架構相同的情況下,如2.6.18-92.e15 i686和2.6.18-194.e15 i686內核,可以直接二進制編輯模塊,修改模塊的版本信息,這樣就可以加載到非當前內核版本中了。