1. 引言
Linux系統(tǒng)開(kāi)放源代碼、系統(tǒng)漏洞少,在面對(duì)病毒和黑客入侵時(shí)能提供更好的安全性和穩(wěn)定性,基于以上這些優(yōu)點(diǎn),近年來(lái)對(duì)Linux操作系統(tǒng)及其相關(guān)技術(shù)的應(yīng)用和研究越來(lái)越多。對(duì)Linux操作系統(tǒng)擴(kuò)充或裁剪功能需要在重新編譯內(nèi)核上花費(fèi)大量的時(shí)間。LKM機(jī)制由于大大縮短了開(kāi)發(fā)和測(cè)試的時(shí)間,在 Linux開(kāi)發(fā)、研究的過(guò)程中起到了舉足輕重的作用。
LKM主要包括內(nèi)核模塊在操作系統(tǒng)中的加載和卸載兩部分功能,內(nèi)核模塊是一些在啟動(dòng)的操作系統(tǒng)內(nèi)核需要時(shí)可以載入內(nèi)核執(zhí)行的代碼塊,不需要時(shí)由操作系統(tǒng)卸載。它們擴(kuò)展了操作系統(tǒng)內(nèi)核功能卻不需要重新編譯內(nèi)核、啟動(dòng)系統(tǒng)。如果沒(méi)有內(nèi)核模塊,就不得不反復(fù)編譯生成操作系統(tǒng)的內(nèi)核鏡像來(lái)加入新功能,當(dāng)附加的功能很多時(shí),還會(huì)使內(nèi)核變得臃腫。
2. LKM的編寫和編譯
2.1 內(nèi)核模塊的基本結(jié)構(gòu)
一個(gè)內(nèi)核模塊至少包含兩個(gè)函數(shù),模塊被加載時(shí)執(zhí)行的初始化函數(shù)init_module()和模塊
被卸載時(shí)執(zhí)行的結(jié)束函數(shù)cleanup_module()。在最新內(nèi)核穩(wěn)定版本2.6中,兩個(gè)函數(shù)可以起
任意的名字,通過(guò)宏module_init()和module_exit()實(shí)現(xiàn)。唯一需要注意的地方是函數(shù)必須在宏的使用前定義。例如:
static int __init hello_init(void){}
static void __exit hello_exit(void ){}
module_init(hello_init);
module_exit(hello_exit);
這里聲明函數(shù)為static的目的是使函數(shù)在文件以外不可見(jiàn),__init的作用是在完成初始化后收回該函數(shù)占用的內(nèi)存,宏__exit用于模塊被編譯進(jìn)內(nèi)核時(shí)忽略結(jié)束函數(shù)。這兩個(gè)宏只針對(duì)模塊被編譯進(jìn)內(nèi)核的情況,而對(duì)動(dòng)態(tài)加載模塊是無(wú)效的。這是因?yàn)榫幾g進(jìn)內(nèi)核的模塊是沒(méi)有清理收尾工作的,而動(dòng)態(tài)加載模塊卻需要自己完成這些工作。
2.2 內(nèi)核模塊的編譯
編譯時(shí)需要提供一個(gè)makefile來(lái)隱藏底層大量的復(fù)雜操作,使用戶通過(guò)make命令就可以完成編譯的任務(wù)。下面就是一個(gè)簡(jiǎn)單的編譯hello.c的makefile文件:
obj-m += hello.ko
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
編譯后獲得可加載的模塊文件hello.ko。
內(nèi)核版本2.6中使用.ko文件后綴代替了.o,這是為了與普通可執(zhí)行文件相區(qū)別。
3. LKM的主要功能
3.1 模塊的加載
模塊的加載有兩種方法,第一種是使用insmod命令加載,另一種是當(dāng)內(nèi)核發(fā)現(xiàn)需要加載某個(gè)模塊時(shí),請(qǐng)求內(nèi)核后臺(tái)進(jìn)程kmod加載適當(dāng)?shù)哪K。當(dāng)內(nèi)核需要加載模塊時(shí),kmod被喚醒并執(zhí)行modprobe,同時(shí)傳遞需加載模塊的名字作為參數(shù)。modprobe像insmod一樣將模塊加載進(jìn)內(nèi)核,不同的是在模塊被加載時(shí)查看它是否涉及到當(dāng)前沒(méi)有定義在內(nèi)核中的任何符號(hào)。如果有,在當(dāng)前模塊路徑的其他模塊中查找。如果找到,它們也會(huì)被加載到內(nèi)核中。但在這種情況下使用insmod,會(huì)以“未解析符號(hào)”信息結(jié)束。
關(guān)于模塊加載,可以用圖3.1來(lái)簡(jiǎn)要描述:
insmod程序必須找到要求加載的內(nèi)核模塊,這些內(nèi)核模塊是已鏈接的目標(biāo)文件,與其他文件不同的是,它們被鏈接成可重定位映象即映象沒(méi)有被鏈接到特定地址上。insmod將執(zhí)行一個(gè)特權(quán)級(jí)系統(tǒng)調(diào)用來(lái)查找內(nèi)核的輸出符號(hào),這些符號(hào)都以符號(hào)名和數(shù)值形式如地址值成對(duì)保存。內(nèi)核輸出符號(hào)表被保存在內(nèi)核維護(hù)的模塊鏈表的第一個(gè)module結(jié)構(gòu)中。只有特殊符號(hào)才被添加,它們?cè)趦?nèi)核編譯與鏈接時(shí)確定。insmod將模塊讀入虛擬內(nèi)存并通過(guò)使用內(nèi)核輸出符號(hào)來(lái)修改其未解析的內(nèi)核函數(shù)和資源的引用地址。這些工作采取由insmod程序直接將符號(hào)的地址寫入模塊中相應(yīng)地址來(lái)進(jìn)行。
當(dāng)insmod修改完模塊對(duì)內(nèi)核輸出符號(hào)的引用后,它將再次使用特權(quán)級(jí)系統(tǒng)調(diào)用申請(qǐng)足夠的空間容納新模塊。內(nèi)核將為其分配一個(gè)新的module結(jié)構(gòu)以及足夠的內(nèi)核內(nèi)存來(lái)保存新模塊,并將其插入到內(nèi)核模塊鏈表的尾部,最后將新模塊標(biāo)志為UNINITIALIZED。insmod將模塊拷貝到已分配空間中,如果為它分配的內(nèi)核內(nèi)存已用完,將再次申請(qǐng),但模塊被多次加載必然處于不同的地址。另外此重定位工作包括使用適當(dāng)?shù)刂穪?lái)修改模塊映象。如果新模塊也希望將其符號(hào)輸出到系統(tǒng)中,insmod將為其構(gòu)造輸出符號(hào)映象表。每個(gè)內(nèi)核模塊必須包含模塊初始化和結(jié)束函數(shù),所以為了避免沖突它們的符號(hào)被設(shè)計(jì)成不輸出,但是insmod必須知道這些地址,這樣可以將它們傳遞給內(nèi)核。在所有這些工作完成以后,insmod將調(diào)用初始化代碼并執(zhí)行一個(gè)特權(quán)級(jí)系統(tǒng)調(diào)用將模塊的初始化和結(jié)束函數(shù)地址傳遞給內(nèi)核。當(dāng)將一個(gè)新模塊加載到內(nèi)核中時(shí),內(nèi)核必須更新其符號(hào)表并修改那些被新模塊使用的老模塊。那些依賴于其他模塊的模塊必須在其符號(hào)表尾部維護(hù)一個(gè)引用鏈表并在其module數(shù)據(jù)結(jié)構(gòu)中指向它。內(nèi)核調(diào)用模塊的初始化函數(shù),如果成功將安裝此模塊。模塊的結(jié)束函數(shù)地址被存儲(chǔ)在其module結(jié)構(gòu)中,將在模塊卸載時(shí)由內(nèi)核調(diào)用,模塊的狀態(tài)最后被設(shè)置成RUNNING。
3.2 模塊的卸載
模塊可以使用rmmod命令刪除,但是請(qǐng)求加載模塊在其使用計(jì)數(shù)為0時(shí),自動(dòng)被系統(tǒng)刪除。kmod在其每次idle定時(shí)器到期時(shí)都執(zhí)行一個(gè)系統(tǒng)調(diào)用,將系統(tǒng)中所有不再使用的請(qǐng)求加載模塊刪除。
關(guān)于模塊卸載,可以用圖3.2來(lái)描述:
內(nèi)核中其他部分還在使用的模塊不能被卸載。例如系統(tǒng)中安裝了多個(gè)VFAT文件系統(tǒng)則不能卸載VFAT模塊。執(zhí)行l(wèi)smod將看到每個(gè)模塊的引用計(jì)數(shù)。模塊的引用計(jì)數(shù)被保存在其映象的第一個(gè)常字中,這個(gè)字還包含autoclean和visited標(biāo)志。如果模塊被標(biāo)記成autoclean,則內(nèi)核知道此模塊可以自動(dòng)卸載。visited標(biāo)志表示此模塊正被一個(gè)或多個(gè)文件系統(tǒng)部分使用,只要有其他部分使用此模塊則這個(gè)標(biāo)志被置位。每次系統(tǒng)要將沒(méi)有被使用的請(qǐng)求加載模塊刪除時(shí),內(nèi)核將在所有模塊中掃描,但是一般只查看那些被標(biāo)志為autoclean并處于running狀態(tài)的模塊。如果某模塊的 visited標(biāo)記被清除則它將被刪除。其他依賴于它的模塊將修改各自的引用域,表示它們間的依賴關(guān)系不復(fù)存在。此模塊占有的內(nèi)核內(nèi)存將被回收。
4. LKM的應(yīng)用
零拷貝基本思想是:數(shù)據(jù)分組從網(wǎng)絡(luò)設(shè)備到用戶程序空間傳遞的過(guò)程中,減少數(shù)據(jù)拷貝次數(shù),減少系統(tǒng)調(diào)用,實(shí)現(xiàn)CPU的零參與,徹底消除CPU在這方面的負(fù)載。零拷貝的實(shí)現(xiàn)分為實(shí)現(xiàn)DMA數(shù)據(jù)傳輸和地址映射兩個(gè)部分。其中DMA數(shù)據(jù)傳輸與本文關(guān)系不大,就不詳細(xì)敘述了,這里主要介紹應(yīng)用LKM機(jī)制實(shí)現(xiàn)的地址映射。
地址映射的基本原理是在內(nèi)核空間申請(qǐng)內(nèi)存,通過(guò)proc文件系統(tǒng)和mmap函數(shù)將其映射到用戶空間來(lái)允許應(yīng)用程序訪問(wèn),這樣就消除了內(nèi)核空間到應(yīng)用程序空間的數(shù)據(jù)拷貝。地址映射部分的實(shí)現(xiàn)主要分為以下三步:
第一,建立LKM的基本結(jié)構(gòu),包括編寫初始化和結(jié)束函數(shù)等。
第二,聲明完成映射功能所需要的函數(shù),主要有分配和初始化內(nèi)核內(nèi)存函數(shù)init_mem(),釋放內(nèi)核內(nèi)存函數(shù)del_mem(),向內(nèi)核內(nèi)存輸入內(nèi)容的函數(shù)put_mem()等。
第三,在初始化函數(shù)中應(yīng)用第二步建立的函數(shù)分配一塊內(nèi)存空間、輸入內(nèi)容、建立proc文件系統(tǒng)入口。在結(jié)束函數(shù)中釋放已分配的內(nèi)核內(nèi)存,刪除proc文件系統(tǒng)入口。
編寫應(yīng)用程序測(cè)試該LKM,發(fā)現(xiàn)已經(jīng)達(dá)到了映射內(nèi)核內(nèi)存到應(yīng)用程序空間的目的。在實(shí)現(xiàn)零拷貝的過(guò)程中采用LKM機(jī)制不但便于調(diào)試而且大大減少了開(kāi)發(fā)時(shí)間。
5. LKM與普通應(yīng)用程序的比較
LKM與普通應(yīng)用程序之間的區(qū)別主要體現(xiàn)在四個(gè)方面。
第一,也是最重要的區(qū)別,普通應(yīng)用程序運(yùn)行在用戶空間,而LKM運(yùn)行在內(nèi)核空間。通過(guò)區(qū)分不同的運(yùn)行空間,操作系統(tǒng)能夠安全地保護(hù)操作系統(tǒng)中一些重要數(shù)據(jù)結(jié)構(gòu)的內(nèi)容不被普通應(yīng)用程序所修改,達(dá)到保證操作系統(tǒng)正常運(yùn)轉(zhuǎn)的目的。
第二,普通應(yīng)用程序的目標(biāo)很明確,它們從頭至尾都是為了完成某一項(xiàng)特定任務(wù)。而LKM是在內(nèi)核中注冊(cè)并為后續(xù)應(yīng)用程序的請(qǐng)求提供服務(wù)的。
第三,普通應(yīng)用程序可以調(diào)用并沒(méi)有在其中定義的函數(shù),但一個(gè)LKM是鏈接到內(nèi)核上的,它所能調(diào)用的函數(shù)只有內(nèi)核導(dǎo)出來(lái)的那些函數(shù)。
第四,普通應(yīng)用程序和LKM處理錯(cuò)誤的方式不同。當(dāng)應(yīng)用程序中出現(xiàn)錯(cuò)誤時(shí)并不會(huì)給系統(tǒng)造成很大的傷害。LKM則不然,在其中出現(xiàn)的錯(cuò)誤對(duì)子系統(tǒng)來(lái)說(shuō)通常是致命的,至少對(duì)于當(dāng)前正在運(yùn)行的進(jìn)程而言。LKM中的一個(gè)錯(cuò)誤常常會(huì)導(dǎo)致整個(gè)系統(tǒng)崩潰。
6. 編寫LKM需要注意的問(wèn)題
LKM運(yùn)行在內(nèi)核空間,它們擁有對(duì)整個(gè)系統(tǒng)所有資源的訪問(wèn)權(quán)限,因此,編寫LKM首先要注意就是安全問(wèn)題,而且還應(yīng)該避免將可能導(dǎo)致出現(xiàn)安全問(wèn)題的代碼帶到LKM中。
LKM加載后是作為操作系統(tǒng)內(nèi)核的一部分運(yùn)行的,因此,在設(shè)計(jì)、編寫操作系統(tǒng)內(nèi)核過(guò)程中應(yīng)該注意的問(wèn)題在LKM中也應(yīng)該引起足夠的重視。在這里,主要指的是并發(fā)問(wèn)題和指針引用問(wèn)題。并發(fā)是指在同一時(shí)間有多個(gè)進(jìn)程在操作系統(tǒng)內(nèi)核中同時(shí)運(yùn)行。并發(fā)結(jié)合共享資源最終會(huì)導(dǎo)致競(jìng)態(tài)條件,在這種情況下應(yīng)該對(duì)各個(gè)并發(fā)進(jìn)程訪問(wèn)共享資源進(jìn)行嚴(yán)格的控制。如果在LKM中出現(xiàn)指針引用錯(cuò)誤,內(nèi)核將沒(méi)有辦法將內(nèi)存的虛擬地址映射到物理地址,從而導(dǎo)致出現(xiàn)內(nèi)核中的意外,如內(nèi)存訪問(wèn)沖突、除0以及非法操作等。
7. LKM的不足之處
LKM雖然在設(shè)備驅(qū)動(dòng)程序的編寫和擴(kuò)充內(nèi)核功能中扮演著非常重要的角色,但它仍有許多不足的地方。
第一,LKM對(duì)于內(nèi)核版本的依賴性過(guò)強(qiáng),每一個(gè)LKM都是靠?jī)?nèi)核提供的函數(shù)和數(shù)據(jù)結(jié)構(gòu)組織起來(lái)的。當(dāng)這些內(nèi)核函數(shù)和數(shù)據(jù)結(jié)構(gòu)因?yàn)閮?nèi)核版本變化而發(fā)生變動(dòng)時(shí),原先的LKM不經(jīng)過(guò)修改就可能不能正常運(yùn)行。
第二,雖然現(xiàn)在有針對(duì)內(nèi)核編程調(diào)試的工具kgdb,但是在LKM編寫過(guò)程中調(diào)試仍非常麻煩,而且在調(diào)試過(guò)程中,系統(tǒng)所能提供的出錯(cuò)信息極為晦澀。
創(chuàng)新點(diǎn):針對(duì)Linux內(nèi)核,利用LKM,在實(shí)現(xiàn)了數(shù)據(jù)的零拷貝(Zero-copy)的過(guò)程中,將LKM與普通應(yīng)用程序進(jìn)行比較,提出了LKM的優(yōu)勢(shì)和不足。
評(píng)論