亚洲韩日午夜视频,欧美日韩在线精品一区二区三区,韩国超清无码一区二区三区,亚洲国产成人影院播放,久草新在线,在线看片AV色

您好,歡迎來到思海網(wǎng)絡(luò),我們將竭誠為您提供優(yōu)質(zhì)的服務(wù)! 誠征網(wǎng)絡(luò)推廣 | 網(wǎng)站備案 | 幫助中心 | 軟件下載 | 購買流程 | 付款方式 | 聯(lián)系我們 [ 會員登錄/注冊 ]
促銷推廣
客服中心
業(yè)務(wù)咨詢
有事點擊這里…  531199185
有事點擊這里…  61352289
點擊這里給我發(fā)消息  81721488
有事點擊這里…  376585780
有事點擊這里…  872642803
有事點擊這里…  459248018
有事點擊這里…  61352288
有事點擊這里…  380791050
技術(shù)支持
有事點擊這里…  714236853
有事點擊這里…  719304487
有事點擊這里…  1208894568
有事點擊這里…  61352289
在線客服
有事點擊這里…  531199185
有事點擊這里…  61352288
有事點擊這里…  983054746
有事點擊這里…  893984210
當前位置:首頁 >> 技術(shù)文章 >> 文章瀏覽
技術(shù)文章

Linux內(nèi)核模塊編寫詳解

添加時間:2016-1-24 2:31:13  添加: 思海網(wǎng)絡(luò) 
內(nèi)核編程常常看起來像是黑魔法,而在亞瑟 C 克拉克的眼中,它八成就是了。Linux內(nèi)核和它的用戶空間是大不相同的:拋開漫不經(jīng)心,你必須小心翼翼,因為你編程中的一個bug就會影響到整個系統(tǒng)。浮點運算做起來可不容易,堆棧固定而狹小,而你寫的代碼總是異步的,因此你需要想想并發(fā)會導(dǎo)致什么。而除了所有這一切之外,Linux內(nèi)核只是一個很大的、很復(fù)雜的C程序,它對每個人開放,任何人都去讀它、學習它并改進它,而你也可以是其中之一。

學習內(nèi)核編程的最簡單的方式也許就是寫個內(nèi)核模塊:一段可以動態(tài)加載進內(nèi)核的代碼。模塊所能做的事是有限的——例如,他們不能在類似進程描述符這樣的公共數(shù)據(jù)結(jié)構(gòu)中增減字段(LCTT譯注:可能會破壞整個內(nèi)核及系統(tǒng)的功能)。但是,在其它方面,他們是成熟的內(nèi)核級的代碼,可以在需要時隨時編譯進內(nèi)核(這樣就可以摒棄所有的限制了)。完全可以在Linux源代碼樹以外來開發(fā)并編譯一個模塊(這并不奇怪,它稱為樹外開發(fā)),如果你只是想稍微玩玩,而并不想提交修改以包含到主線內(nèi)核中去,這樣的方式是很方便的。

在本教程中,我們將開發(fā)一個簡單的內(nèi)核模塊用以創(chuàng)建一個/dev/reverse設(shè)備。寫入該設(shè)備的字符串將以相反字序的方式讀回(“Hello World”讀成“World Hello”)。這是一個很受歡迎的程序員面試難題,當你利用自己的能力在內(nèi)核級別實現(xiàn)這個功能時,可以使你得到一些加分。在開始前,有一句忠告:你的模塊中的一個bug就會導(dǎo)致系統(tǒng)崩潰(雖然可能性不大,但還是有可能的)和數(shù)據(jù)丟失。在開始前,請確保你已經(jīng)將重要數(shù)據(jù)備份,或者,采用一種更好的方式,在虛擬機中進行試驗。

盡可能不要用root身份

默認情況下,/dev/reverse只有root可以使用,因此你只能使用sudo來運行你的測試程序。要解決該限制,可以創(chuàng)建一個包含以下內(nèi)容的/lib/udev/rules.d/99-reverse.rules文件:

SUBSYSTEM=="misc", KERNEL=="reverse", MODE="0666"
別忘了重新插入模塊。讓非root用戶訪問設(shè)備節(jié)點往往不是一個好主意,但是在開發(fā)其間卻是十分有用的。這并不是說以root身份運行二進制測試文件也不是個好主意。
模塊的構(gòu)造

