1. 程式人生 > >轉:寫一個塊裝置驅動

轉:寫一個塊裝置驅動

----------------------- Page 1-----------------------

第 1章

+---------------------------------------------------+
|                 寫一個塊裝置驅動                   |
+---------------------------------------------------+
| 作者:趙磊                                         |
| 網名:OstrichFly、飛翔的鴕鳥                       |
| email: [email protected]

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

同樣是讀書,讀小說可以行雲流水,讀完後心情舒暢,意猶未盡;讀電腦書卻舉步艱難,讀完後目光呆

滯,也是意猶未盡,只不過未盡的是痛苦的回憶

研究證明,痛苦的記憶比快樂的更難忘記,因此電腦書中的內容比小說記得持久

而這套教程的目的是要打破這種狀況,以至於讀者在忘記小說內容忘記本文

在這套教程中,我們通過寫一個建立在記憶體中的塊裝置驅動,來學習 linux核心和相關裝置驅動知識

選擇寫塊裝置驅動的原因是:

1 :容易上手
2 :可以牽連出更多的核心知識
3 :像本文這樣的塊裝置驅動教程不多,所以需要一個

好吧,扯淡到此結束,我們開始寫了

本章的目的用盡可能最簡單的方法寫出一個能用的塊裝置驅動

所謂的能用,是指我們可以對這個驅動生成的塊裝置進行 mkfs ,mount和讀寫檔案
為了儘可能簡單,這個驅動的規模不是 1               行,也不是 5     行,而是 1     行以內

這裡插一句,我們不打算在這裡介紹如何寫模組,理由是介紹的文章已經滿天飛舞了

如果你能看得懂、並且成功地編譯、運行了這段程式碼,我們認為你已經達到了本教程的入學資格,

當然,如果你不幸的卡在這段程式碼中,那麼請等到搞定它以後再往下看:

mod.c:
#include <linux/module.h>

static int __init init_base(void)
{
        printk("----Hello. World----/n");
        return 0;

----------------------- Page 2-----------------------

}

static void __exit exit_base(void)
{
        printk("----Bye----/n");
}

module_init(init_base);
module_exit(exit_base);

MODULE_LICENSE ("GPL");
MODULE_AUTHOR("Zhao Lei");
MODULE_DESCRIPTION("For test");

Makefile:
obj-m := mod.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
        $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
clean:
        $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean
        rm -rf Module.markers modules.order Module.symvers

好了,這裡我們假定你已經搞定上面的最簡單的模組了,懂得什麼是看模組,以及簡單模組的編寫、編

譯、載入和解除安裝

還有就是,什麼是塊裝置,什麼是塊裝置驅動,這個也請自行 google吧,因為我們已經迫不及待要寫完

程式下課

為了建立一個可用的塊裝置,我們需要做 ......1件事情:
1 :用add_disk()函式向系統中新增這個塊裝置

   新增一個全域性的

   static struct gendisk *simp_blkdev_disk;

   然後申明模組的入口和出口:

   module_init(simp_blkdev_init);
   module_exit(simp_blkdev_exit);

   然後在入口處新增這個裝置、出口處私房這個裝置:

   static int __init simp_blkdev_init(void)
    {
           add_disk(simp_blkdev_disk);
        return 0;

----------------------- Page 3-----------------------

   }
   static void __exit simp_blkdev_exit(void)
    {
           del_gendisk(simp_blkdev_disk);
   }

當然,在新增裝置之前我們需要申請這個裝置的資源,這用到了alloc_disk()函式,因此模組入口函
數simp_blkdev_init(void)應該是:
   static int __init simp_blkdev_init(void)
    {
        simp_blkdev_disk = alloc_disk(1);
        if (!simp_blkdev_disk) {
                ret = -ENOMEM;
                goto err_alloc_disk;
        }

           add_disk(simp_blkdev_disk);

        return 0;

   err_alloc_disk:
        return ret;
   }

還有別忘了在解除安裝模組的程式碼中也加一個行清理函式:

  put_disk(simp_blkdev_disk);

還有就是,裝置有關的屬性也是需要設定的,因此在 alloc_disk()和 add_disk()之間我們需要:
        strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
        simp_blkdev_disk->major = ?1;
        simp_blkdev_disk->first_minor = 0;
        simp_blkdev_disk->fops = ?2;
        simp_blkdev_disk->queue = ?3;
        set_capacity(simp_blkdev_disk, ?4);

