1. 程式人生 > >LINUX塊裝置驅動3

LINUX塊裝置驅動3

+---------------------------------------------------+
|                 寫一個塊裝置驅動                  |
+---------------------------------------------------+
| 作者:趙磊                                        |
| email: [email protected]                       |
+---------------------------------------------------+
| 文章版權歸原作者所有。                            |
| 大家可以自由轉載這篇文章,但原版權資訊必須保留。  |
| 如需用於商業用途,請務必與原作者聯絡,若因未取得  |
| 授權而收起的版權爭議,由侵權者自行負責。          |
+---------------------------------------------------+

上 一章中我們討論了mm的衣服問題,併成功地為她換上了一件輕如鴻毛、關鍵是薄如蟬翼的新衣服。
而這一章中,我們打算稍稍再前進一步,也就是:給她 脫光。
目的是更加符合我們的審美觀、並且能夠更加深入地瞭解該mm(喜歡制服皮草的讀者除外)。
付出的代價是這一章的內容要稍稍複雜一 些。

雖然noop排程器確實已經很簡單了,簡單到比我們的驅動程式還簡單,在2.6.27中的120行程式碼量已經充分說明了這個問題。
但 顯而易見的是,不管它多簡單,只要它存在,我們就把它看成累贅。
這裡我們不打算再次去反覆磨嘴皮子論證不使用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)這麼一行。
偷 懶省略掉解開謎題的所有關鍵步驟後,這裡可以得出一個作者相信但讀者不一定相信的正確結論:
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對應的記憶體地址是否分成幾段的問題
這些好處看起來都很自然,正如某些行政不作為的“有關部門” 認為老百姓納稅養他們也自然,
但不久我們就會看到不很自然的情況。

如果讀者是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;
                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出的一大堆文章撞衫。
這裡我們只說一 句話:
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意味著預讀,精心設計的預讀請求可以提高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);
   #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指標,第二個引數成功時為0,失敗時 為-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)) {
                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>,
就得到了本章的最終程式碼。

在結束本章之前,我們還是試驗一下:
首先還 是編譯和載入:
# 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章將會出現一些輕鬆的內容讓讀者得到充分休息。

<未 完,待續>