由于大多數(shù)的Linux內(nèi)核模塊是用C寫的(除了底層的特定于體系結(jié)構(gòu)的部分),所以推薦你將你的模塊以單一文件形式保存(例如,reverse.c)。我們已經(jīng)把完整的源代碼放在GitHub上——這里我們將看其中的一些片段。開始時,我們先要包含一些常見的文件頭,并用預(yù)定義的宏來描述模塊:

這里一切都直接明了,除了MODULE_LICENSE():它不僅僅是一個標記。內(nèi)核堅定地支持GPL兼容代碼,因此如果你把許可證設(shè)置為其它非GPL兼容的(如,“Proprietary”[專利]),某些特定的內(nèi)核功能將在你的模塊中不可用。

bash/shell Code復(fù)制內(nèi)容到剪貼板1.#include <linux/init.h>   
2.#include <linux/kernel.h>   
3.#include <linux/module.h>   
4.MODULE_LICENSE("GPL");   
5.MODULE_AUTHOR("Valentine Sinitsyn <valentine.sinitsyn@gmail.com>");   
6.MODULE_DEION("In-kernel phrase reverser");   
什么時候不該寫內(nèi)核模塊

內(nèi)核編程很有趣,但是在現(xiàn)實項目中寫(尤其是調(diào)試)內(nèi)核代碼要求特定的技巧。通常來講,在沒有其它方式可以解決你的問題時,你才應(yīng)該在內(nèi)核級別解決它。以下情形中,可能你在用戶空間中解決它更好:

你要開發(fā)一個USB驅(qū)動 —— 請查看libusb。
你要開發(fā)一個文件系統(tǒng) —— 試試FUSE。
你在擴展Netfilter —— 那么libnetfilter_queue對你有所幫助。
通常,內(nèi)核里面代碼的性能會更好,但是對于許多項目而言,這點性能丟失并不嚴重。
由于內(nèi)核編程總是異步的,沒有一個main()函數(shù)來讓Linux順序執(zhí)行你的模塊。取而代之的是,你要為各種事件提供回調(diào)函數(shù),像這個:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.static int __init reverse_init(void)   
2.{   
3.    printk(KERN_INFO "reverse device has been registered\n");   
4.    return 0;   
5.}   
6.static void __exit reverse_exit(void)   
7.{   
8.    printk(KERN_INFO "reverse device has been unregistered\n");   
9.}   
10.module_init(reverse_init);   
11.module_exit(reverse_exit);  
這里,我們定義的函數(shù)被稱為模塊的插入和刪除。只有第一個的插入函數(shù)是必要的。目前,它們只是打印消息到內(nèi)核環(huán)緩沖區(qū)(可以在用戶空間通過dmesg命令訪問);KERN_INFO是日志級別(注意,沒有逗號)。__init和__exit是屬性 —— 聯(lián)結(jié)到函數(shù)(或者變量)的元數(shù)據(jù)片。屬性在用戶空間的C代碼中是很罕見的,但是內(nèi)核中卻很普遍。所有標記為__init的,會在初始化后釋放內(nèi)存以供重用(還記得那條過去內(nèi)核的那條“Freeing unused kernel memory…[釋放未使用的內(nèi)核內(nèi)存……]”信息嗎?)。__exit表明,當代碼被靜態(tài)構(gòu)建進內(nèi)核時,該函數(shù)可以安全地優(yōu)化了,不需要清理收尾。最后,module_init()和module_exit()這兩個宏將reverse_init()和reverse_exit()函數(shù)設(shè)置成為我們模塊的生命周期回調(diào)函數(shù)。實際的函數(shù)名稱并不重要,你可以稱它們?yōu)閕nit()和exit(),或者start()和stop(),你想叫什么就叫什么吧。他們都是靜態(tài)聲明,你在外部模塊是看不到的。事實上,內(nèi)核中的任何函數(shù)都是不可見的,除非明確地被導(dǎo)出。然而,在內(nèi)核程序員中,給你的函數(shù)加上模塊名前綴是約定俗成的。

這些都是些基本概念 – 讓我們來做更多有趣的事情吧。模塊可以接收參數(shù),就像這樣:

# modprobe foo bar=1

