這里要介紹的是如何寫一個(gè)類似hello world的簡單測(cè)試驅(qū)動(dòng)程序。而這個(gè)驅(qū)動(dòng)的唯一功能就是輸出hello world。首先我們來了解下linux內(nèi)核下調(diào)試程序的一個(gè)重要函數(shù)printk以及幾個(gè)重要概念。
printk類似c語言的printf,是內(nèi)核中輸出打印信息的函數(shù)。以后驅(qū)動(dòng)調(diào)試中的重要性不言而喻,下面先做一個(gè)簡單介紹。
printk的級(jí)別
日志級(jí)別一共有8個(gè)級(jí)別,printk的日志級(jí)別定義如下(在include/linux/kernel.h中):
#define KERN_EMERG 0/*緊急事件消息,系統(tǒng)崩潰之前提示,表示系統(tǒng)不可用*/
#define KERN_ALERT 1/*報(bào)告消息,表示必須立即采取措施*/
#define KERN_CRIT 2/*臨界條件,通常涉及嚴(yán)重的硬件或軟件操作失敗*/
#define KERN_ERR 3/*錯(cuò)誤條件,驅(qū)動(dòng)程序常用KERN_ERR來報(bào)告硬件的錯(cuò)誤*/
#define KERN_WARNING 4/*警告條件,對(duì)可能出現(xiàn)問題的情況進(jìn)行警告*/
#define KERN_NOTICE 5/*正常但又重要的條件,用于提醒*/
#define KERN_INFO 6/*提示信息,如驅(qū)動(dòng)程序啟動(dòng)時(shí),打印硬件信息*/
#define KERN_DEBUG 7/*調(diào)試級(jí)別的消息*/
沒有指定日志級(jí)別的printk語句默認(rèn)采用的級(jí)別是:DEFAULT_ MESSAGE_LOGLEVEL(這個(gè)默認(rèn)級(jí)別一般為<4>,即與KERN_WARNING在一個(gè)級(jí)別上),其定義在kernel/printk.c中可以找到。在驅(qū)動(dòng)調(diào)試過程中打開所有日志信息可使用echo 7 > /proc/sys/kernel/printk,相對(duì)應(yīng)關(guān)閉日志使用echo 0 > /proc/sys/kernel/printk。
下面再來介紹幾個(gè)重要的概念,這些概念可以先做一個(gè)了解,后續(xù)還會(huì)提到。
內(nèi)核空間和用戶空間
linux系統(tǒng)分為兩個(gè)級(jí)別。內(nèi)核運(yùn)行在最高級(jí)別,可以進(jìn)行所有的操作。而應(yīng)用程序運(yùn)行在最低級(jí)別,處理器控制著對(duì)硬件的直接訪問以及對(duì)內(nèi)存的非授權(quán)訪問。內(nèi)核空間和用戶空間不僅有不同的優(yōu)先級(jí)等級(jí),而且有不同的內(nèi)存映射,有各自的地址空間。詳見內(nèi)存管理。
應(yīng)用程序只能通過系統(tǒng)調(diào)用或中斷從用戶空間切換到內(nèi)核空間,其中系統(tǒng)調(diào)用是軟中斷(0x80號(hào)中斷)。執(zhí)行系統(tǒng)調(diào)用的系統(tǒng)代碼運(yùn)行在進(jìn)程上下文中,它代表調(diào)用進(jìn)程執(zhí)行操作,因此能夠訪問進(jìn)程地址空間的所有數(shù)據(jù)。而處理硬件中斷的內(nèi)核代碼和進(jìn)程是異步的,與任何一個(gè)特定進(jìn)程無關(guān)。
內(nèi)核中的并發(fā)
內(nèi)核編程區(qū)別于常見應(yīng)用程序編程的地方在于對(duì)并發(fā)的處理。大部分應(yīng)用程序除多線程外,通常是順序執(zhí)行的,不需要關(guān)心由于其他事情的發(fā)生而改變它的運(yùn)行環(huán)境。內(nèi)核代碼不是這樣,同一時(shí)刻,可能有多個(gè)進(jìn)程使用訪問同一個(gè)模塊。
內(nèi)核編程要考慮并發(fā)問題的原因:1.linux是通常正在運(yùn)行多個(gè)并發(fā)進(jìn)程,并且可能有多個(gè)進(jìn)程同時(shí)使用我們的驅(qū)動(dòng)程序。2.大多數(shù)設(shè)備能夠中斷處理器,而中斷處理程序異步進(jìn)行,而且可能在驅(qū)動(dòng)程序正試圖處理其它任務(wù)時(shí)被調(diào)用。3.一些類似內(nèi)核定時(shí)器的代碼在異步運(yùn)行。4.運(yùn)行在對(duì)稱多處理器上(SMP),不止一個(gè)cpu在運(yùn)行驅(qū)動(dòng)程序。5.內(nèi)核代碼是可搶占的。
當(dāng)前進(jìn)程
內(nèi)核代碼可通過訪問全局項(xiàng)current來獲得當(dāng)前進(jìn)程。current指針指向當(dāng)前正在運(yùn)行的進(jìn)程。在open、read、等系統(tǒng)調(diào)用的執(zhí)行過程中,當(dāng)前進(jìn)程指的是調(diào)用這些系統(tǒng)調(diào)用的進(jìn)程。內(nèi)核代碼可以通過current指針獲得與當(dāng)前進(jìn)程相關(guān)的信息。
內(nèi)核中帶“__”的函數(shù):內(nèi)核API函數(shù)具有這種名稱的,通常都是一些接口的底層函數(shù),應(yīng)該謹(jǐn)慎使用。實(shí)質(zhì)上,這里的雙下劃線就是要告訴
程序員:謹(jǐn)慎調(diào)用,否則后果自負(fù)。以__init為例,__init表明該函數(shù)僅在初始化期間使用。在模塊被裝載之后,模塊裝載器就會(huì)將初始化函數(shù)扔掉,這樣可以將函數(shù)占用的內(nèi)存釋放出來,已做它用。注意,不要在結(jié)束初始化之后仍要使用的函數(shù)(或者數(shù)據(jù)結(jié)構(gòu))上使用__init、__initdata標(biāo)記。這里摘抄網(wǎng)上的一段總結(jié),如下。
__init, __initdata等屬性標(biāo)志,是要把這種屬性的代碼放入目標(biāo)文件的.init.text節(jié),數(shù)據(jù)放入.init.data節(jié)──這一過程是通過編譯內(nèi)核時(shí)為相關(guān)目標(biāo)平臺(tái)提供了xxx.lds鏈接腳本來指導(dǎo)ld完成的。
對(duì)編譯成module的代碼和數(shù)據(jù)來說,當(dāng)模塊加載時(shí),__init屬性的函數(shù)就被執(zhí)行;
對(duì)靜態(tài)編入內(nèi)核的代碼和數(shù)據(jù)來說,當(dāng)內(nèi)核引導(dǎo)時(shí),do_basic_setup()函數(shù)調(diào)用do_initcalls()函數(shù),后者負(fù)責(zé)所有.init節(jié)函數(shù)的執(zhí)行。
在初始化完成后,用這些關(guān)鍵字標(biāo)識(shí)的函數(shù)或數(shù)據(jù)所占的內(nèi)存會(huì)被釋放掉。
1) 所有標(biāo)識(shí)為__init的函數(shù)在鏈接的時(shí)候都放在.init.text這個(gè)區(qū)段內(nèi),在這個(gè)區(qū)段中,函數(shù)的擺放順序是和鏈接的順序有關(guān)的,是不確定的。
2) 所有的__init函數(shù)在區(qū)段.initcall.init中還保存了一份函數(shù)指針,在初始化時(shí)內(nèi)核會(huì)通過這些函數(shù)指針調(diào)用這些__init函數(shù)指針,并在整個(gè)初始化完成后,釋放整個(gè)init區(qū)段(包括.init.text,.initcall.init等),注意,這些函數(shù)在內(nèi)核初始化過程中的調(diào)用順序只和這里的函數(shù)指針的順序有關(guān),和1)中所述的這些函數(shù)本身在.init.text區(qū)段中的順序無關(guān)。
下面我們來看一個(gè)驅(qū)動(dòng)程序的hello world程序是如何實(shí)現(xiàn)的:
內(nèi)核模塊的編譯與應(yīng)用程序的編譯有些區(qū)別,此hello world模塊的編譯命令為:
make -C /xxx/xxx/kernel_src/ M=$(PWD) modules
其中/xxx/xxx/kernel_src/ 為已經(jīng)配置編譯過的內(nèi)核源碼路徑,ubuntu下一般在/lib/modules/$(shell uname -r)/build目錄下。
此函數(shù)只有兩個(gè)函數(shù),一個(gè)是hello_init,在insmod的時(shí)候執(zhí)行,這個(gè)是模塊的初始化函數(shù),另一個(gè)是hello_exit,在rmmod的時(shí)候執(zhí)行,是模塊卸載時(shí)要執(zhí)行的函數(shù)。此模塊的唯一功能就是在insmod的時(shí)候輸出Hello,world,在rmmod的時(shí)候輸出Goodbye,cruel world。
在編寫應(yīng)用程序時(shí),我們一般都是由多個(gè)源文件組成的,這個(gè)時(shí)候編譯肯定就不能繼續(xù)使用命令行編譯了,就要使用到Makefile。同樣,驅(qū)動(dòng)模塊的編譯也需要使用的makefile,下面就是一個(gè)在編譯含有多個(gè)源碼文件的驅(qū)動(dòng)模塊時(shí)可以參考的Makefile文件。
Makefile模板