SIMP_BLKDEV_DISKNAME其實是這個塊裝置的名稱,為了紳士一些,我們把它定義成巨集了:
#define SIMP_BLKDEV_DISKNAME        "simp_blkdev"

這裡又引出了 4個問號 (天哪,是不是有種受騙的感覺,像是陪老婆去做頭髮)
第 1個問號:

  每個裝置需要對應的主、從驅動號

  我們的裝置當然也需要,但很明顯我不是腦科醫生,因此跟寫 linux的那幫瘋子不熟,得不到預先為我

----------------------- Page 4-----------------------

保留的裝置號

  還有一種方法是使用動態分配的裝置號,但在這一章中我們希望儘可能做得簡單,因此也不採用這種方

  那麼我們採用的是:搶別人的裝置號

  我們手頭沒有 AK47 ,因此不敢幹的太轟轟烈烈,而偷偷摸摸的事情倒是可以考慮的
  柿子要撿軟的捏,而我們試圖找出一個不怎麼用得上的裝置,然後搶他的 ID
  開啟 linux/include/linux/major.h ,把所有的裝置一個個看下來,我們覺得最勝任被搶裝置號的
傢伙非COMPAQ_SMART2_XXX莫屬

  第一因為它不強勢,基本不會被用到,因此也不會造成衝突;第二因為它有錢,從

COMPAQ_SMART2_MAJOR到 COMPAQ_SMART2_MAJOR7有那 8個之多的裝置號可以被搶,不過癮的話還
有它妹妹:COMPAQ_CISS_MAJOR~COMPAQ_CISS_MAJOR7

  為了讓搶劫顯得紳士一些,我們在外面             定義一個巨集:

  #define SIMP_BLKDEV_DEVICEMAJOR        COMPAQ_SMART2_MAJOR
  然後在?1的位置填上 SIMP_BLKDEV_DEVICEMAJOR
第 2個問號:
  gendisk結構需要設定fops指標,雖然我們用不到,但該設還是要設的

  好吧,就設個空得給它:

  在全域性部分新增:

  struct block_device_operations simp_blkdev_fops = {
           .owner                = THIS_MODULE,
  };
  然後把?2的位置填上&simp_blkdev_fops
第 3個問號:

  這個比較麻煩一些

  首先介紹請求佇列的概念。對大多數塊裝置來說,系統會把對塊裝置的訪問需求用 bio和 bio_vec表

示,然後提交給通用塊層

  通用塊層為了減少塊裝置在尋道時損失的時間,使用 I/O排程器對這些訪問需求進行排序,以儘可能提

高塊裝置效率

  關於 I/O排程器在本章中不打算進行深入的講解,但我們必須知道的是:
  1 :I/O排程器把排序後的訪問需求通過 request_queue結構傳遞給塊裝置驅動程式處理
  2 :我們的驅動程式需要設定一個 request_queue結構
  申請 request_queue結構的函式是 blk_init_queue() ,而呼叫 blk_init_queue()函式時需要傳

入一個函式的地址,這個函式擔負著處理對塊裝置資料的請求

  因此我們需要做的就是:

  1 :實現一個 static void simp_blkdev_do_request(struct request_queue *q)函式
  2 :加入一個全域性變數,指向塊裝置需要的請求佇列:
     static struct request_queue *simp_blkdev_queue;
  3 :在載入模組時用 simp_blkdev_do_request()函式的地址作引數呼叫 blk_init_queue()初始化

一個請求佇列:

     simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
     if (!simp_blkdev_queue) {

----------------------- Page 5-----------------------

             ret = -ENOMEM;
             goto err_init_queue;
     }
  4 :解除安裝模組時把simp_blkdev_queue還回去:
     blk_cleanup_queue(simp_blkdev_queue);
  5 :在?3的位置填上 simp_blkdev_queue