modinfo命令顯示了模塊接受的所有參數(shù),而這些也可以在/sys/module//parameters下作為文件使用。我們的模塊需要一個緩沖區(qū)來存儲參數(shù) —— 讓我們把這大小設(shè)置為用戶可配置。在MODULE_DEION()下添加如下三行:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.static unsigned long buffer_size = 8192;   
2.module_param(buffer_size, ulong, (S_IRUSR | S_IRGRP | S_IROTH));   
3.MODULE_PARM_DESC(buffer_size, "Internal buffer size");  
這兒,我們定義了一個變量來存儲該值,封裝成一個參數(shù),并通過sysfs來讓所有人可讀。這個參數(shù)的描述(最后一行)出現(xiàn)在modinfo的輸出中。

由于用戶可以直接設(shè)置buffer_size,我們需要在reverse_init()來清除無效取值。你總該檢查來自內(nèi)核之外的數(shù)據(jù) —— 如果你不這么做,你就是將自己置身于內(nèi)核異常或安全漏洞之中。

bash/shell Code復(fù)制內(nèi)容到剪貼板1.static int __init reverse_init()   
2.{   
3.    if (!buffer_size)   
4.        return -1;   
5.    printk(KERN_INFO   
6.        "reverse device has been registered, buffer size is %lu bytes\n",   
7.        buffer_size);   
8.    return 0;   
9.}  
來自模塊初始化函數(shù)的非0返回值意味著模塊執(zhí)行失敗。

導(dǎo)航

但你開發(fā)模塊時,Linux內(nèi)核就是你所需一切的源頭。然而,它相當大,你可能在查找你所要的內(nèi)容時會有困難。幸運的是,在龐大的代碼庫面前,有許多工具使這個過程變得簡單。首先,是Cscope —— 在終端中運行的一個比較經(jīng)典的工具。你所要做的,就是在內(nèi)核源代碼的頂級目錄中運行make cscope && cscope。Cscope和Vim以及Emacs整合得很好,因此你可以在你最喜愛的編輯器中使用它。

如果基于終端的工具不是你的最愛,那么就訪問http://lxr.free-electrons.com吧。它是一個基于web的內(nèi)核導(dǎo)航工具,即使它的功能沒有Cscope來得多(例如,你不能方便地找到函數(shù)的用法),但它仍然提供了足夠多的快速查詢功能。
現(xiàn)在是時候來編譯模塊了。你需要你正在運行的內(nèi)核版本頭文件(linux-headers,或者等同的軟件包)和build-essential(或者類似的包)。接下來,該創(chuàng)建一個標準的Makefile模板:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.obj-m += reverse.o   
2.all:   
3.    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules   
4.clean:   
5.    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean  
現(xiàn)在,調(diào)用make來構(gòu)建你的第一個模塊。如果你輸入的都正確,在當前目錄內(nèi)會找到reverse.ko文件。使用sudo insmod reverse.ko插入內(nèi)核模塊,然后運行如下命令:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.$ dmesg | tail -1   
2.[ 5905.042081] reverse device has been registered, buffer size is 8192 bytes  
恭喜了!然而,目前這一行還只是假象而已 —— 還沒有設(shè)備節(jié)點呢。讓我們來搞定它。

混雜設(shè)備

在Linux中,有一種特殊的字符設(shè)備類型,叫做“混雜設(shè)備”(或者簡稱為“misc”)。它是專為單一接入點的小型設(shè)備驅(qū)動而設(shè)計的,而這正是我們所需要的。所有混雜設(shè)備共享同一個主設(shè)備號(10),因此一個驅(qū)動(drivers/char/misc.c)就可以查看它們所有設(shè)備了,而這些設(shè)備用次設(shè)備號來區(qū)分。從其他意義來說,它們只是普通字符設(shè)備。

