怎么在Linux中編寫內(nèi)核模塊?相信很多沒(méi)有經(jīng)驗(yàn)的人對(duì)此束手無(wú)策,為此本文總結(jié)了問(wèn)題出現(xiàn)的原因和解決方法,通過(guò)這篇文章希望你能解決這個(gè)問(wèn)題。
創(chuàng)新互聯(lián)公司-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比吳橋網(wǎng)站開(kāi)發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫(kù),直接使用。一站式吳橋網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋吳橋地區(qū)。費(fèi)用合理售后完善,10年實(shí)體公司更值得信賴。
盡可能不要用root身份
默認(rèn)情況下,/dev/reverse只有root可以使用,因此你只能使用sudo來(lái)運(yùn)行你的測(cè)試程序。要解決該限制,可以創(chuàng)建一個(gè)包含以下內(nèi)容的/lib/udev/rules.d/99-reverse.rules文件:
SUBSYSTEM=="misc", KERNEL=="reverse", MODE="0666"
別忘了重新插入模塊。讓非root用戶訪問(wèn)設(shè)備節(jié)點(diǎn)往往不是一個(gè)好主意,但是在開(kāi)發(fā)其間卻是十分有用的。這并不是說(shuō)以root身份運(yùn)行二進(jìn)制測(cè)試文件也不是個(gè)好主意。
模塊的構(gòu)造
由于大多數(shù)的Linux內(nèi)核模塊是用C寫的(除了底層的特定于體系結(jié)構(gòu)的部分),所以推薦你將你的模塊以單一文件形式保存(例如,reverse.c)。我們已經(jīng)把完整的源代碼放在GitHub上——這里我們將看其中的一些片段。開(kāi)始時(shí),我們先要包含一些常見(jiàn)的文件頭,并用預(yù)定義的宏來(lái)描述模塊:
這里一切都直接明了,除了MODULE_LICENSE():它不僅僅是一個(gè)標(biāo)記。內(nèi)核堅(jiān)定地支持GPL兼容代碼,因此如果你把許可證設(shè)置為其它非GPL兼容的(如,“Proprietary”[專利]),某些特定的內(nèi)核功能將在你的模塊中不可用。
bash/shell Code復(fù)制內(nèi)容到剪貼板
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Valentine Sinitsyn <valentine.sinitsyn@gmail.com>");
MODULE_DESCRIPTION("In-kernel phrase reverser");
什么時(shí)候不該寫內(nèi)核模塊
內(nèi)核編程很有趣,但是在現(xiàn)實(shí)項(xiàng)目中寫(尤其是調(diào)試)內(nèi)核代碼要求特定的技巧。通常來(lái)講,在沒(méi)有其它方式可以解決你的問(wèn)題時(shí),你才應(yīng)該在內(nèi)核級(jí)別解決它。以下情形中,可能你在用戶空間中解決它更好:
你要開(kāi)發(fā)一個(gè)USB驅(qū)動(dòng) —— 請(qǐng)查看libusb。
你要開(kāi)發(fā)一個(gè)文件系統(tǒng) —— 試試FUSE。
你在擴(kuò)展Netfilter —— 那么libnetfilter_queue對(duì)你有所幫助。
通常,內(nèi)核里面代碼的性能會(huì)更好,但是對(duì)于許多項(xiàng)目而言,這點(diǎn)性能丟失并不嚴(yán)重。
由于內(nèi)核編程總是異步的,沒(méi)有一個(gè)main()函數(shù)來(lái)讓Linux順序執(zhí)行你的模塊。取而代之的是,你要為各種事件提供回調(diào)函數(shù),像這個(gè):
bash/shell Code復(fù)制內(nèi)容到剪貼板
static int __init reverse_init(void)
{
printk(KERN_INFO "reverse device has been registered\n");
return 0;
}
static void __exit reverse_exit(void)
{
printk(KERN_INFO "reverse device has been unregistered\n");
}
module_init(reverse_init);
module_exit(reverse_exit);
這里,我們定義的函數(shù)被稱為模塊的插入和刪除。只有第一個(gè)的插入函數(shù)是必要的。目前,它們只是打印消息到內(nèi)核環(huán)緩沖區(qū)(可以在用戶空間通過(guò)dmesg命令訪問(wèn));KERN_INFO是日志級(jí)別(注意,沒(méi)有逗號(hào))。__init和__exit是屬性 —— 聯(lián)結(jié)到函數(shù)(或者變量)的元數(shù)據(jù)片。屬性在用戶空間的C代碼中是很罕見(jiàn)的,但是內(nèi)核中卻很普遍。所有標(biāo)記為_(kāi)_init的,會(huì)在初始化后釋放內(nèi)存以供重用(還記得那條過(guò)去內(nèi)核的那條“Freeing unused kernel memory…[釋放未使用的內(nèi)核內(nèi)存……]”信息嗎?)。__exit表明,當(dāng)代碼被靜態(tài)構(gòu)建進(jìn)內(nèi)核時(shí),該函數(shù)可以安全地優(yōu)化了,不需要清理收尾。最后,module_init()和module_exit()這兩個(gè)宏將reverse_init()和reverse_exit()函數(shù)設(shè)置成為我們模塊的生命周期回調(diào)函數(shù)。實(shí)際的函數(shù)名稱并不重要,你可以稱它們?yōu)閕nit()和exit(),或者start()和stop(),你想叫什么就叫什么吧。他們都是靜態(tài)聲明,你在外部模塊是看不到的。事實(shí)上,內(nèi)核中的任何函數(shù)都是不可見(jiàn)的,除非明確地被導(dǎo)出。然而,在內(nèi)核程序員中,給你的函數(shù)加上模塊名前綴是約定俗成的。
這些都是些基本概念 – 讓我們來(lái)做更多有趣的事情吧。模塊可以接收參數(shù),就像這樣:
# modprobe foo bar=1
modinfo命令顯示了模塊接受的所有參數(shù),而這些也可以在/sys/module//parameters下作為文件使用。我們的模塊需要一個(gè)緩沖區(qū)來(lái)存儲(chǔ)參數(shù) —— 讓我們把這大小設(shè)置為用戶可配置。在MODULE_DESCRIPTION()下添加如下三行:
bash/shell Code復(fù)制內(nèi)容到剪貼板
static unsigned long buffer_size = 8192;
module_param(buffer_size, ulong, (S_IRUSR | S_IRGRP | S_IROTH));
MODULE_PARM_DESC(buffer_size, "Internal buffer size");
這兒,我們定義了一個(gè)變量來(lái)存儲(chǔ)該值,封裝成一個(gè)參數(shù),并通過(guò)sysfs來(lái)讓所有人可讀。這個(gè)參數(shù)的描述(最后一行)出現(xiàn)在modinfo的輸出中。
由于用戶可以直接設(shè)置buffer_size,我們需要在reverse_init()來(lái)清除無(wú)效取值。你總該檢查來(lái)自內(nèi)核之外的數(shù)據(jù) —— 如果你不這么做,你就是將自己置身于內(nèi)核異?;虬踩┒粗?。
bash/shell Code復(fù)制內(nèi)容到剪貼板
static int __init reverse_init()
{
if (!buffer_size)
return -1;
printk(KERN_INFO
"reverse device has been registered, buffer size is %lu bytes\n",
buffer_size);
return 0;
}
來(lái)自模塊初始化函數(shù)的非0返回值意味著模塊執(zhí)行失敗。
導(dǎo)航
但你開(kāi)發(fā)模塊時(shí),Linux內(nèi)核就是你所需一切的源頭。然而,它相當(dāng)大,你可能在查找你所要的內(nèi)容時(shí)會(huì)有困難。幸運(yùn)的是,在龐大的代碼庫(kù)面前,有許多工具使這個(gè)過(guò)程變得簡(jiǎn)單。首先,是Cscope —— 在終端中運(yùn)行的一個(gè)比較經(jīng)典的工具。你所要做的,就是在內(nèi)核源代碼的頂級(jí)目錄中運(yùn)行make cscope && cscope。Cscope和Vim以及Emacs整合得很好,因此你可以在你最喜愛(ài)的編輯器中使用它。
如果基于終端的工具不是你的最愛(ài),那么就訪問(wèn)http://lxr.free-electrons.com吧。它是一個(gè)基于web的內(nèi)核導(dǎo)航工具,即使它的功能沒(méi)有Cscope來(lái)得多(例如,你不能方便地找到函數(shù)的用法),但它仍然提供了足夠多的快速查詢功能。
現(xiàn)在是時(shí)候來(lái)編譯模塊了。你需要你正在運(yùn)行的內(nèi)核版本頭文件(linux-headers,或者等同的軟件包)和build-essential(或者類似的包)。接下來(lái),該創(chuàng)建一個(gè)標(biāo)準(zhǔn)的Makefile模板:
bash/shell Code復(fù)制內(nèi)容到剪貼板
obj-m += reverse.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
現(xiàn)在,調(diào)用make來(lái)構(gòu)建你的第一個(gè)模塊。如果你輸入的都正確,在當(dāng)前目錄內(nèi)會(huì)找到reverse.ko文件。使用sudo insmod reverse.ko插入內(nèi)核模塊,然后運(yùn)行如下命令:
bash/shell Code復(fù)制內(nèi)容到剪貼板
$ dmesg | tail -1
[ 5905.042081] reverse device has been registered, buffer size is 8192 bytes
恭喜了!然而,目前這一行還只是假象而已 —— 還沒(méi)有設(shè)備節(jié)點(diǎn)呢。讓我們來(lái)搞定它。
混雜設(shè)備
在Linux中,有一種特殊的字符設(shè)備類型,叫做“混雜設(shè)備”(或者簡(jiǎn)稱為“misc”)。它是專為單一接入點(diǎn)的小型設(shè)備驅(qū)動(dòng)而設(shè)計(jì)的,而這正是我們所需要的。所有混雜設(shè)備共享同一個(gè)主設(shè)備號(hào)(10),因此一個(gè)驅(qū)動(dòng)(drivers/char/misc.c)就可以查看它們所有設(shè)備了,而這些設(shè)備用次設(shè)備號(hào)來(lái)區(qū)分。從其他意義來(lái)說(shuō),它們只是普通字符設(shè)備。
要為該設(shè)備注冊(cè)一個(gè)次設(shè)備號(hào)(以及一個(gè)接入點(diǎn)),你需要聲明struct misc_device,填上所有字段(注意語(yǔ)法),然后使用指向該結(jié)構(gòu)的指針作為參數(shù)來(lái)調(diào)用misc_register()。為此,你也需要包含linux/miscdevice.h頭文件:
bash/shell Code復(fù)制內(nèi)容到剪貼板
static struct miscdevice reverse_misc_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "reverse",
.fops = &reverse_fops
};
static int __init reverse_init()
{
...
misc_register(&reverse_misc_device);
printk(KERN_INFO ...
}
這兒,我們?yōu)槊麨椤皉everse”的設(shè)備請(qǐng)求一個(gè)第一個(gè)可用的(動(dòng)態(tài)的)次設(shè)備號(hào);省略號(hào)表明我們之前已經(jīng)見(jiàn)過(guò)的省略的代碼。別忘了在模塊卸下后注銷掉該設(shè)備。
bash/shell Code復(fù)制內(nèi)容到剪貼板
static void __exit reverse_exit(void)
{
misc_deregister(&reverse_misc_device);
...
}
‘fops’字段存儲(chǔ)了一個(gè)指針,指向一個(gè)file_operations結(jié)構(gòu)(在Linux/fs.h中聲明),而這正是我們模塊的接入點(diǎn)。reverse_fops定義如下:
bash/shell Code復(fù)制內(nèi)容到剪貼板
static struct file_operations reverse_fops = {
.owner = THIS_MODULE,
.open = reverse_open,
...
.llseek = noop_llseek
};
另外,reverse_fops包含了一系列回調(diào)函數(shù)(也稱之為方法),當(dāng)用戶空間代碼打開(kāi)一個(gè)設(shè)備,讀寫或者關(guān)閉文件描述符時(shí),就會(huì)執(zhí)行。如果你要忽略這些回調(diào),可以指定一個(gè)明確的回調(diào)函數(shù)來(lái)替代。這就是為什么我們將llseek設(shè)置為noop_llseek(),(顧名思義)它什么都不干。這個(gè)默認(rèn)實(shí)現(xiàn)改變了一個(gè)文件指針,而且我們現(xiàn)在并不需要我們的設(shè)備可以尋址(這是今天留給你們的家庭作業(yè))。
關(guān)閉和打開(kāi)
讓我們來(lái)實(shí)現(xiàn)該方法。我們將給每個(gè)打開(kāi)的文件描述符分配一個(gè)新的緩沖區(qū),并在它關(guān)閉時(shí)釋放。這實(shí)際上并不安全:如果一個(gè)用戶空間應(yīng)用程序泄漏了描述符(也許是故意的),它就會(huì)霸占RAM,并導(dǎo)致系統(tǒng)不可用。在現(xiàn)實(shí)世界中,你總得考慮到這些可能性。但在本教程中,這種方法不要緊。
我們需要一個(gè)結(jié)構(gòu)函數(shù)來(lái)描述緩沖區(qū)。內(nèi)核提供了許多常規(guī)的數(shù)據(jù)結(jié)構(gòu):鏈接列表(雙聯(lián)的),哈希表,樹(shù)等等之類。不過(guò),緩沖區(qū)常常從頭設(shè)計(jì)。我們將調(diào)用我們的“struct buffer”:
bash/shell Code復(fù)制內(nèi)容到剪貼板
struct buffer {
char *data, *end, *read_ptr;
unsigned long size;
};
data是該緩沖區(qū)存儲(chǔ)的一個(gè)指向字符串的指針,而end指向字符串結(jié)尾后的第一個(gè)字節(jié)。read_ptr是read()開(kāi)始讀取數(shù)據(jù)的地方。緩沖區(qū)的size是為了保證完整性而存儲(chǔ)的 —— 目前,我們還沒(méi)有使用該區(qū)域。你不能假設(shè)使用你結(jié)構(gòu)體的用戶會(huì)正確地初始化所有這些東西,所以最好在函數(shù)中封裝緩沖區(qū)的分配和收回。它們通常命名為buffer_alloc()和buffer_free()。
bash/shell Code復(fù)制內(nèi)容到剪貼板
static struct buffer buffer_alloc(unsigned long size)
{ struct buffer *buf; buf = kzalloc(sizeof(buf), GFP_KERNEL);
if
(unlikely(!buf)) goto out; … out: return buf;
}
內(nèi)核內(nèi)存使用kmalloc()來(lái)分配,并使用kfree()來(lái)釋放;kzalloc()的風(fēng)格是將內(nèi)存設(shè)置為全零。不同于標(biāo)準(zhǔn)的malloc(),它的內(nèi)核對(duì)應(yīng)部分收到的標(biāo)志指定了第二個(gè)參數(shù)中請(qǐng)求的內(nèi)存類型。這里,GFP_KERNEL是說(shuō)我們需要一個(gè)普通的內(nèi)核內(nèi)存(不是在DMA或高內(nèi)存區(qū)中)以及如果需要的話函數(shù)可以睡眠(重新調(diào)度進(jìn)程)。sizeof(*buf)是一種常見(jiàn)的方式,它用來(lái)獲取可通過(guò)指針訪問(wèn)的結(jié)構(gòu)體的大小。
你應(yīng)該隨時(shí)檢查kmalloc()的返回值:訪問(wèn)NULL指針將導(dǎo)致內(nèi)核異常。同時(shí)也需要注意unlikely()宏的使用。它(及其相對(duì)宏likely())被廣泛用于內(nèi)核中,用于表明條件幾乎總是真的(或假的)。它不會(huì)影響到控制流程,但是能幫助現(xiàn)代處理器通過(guò)分支預(yù)測(cè)技術(shù)來(lái)提升性能。
最后,注意goto語(yǔ)句。它們常常為認(rèn)為是邪惡的,但是,Linux內(nèi)核(以及一些其它系統(tǒng)軟件)采用它們來(lái)實(shí)施集中式的函數(shù)退出。這樣的結(jié)果是減少嵌套深度,使代碼更具可讀性,而且非常像更高級(jí)語(yǔ)言中的try-catch區(qū)塊。
有了buffer_alloc()和buffer_free(),open和close方法就變得很簡(jiǎn)單了。
bash/shell Code復(fù)制內(nèi)容到剪貼板
static int reverse_open(struct inode *inode, struct file *file)
{
int err = 0;
file->private_data = buffer_alloc(buffer_size);
...
return err;
}
struct file是一個(gè)標(biāo)準(zhǔn)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),用以存儲(chǔ)打開(kāi)的文件的信息,如當(dāng)前文件位置(file->f_pos)、標(biāo)志(file->f_flags),或者打開(kāi)模式(file->f_mode)等。另外一個(gè)字段file->privatedata用于關(guān)聯(lián)文件到一些專有數(shù)據(jù),它的類型是void *,而且它在文件擁有者以外,對(duì)內(nèi)核不透明。我們將一個(gè)緩沖區(qū)存儲(chǔ)在那里。
如果緩沖區(qū)分配失敗,我們通過(guò)返回否定值(-ENOMEM)來(lái)為調(diào)用的用戶空間代碼標(biāo)明。一個(gè)C庫(kù)中調(diào)用的open(2)系統(tǒng)調(diào)用(如glibc)將會(huì)檢測(cè)這個(gè)并適當(dāng)?shù)卦O(shè)置errno 。
學(xué)習(xí)如何讀和寫
“read”和“write”方法是真正完成工作的地方。當(dāng)數(shù)據(jù)寫入到緩沖區(qū)時(shí),我們放棄之前的內(nèi)容和反向地存儲(chǔ)該字段,不需要任何臨時(shí)存儲(chǔ)。read方法僅僅是從內(nèi)核緩沖區(qū)復(fù)制數(shù)據(jù)到用戶空間。但是如果緩沖區(qū)還沒(méi)有數(shù)據(jù),revers_eread()會(huì)做什么呢?在用戶空間中,read()調(diào)用會(huì)在有可用數(shù)據(jù)前阻塞它。在內(nèi)核中,你就必須等待。幸運(yùn)的是,有一項(xiàng)機(jī)制用于處理這種情況,就是‘wait queues’。
想法很簡(jiǎn)單。如果當(dāng)前進(jìn)程需要等待某個(gè)事件,它的描述符(struct task_struct存儲(chǔ)‘current’信息)被放進(jìn)非可運(yùn)行(睡眠中)狀態(tài),并添加到一個(gè)隊(duì)列中。然后schedule()就被調(diào)用來(lái)選擇另一個(gè)進(jìn)程運(yùn)行。生成事件的代碼通過(guò)使用隊(duì)列將等待進(jìn)程放回TASK_RUNNING狀態(tài)來(lái)喚醒它們。調(diào)度程序?qū)⒃谝院笤谀硞€(gè)地方選擇它們之一。Linux有多種非可運(yùn)行狀態(tài),最值得注意的是TASK_INTERRUPTIBLE(一個(gè)可以通過(guò)信號(hào)中斷的睡眠)和TASK_KILLABLE(一個(gè)可被殺死的睡眠中的進(jìn)程)。所有這些都應(yīng)該正確處理,并等待隊(duì)列為你做這些事。
一個(gè)用以存儲(chǔ)讀取等待隊(duì)列頭的天然場(chǎng)所就是結(jié)構(gòu)緩沖區(qū),所以從為它添加wait_queue_headt read\queue字段開(kāi)始。你也應(yīng)該包含linux/sched.h頭文件??梢允褂肈ECLARE_WAITQUEUE()宏來(lái)靜態(tài)聲明一個(gè)等待隊(duì)列。在我們的情況下,需要?jiǎng)討B(tài)初始化,因此添加下面這行到buffer_alloc():
bash/shell Code復(fù)制內(nèi)容到剪貼板
init_waitqueue_head(&buf->read_queue);
我們等待可用數(shù)據(jù);或者等待read_ptr != end條件成立。我們也想要讓等待操作可以被中斷(如,通過(guò)Ctrl+C)。因此,“read”方法應(yīng)該像這樣開(kāi)始:
bash/shell Code復(fù)制內(nèi)容到剪貼板
static ssize_t reverse_read(struct file *file, char __user * out,
size_t size, loff_t * off)
{
struct buffer *buf = file->private_data;
ssize_t result;
while (buf->read_ptr == buf->end) {
if (file->f_flags & O_NONBLOCK) {
result = -EAGAIN;
goto out;
}
if (wait_event_interruptible
(buf->read_queue, buf->read_ptr != buf->end)) {
result = -ERESTARTSYS;
goto out;
}
}
...
我們讓它循環(huán),直到有可用數(shù)據(jù),如果沒(méi)有則使用wait_event_interruptible()(它是一個(gè)宏,不是函數(shù),這就是為什么要通過(guò)值的方式給隊(duì)列傳遞)來(lái)等待。好吧,如果wait_event_interruptible()被中斷,它返回一個(gè)非0值,這個(gè)值代表-ERESTARTSYS。這段代碼意味著系統(tǒng)調(diào)用應(yīng)該重新啟動(dòng)。file->f_flags檢查以非阻塞模式打開(kāi)的文件數(shù):如果沒(méi)有數(shù)據(jù),返回-EAGAIN。
我們不能使用if()來(lái)替代while(),因?yàn)榭赡苡性S多進(jìn)程正等待數(shù)據(jù)。當(dāng)write方法喚醒它們時(shí),調(diào)度程序以不可預(yù)知的方式選擇一個(gè)來(lái)運(yùn)行,因此,在這段代碼有機(jī)會(huì)執(zhí)行的時(shí)候,緩沖區(qū)可能再次空出?,F(xiàn)在,我們需要將數(shù)據(jù)從buf->data 復(fù)制到用戶空間。copy_to_user()內(nèi)核函數(shù)就干了此事:
bash/shell Code復(fù)制內(nèi)容到剪貼板
size = min(size, (size_t) (buf->end - buf->read_ptr));
if (copy_to_user(out, buf->read_ptr, size)) {
result = -EFAULT;
goto out;
}
如果用戶空間指針錯(cuò)誤,那么調(diào)用可能會(huì)失敗;如果發(fā)生了此事,我們就返回-EFAULT。記住,不要相信任何來(lái)自內(nèi)核外的事物!
bash/shell Code復(fù)制內(nèi)容到剪貼板
buf->read_ptr += size;
result = size;
out:
return result;
}
為了使數(shù)據(jù)在任意塊可讀,需要進(jìn)行簡(jiǎn)單運(yùn)算。該方法返回讀入的字節(jié)數(shù),或者一個(gè)錯(cuò)誤代碼。
寫方法更簡(jiǎn)短。首先,我們檢查緩沖區(qū)是否有足夠的空間,然后我們使用copy_from_userspace()函數(shù)來(lái)獲取數(shù)據(jù)。再然后read_ptr和結(jié)束指針會(huì)被重置,并且反轉(zhuǎn)存儲(chǔ)緩沖區(qū)內(nèi)容:
bash/shell Code復(fù)制內(nèi)容到剪貼板
buf->end = buf->data + size;
buf->read_ptr = buf->data;
if (buf->end > buf->data)
reverse_phrase(buf->data, buf->end - 1);
這里, reverse_phrase()干了所有吃力的工作。它依賴于reverse_word()函數(shù),該函數(shù)相當(dāng)簡(jiǎn)短并且標(biāo)記為內(nèi)聯(lián)。這是另外一個(gè)常見(jiàn)的優(yōu)化;但是,你不能過(guò)度使用。因?yàn)檫^(guò)多的內(nèi)聯(lián)會(huì)導(dǎo)致內(nèi)核映像徒然增大。
最后,我們需要喚醒read_queue中等待數(shù)據(jù)的進(jìn)程,就跟先前講過(guò)的那樣。wake_up_interruptible()就是用來(lái)干此事的:
bash/shell Code復(fù)制內(nèi)容到剪貼板
wake_up_interruptible(&buf->read_queue);
耶!你現(xiàn)在已經(jīng)有了一個(gè)內(nèi)核模塊,它至少已經(jīng)編譯成功了。現(xiàn)在,是時(shí)候來(lái)測(cè)試了。
調(diào)試內(nèi)核代碼
或許,內(nèi)核中最常見(jiàn)的調(diào)試方法就是打印。如果你愿意,你可以使用普通的printk() (假定使用KERN_DEBUG日志等級(jí))。然而,那兒還有更好的辦法。如果你正在寫一個(gè)設(shè)備驅(qū)動(dòng),這個(gè)設(shè)備驅(qū)動(dòng)有它自己的“struct device”,可以使用pr_debug()或者dev_dbg():它們支持動(dòng)態(tài)調(diào)試(dyndbg)特性,并可以根據(jù)需要啟用或者禁用(請(qǐng)查閱Documentation/dynamic-debug-howto.txt)。對(duì)于單純的開(kāi)發(fā)消息,使用pr_devel(),除非設(shè)置了DEBUG,否則什么都不會(huì)做。要為我們的模塊啟用DEBUG,請(qǐng)?zhí)砑右韵滦械組akefile中:
bash/shell Code復(fù)制內(nèi)容到剪貼板
CFLAGS_reverse.o := -DDEBUG
完了之后,使用dmesg來(lái)查看pr_debug()或pr_devel()生成的調(diào)試信息。 或者,你可以直接發(fā)送調(diào)試信息到控制臺(tái)。要想這么干,你可以設(shè)置console_loglevel內(nèi)核變量為8或者更大的值(echo 8 /proc/sys/kernel/printk),或者在高日志等級(jí),如KERN_ERR,來(lái)臨時(shí)打印要查詢的調(diào)試信息。很自然,在發(fā)布代碼前,你應(yīng)該移除這樣的調(diào)試聲明。
注意內(nèi)核消息出現(xiàn)在控制臺(tái),不要在Xterm這樣的終端模擬器窗口中去查看;這也是在內(nèi)核開(kāi)發(fā)時(shí),建議你不在X環(huán)境下進(jìn)行的原因。
驚喜,驚喜!
編譯模塊,然后加載進(jìn)內(nèi)核:
bash/shell Code復(fù)制內(nèi)容到剪貼板
$ make
$ sudo insmod reverse.ko buffer_size=2048
$ lsmod
reverse 2419 0
$ ls -l /dev/reverse
crw-rw-rw- 1 root root 10, 58 Feb 22 15:53 /dev/reverse
一切似乎就位。現(xiàn)在,要測(cè)試模塊是否正常工作,我們將寫一段小程序來(lái)翻轉(zhuǎn)它的第一個(gè)命令行參數(shù)。main()(再三檢查錯(cuò)誤)可能看上去像這樣:
bash/shell Code復(fù)制內(nèi)容到剪貼板
int fd = open("/dev/reverse", O_RDWR);
write(fd, argv[1], strlen(argv[1]));
read(fd, argv[1], strlen(argv[1]));
printf("Read: %s\n", argv[1]);
像這樣運(yùn)行:
bash/shell Code復(fù)制內(nèi)容到剪貼板
$ ./test 'A quick brown fox jumped over the lazy dog'
Read: dog lazy the over jumped fox brown quick A
它工作正常!玩得更逗一點(diǎn):試試傳遞單個(gè)單詞或者單個(gè)字母的短語(yǔ),空的字符串或者是非英語(yǔ)字符串(如果你有這樣的鍵盤布局設(shè)置),以及其它任何東西。
現(xiàn)在,讓我們讓事情變得更好玩一點(diǎn)。我們將創(chuàng)建兩個(gè)進(jìn)程,它們共享一個(gè)文件描述符(及其內(nèi)核緩沖區(qū))。其中一個(gè)會(huì)持續(xù)寫入字符串到設(shè)備,而另一個(gè)將讀取這些字符串。在下例中,我們使用了fork(2)系統(tǒng)調(diào)用,而pthreads也很好用。我也省略打開(kāi)和關(guān)閉設(shè)備的代碼,并在此檢查代碼錯(cuò)誤(又來(lái)了):
bash/shell Code復(fù)制內(nèi)容到剪貼板
char *phrase = "A quick brown fox jumped over the lazy dog";
if (fork())
/* Parent is the writer */
while (1)
write(fd, phrase, len);
else
/* child is the reader */
while (1) {
read(fd, buf, len);
printf("Read: %s\n", buf);
}
你希望這個(gè)程序會(huì)輸出什么呢?下面就是在我的筆記本上得到的東西:
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ā)生了什么呢?就像舉行了一場(chǎng)比賽。我們認(rèn)為read和write是原子操作,或者從頭到尾一次執(zhí)行一個(gè)指令。然而,內(nèi)核確實(shí)無(wú)序并發(fā)的,隨便就重新調(diào)度了reverse_phrase()函數(shù)內(nèi)部某個(gè)地方運(yùn)行著的寫入操作的內(nèi)核部分。如果在寫入操作結(jié)束前就調(diào)度了read()操作呢?就會(huì)產(chǎn)生數(shù)據(jù)不完整的狀態(tài)。這樣的bug非常難以找到。但是,怎樣來(lái)處理這個(gè)問(wèn)題呢?
基本上,我們需要確保在寫方法返回前沒(méi)有read方法能被執(zhí)行。如果你曾經(jīng)編寫過(guò)一個(gè)多線程的應(yīng)用程序,你可能見(jiàn)過(guò)同步原語(yǔ)(鎖),如互斥鎖或者信號(hào)。Linux也有這些,但有些細(xì)微的差別。內(nèi)核代碼可以運(yùn)行進(jìn)程上下文(用戶空間代碼的“代表”工作,就像我們使用的方法)和終端上下文(例如,一個(gè)IRQ處理線程)。如果你已經(jīng)在進(jìn)程上下文中和并且你已經(jīng)得到了所需的鎖,你只需要簡(jiǎn)單地睡眠和重試直到成功為止。在中斷上下文時(shí)你不能處于休眠狀態(tài),因此代碼會(huì)在一個(gè)循環(huán)中運(yùn)行直到鎖可用。關(guān)聯(lián)原語(yǔ)被稱為自旋鎖,但在我們的環(huán)境中,一個(gè)簡(jiǎn)單的互斥鎖 —— 在特定時(shí)間內(nèi)只有唯一一個(gè)進(jìn)程能“占有”的對(duì)象 —— 就足夠了。處于性能方面的考慮,現(xiàn)實(shí)的代碼可能也會(huì)使用讀-寫信號(hào)。
鎖總是保護(hù)某些數(shù)據(jù)(在我們的環(huán)境中,是一個(gè)“struct buffer”實(shí)例),而且也常常會(huì)把它們嵌入到它們所保護(hù)的結(jié)構(gòu)體中。因此,我們添加一個(gè)互斥鎖(‘struct mutex lock’)到“struct buffer”中。我們也必須用mutex_init()來(lái)初始化互斥鎖;buffer_alloc是用來(lái)處理這件事的好地方。使用互斥鎖的代碼也必須包含linux/mutex.h。
互斥鎖很像交通信號(hào)燈 —— 要是司機(jī)不看它和不聽(tīng)它的,它就沒(méi)什么用。因此,在對(duì)緩沖區(qū)做操作并在操作完成時(shí)釋放它之前,我們需要更新reverse_read()和reverse_write()來(lái)獲取互斥鎖。讓我們來(lái)看看read方法 —— write的工作原理相同:
bash/shell Code復(fù)制內(nèi)容到剪貼板
static ssize_t reverse_read(struct file *file, char __user * out,
size_t size, loff_t * off)
{
struct buffer *buf = file->private_data;
ssize_t result;
if (mutex_lock_interruptible(&buf->lock)) {
result = -ERESTARTSYS;
goto out;
}
我們?cè)诤瘮?shù)一開(kāi)始就獲取鎖。mutex_lock_interruptible()要么得到互斥鎖然后返回,要么讓進(jìn)程睡眠,直到有可用的互斥鎖。就像前面一樣,_interruptible后綴意味著睡眠可以由信號(hào)來(lái)中斷。
bash/shell Code復(fù)制內(nèi)容到剪貼板
while (buf->read_ptr == buf->end) {
mutex_unlock(&buf->lock);
/* ... wait_event_interruptible() here ... */
if (mutex_lock_interruptible(&buf->lock)) {
result = -ERESTARTSYS;
goto out;
}
}
下面是我們的“等待數(shù)據(jù)”循環(huán)。當(dāng)獲取互斥鎖時(shí),或者發(fā)生稱之為“死鎖”的情境時(shí),不應(yīng)該讓進(jìn)程睡眠。因此,如果沒(méi)有數(shù)據(jù),我們釋放互斥鎖并調(diào)用wait_event_interruptible()。當(dāng)它返回時(shí),我們重新獲取互斥鎖并像往常一樣繼續(xù):
bash/shell Code復(fù)制內(nèi)容到剪貼板
if (copy_to_user(out, buf->read_ptr, size)) {
result = -EFAULT;
goto out_unlock;
}
...
out_unlock:
mutex_unlock(&buf->lock);
out:
return result;
看完上述內(nèi)容,你們掌握怎么在Linux中編寫內(nèi)核模塊的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!
網(wǎng)頁(yè)名稱:怎么在Linux中編寫內(nèi)核模塊
文章轉(zhuǎn)載:http://jinyejixie.com/article24/ggeece.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站策劃、關(guān)鍵詞優(yōu)化、ChatGPT、外貿(mào)網(wǎng)站建設(shè)、面包屑導(dǎo)航、搜索引擎優(yōu)化
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)