第 4個問號:

  這個還好,比前面的簡單多了,這裡需要設定塊裝置的大小

  塊裝置的大小使用扇區作為單位設定,而扇區的大小預設是 512位元組
  當然,在把位元組為單位的大小轉換為以扇區為單位時,我們需要除以 512 ,或者右移9位可能更快一些

  同樣,我們試圖把這一步也做得紳士一些,因此使用巨集定義了塊裝置的大小,目前我們定為 16M :
  #define SIMP_BLKDEV_BYTES        (16*1024*1024)
  然後在?4的位置填上 SIMP_BLKDEV_BYTES>>9

看到這裡,是不是有種身陷茫茫大海的無助感?並且一波未平,一波 起,在搞定這 4個問號的同時,
居然又引入了 simp_blkdev_do_request函式!
當然,如果在身陷茫茫波濤中時你認為到處都是海,因此絕望,那麼恭喜你可以不必捱到 65歲再退休;

反之,如果你認為到處都是沒有三聚氰胺鮮魚,並且隨便哪個方向都是岸時,那麼也恭喜你,你可以活

著回來繼續享受身為納稅人的榮譽

為了理清思路,我們把目前為止涉及到的程式碼整理出來:

#define SIMP_BLKDEV_DEVICEMAJOR        COMPAQ_SMART2_MAJOR
#define SIMP_BLKDEV_DISKNAME        "simp_blkdev"
#define SIMP_BLKDEV_BYTES        (16*1024*1024)

static struct request_queue *simp_blkdev_queue;
static struct gendisk *simp_blkdev_disk;

static void simp_blkdev_do_request(struct request_queue *q);

struct block_device_operations simp_blkdev_fops = {
         .owner                = THIS_MODULE,
};

static int __init simp_blkdev_init(void)
{
        int ret;

        simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
        if (!simp_blkdev_queue) {
                ret = -ENOMEM;

----------------------- Page 6-----------------------

                goto err_init_queue;
        }

        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_init_queue:
        return ret;
}

static void __exit simp_blkdev_exit(void)
{
        del_gendisk(simp_blkdev_disk);
        put_disk(simp_blkdev_disk);
        blk_cleanup_queue(simp_blkdev_queue);
}

module_init(simp_blkdev_init);
module_exit(simp_blkdev_exit);

剩下部分的不多了,真的不多了。請相信我,因為我不在質監局上班

我寫的文章誠實可靠,並且不拿你納稅的錢

我們還有一個最重要的函式需要實現,就是負責處理塊裝置請求的 simp_blkdev_do_request()

首先我們看看究竟把塊裝置的資料以什麼方式放在記憶體中

畢竟這是在第 1章,因此我們將使用最 simple的方式實現,也就是,陣列

----------------------- Page 7-----------------------

我們在全域性程式碼中定義:

unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
對驅動程式來說,這個陣列看起來大了一些,如果不幸被懂行的人看到,將1  %遭到最無情、最嚴重的

鄙視

而我們卻從極少數公僕那裡學到了最有效的應對之策,那就是:無視他,然後把他定為成“不明真相的群

眾”

然後我們著手實現simp_blkdev_do_request
這裡介紹 elv_next_request()函式,原型是:
struct request *elv_next_request(struct request_queue *q);
用來從一個請求佇列中拿出一條請求(其實嚴格來說,拿出的可能是請求中的一段)
隨後的處理請求本質上是根據rq_data_dir(req)返回的該請求的方向(讀/寫) ,把塊裝置中的資料裝
入 req->buffer、或是把req->buffer中的資料寫入塊裝置
剛才已經提及了與 request結構相關的 rq_data_dir()巨集和 .buffer成員,其他幾個相關的結構成員

和函式是:

request.sector :請求的開始磁軌
request.current_nr_sectors :請求磁軌數
end_request() :結束一個請求,第2個引數表示請求處理結果,成功時設定為 1 ,失敗時設定為                                          或

者錯誤號