要為該設(shè)備注冊一個次設(shè)備號(以及一個接入點),你需要聲明struct misc_device,填上所有字段(注意語法),然后使用指向該結(jié)構(gòu)的指針作為參數(shù)來調(diào)用misc_register()。為此,你也需要包含linux/miscdevice.h頭文件:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.static struct miscdevice reverse_misc_device = {   
2.    .minor = MISC_DYNAMIC_MINOR,   
3.    .name = "reverse",   
4.    .fops = &reverse_fops   
5.};   
6.static int __init reverse_init()   
7.{   
8.    ...   
9.    misc_register(&reverse_misc_device);   
10.    printk(KERN_INFO ...   
11.}   
12.  
這兒,我們?yōu)槊麨椤皉everse”的設(shè)備請求一個第一個可用的(動態(tài)的)次設(shè)備號;省略號表明我們之前已經(jīng)見過的省略的代碼。別忘了在模塊卸下后注銷掉該設(shè)備。

bash/shell Code復(fù)制內(nèi)容到剪貼板1.static void __exit reverse_exit(void)   
2.{   
3.    misc_deregister(&reverse_misc_device);   
4.    ...   
5.}   
6.  
‘fops’字段存儲了一個指針,指向一個file_operations結(jié)構(gòu)(在Linux/fs.h中聲明),而這正是我們模塊的接入點。reverse_fops定義如下:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.static struct file_operations reverse_fops = {   
2.    .owner = THIS_MODULE,   
3.    .open = reverse_open,   
4.    ...   
5.    .llseek = noop_llseek   
6.};  
另外,reverse_fops包含了一系列回調(diào)函數(shù)(也稱之為方法),當用戶空間代碼打開一個設(shè)備,讀寫或者關(guān)閉文件描述符時,就會執(zhí)行。如果你要忽略這些回調(diào),可以指定一個明確的回調(diào)函數(shù)來替代。這就是為什么我們將llseek設(shè)置為noop_llseek(),(顧名思義)它什么都不干。這個默認實現(xiàn)改變了一個文件指針,而且我們現(xiàn)在并不需要我們的設(shè)備可以尋址(這是今天留給你們的家庭作業(yè))。

關(guān)閉和打開

讓我們來實現(xiàn)該方法。我們將給每個打開的文件描述符分配一個新的緩沖區(qū),并在它關(guān)閉時釋放。這實際上并不安全:如果一個用戶空間應(yīng)用程序泄漏了描述符(也許是故意的),它就會霸占RAM,并導(dǎo)致系統(tǒng)不可用。在現(xiàn)實世界中,你總得考慮到這些可能性。但在本教程中,這種方法不要緊。

我們需要一個結(jié)構(gòu)函數(shù)來描述緩沖區(qū)。內(nèi)核提供了許多常規(guī)的數(shù)據(jù)結(jié)構(gòu):鏈接列表(雙聯(lián)的),哈希表,樹等等之類。不過,緩沖區(qū)常常從頭設(shè)計。我們將調(diào)用我們的“struct buffer”:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.struct buffer {   
2.    char *data, *end, *read_ptr;   
3.    unsigned long size;   
4.};  
data是該緩沖區(qū)存儲的一個指向字符串的指針,而end指向字符串結(jié)尾后的第一個字節(jié)。read_ptr是read()開始讀取數(shù)據(jù)的地方。緩沖區(qū)的size是為了保證完整性而存儲的 —— 目前,我們還沒有使用該區(qū)域。你不能假設(shè)使用你結(jié)構(gòu)體的用戶會正確地初始化所有這些東西,所以最好在函數(shù)中封裝緩沖區(qū)的分配和收回。它們通常命名為buffer_alloc()和buffer_free()。

bash/shell Code復(fù)制內(nèi)容到剪貼板1.static struct buffer buffer_alloc(unsigned long size)    
2.{ struct buffer *buf; buf = kzalloc(sizeof(buf), GFP_KERNEL);   
3. if    
4.(unlikely(!buf)) goto out; … out: return buf;    
5.}  
內(nèi)核內(nèi)存使用kmalloc()來分配,并使用kfree()來釋放;kzalloc()的風格是將內(nèi)存設(shè)置為全零。不同于標準的malloc(),它的內(nèi)核對應(yīng)部分收到的標志指定了第二個參數(shù)中請求的內(nèi)存類型。這里,GFP_KERNEL是說我們需要一個普通的內(nèi)核內(nèi)存(不是在DMA或高內(nèi)存區(qū)中)以及如果需要的話函數(shù)可以睡眠(重新調(diào)度進程)。sizeof(*buf)是一種常見的方式,它用來獲取可通過指針訪問的結(jié)構(gòu)體的大小。

