- 論壇徽章:
- 0
|
第3章
+---------------------------------------------------+
| 寫一個塊設備驅動 |
+---------------------------------------------------+
| 作者:趙磊 |
| email: zhaoleidd@hotmail.com |
+---------------------------------------------------+
| 文章版權歸原作者所有。 |
| 大家可以自由轉載這篇文章,但原版權信息必須保留。 |
| 如需用于商業(yè)用途,請務必與原作者聯(lián)系,若因未取得 |
| 授權而收起的版權爭議,由侵權者自行負責。 |
+---------------------------------------------------+
上一章中我們討論了mm的衣服問題,并成功地為她換上了一件輕如鴻毛、關鍵是薄如蟬翼的新衣服。
而這一章中,我們打算稍稍再前進一步,也就是:給她脫光。
目的是更加符合我們的審美觀、并且能夠更加深入地了解該mm(喜歡制服皮草的讀者除外)。
付出的代價是這一章的內容要稍稍復雜一些。
雖然noop調度器確實已經(jīng)很簡單了,簡單到比我們的驅動程序還簡單,在2.6.27中的120行代碼量已經(jīng)充分說明了這個問題。
但顯而易見的是,不管它多簡單,只要它存在,我們就把它看成累贅。
這里我們不打算再次去反復磨嘴皮子論證不使用I/O調度器能給我們的驅動程序帶來什么樣的好處、面臨的困難、以及如何與國際接軌的諸多事宜,
畢竟現(xiàn)在不是在討論汽油降價,而我們也不是中石油。我們更關心的是實實在在地做一些對驅動程序有益的事情。
不過I/O調度器這層遮體衣服倒也不是這么容易脫掉的,因為實際上我們還使用了它捆綁的另一個功能,就是請求隊列。
因此我們在前兩章中的程序才如此簡單。
從細節(jié)上來說,請求隊列request_queue中有個make_request_fn成員變量,我們看它的定義:
struct request_queue
{
...
make_request_fn *make_request_fn;
...
}
它實際上是:
typedef int (make_request_fn) (struct request_queue *q, struct bio *bio);
也就是一個函數(shù)的指針。
如果上面這段話讓讀者感到莫名其妙,那么請搬個板凳坐下,Let's Begin the Story。
對通用塊層的訪問,比如請求讀某個塊設備上的一段數(shù)據(jù),通常是準備一個bio,然后調用generic_make_request()函數(shù)來實現(xiàn)的。
調用者是幸運的,因為他往往不需要去關心generic_make_request()函數(shù)如何做的,只需要知道這個神奇的函數(shù)會為他搞定所有的問題就OK了。
而我們卻沒有這么幸運,因為對一個塊設備驅動的設計者來說,如果不知道generic_make_request()函數(shù)的內部情況,很可能會讓驅動的使用者得不到安全感。
了解generic_make_request()內部的有效方法還是RTFSC,但這里會給出一些提示。
我們可以在generic_make_request()中找到__generic_make_request(bio)這么一句,
然后在__generic_make_request()函數(shù)中找到ret = q->make_request_fn(q, bio)這么一行。
偷懶省略掉解開謎題的所有關鍵步驟后,這里可以得出一個作者相信但讀者不一定相信的正確結論:
generic_make_request()最終是通過調用request_queue.make_request_fn函數(shù)完成bio所描述的請求處理的。
Story到此結束,現(xiàn)在我們可以解釋剛才為什么列出那段莫名其妙的數(shù)據(jù)結構的意圖了。
對于塊設備驅動來說,正是request_queue.make_request_fn函數(shù)負責處理這個塊設備上的所有請求。
也就是說,只要我們實現(xiàn)了request_queue.make_request_fn,那么塊設備驅動的Primary Mission就接近完成了。
在本章中,我們要做的就是:
1:讓request_queue.make_request_fn指向我們設計的make_request函數(shù)
2:把我們設計的make_request函數(shù)寫出來
如果讀者現(xiàn)在已經(jīng)意氣風發(fā)地拿起鍵盤躍躍欲試了,作者一定會假裝謙虛地問讀者一個問題:
你的鉆研精神遇到城管了?
如果這句話問得讀者莫名其妙的話,作者將補充另一個問題:
前兩章中明顯沒有實現(xiàn)make_request函數(shù),那時的驅動程序倒是如何工作的?
然后就是清清嗓子自問自答。
前兩章確實沒有用到make_request函數(shù),但當我們使用blk_init_queue()獲得request_queue時,
萬能的系統(tǒng)知道我們搞IT的都低收入,因此救濟了我們一個,這就是大名鼎鼎的__make_request()函數(shù)。
request_queue.make_request_fn指向了__make_request()函數(shù),因此對塊設備的所有請求被導向了__make_request()函數(shù)中。
__make_request()函數(shù)不是吃素的,馬上喊上了他的兄弟,也就是I/O調度器來幫忙,結果就是bio請求被I/O調度器處理了。
同時,__make_request()自身也沒閑著,它把bio這條咸魚嗅了嗅,舔了舔,然后放到嘴里嚼了嚼,把魚刺魚鱗剔掉,
然后情意綿綿地通過do_request函數(shù)(也就是blk_init_queue的第一個參數(shù))喂到驅動程序作者的口中。
這就解釋了前兩章中我們如何通過simp_blkdev_do_request()函數(shù)處理塊設備請求的。
我們理解__make_request()函數(shù)本意不錯,它把bio這條咸魚嚼成request_queue喂給do_request函數(shù),能讓我們的到如下好處:
1:request.buffer不在高端內存
這意味著我們不需要考慮映射高端內存到虛存的情況
2:request.buffer的內存是連續(xù)的
因此我們不需要考慮request.buffer對應的內存地址是否分成幾段的問題
這些好處看起來都很自然,正如某些行政不作為的“有關部門”認為老百姓納稅養(yǎng)他們也自然,
但不久我們就會看到不很自然的情況。
如果讀者是mm,或許會認為一個摔鍋把咸魚嚼好了含情脈脈地喂過來是一件很浪漫的事情(也希望這位讀者與作者聯(lián)系),
但對于大多數(shù)男性IT工作者來說,除非取向問題,否則......
因此現(xiàn)在我們寧可把__make_request()函數(shù)一腳踢飛,然后自己去嚼bio這條咸魚。
當然,踢飛__make_request()函數(shù)也意味著擺脫了I/O調度器的處理。
踢飛__make_request()很容易,使用blk_alloc_queue()函數(shù)代替blk_init_queue()函數(shù)來獲取request_queue就行了。
也就是說,我們把原先的
simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
改成了
simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
這樣。
至于嚼人家口水渣的simp_blkdev_do_request()函數(shù),我們也一并扔掉:
把simp_blkdev_do_request()函數(shù)從頭到尾刪掉。
同時,由于現(xiàn)在要脫光,所以上一章中我們費好大勁換上的那件薄內衣也不需要了,
也就是把上一章中增加的elevator_init()這部分的函數(shù)也刪了,也就是刪掉如下部分:
old_e = simp_blkdev_queue->elevator;
if (IS_ERR_VALUE(elevator_init(simp_blkdev_queue, "noop")))
printk(KERN_WARNING "Switch elevator failed, using default\n");
else
elevator_exit(old_e);
到這里我們已經(jīng)成功地讓__make_request()升空了,但要自己嚼bio,還需要添加一些東西:
首先給request_queue指定我們自己的bio處理函數(shù),這是通過blk_queue_make_request()函數(shù)實現(xiàn)的,把這面這行加在blk_alloc_queue()之后:
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
然后實現(xiàn)我們自己的simp_blkdev_make_request()函數(shù),
然后編譯。
如果按照上述的描述修改出的代碼讓讀者感到信心不足,我們在此列出修改過的simp_blkdev_init()函數(shù):
static int __init simp_blkdev_init(void)
{
int ret;
simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
if (!simp_blkdev_queue) {
ret = -ENOMEM;
goto err_alloc_queue;
}
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
simp_blkdev_disk = alloc_disk(1);
if (!simp_blkdev_disk) {
ret = -ENOMEM;
goto err_alloc_disk;
}
strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;
simp_blkdev_disk->first_minor = 0;
simp_blkdev_disk->fops = &simp_blkdev_fops;
simp_blkdev_disk->queue = simp_blkdev_queue;
set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
add_disk(simp_blkdev_disk);
return 0;
err_alloc_disk:
blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
return ret;
}
這里還把err_init_queue也改成了err_alloc_queue,希望讀者不要打算就這一點進行提問。
正如本章開頭所述,這一章的內容可能要復雜一些,而現(xiàn)在看來似乎已經(jīng)做到了。
而現(xiàn)在的進度大概是......一半!
不過值得安慰的是,余下的內容只有我們的simp_blkdev_make_request()函數(shù)了。
首先給出函數(shù)原型:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio);
該函數(shù)用來處理一個bio請求。
函數(shù)接受struct request_queue *q和struct bio *bio作為參數(shù),與請求有關的信息在bio參數(shù)中,
而struct request_queue *q并沒有經(jīng)過__make_request()的處理,這也意味著我們不能用前幾章那種方式使用q。
因此這里我們關注的是:bio。
關于bio和bio_vec的格式我們仍然不打算在這里做過多的解釋,理由同樣是因為我們要避免與google出的一大堆文章撞衫。
這里我們只說一句話:
bio對應塊設備上一段連續(xù)空間的請求,bio中包含的多個bio_vec用來指出這個請求對應的每段內存。
因此simp_blkdev_make_request()本質上是在一個循環(huán)中搞定bio中的每個bio_vec。
這個神奇的循環(huán)是這樣的:
dsk_mem = simp_blkdev_data + (bio->bi_sector << 9);
bio_for_each_segment(bvec, bio, i) {
void *iovec_mem;
switch (bio_rw(bio)) {
case READ:
case READA:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
case WRITE:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(dsk_mem, iovec_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
default:
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": unknown value of bio_rw: %lu\n",
bio_rw(bio));
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem += bvec->bv_len;
}
bio請求的塊設備起始扇區(qū)和扇區(qū)數(shù)存儲在bio.bi_sector和bio.bi_size中,
我們首先通過bio.bi_sector獲得這個bio請求在我們的塊設備內存中的起始部分位置,存入dsk_mem。
然后遍歷bio中的每個bio_vec,這里我們使用了系統(tǒng)提供的bio_for_each_segment宏。
循環(huán)中的代碼看上去有些眼熟,無非是根據(jù)請求的類型作相應的處理。READA意味著預讀,精心設計的預讀請求可以提高I/O效率,
這有點像內存中的prefetch(),我們同樣不在這里做更詳細的介紹,因為這本身就能寫一整篇文章,對于我們的基于內存的塊設備驅動,
只要按照READ請求同樣處理就OK了。
在很眼熟的memcpy前后,我們發(fā)現(xiàn)了kmap和kunmap這兩個新面孔。
這也證明了咸魚要比爛肉難啃的道理。
bio_vec中的內存地址是使用page *描述的,這也意味著內存頁面有可能處于高端內存中而無法直接訪問。
這種情況下,常規(guī)的處理方法是用kmap映射到非線性映射區(qū)域進行訪問,當然,訪問完后要記得把映射的區(qū)域還回去,
不要仗著你內存大就不還,實際上在i386結構中,你內存越大可用的非線性映射區(qū)域越緊張。
關于高端內存的細節(jié)也請自行google,反正在我的印象中intel總是有事沒事就弄些硬件限制給程序員找麻煩以幫助程序員的就業(yè)。
所幸的是逐漸流行的64位機的限制應該不那么容易突破了,至少我這么認為。
switch中的default用來處理其它情況,而我們的處理卻很簡單,拋出一條錯誤信息,然后調用bio_endio()告訴上層這個bio錯了。
不過這個萬惡的bio_endio()函數(shù)在2.6.24中改了,如果我們的驅動程序是內核的一部分,那么我們只要同步更新調用bio_endio()的語句就行了,
但現(xiàn)在的情況顯然不是,而我們又希望這個驅動程序能夠同時適應2.6.24之前和之后的內核,因此這里使用條件編譯來比較內核版本。
同時,由于使用到了LINUX_VERSION_CODE和KERNEL_VERSION宏,因此還需要增加#include <linux/version.h>。
循環(huán)的最后把這一輪循環(huán)中完成處理的字節(jié)數(shù)加到dsk_mem中,這樣dsk_mem指向在下一個bio_vec對應的塊設備中的數(shù)據(jù)。
讀者或許開始耐不住性子想這一章怎么還不結束了,是的,馬上就結束,不過我們還要在循環(huán)的前后加上一丁點:
1:循環(huán)之前的變量聲明:
struct bio_vec *bvec;
int i;
void *dsk_mem;
2:循環(huán)之前檢測訪問請求是否超越了塊設備限制:
if ((bio->bi_sector << 9) + bio->bi_size > SIMP_BLKDEV_BYTES) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)bio->bi_sector, bio->bi_size);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
3:循環(huán)之后結束這個bio,并返回成功:
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, bio->bi_size, 0);
#else
bio_endio(bio, 0);
#endif
return 0;
bio_endio用于返回這個對bio請求的處理結果,在2.6.24之后的內核中,第一個參數(shù)是被處理的bio指針,第二個參數(shù)成功時為0,失敗時為-ERRNO。
在2.6.24之前的內核中,中間還多了個unsigned int bytes_done,用于返回搞定了的字節(jié)數(shù)。
現(xiàn)在可以長長地舒一口氣了,我們完工了。
還是附上simp_blkdev_make_request()的完成代碼:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
struct bio_vec *bvec;
int i;
void *dsk_mem;
if ((bio->bi_sector << 9) + bio->bi_size > SIMP_BLKDEV_BYTES) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)bio->bi_sector, bio->bi_size);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem = simp_blkdev_data + (bio->bi_sector << 9);
bio_for_each_segment(bvec, bio, i) {
void *iovec_mem;
switch (bio_rw(bio)) {
case READ:
case READA:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
case WRITE:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(dsk_mem, iovec_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
default:
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": unknown value of bio_rw: %lu\n",
bio_rw(bio));
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem += bvec->bv_len;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, bio->bi_size, 0);
#else
bio_endio(bio, 0);
#endif
return 0;
}
讀者可以直接用本章的simp_blkdev_make_request()函數(shù)替換掉上一章的simp_blkdev_do_request()函數(shù),
然后用本章的simp_blkdev_init()函數(shù)替換掉上一章的同名函數(shù),再在文件頭部增加#include <linux/version.h>,
就得到了本章的最終代碼。
在結束本章之前,我們還是試驗一下:
首先還是編譯和加載:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step3 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
CC [M] /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.o
Building modules, stage 2.
MODPOST
CC /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.mod.o
LD [M] /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#
然后使用上一章中的方法看看sysfs中的這個設備的信息:
# ls /sys/block/simp_blkdev
dev holders range removable size slaves stat subsystem uevent
#
我們發(fā)現(xiàn)我們的驅動程序在sysfs目錄中的queue子目錄不見了。
這并不奇怪,否則就要抓狂了。
本章中我們實現(xiàn)自己的make_request函數(shù)來處理bio,以此擺脫了I/O調度器和通用的__make_request()對bio的處理。
由于我們的塊設備中的數(shù)據(jù)都是存在于內存中,不牽涉到DMA操作、并且不需要尋道,因此這應該是最適合這種形態(tài)的塊設備的處理方式。
在linux中類似的驅動程序大多使用了本章中的處理方式,但對大多數(shù)基于物理磁盤的塊設備驅動來說,使用適合的I/O調度器更能提高性能。
同時,__make_request()中包含的回彈機制對需要進行DMA操作的塊設備驅動來說,也能提供不錯幫助。
雖然說量變產(chǎn)生質變,通常質變比量變要復雜得多。
同理,相比前一章,把mm衣服脫光也比讓她換一件薄一些的衣服要困難得多。
不過無論如何,我們總算連哄帶騙地讓mm脫下來了,而付出了滿頭大汗的代價:
本章內容的復雜度相比前一章大大加深了。
如果本章的內容不幸使讀者感覺頭部體積有所增加的話,作為彌補,我們將宣布一個好消息:
因為根據(jù)慣例,隨后的1、2章將會出現(xiàn)一些輕松的內容讓讀者得到充分休息。
<未完,待續(xù)> |
|