因此我們的 simp_blkdev_do_request()函式為:
static void simp_blkdev_do_request(struct request_queue *q)
{
        struct request *req;
        while ((req = elv_next_request(q)) != NULL) {
                if ((req->sector + req->current_nr_sectors) << 9
                        > SIMP_BLKDEV_BYTES) {
                        printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                 ": bad request: block=%llu, count=%u/n",
                                 (unsigned long long)req->sector,
                                req->current_nr_sectors);
                        end_request(req, 0);
                        continue;
                }

                switch (rq_data_dir(req)) {
                case READ:
                        memcpy(req->buffer,
                                simp_blkdev_data + (req->sector << 9),
                                req->current_nr_sectors << 9);
                        end_request(req, 1);
                        break;

----------------------- Page 8-----------------------

                case WRITE:
                        memcpy(simp_blkdev_data + (req->sector << 9),
                                req->buffer, req->current_nr_sectors << 9);
                        end_request(req, 1);
                        break;
                default:
                        /* No default because rq_data_dir(req) is 1 bit */
                        break;
                }
        }
}
函式使用 elv_next_request()遍歷struct request_queue *q中使用 struct request *req表

示的每一段,首先判斷這個請求是否超過了我們的塊裝置的最大容量,

然後根據請求的方向rq_data_dir(req)進行相應的請求處理。由於我們使用的是指簡單的陣列,因此
請求處理僅僅是 2條memcpy
memcpy 中也牽涉到了扇區號到線性地址的轉換操作,我想對堅持到這裡的讀者來說,這個操作應該不需

要進一步解釋了

編碼到此結束,然後我們試試這個程式:

首先編譯:

# make
make -C /lib/modules/2.6.18-53.el5/build 
SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step1 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
#

載入模組

# insmod simp_blkdev.ko
#
用 lsmod看看
這裡我們注意到,該模組的 Used by為                   ,因為它既沒有被其他模組使用,也沒有被mount
# lsmod
Module                  Size  Used by
simp_blkdev         16784  8  
...
#
如果當前系統支援 udev ,在呼叫 add_disk()函式時即插即用機制會自動為我們在/dev/目錄下建立設

----------------------- Page 9-----------------------

備檔案

裝置檔案的名稱為我們在 gendisk.disk_name中設定的 simp_blkdev ,主、從裝置號也是我們在程式
中設定的 72和
如果當前系統不支援 udev ,那麼很不幸,你需要自己用 mknod /dev/simp_blkdev  b 72  來建立

裝置檔案了

# ls -l /dev/simp_blkdev
brw-r----- 1 root disk 72, 0 11-10 18:13 /dev/simp_blkdev
#
在塊裝置中建立檔案系統,這裡我們建立常用的 ext3

當然,作為通用的塊裝置,建立其他型別的檔案系統也沒問題

# mkfs.ext3 /dev/simp_blkdev
mke2fs 1.39 (29-May-2  6)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
4096 inodes, 16384 blocks
819 blocks (5.  %) reserved for the super user
First data block=1
Maximum filesystem blocks=16777216
2 block groups
8192 blocks per group, 8192 fragments per group
2048 inodes per group
Superblock backups stored on blocks:
        8193

Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done

This filesystem will be automatically checked every 38 mounts or
180 days, whichever comes first.  Use tune2fs -c or -i to override.
#
如果這是第一次使用,建議建立一個目錄用來 mount這個裝置中的檔案系統
當然,這不是必需的。如果你對mount之類的用法很熟,你完全能夠自己決定在這裡幹什麼,甚至把這
個裝置 mount成 root
# mkdir -p /mnt/temp1
#
把建立好檔案系統的塊裝置 mount到剛才建立的目錄中
# mount /dev/simp_blkdev /mnt/temp1
#
看看現在的 mount表
# mount

----------------------- Page 10-----------------------

...
/dev/simp_blkdev on /mnt/temp1 type ext3 (rw)
#
看看現在的模組引用計數,從剛才的                變成 1了,
原因是我們 mount了
# lsmod
Module                  Size  Used by
simp_blkdev         16784  8  1
...
#
看看檔案系統的內容,有個 mkfs時自動建立的lost+found目錄
# ls /mnt/temp1
lost+found
#

隨便拷點東西進去

# cp /etc/init.d/* /mnt/temp1
#

再看看