你應(yīng)該隨時檢查kmalloc()的返回值:訪問NULL指針將導(dǎo)致內(nèi)核異常。同時也需要注意unlikely()宏的使用。它(及其相對宏likely())被廣泛用于內(nèi)核中,用于表明條件幾乎總是真的(或假的)。它不會影響到控制流程,但是能幫助現(xiàn)代處理器通過分支預(yù)測技術(shù)來提升性能。

最后,注意goto語句。它們常常為認為是邪惡的,但是,Linux內(nèi)核(以及一些其它系統(tǒng)軟件)采用它們來實施集中式的函數(shù)退出。這樣的結(jié)果是減少嵌套深度,使代碼更具可讀性,而且非常像更高級語言中的try-catch區(qū)塊。

有了buffer_alloc()和buffer_free(),open和close方法就變得很簡單了。

bash/shell Code復(fù)制內(nèi)容到剪貼板1.static int reverse_open(struct inode *inode, struct file *file)   
2.{   
3.    int err = 0;   
4.    file->private_data = buffer_alloc(buffer_size);   
5.    ...   
6.    return err;   
7.}   
8.  
struct file是一個標準的內(nèi)核數(shù)據(jù)結(jié)構(gòu),用以存儲打開的文件的信息,如當前文件位置(file->f_pos)、標志(file->f_flags),或者打開模式(file->f_mode)等。另外一個字段file->privatedata用于關(guān)聯(lián)文件到一些專有數(shù)據(jù),它的類型是void *,而且它在文件擁有者以外,對內(nèi)核不透明。我們將一個緩沖區(qū)存儲在那里。

如果緩沖區(qū)分配失敗,我們通過返回否定值(-ENOMEM)來為調(diào)用的用戶空間代碼標明。一個C庫中調(diào)用的open(2)系統(tǒng)調(diào)用(如glibc)將會檢測這個并適當?shù)卦O(shè)置errno 。

學習如何讀和寫

“read”和“write”方法是真正完成工作的地方。當數(shù)據(jù)寫入到緩沖區(qū)時,我們放棄之前的內(nèi)容和反向地存儲該字段,不需要任何臨時存儲。read方法僅僅是從內(nèi)核緩沖區(qū)復(fù)制數(shù)據(jù)到用戶空間。但是如果緩沖區(qū)還沒有數(shù)據(jù),revers_eread()會做什么呢?在用戶空間中,read()調(diào)用會在有可用數(shù)據(jù)前阻塞它。在內(nèi)核中,你就必須等待。幸運的是,有一項機制用于處理這種情況,就是‘wait queues’。

想法很簡單。如果當前進程需要等待某個事件,它的描述符(struct task_struct存儲‘current’信息)被放進非可運行(睡眠中)狀態(tài),并添加到一個隊列中。然后schedule()就被調(diào)用來選擇另一個進程運行。生成事件的代碼通過使用隊列將等待進程放回TASK_RUNNING狀態(tài)來喚醒它們。調(diào)度程序?qū)⒃谝院笤谀硞地方選擇它們之一。Linux有多種非可運行狀態(tài),最值得注意的是TASK_INTERRUPTIBLE(一個可以通過信號中斷的睡眠)和TASK_KILLABLE(一個可被殺死的睡眠中的進程)。所有這些都應(yīng)該正確處理,并等待隊列為你做這些事。

一個用以存儲讀取等待隊列頭的天然場所就是結(jié)構(gòu)緩沖區(qū),所以從為它添加wait_queue_headt read\queue字段開始。你也應(yīng)該包含linux/sched.h頭文件。可以使用DECLARE_WAITQUEUE()宏來靜態(tài)聲明一個等待隊列。在我們的情況下,需要動態(tài)初始化,因此添加下面這行到buffer_alloc():

