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

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

Linux內核模塊編寫詳解

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

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

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

盡可能不要用root身份

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

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

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

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

bash/shell Code復制內容到剪貼板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");   
什么時候不該寫內核模塊

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

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

bash/shell Code復制內容到剪貼板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);  
這里,我們定義的函數被稱為模塊的插入和刪除。只有第一個的插入函數是必要的。目前,它們只是打印消息到內核環緩沖區(可以在用戶空間通過dmesg命令訪問);KERN_INFO是日志級別(注意,沒有逗號)。__init和__exit是屬性 —— 聯結到函數(或者變量)的元數據片。屬性在用戶空間的C代碼中是很罕見的,但是內核中卻很普遍。所有標記為__init的,會在初始化后釋放內存以供重用(還記得那條過去內核的那條“Freeing unused kernel memory…[釋放未使用的內核內存……]”信息嗎?)。__exit表明,當代碼被靜態構建進內核時,該函數可以安全地優化了,不需要清理收尾。最后,module_init()和module_exit()這兩個宏將reverse_init()和reverse_exit()函數設置成為我們模塊的生命周期回調函數。實際的函數名稱并不重要,你可以稱它們為init()和exit(),或者start()和stop(),你想叫什么就叫什么吧。他們都是靜態聲明,你在外部模塊是看不到的。事實上,內核中的任何函數都是不可見的,除非明確地被導出。然而,在內核程序員中,給你的函數加上模塊名前綴是約定俗成的。

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

# modprobe foo bar=1

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

bash/shell Code復制內容到剪貼板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");  
這兒,我們定義了一個變量來存儲該值,封裝成一個參數,并通過sysfs來讓所有人可讀。這個參數的描述(最后一行)出現在modinfo的輸出中。

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

bash/shell Code復制內容到剪貼板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.}  
來自模塊初始化函數的非0返回值意味著模塊執行失敗。

導航

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

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

bash/shell Code復制內容到剪貼板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  
現在,調用make來構建你的第一個模塊。如果你輸入的都正確,在當前目錄內會找到reverse.ko文件。使用sudo insmod reverse.ko插入內核模塊,然后運行如下命令:

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

混雜設備

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

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

bash/shell Code復制內容到剪貼板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.  
這兒,我們為名為“reverse”的設備請求一個第一個可用的(動態的)次設備號;省略號表明我們之前已經見過的省略的代碼。別忘了在模塊卸下后注銷掉該設備。

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

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

關閉和打開

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

我們需要一個結構函數來描述緩沖區。內核提供了許多常規的數據結構:鏈接列表(雙聯的),哈希表,樹等等之類。不過,緩沖區常常從頭設計。我們將調用我們的“struct buffer”:

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

bash/shell Code復制內容到剪貼板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.}  
內核內存使用kmalloc()來分配,并使用kfree()來釋放;kzalloc()的風格是將內存設置為全零。不同于標準的malloc(),它的內核對應部分收到的標志指定了第二個參數中請求的內存類型。這里,GFP_KERNEL是說我們需要一個普通的內核內存(不是在DMA或高內存區中)以及如果需要的話函數可以睡眠(重新調度進程)。sizeof(*buf)是一種常見的方式,它用來獲取可通過指針訪問的結構體的大小。

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

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

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

bash/shell Code復制內容到剪貼板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是一個標準的內核數據結構,用以存儲打開的文件的信息,如當前文件位置(file->f_pos)、標志(file->f_flags),或者打開模式(file->f_mode)等。另外一個字段file->privatedata用于關聯文件到一些專有數據,它的類型是void *,而且它在文件擁有者以外,對內核不透明。我們將一個緩沖區存儲在那里。

如果緩沖區分配失敗,我們通過返回否定值(-ENOMEM)來為調用的用戶空間代碼標明。一個C庫中調用的open(2)系統調用(如glibc)將會檢測這個并適當地設置errno 。

學習如何讀和寫

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

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

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

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

bash/shell Code復制內容到剪貼板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.    }   
...

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

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

bash/shell Code復制內容到剪貼板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.   }  
如果用戶空間指針錯誤,那么調用可能會失敗;如果發生了此事,我們就返回-EFAULT。記住,不要相信任何來自內核外的事物!

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

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

bash/shell Code復制內容到剪貼板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()函數,該函數相當簡短并且標記為內聯。這是另外一個常見的優化;但是,你不能過度使用。因為過多的內聯會導致內核映像徒然增大。

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

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

耶!你現在已經有了一個內核模塊,它至少已經編譯成功了。現在,是時候來測試了。

調試內核代碼

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

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

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

編譯模塊,然后加載進內核:

bash/shell Code復制內容到剪貼板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  
一切似乎就位。現在,要測試模塊是否正常工作,我們將寫一段小程序來翻轉它的第一個命令行參數。main()(再三檢查錯誤)可能看上去像這樣:

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

bash/shell Code復制內容到剪貼板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
...
這里發生了什么呢?就像舉行了一場比賽。我們認為read和write是原子操作,或者從頭到尾一次執行一個指令。然而,內核確實無序并發的,隨便就重新調度了reverse_phrase()函數內部某個地方運行著的寫入操作的內核部分。如果在寫入操作結束前就調度了read()操作呢?就會產生數據不完整的狀態。這樣的bug非常難以找到。但是,怎樣來處理這個問題呢?

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

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

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

bash/shell Code復制內容到剪貼板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.  
我們在函數一開始就獲取鎖。mutex_lock_interruptible()要么得到互斥鎖然后返回,要么讓進程睡眠,直到有可用的互斥鎖。就像前面一樣,_interruptible后綴意味著睡眠可以由信號來中斷。

bash/shell Code復制內容到剪貼板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.    }  
下面是我們的“等待數據”循環。當獲取互斥鎖時,或者發生稱之為“死鎖”的情境時,不應該讓進程睡眠。因此,如果沒有數據,我們釋放互斥鎖并調用wait_event_interruptible()。當它返回時,我們重新獲取互斥鎖并像往常一樣繼續:

bash/shell Code復制內容到剪貼板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;  
最后,當函數結束,或者在互斥鎖被獲取過程中發生錯誤時,互斥鎖被解鎖。重新編譯模塊(別忘了重新加載),然后再次進行測試。現在你應該沒發現毀壞的數據了。

關鍵字:Linux、內核模塊
分享到:

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