# ls /mnt/temp1
acpid           conman              functions  irqbalance    mdmpd 
NetworkManagerDispatcher  rdisc            sendmail        winbind
anacron         cpuspeed            gpm        kdump         messagebus      nfs 
readahead_early  setroubleshoot  wpa_supplicant
apmd            crond               haldaemon  killall       microcode_ctl 
nfslock                   readahead_later  single          xfs
atd             cups                halt       krb524        multipathd 
nscd                      restorecond      smartd          xinetd
auditd          cups-config-daemon  hidd       kudzu         netconsole 
ntpd                      rhnsd            smb             ypbind
autofs          dhcdbd              ip6tables  lost+found    netfs 
pand                      rpcgssd          sshd            yum-updatesd
avahi-daemon    dund                ipmi       lvm2-monitor  netplugd 
pcscd                     rpcidmapd        syslog
avahi-dnsconfd  firstboot           iptables   mcstrans      network 
portmap                   rpcsvcgssd       vmware
bluetooth       frecord             irda       mdmonitor     NetworkManager 
psacct                    saslauthd        vncserver
#

現在這個塊裝置的使用情況是

# df
檔案系統               1K-塊        已用     可用 已用% 掛載點
...
/dev/simp_blkdev         15863      1440     13604  10% /mnt/temp1
#

----------------------- Page 11-----------------------

再全刪了玩玩

# rm -rf /mnt/temp1/*
#

看看刪完了沒有

# ls /mnt/temp1
#
好了,大概玩夠了,我們把檔案系統umount掉
# umount /mnt/temp1
#

模組的引用計數應該還原成          了吧

# lsmod
Module                  Size  Used by
simp_blkdev         16784  8  
...
#

最後一步,移除模組

# rmmod simp_blkdev
#

這是這部教程的第 1章,不好意思的是,內容比預期還是難了一些

當初還有一種考慮是在本章中僅僅實現一個寫了就丟的塊裝置驅動,也就是說,對這個塊裝置的操作只

能到 mkfs這一部,而不能繼續mount ,因為剛才寫的資料全被扔了
或者更簡單些,僅僅寫一個 hello world的模組

但最後還是寫成了現在這樣沒,因為我覺得拿出一個真正可用的塊裝置驅動程式對讀者來說更有成就感

無論如何,本章是一個開始,而你,已經跨入了學習塊裝置驅動教室的大門,或者通俗來說,上了賊船

而在後續的章節中,我們將陸續完善對這個程式,通過追加或者強化這個程式,來學習與塊裝置有關、

或與塊裝置無關但與 linux有關的方方面面
總之,我希望通過這部教程,起碼讓讀者學到有用的知識,或者更進一步,引導讀者對 linux的興趣,

甚至領悟學習一切科學所需要的鑽研精神

作為第一章的結尾,引用我在另一篇文章中的序言:

謹以此文向讀者示範什麼叫做嚴謹的研究

呼喚踏實的治學態度,反對浮躁的論壇風氣

--OstrichFly

<未完,待續>

================================================================================

----------------------- Page 12-----------------------

================================================================================
====================================================================

第 2章

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

上一章不但實現了一個最簡單的塊裝置驅動程式,而且可能也成功地嚇退了不少準備繼續看下去的讀者

因為第一章看起來好像太難了

不過讀者也不要過於埋怨作者,因為大多數情況下第一次都不是什麼好的體驗......

對於堅持到這裡的讀者,這一章中,我們準備了一些簡單的內容來犒勞大家

關於塊裝置與 I/O排程器的關係,我們在上一章中已經有所提及
I/O排程器可以通過合併請求、重排塊裝置操作順序等方式提高塊裝置訪問的順序

就好像吃街邊的大排檔,如果點一個冷門的品種,可能會等更長的時間,

而如果點的恰好與旁邊桌子上剛點的相同,那麼會很快上來,因為廚師八成索性一起炒了

然而 I/O排程器和塊裝置的情況卻有一些微妙的區別,大概可以類比成人家點了個西紅柿雞蛋湯你接著

就點了個西紅柿炒蛋

聰明的廚師一定會先做你的菜,因為隨後可以直接往鍋里加水煮湯,可憐比你先來的人喝的卻是你的刷

鍋水

兩個菜一鍋煮表現在塊裝置上可以類比成先後訪問塊裝置的同一個位置,這倒是與 I/O排程器無關,有
空學習 linux快取策略時可以想想這種情況

一個女孩子換了好多件衣服問我漂不漂亮,而我的評價只要一眼就能拿出來

對方總覺得衣服要牌子好、面料好、搭配合理、要符合個人的氣質、要有文化,而我的標準卻簡單的多 :

越薄越好