bash/shell Code復(fù)制內(nèi)容到剪貼板1.init_waitqueue_head(&buf->read_queue);  
我們等待可用數(shù)據(jù);或者等待read_ptr != end條件成立。我們也想要讓等待操作可以被中斷(如,通過Ctrl+C)。因此,“read”方法應(yīng)該像這樣開始:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.static ssize_t reverse_read(struct file *file, char __user * out,   
2.        size_t size, loff_t * off)   
3.{   
4.    struct buffer *buf = file->private_data;   
5.    ssize_t result;   
6.    while (buf->read_ptr == buf->end) {   
7.        if (file->f_flags & O_NONBLOCK) {   
8.            result = -EAGAIN;   
9.            goto out;   
10.        }   
11.        if (wait_event_interruptible   
12.        (buf->read_queue, buf->read_ptr != buf->end)) {   
13.            result = -ERESTARTSYS;   
14.            goto out;   
15.        }   
16.    }   
...

我們讓它循環(huán),直到有可用數(shù)據(jù),如果沒有則使用wait_event_interruptible()(它是一個宏,不是函數(shù),這就是為什么要通過值的方式給隊列傳遞)來等待。好吧,如果wait_event_interruptible()被中斷,它返回一個非0值,這個值代表-ERESTARTSYS。這段代碼意味著系統(tǒng)調(diào)用應(yīng)該重新啟動。file->f_flags檢查以非阻塞模式打開的文件數(shù):如果沒有數(shù)據(jù),返回-EAGAIN。

我們不能使用if()來替代while(),因為可能有許多進程正等待數(shù)據(jù)。當write方法喚醒它們時,調(diào)度程序以不可預(yù)知的方式選擇一個來運行,因此,在這段代碼有機會執(zhí)行的時候,緩沖區(qū)可能再次空出。現(xiàn)在,我們需要將數(shù)據(jù)從buf->data 復(fù)制到用戶空間。copy_to_user()內(nèi)核函數(shù)就干了此事:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.size = min(size, (size_t) (buf->end - buf->read_ptr));   
2.   if (copy_to_user(out, buf->read_ptr, size)) {   
3.       result = -EFAULT;   
4.       goto out;   
5.   }  
如果用戶空間指針錯誤,那么調(diào)用可能會失敗;如果發(fā)生了此事,我們就返回-EFAULT。記住,不要相信任何來自內(nèi)核外的事物!

bash/shell Code復(fù)制內(nèi)容到剪貼板1.   buf->read_ptr += size;   
2.    result = size;   
3.out:   
4.    return result;   
5.}  
為了使數(shù)據(jù)在任意塊可讀,需要進行簡單運算。該方法返回讀入的字節(jié)數(shù),或者一個錯誤代碼。

寫方法更簡短。首先,我們檢查緩沖區(qū)是否有足夠的空間,然后我們使用copy_from_userspace()函數(shù)來獲取數(shù)據(jù)。再然后read_ptr和結(jié)束指針會被重置,并且反轉(zhuǎn)存儲緩沖區(qū)內(nèi)容:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.buf->end = buf->data + size;   
2.   buf->read_ptr = buf->data;   
3.   if (buf->end > buf->data)   
4.       reverse_phrase(buf->data, buf->end - 1);  
這里, reverse_phrase()干了所有吃力的工作。它依賴于reverse_word()函數(shù),該函數(shù)相當簡短并且標記為內(nèi)聯(lián)。這是另外一個常見的優(yōu)化;但是,你不能過度使用。因為過多的內(nèi)聯(lián)會導(dǎo)致內(nèi)核映像徒然增大。

最后,我們需要喚醒read_queue中等待數(shù)據(jù)的進程,就跟先前講過的那樣。wake_up_interruptible()就是用來干此事的:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.wake_up_interruptible(&buf->read_queue);  

耶!你現(xiàn)在已經(jīng)有了一個內(nèi)核模塊,它至少已經(jīng)編譯成功了。現(xiàn)在,是時候來測試了。

調(diào)試內(nèi)核代碼

或許,內(nèi)核中最常見的調(diào)試方法就是打印。如果你愿意,你可以使用普通的printk() (假定使用KERN_DEBUG日志等級)。然而,那兒還有更好的辦法。如果你正在寫一個設(shè)備驅(qū)動,這個設(shè)備驅(qū)動有它自己的“struct device”,可以使用pr_debug()或者dev_dbg():它們支持動態(tài)調(diào)試(dyndbg)特性,并可以根據(jù)需要啟用或者禁用(請查閱Documentation/dynamic-debug-howto.txt)。對于單純的開發(fā)消息,使用pr_devel(),除非設(shè)置了DEBUG,否則什么都不會做。要為我們的模塊啟用DEBUG,請?zhí)砑右韵滦械組akefile中:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.CFLAGS_reverse.o := -DDEBUG  
完了之后,使用dmesg來查看pr_debug()或pr_devel()生成的調(diào)試信息。 或者,你可以直接發(fā)送調(diào)試信息到控制臺。要想這么干,你可以設(shè)置console_loglevel內(nèi)核變量為8或者更大的值(echo 8 /proc/sys/kernel/printk),或者在高日志等級,如KERN_ERR,來臨時打印要查詢的調(diào)試信息。很自然,在發(fā)布代碼前,你應(yīng)該移除這樣的調(diào)試聲明。

