第 3章
+---------------------------------------------------+
| 寫一個塊設備驅動 |
+---------------------------------------------------+
| 作者:趙磊 |
| email: [email protected] |
+---------------------------------------------------+
| 文章版權歸原作者所有。 |
| 大家可以自由轉載這篇文章,但原版權信息必須保留。 |
| 如需用於商業用途,請務必與原作者聯繫,若因未取得 |
| 授權而收起的版權爭議,由侵權者自行負責。 |
----------------------- Page 16-----------------------
+---------------------------------------------------+
上一章中我們討論了mm的衣服問題,併成功地爲她換上了一件輕如鴻毛、關鍵是薄如蟬翼的新衣服
而這一章中,我們打算稍稍再前進一步,也就是:給她脫光
目的是更加符合我們的審美觀、並且能夠更加深入地瞭解該mm(喜歡制服皮草的讀者除外)
付出的代價是這一章的內容要稍稍複雜一些
雖然 noop調度器確實已經很簡單了,簡單到比我們的驅動程序還簡單,在 2.6.27中的 12 行代碼量已
經充分說明了這個問題
但顯而易見的是,不管它多簡單,只要它存在,我們就把它看成累贅
這裏我們不打算再次去反覆磨嘴皮子論證不使用 I/O調度器能給我們的驅動程序帶來什麼樣的好處、面
臨的困難、以及如何與國際接軌的諸多事宜,
畢竟現在不是在討論汽油降價,而我們也不是中石油。我們更關心的是實實在在地做一些對驅動程序有
益的事情
不過 I/O調度器這層遮體衣服倒也不是這麼容易脫掉的,因爲實際上我們還使用了它捆綁的另一個功能 ,
就是請求隊列
因此我們在前兩章中的程序才如此簡單
從細節上來說,請求隊列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);
也就是一個函數的指針
如果上面這段話讓讀者感到莫名其妙,那麼請搬個板凳坐下,Let's Begin the Story
對通用塊層的訪問,比如請求讀某個塊設備上的一段數據,通常是準備一個 bio ,然後調用
generic_make_request()函數來實現的
調用者是幸運的,因爲他往往不需要去關心 generic_make_request()函數如何做的,只需要知道這個
神奇的函數會爲他搞定所有的問題就 OK了
而我們卻沒有這麼幸運,因爲對一個塊設備驅動的設計者來說,如果不知道generic_make_request()
函數的內部情況,很可能會讓驅動的使用者得不到安全感
瞭解generic_make_request()內部的有效方法還是 RTFSC ,但這裏會給出一些提示
我們可以在 generic_make_request()中找到__generic_make_request(bio)這麼一句,
然後在__generic_make_request()函數中找到 ret = q->make_request_fn(q, bio)這麼一行
偷懶省略掉解開謎題的所有關鍵步驟後,這裏可以得出一個作者相信但讀者不一定相信的正確結論:
----------------------- Page 17-----------------------
generic_make_request()最終是通過調用 request_queue.make_request_fn函數完成 bio所描述
的請求處理的
Story到此結束,現在我們可以解釋剛纔爲什麼列出那段莫名其妙的數據結構的意圖了
對於塊設備驅動來說,正是 request_queue.make_request_fn函數負責處理這個塊設備上的所有請
求
也就是說,只要我們實現了 request_queue.make_request_fn ,那麼塊設備驅動的Primary
Mission就接近完成了
在本章中,我們要做的就是:
1 :讓request_queue.make_request_fn指向我們設計的 make_request函數
2 :把我們設計的 make_request函數寫出來
如果讀者現在已經意氣風發地拿起鍵盤躍躍欲試了,作者一定會假裝謙虛地問讀者一個問題:
你的鑽研精神遇到城管了 ?
如果這句話問得讀者莫名其妙的話,作者將補充另一個問題:
前兩章中明顯沒有實現make_request函數,那時的驅動程序倒是如何工作的 ?
然後就是清清嗓子自問自答
前兩章確實沒有用到 make_request函數,但當我們使用 blk_init_queue()獲得 request_queue時,
萬能的系統知道我們搞 IT的都低收入,因此救濟了我們一個,這就是大名鼎鼎的__make_request()函
數
request_queue.make_request_fn指向了__make_request()函數,因此對塊設備的所有請求被導
向了__make_request()函數中
__make_request()函數不是吃素的,馬上喊上了他的兄弟,也就是I/O調度器來幫忙,結果就是bio
請求被I/O調度器處理了
同時,__make_request()自身也沒閒着,它把bio這條鹹魚嗅了嗅,舔了舔,然後放到嘴裏嚼了嚼,
把魚刺魚鱗剔掉,
然後情意綿綿地通過 do_request函數(也就是 blk_init_queue的第一個參數)喂到驅動程序作者的口
中
這就解釋了前兩章中我們如何通過 simp_blkdev_do_request()函數處理塊設備請求的
我們理解__make_request()函數本意不錯,它把bio這條鹹魚嚼成 request_queue餵給
do_request函數,能讓我們的到如下好處:
1 :request.buffer不在高端內存
這意味着我們不需要考慮映射高端內存到虛存的情況
2 :request.buffer的內存是連續的
因此我們不需要考慮request.buffer對應的內存地址是否分成幾段的問題
這些好處看起來都很自然,正如某些行政不作爲的“有關部門”認爲老百姓納稅養他們也自然,
但不久我們就會看到不很自然的情況
----------------------- Page 18-----------------------
如果讀者是 mm ,或許會認爲一個摔鍋把鹹魚嚼好了含情脈脈地餵過來是一件很浪 的事情 (也希望這位
讀者與作者聯繫) ,
但對於大多數男性IT工作者來說,除非取向問題,否則......
因此現在我們寧可把__make_request()函數一腳踢飛,然後自己去嚼bio這條鹹魚
當然,踢飛__make_request()函數也意味着擺脫了 I/O調度器的處理
踢飛__make_request()很容易,使用 blk_alloc_queue()函數代替blk_init_queue()函數來獲取
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()函數,我們也一併扔掉:
把simp_blkdev_do_request()函數從頭到尾刪掉
同時,由於現在要脫光,所以上一章中我們費好大勁換上的那件薄內衣也不需要了,
也就是把上一章中增加的 elevator_init()這部分的函數也刪了,也就是刪掉如下部分:
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);
到這裏我們已經成功地讓__make_request()升空了,但要自己嚼bio ,還需要添加一些東西:
首先給request_queue指定我們自己的 bio處理函數,這是通過blk_queue_make_request()函數
實現的,把這面這行加在 blk_alloc_queue()之後:
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
然後實現我們自己的 simp_blkdev_make_request()函數,
然後編譯
如果按照上述的描述修改出的代碼讓讀者感到信心不足,我們在此列出修改過的 simp_blkdev_init()
函數:
static int __init simp_blkdev_init(void)
{
int ret;
simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
if (!simp_blkdev_queue) {
ret = -ENOMEM;
----------------------- Page 19-----------------------
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 ,希望讀者不要打算就這一點進行提問
正如本章開頭所述,這一章的內容可能要複雜一些,而現在看來似乎已經做到了
而現在的進度大概是 ......一半!
不過值得安慰的是,餘下的內容只有我們的 simp_blkdev_make_request()函數了
首先給出函數原型:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio);
該函數用來處理一個 bio請求
函數接受struct request_queue *q和 struct bio *bio作爲參數,與請求有關的信息在 bio參數
中,
而 struct request_queue *q並沒有經過__make_request()的處理,這也意味着我們不能用前幾章
那種方式使用 q
因此這裏我們關注的是:bio
關於 bio和 bio_vec的格式我們仍然不打算在這裏做過多的解釋,理由同樣是因爲我們要避免與
google出的一大堆文章撞衫
----------------------- Page 20-----------------------
這裏我們只說一句話:
bio對應塊設備上一段連續空間的請求,bio中包含的多個 bio_vec用來指出這個請求對應的每段內存
因此 simp_blkdev_make_request()本質上是在一個循環中搞定 bio中的每個 bio_vec
這個神奇的循環是這樣的:
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請求的塊設備起始扇區和扇區數存儲在 bio.bi_sector和 bio.bi_size中,
我們首先通過 bio.bi_sector獲得這個 bio請求在我們的塊設備內存中的起始部分位置,存入
dsk_mem
然後遍歷bio中的每個 bio_vec ,這裏我們使用了系統提供的 bio_for_each_segment宏
循環中的代碼看上去有些眼熟,無非是根據請求的類型作相應的處理 READA意味着預讀,精心設計的
----------------------- Page 21-----------------------
預讀請求可以提高I/O效率,
這有點像內存中的 prefetch() ,我們同樣不在這裏做更詳細的介紹,因爲這本身就能寫一整篇文章,
對於我們的基於內存的塊設備驅動,
只要按照READ請求同樣處理就 OK了
在很眼熟的 memcpy前後,我們發現了kmap和 kunmap這兩個新面孔
這也證明了鹹魚要比爛肉難啃的道理
bio_vec中的內存地址是使用 page *描述的,這也意味着內存頁面有可能處於高端內存中而無法直接訪
問
這種情況下,常規的處理方法是用 kmap映射到非線性映射區域進行訪問,當然,訪問完後要記得把映射
的區域還回去,
不要仗着你內存大就不還,實際上在 i386結構中,你內存越大可用的非線性映射區域越緊張
關於高端內存的細節也請自行 google ,反正在我的印象中 intel總是有事沒事就弄些硬件限制給程序
員找麻煩以幫助程序員的就業
所幸的是逐漸流行的 64位機的限制應該不那麼容易突破了,至少我這麼認爲
switch中的 default用來處理其它情況,而我們的處理卻很簡單,拋出一條錯誤信息,然後調用
bio_endio()告訴上層這個 bio錯了
不過這個萬惡的 bio_endio()函數在 2.6.24中改了,如果我們的驅動程序是內核的一部分,那麼我們
只要同步更新調用 bio_endio()的語句就行了,
但現在的情況顯然不是,而我們又希望這個驅動程序能夠同時適應2.6.24之前和之後的內核,因此這裏
使用條件編譯來比較內核版本
同時,由於使用到了 LINUX_VERSION_CODE和 KERNEL_VERSION宏,因此還需要增加#include
<linux/version.h>
循環的最後把這一輪循環中完成處理的字節數加到 dsk_mem中,這樣 dsk_mem指向在下一個 bio_vec
對應的塊設備中的數據
讀者或許開始耐不住性子想這一章怎麼還不結束了,是的,馬上就結束,不過我們還要在循環的前後加
上一丁點:
1 :循環之前的變量聲明:
struct bio_vec *bvec;
int i;
void *dsk_mem;
2 :循環之前檢測訪問請求是否超越了塊設備限制:
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);
----------------------- Page 22-----------------------
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
3 :循環之後結束這個 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之後的內核中,第一個參數是被處理的
bio指針,第二個參數成功時爲 ,失敗時爲-ERRNO
在 2.6.24之前的內核中,中間還多了個 unsigned int bytes_done ,用於返回搞定了的字節數
現在可以長長地舒一口氣了,我們完工了
還是附上 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)) {
----------------------- Page 23-----------------------
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()函數替換掉上一章的
simp_blkdev_do_request()函數,
然後用本章的 simp_blkdev_init()函數替換掉上一章的同名函數,再在文件頭部增加#include
<linux/version.h> ,
就得到了本章的最終代碼
在結束本章之前,我們還是試驗一下:
首先還是編譯和加載:
----------------------- Page 24-----------------------
# 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
#
我們發現我們的驅動程序在 sysfs目錄中的 queue子目錄不見了
這並不奇怪,否則就要抓狂了
本章中我們實現自己的 make_request函數來處理 bio ,以此擺脫了 I/O調度器和通用的
__make_request()對 bio的處理
由於我們的塊設備中的數據都是存在於內存中,不牽涉到 DMA操作、並且不需要尋道,因此這應該是最
適合這種形態的塊設備的處理方式
在 linux中類似的驅動程序大多使用了本章中的處理方式,但對大多數基於物理磁盤的塊設備驅動來說 ,
使用適合的 I/O調度器更能提高性能
同時,__make_request()中包含的回彈機制對需要進行 DMA操作的塊設備驅動來說,也能提供不錯幫
助
雖然說量變產生質變,通常質變比量變要複雜得多
同理,相比前一章,把mm衣服脫光也比讓她換一件薄一些的衣服要困難得多
不過無論如何,我們總算連哄帶騙地讓mm脫下來了,而付出了滿頭大汗的代價:
本章內容的複雜度相比前一章大大加深了
如果本章的內容不幸使讀者感覺頭部體積有所增加的話,作爲彌補,我們將宣佈一個好消息:
因爲根據慣例,隨後的 1、2章將會出現一些輕鬆的內容讓讀者得到充分休息
<未完,待續>