所謂臭氣相投,我寫的塊裝置驅動程式對I/O排程器的要求大概也是如此

究其原因倒不是因為塊裝置驅動程式好色,而是這個所謂塊裝置中的資料都是在記憶體中的

這也意味著我們的“塊裝置”讀寫迅速、並且不存在磁碟之類裝置通常面臨的尋道時間

因此對這個“塊裝置”而言,一個複雜的 I/O排程器不但發揮不了絲毫作用,反而其本身將白白耗掉不少內

----------------------- Page 13-----------------------

存和 CPU
同樣的情況還出現在固態硬碟、U盤、記憶棒之類驅動中                  將來固態硬碟流行之時,大概就是 I/O排程

器消亡之日了

這裡我們試圖給我們的塊裝置驅動選擇一個最簡單的 I/O排程器
目前linux中包含anticipatory、cfq、deadline和 noop這 4個 I/O排程器
2.6.18之前的 linux預設使用 anticipatory ,而之後的預設使用 cfq
關於這 4個排程器的原理和特性我們不打算在這裡介紹,原因是相關的介紹滿網都是
但我們還是不能避免在這裡提及一下 noop排程器,因為我們馬上要用到它
noop顧名思義,是一個基本上不幹事的排程器。它基本不對請求進行什麼附加的處理,僅僅假惺惺地告

訴通用塊裝置層:我處理完了

但與吃空餉的公僕不同,noop的存在還是有不少進步意義的。至少我們現在就需要一個不要沒事添亂的
I/O排程器

選擇一個指定的 I/O排程器需要這個函式:
int elevator_init(struct request_queue *q, char *name);
q是請求佇列的指標,name是需要設定的 I/O排程器的名稱
如果 name為 NULL ,那麼核心會首先嚐試選擇啟動引數"elevator="中指定的排程器,

不成功的話就去選擇編譯核心時指定的預設排程器,

如果運氣太背還是不成功,就去選擇 "noop"排程器
不要問我怎麼知道的,一切皆在 RTFSC(Read the F**ing Source Code --Linus Torvalds)

對於我們的程式碼,就是在 simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, 
NULL)後面加上:
elevator_init(simp_blkdev_queue, "noop");

但問題是在 blk_init_queue()函式中系統已經幫我們申請一個了,因此這裡我們需要費點周折,把老

的那個送回去

所以我們的程式碼應該是:

simp_blkdev_init()函式開頭處:
elevator_t *old_e;
blk_init_queue()函式之後:
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);

為方便閱讀並提高本文在 google磁碟中的佔用率,我們給出修改後的整個 simp_blkdev_init()函式:

static int __init simp_blkdev_init(void)
{

----------------------- Page 14-----------------------

        int ret;
        elevator_t *old_e;

        simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
        if (!simp_blkdev_queue) {
                ret = -ENOMEM;
                goto err_init_queue;
        }

        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);

        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_init_queue:
        return ret;
}

本章的改動很小,我們現在測試一下這段程式碼:

首先我們像原先那樣編譯模組並載入:

# make
make -C /lib/modules/2.6.18-53.el5/build 
SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step2 modules

----------------------- Page 15-----------------------

make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step2/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step2/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step2/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#
然後看一看咱們的這個塊裝置現在使用的 I/O排程器:
# cat /sys/block/simp_blkdev/queue/scheduler
[noop] anticipatory deadline cfq
#

看樣子是成功了

哦,上一章中忘了看老程式的排程器資訊了,這裡補上老程式的情況:

# cat /sys/block/simp_blkdev/queue/scheduler
noop anticipatory deadline [cfq]
#

OK ,我們完成簡單的一章,並且用事實說明了作者並沒有在開頭撒謊

當然,作者也會力圖讓接下來的章節同樣比小說易讀

<未完,待續>

================================================================================
================================================================================
====================================================================

第 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章將會出現一些輕鬆的內容讓讀者得到充分休息

<未完,待續>

================================================================================
================================================================================
====================================================================

----------------------- Page 25-----------------------

第 4章

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

上一章結束時說過,本章會準備一些不需要動腦子的內容,現在我們開始履行諾言

看上去簡單的事情實際上往往會被弄得很複雜,比如取消公僕們的招待費用問題;