注意內(nèi)核消息出現(xiàn)在控制臺,不要在Xterm這樣的終端模擬器窗口中去查看;這也是在內(nèi)核開發(fā)時,建議你不在X環(huán)境下進行的原因。
驚喜,驚喜!

編譯模塊,然后加載進內(nèi)核:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.$ make  
2.$ sudo insmod reverse.ko buffer_size=2048   
3.$ lsmod   
4.reverse 2419 0   
5.$ ls -l /dev/reverse   
6.crw-rw-rw- 1 root root 10, 58 Feb 22 15:53 /dev/reverse  
一切似乎就位。現(xiàn)在,要測試模塊是否正常工作,我們將寫一段小程序來翻轉(zhuǎn)它的第一個命令行參數(shù)。main()(再三檢查錯誤)可能看上去像這樣:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.int fd = open("/dev/reverse", O_RDWR);   
2.write(fd, argv[1], strlen(argv[1]));   
3.read(fd, argv[1], strlen(argv[1]));   
4.printf("Read: %s\n", argv[1]);  
像這樣運行:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.$ ./test 'A quick brown fox jumped over the lazy dog'  
2.Read: dog lazy the over jumped fox brown quick A  
它工作正常!玩得更逗一點:試試傳遞單個單詞或者單個字母的短語,空的字符串或者是非英語字符串(如果你有這樣的鍵盤布局設(shè)置),以及其它任何東西。
現(xiàn)在,讓我們讓事情變得更好玩一點。我們將創(chuàng)建兩個進程,它們共享一個文件描述符(及其內(nèi)核緩沖區(qū))。其中一個會持續(xù)寫入字符串到設(shè)備,而另一個將讀取這些字符串。在下例中,我們使用了fork(2)系統(tǒng)調(diào)用,而pthreads也很好用。我也省略打開和關(guān)閉設(shè)備的代碼,并在此檢查代碼錯誤(又來了):

bash/shell Code復(fù)制內(nèi)容到剪貼板1.char *phrase = "A quick brown fox jumped over the lazy dog";   
2.if (fork())   
3.       
4./* Parent is the writer */   
5.    while (1)   
6.        write(fd, phrase, len);   
7.else  
8.       
9./* child is the reader */   
10.    while (1) {   
11.        read(fd, buf, len);   
12.        printf("Read: %s\n", buf);   
13.}   
14.  
你希望這個程序會輸出什么呢?下面就是在我的筆記本上得到的東西:

Read: dog lazy the over jumped fox brown quick A
Read: A kcicq brown fox jumped over the lazy dog
Read: A kciuq nworb xor jumped fox brown quick A
Read: A kciuq nworb xor jumped fox brown quick A
...
這里發(fā)生了什么呢?就像舉行了一場比賽。我們認為read和write是原子操作,或者從頭到尾一次執(zhí)行一個指令。然而,內(nèi)核確實無序并發(fā)的,隨便就重新調(diào)度了reverse_phrase()函數(shù)內(nèi)部某個地方運行著的寫入操作的內(nèi)核部分。如果在寫入操作結(jié)束前就調(diào)度了read()操作呢?就會產(chǎn)生數(shù)據(jù)不完整的狀態(tài)。這樣的bug非常難以找到。但是,怎樣來處理這個問題呢?

基本上,我們需要確保在寫方法返回前沒有read方法能被執(zhí)行。如果你曾經(jīng)編寫過一個多線程的應(yīng)用程序,你可能見過同步原語(鎖),如互斥鎖或者信號。Linux也有這些,但有些細微的差別。內(nèi)核代碼可以運行進程上下文(用戶空間代碼的“代表”工作,就像我們使用的方法)和終端上下文(例如,一個IRQ處理線程)。如果你已經(jīng)在進程上下文中和并且你已經(jīng)得到了所需的鎖,你只需要簡單地睡眠和重試直到成功為止。在中斷上下文時你不能處于休眠狀態(tài),因此代碼會在一個循環(huán)中運行直到鎖可用。關(guān)聯(lián)原語被稱為自旋鎖,但在我們的環(huán)境中,一個簡單的互斥鎖 —— 在特定時間內(nèi)只有唯一一個進程能“占有”的對象 —— 就足夠了。處于性能方面的考慮,現(xiàn)實的代碼可能也會使用讀-寫信號。

鎖總是保護某些數(shù)據(jù)(在我們的環(huán)境中,是一個“struct buffer”實例),而且也常常會把它們嵌入到它們所保護的結(jié)構(gòu)體中。因此,我們添加一個互斥鎖(‘struct mutex lock’)到“struct buffer”中。我們也必須用mutex_init()來初始化互斥鎖;buffer_alloc是用來處理這件事的好地方。使用互斥鎖的代碼也必須包含linux/mutex.h。

互斥鎖很像交通信號燈 —— 要是司機不看它和不聽它的,它就沒什么用。因此,在對緩沖區(qū)做操作并在操作完成時釋放它之前,我們需要更新reverse_read()和reverse_write()來獲取互斥鎖。讓我們來看看read方法 —— write的工作原理相同:

bash/shell Code復(fù)制內(nèi)容到剪貼板1.static ssize_t reverse_read(struct file *file, char __user * out,   
2.        size_t size, loff_t * off)   
3.{   
4.    struct buffer *buf = file->private_data;   
5.    ssize_t result;   
6.    if (mutex_lock_interruptible(&buf->lock)) {   
7.        result = -ERESTARTSYS;   
8.        goto out;   
9.}   
10.  
我們在函數(shù)一開始就獲取鎖。mutex_lock_interruptible()要么得到互斥鎖然后返回,要么讓進程睡眠,直到有可用的互斥鎖。就像前面一樣,_interruptible后綴意味著睡眠可以由信號來中斷。

bash/shell Code復(fù)制內(nèi)容到剪貼板1.while (buf->read_ptr == buf->end) {   
2.        mutex_unlock(&buf->lock);   
3./* ... wait_event_interruptible() here ... */   
4.        if (mutex_lock_interruptible(&buf->lock)) {   
5.            result = -ERESTARTSYS;   
6.            goto out;   
7.        }   
8.    }  
下面是我們的“等待數(shù)據(jù)”循環(huán)。當獲取互斥鎖時,或者發(fā)生稱之為“死鎖”的情境時,不應(yīng)該讓進程睡眠。因此,如果沒有數(shù)據(jù),我們釋放互斥鎖并調(diào)用wait_event_interruptible()。當它返回時,我們重新獲取互斥鎖并像往常一樣繼續(xù):

bash/shell Code復(fù)制內(nèi)容到剪貼板1.if (copy_to_user(out, buf->read_ptr, size)) {   
2.        result = -EFAULT;   
3.        goto out_unlock;   
4.    }   
5.    ...   
6.out_unlock:   
7.    mutex_unlock(&buf->lock);   
8.out:   
9.    return result;  
最后,當函數(shù)結(jié)束,或者在互斥鎖被獲取過程中發(fā)生錯誤時,互斥鎖被解鎖。重新編譯模塊(別忘了重新加載),然后再次進行測試。現(xiàn)在你應(yīng)該沒發(fā)現(xiàn)毀壞的數(shù)據(jù)了。

關(guān)鍵字:Linux、內(nèi)核模塊
分享到:

頂部 】 【 關(guān)閉
版權(quán)所有:佛山思海電腦網(wǎng)絡(luò)有限公司 ©1998-2024 All Rights Reserved.
聯(lián)系電話:(0757)22630313、22633833
中華人民共和國增值電信業(yè)務(wù)經(jīng)營許可證: 粵B1.B2-20030321 備案號:粵B2-20030321-1
網(wǎng)站公安備案編號:44060602000007 交互式欄目專項備案編號:200303DD003  
察察 工商 網(wǎng)安 舉報有獎  警警  手機打開網(wǎng)站