看上去複雜的事情真正做起來也可能很簡單,比如本章中要讓我們的塊裝置支援分割槽操作

談到分割槽,不懂電腦的人想到了去找“專家”幫忙;電腦入門者想到了“高手”這個名詞;

漸入佳境者想到了 fdisk ;資深級玩家想到了 dm ;紅點玩家想到了隱藏的系統恢復區;
程式設計師想到了分割槽表;病毒製造者想到了把分割槽表清空......

作為塊裝置驅動程式的設計者,我們似乎需要想的比他們更多一些,

我們大概需要在驅動程式開始識別塊裝置時訪問裝置上的分割槽表,讀出裡面的資料進行分析,

找出這個塊裝置中包含哪一類的分割槽(奇怪吧,但真相是分割槽表確實有很多種,只是我們經常遇到的大概
只有 ibm型別罷了)、

幾個分割槽,每個分割槽在塊裝置上的區域等資訊,再在驅動程式中對每個分割槽進行註冊、建立其管理信

息 ......

讀到這裡,正在繫鞋帶準備溜之大吉的同學們請稍等片刻聽我說完,

雖然實際上作者也鼓勵同學們多作嘗試,甚至是這種無謂的嘗試,但本章中的做法卻比上述的內容簡單

得多

因為這一回 linux居然幫了我們的忙,並且不是I/O排程器的那種倒忙

開啟 linux程式碼,我們會在 fs/partitions/目錄中發現一些檔案,這些友好的檔案將會默默無聞地幫

我們的大忙

而我們需要做的居然如此簡單,還記得 alloc_disk()函式嗎?
我們一直用 1作引數來呼叫它的,但現在,我們換成 64 ,這意味著設定塊裝置最大支援 63個分割槽
然後 ......不要問然後,因為已經做完了

當然,如果要讓程式碼看起來漂亮一些的話,我們可以考慮用一個巨集來定義最大分割槽數

----------------------- Page 26-----------------------

也就是,在檔案的頭部增加:

/* usable partitions is SIMP_BLKDEV_MAXPARTITIONS - 1 */
#define SIMP_BLKDEV_MAXPARTITIONS      (64)

然後把

simp_blkdev_disk = alloc_disk(1);

改成

simp_blkdev_disk = alloc_disk(SIMP_BLKDEV_MAXPARTITIONS);

好了,真的改好了

上一章那樣改得太多看起來會讓讀者不爽,那麼這裡改得太少,是不是也同樣不爽?

大概有關部門深信老百姓接受不了有害物質含量過少的食品,因此制定了食品中三聚氰胺含量的標準

於是,今後我們大概會制定出一系列標準,比如插入多深才能叫強姦什麼的

為了達到所謂的標準,我們破例補充介紹一下 alloc_disk()函式:

這個函式的原型為:

struct gendisk *alloc_disk(int minors);
用於申請一個 gendisk結構,並做好一些初始化工作
minors用於指定這個裝置使用的次裝置號數量,因為第一個次裝置號已經用於表示整個塊裝置了,
因此餘下的 minors-1個裝置號用於表示塊裝置中的分割槽,這就限制了這個塊裝置中的最大可訪問分割槽

我們注意“最大可訪問分割槽數”這個詞:

“最大”雖然指的是上限,但並不意味這是唯一的上限

極端情況下如果這個塊裝置只有 2個磁軌,那麼無論minors多大,塊裝置本身充其量也只能建立 2個分

這時再談minors值能到達多少簡直就是扯淡,就像腐敗不根除,建多少經濟適用房都是白搭一樣

“可訪問”指的是通過驅動程式可以訪問的分割槽數量,這是因為我們只有那麼多次裝置號

但這個數字並不妨礙使用者在塊裝置上面建多少個區。比如我們把minors設定為 4 ,那麼最大可訪問的分
區數量是 3 ,
足夠變態的使用者完全可以在塊裝置上建立幾十個分割槽,只不過結果是隻能使用前3個分割槽而已

現在我們可以試試這個程式了

與以往相同的是,我們編譯和載入這個模組:

# make
make -C /lib/modules/2.6.18-53.el5/build 
SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step04 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.ko

----------------------- Page 27-----------------------

make[1]: Leaving directory `/usr/src/kernels/2.6.1