Linux驅動開發--寫一個塊設備驅動

原文地址:[原創] 寫一個塊設備驅動
http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=2017377&fromuid=28801784

第1章

+-------------------------------------------------------------------------+
|                 寫一個塊設備驅動                                                  |
+-------------------------------------------------------------------------+
| 作者:趙磊                                                                              |
| 網名:OstrichFly、飛翔的鴕鳥                                            |
| email: [email protected]                                          |
+-------------------------------------------------------------------------+
| 文章版權歸原作者所有。                                                      |
| 大家可以自由轉載這篇文章,但原版權信息必須保留。  |
| 如需用於商業用途,請務必與原作者聯繫,若因未取得  |
| 授權而收起的版權爭議,由侵權者自行負責。                  |
+-------------------------------------------------------------------------+

同樣是讀書,讀小說可以行雲流水,讀完後心情舒暢,意猶未盡;讀電腦書卻舉步艱難,讀完後目光呆滯,也是意猶未盡,只不過未盡的是痛苦的回憶。
研究證明,痛苦的記憶比快樂的更難忘記,因此電腦書中的內容比小說記得持久。
而這套教程的目的是要打破這種狀況,以至於讀者在忘記小說內容忘記本文。

在這套教程中,我們通過寫一個建立在內存中的塊設備驅動,來學習linux內核和相關設備驅動知識。
選擇寫塊設備驅動的原因是:
1:容易上手
2:可以牽連出更多的內核知識
3:像本文這樣的塊設備驅動教程不多,所以需要一個

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

本章的目的用儘可能最簡單的方法寫出一個能用的塊設備驅動。
所謂的能用,是指我們可以對這個驅動生成的塊設備進行mkfs,mount和讀寫文件。
爲了儘可能簡單,這個驅動的規模不是1000行,也不是500行,而是100行以內。

這裏插一句,我們不打算在這裏介紹如何寫模塊,理由是介紹的文章已經滿天飛舞了。
如果你能看得懂、並且成功地編譯、運行了這段代碼,我們認爲你已經達到了本教程的入學資格,
當然,如果你不幸的卡在這段代碼中,那麼請等到搞定它以後再往下看:
mod.c:
#include <linux/module.h>

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

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;
   }
   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的那幫瘋子不熟,得不到預先爲我保留的設備號。
  還有一種方法是使用動態分配的設備號,但在這一章中我們希望儘可能做得簡單,因此也不採用這種方法。
  那麼我們採用的是:搶別人的設備號。
  我們手頭沒有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) {
             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;
                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的方式實現,也就是,數組。
我們在全局代碼中定義:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
對驅動程序來說,這個數組看起來大了一些,如果不幸被懂行的人看到,將100%遭到最無情、最嚴重的鄙視。
而我們卻從極少數公僕那裏學到了最有效的應對之策,那就是:無視他,然後把他定爲成“不明真相的羣衆”。

然後我們着手實現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,失敗時設置爲0或者錯誤號。
因此我們的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;
                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爲0,因爲它既沒有被其他模塊使用,也沒有被mount。
# lsmod
Module                  Size  Used by
simp_blkdev         16784008  0
...
#
如果當前系統支持udev,在調用add_disk()函數時即插即用機制會自動爲我們在/dev/目錄下建立設備文件。
設備文件的名稱爲我們在gendisk.disk_name中設置的simp_blkdev,主、從設備號也是我們在程序中設定的72和0。
如果當前系統不支持udev,那麼很不幸,你需要自己用mknod /dev/simp_blkdev  b 72 0來創建設備文件了。
# 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-2006)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
4096 inodes, 16384 blocks
819 blocks (5.00%) 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
...
/dev/simp_blkdev on /mnt/temp1 type ext3 (rw)
#
看看現在的模塊引用計數,從剛纔的0變成1了,
原因是我們mount了。
# lsmod
Module                  Size  Used by
simp_blkdev         16784008  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
#
再全刪了玩玩
# rm -rf /mnt/temp1/*
#
看看刪完了沒有
# ls /mnt/temp1
#
好了,大概玩夠了,我們把文件系統umount掉
# umount /mnt/temp1
#
模塊的引用計數應該還原成0了吧
# lsmod
Module                  Size  Used by
simp_blkdev         16784008  0
...
#
最後一步,移除模塊
# rmmod simp_blkdev
#

這是這部教程的第1章,不好意思的是,內容比預期還是難了一些。
當初還有一種考慮是在本章中僅僅實現一個寫了就丟的塊設備驅動,也就是說,對這個塊設備的操作只能到mkfs這一部,而不能繼續mount,因爲剛纔寫的數據全被扔了。
或者更簡單些,僅僅寫一個hello world的模塊。
但最後還是寫成了現在這樣沒,因爲我覺得拿出一個真正可用的塊設備驅動程序對讀者來說更有成就感。

無論如何,本章是一個開始,而你,已經跨入了學習塊設備驅動教室的大門,或者通俗來說,上了賊船。
而在後續的章節中,我們將陸續完善對這個程序,通過追加或者強化這個程序,來學習與塊設備有關、或與塊設備無關但與linux有關的方方面面。
總之,我希望通過這部教程,起碼讓讀者學到有用的知識,或者更進一步,引導讀者對linux的興趣,甚至領悟學習一切科學所需要的鑽研精神。

作爲第一章的結尾,引用我在另一篇文章中的序言:
謹以此文向讀者示範什麼叫做嚴謹的研究。
呼喚踏實的治學態度,反對浮躁的論壇風氣。
--OstrichFly

<未完,待續>

第2章

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

上一章不但實現了一個最簡單的塊設備驅動程序,而且可能也成功地嚇退了不少準備繼續看下去的讀者。
因爲第一章看起來好像太難了。
不過讀者也不要過於埋怨作者,因爲大多數情況下第一次都不是什麼好的體驗......

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

關於塊設備與I/O調度器的關係,我們在上一章中已經有所提及。
I/O調度器可以通過合併請求、重排塊設備操作順序等方式提高塊設備訪問的順序。
就好像吃街邊的大排檔,如果點一個冷門的品種,可能會等更長的時間,
而如果點的恰好與旁邊桌子上剛點的相同,那麼會很快上來,因爲廚師八成索性一起炒了。
然而I/O調度器和塊設備的情況卻有一些微妙的區別,大概可以類比成人家點了個西紅柿雞蛋湯你接着就點了個西紅柿炒蛋。
聰明的廚師一定會先做你的菜,因爲隨後可以直接往鍋里加水煮湯,可憐比你先來的人喝的卻是你的刷鍋水。
兩個菜一鍋煮表現在塊設備上可以類比成先後訪問塊設備的同一個位置,這倒是與I/O調度器無關,有空學習linux緩存策略時可以想想這種情況。

一個女孩子換了好多件衣服問我漂不漂亮,而我的評價只要一眼就能拿出來。
對方總覺得衣服要牌子好、面料好、搭配合理、要符合個人的氣質、要有文化,而我的標準卻簡單的多:越薄越好。
所謂臭氣相投,我寫的塊設備驅動程序對I/O調度器的要求大概也是如此。
究其原因倒不是因爲塊設備驅動程序好色,而是這個所謂塊設備中的數據都是在內存中的。
這也意味着我們的“塊設備”讀寫迅速、並且不存在磁盤之類設備通常面臨的尋道時間。
因此對這個“塊設備”而言,一個複雜的I/O調度器不但發揮不了絲毫作用,反而其本身將白白耗掉不少內存和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)
{
        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
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]                      |
+---------------------------------------------------+
| 文章版權歸原作者所有。                            |
| 大家可以自由轉載這篇文章,但原版權信息必須保留。  |
| 如需用於商業用途,請務必與原作者聯繫,若因未取得  |
| 授權而收起的版權爭議,由侵權者自行負責。          |
+---------------------------------------------------+

上一章中我們討論了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章將會出現一些輕鬆的內容讓讀者得到充分休息。

<未完,待續>

第4章

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

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

看上去簡單的事情實際上往往會被弄得很複雜,比如取消公僕們的招待費用問題;
看上去複雜的事情真正做起來也可能很簡單,比如本章中要讓我們的塊設備支持分區操作。

談到分區,不懂電腦的人想到了去找“專家”幫忙;電腦入門者想到了“高手”這個名詞;
漸入佳境者想到了fdisk;資深級玩家想到了dm;紅點玩家想到了隱藏的系統恢復區;
程序員想到了分區表;病毒製造者想到了把分區表清空......

作爲塊設備驅動程序的設計者,我們似乎需要想的比他們更多一些,
我們大概需要在驅動程序開始識別塊設備時訪問設備上的分區表,讀出裏面的數據進行分析,
找出這個塊設備中包含哪一類的分區(奇怪吧,但真相是分區表確實有很多種,只是我們經常遇到的大概只有ibm類型罷了)、
幾個分區,每個分區在塊設備上的區域等信息,再在驅動程序中對每個分區進行註冊、創建其管理信息......
讀到這裏,正在繫鞋帶準備溜之大吉的同學們請稍等片刻聽我說完,
雖然實際上作者也鼓勵同學們多作嘗試,甚至是這種無謂的嘗試,但本章中的做法卻比上述的內容簡單得多。
因爲這一回linux居然幫了我們的忙,並且不是I/O調度器的那種倒忙。

打開linux代碼,我們會在fs/partitions/目錄中發現一些文件,這些友好的文件將會默默無聞地幫我們的大忙。

而我們需要做的居然如此簡單,還記得alloc_disk()函數嗎?
我們一直用1作參數來調用它的,但現在,我們換成64,這意味着設定塊設備最大支持63個分區。
然後......不要問然後,因爲已經做完了。
當然,如果要讓代碼看起來漂亮一些的話,我們可以考慮用一個宏來定義最大分區數。
也就是,在文件的頭部增加:
/* 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
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#
與以往不同的是,這一次加載完模塊後,我們並不直接在塊設備上創建文件系統,而是進行分區:
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won't be recoverable.

Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)

Command (m for help):

關於fdisk我們不打算在這裏介紹,因爲我們試圖讓這篇文檔看起來專家一些。
使用n命令創建第一個主分區:
Command (m for help): n
Command action
   e   extended
   p   primary partition (1-4)
p
Partition number (1-4): 1
First cylinder (1-2, default 1): 1
Last cylinder or +size or +sizeM or +sizeK (1-2, default 2): 1

Command (m for help):
如果細心一些的話,在這裏可以看出一個小麻煩,就是:這塊磁盤一共只有2個磁道。
因此,我們只好指定第一個分區僅佔用1個磁道。畢竟,還要爲第2個分區留一些空間。
然後建立第二個分區:
Command (m for help): n
Command action
   e   extended
   p   primary partition (1-4)
p
Partition number (1-4): 2
First cylinder (2-2, default 2): 2

Command (m for help):
這一步中由於只剩下1個磁道,fdisk便不再問我們Last cylinder,而是自作主張地把最後一個磁道分配給新的分區。
這時我們的分區情況是:
Command (m for help): p

Disk /dev/simp_blkdev: 16 MB, 16777216 bytes
255 heads, 63 sectors/track, 2 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes

           Device Boot      Start         End      Blocks   Id  System
/dev/simp_blkdev1               1           1        8001   83  Linux
/dev/simp_blkdev2               2           2        8032+  83  Linux

Command (m for help):
寫入分區,退出fdisk:
Command (m for help): w
The partition table has been altered!

Calling ioctl() to re-read partition table.
Syncing disks.
#

然後我們在這兩個分區中創建文件系統
# mkfs.ext3 /dev/simp_blkdev1
mke2fs 1.39 (29-May-2006)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
2000 inodes, 8000 blocks
400 blocks (5.00%) reserved for the super user
First data block=1
Maximum filesystem blocks=8388608
1 block group
8192 blocks per group, 8192 fragments per group
2000 inodes per group

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

This filesystem will be automatically checked every 27 mounts or
180 days, whichever comes first.  Use tune2fs -c or -i to override.
# mkfs.ext3 /dev/simp_blkdev2
mke2fs 1.39 (29-May-2006)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
2008 inodes, 8032 blocks
401 blocks (4.99%) reserved for the super user
First data block=1
Maximum filesystem blocks=8388608
1 block group
8192 blocks per group, 8192 fragments per group
2008 inodes per group

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

This filesystem will be automatically checked every 23 mounts or
180 days, whichever comes first.  Use tune2fs -c or -i to override.
#
然後mount設兩個設備:
# mount /dev/simp_blkdev1 /mnt/temp1
# mount /dev/simp_blkdev2 /mnt/temp2
#
看看結果:
# mount
/dev/hda1 on / type ext3 (rw)
proc on /proc type proc (rw)
sysfs on /sys type sysfs (rw)
devpts on /dev/pts type devpts (rw,gid=5,mode=620)
tmpfs on /dev/shm type tmpfs (rw)
none on /proc/sys/fs/binfmt_misc type binfmt_misc (rw)
/dev/simp_blkdev1 on /mnt/temp1 type ext3 (rw)
/dev/simp_blkdev2 on /mnt/temp2 type ext3 (rw)
#
然後讀/寫:
# cp /etc/init.d/* /mnt/temp1/
# cp /etc/passwd /mnt/temp2
# ls /mnt/temp1/
NetworkManager            avahi-dnsconfd      dund       ipmi        lost+found     netfs     portmap          rpcsvcgssd      vncserver
NetworkManagerDispatcher  bluetooth           firstboot  iptables    lvm2-monitor   netplugd  psacct           saslauthd       winbind
acpid                     capi                functions  irda        mcstrans       network   rdisc            sendmail        wpa_supplicant
anacron                   conman              gpm        irqbalance  mdmonitor      nfs       readahead_early  setroubleshoot  xfs
apmd                      cpuspeed            haldaemon  isdn        mdmpd          nfslock   readahead_later  single          ypbind
atd                       crond               halt       kdump       messagebus     nscd      restorecond      smartd          yum-updatesd
auditd                    cups                hidd       killall     microcode_ctl  ntpd      rhnsd            sshd
autofs                    cups-config-daemon  hplip      krb524      multipathd     pand      rpcgssd          syslog
avahi-daemon              dhcdbd              ip6tables  kudzu       netconsole     pcscd     rpcidmapd        vmware-tools
# ls /mnt/temp2
lost+found  passwd
#
收尾工作:
# umount /dev/temp1
# umount /dev/temp2
# rmmod simp_blkdev
#

看起來本章應該結束了,但爲了耽誤大家更多的時間,我們來回憶一下剛纔出現的小麻煩。
我們發現這塊磁盤只有2個磁道,由於分區是以磁道爲邊界的,因此最大隻能創建2個分區。
不過謝天謝地,好歹我們能夠證明我們的程序是支持“多個”分區的......儘管只有2個。

那麼爲什麼系統會認爲我們的塊設備只有2個磁道呢?其實這不怪系統,因爲我們根本沒有告訴系統我們的磁盤究竟有多少個磁道。
因此係統只好去猜、猜、猜,結果就猜成2個磁道了。
好吧,說的細節一些,傳統的磁盤使用8個位表示盤面數、6個位表示每磁道扇區數、10個位表示磁道數,因此盤面、每磁道扇區、磁道的最大數值分別爲255、63和1023。
這也是傳說中啓動操作系統時的1024柱面(磁道)和硬盤容量8G限制的根源。
現代磁盤採用線性尋址方式突破了這一限制,從本質上說,如果你的機器還沒生鏽,那麼你的硬盤無論是內部結構還是訪問方式都與常識中的盤面、每磁道扇區、磁道無關。
但爲了與原先的理解兼容,對於現代磁盤,我們在訪問時還是假設它具有傳統的結構。目前比較通用的假設是:所有磁盤具有最大數目的(也就是恆定的)盤面和每磁道扇區數,而磁盤大小與磁道數與成正比。
因此,對於一塊80G的硬盤,根據假設,這塊磁盤的盤面和每磁道扇區數肯定是255和63,磁道數爲:80*1024*1024*1024/512(字節每扇區)/255(盤面數)/63(每磁道扇區數)=10043(小數部分看作不完整的磁道被丟棄)。
話歸原題,在驅動程序中我們指定了磁盤大小爲16M,共包含16*1024*1024/512=32768個扇區。假設這塊磁盤具有最大盤面和每磁道扇區數後,它的磁道數就是:32768/255/63=2。

我們看起開應該很happy,因爲系統太看得起我們了,竟然把我們的塊設備看成現代磁盤進行磁道數的換算處理。
不過我們也可能unhappy,因爲這造成磁盤最大隻能被分成2個區。(至於爲什麼分區以磁道作爲邊界,可以想象一下磁盤的結構)
但我們的磁盤只有區區16M啊,所以最好還是告訴系統我們的磁盤沒有那麼多的盤面數和每磁道扇區數,這將讓磁道數來得多一些。

在下一章中,我們打算搞定這個問題。

<未完,待續>

第5章

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

既然上一章結束時我們已經預告了本章的內容,
那麼本章中我們就讓這個塊設備有能力告知操作系統它的“物理結構”。

當然,對於基於內存的塊設備來說,什麼樣的物理結構並不重要,
這就如同從酒吧帶mm回家時不需要打聽她的姓名一樣。
但如果不幸遇到的是兼職,並且帶她去不入流的招待所時,
建議最好還是先串供一下姓名、生日和職業等信息,
以便JJ查房時可以僞裝成情侶。
同樣,如果要實現的是真實的物理塊設備驅動,
那麼返回設備的物理結構時大概不能這麼隨意。

對於塊設備驅動程序而言,我們現在需要關注那條目前只有一行的struct block_device_operations simp_blkdev_fops結構。
到目前爲止,它存在的目的僅僅是因爲它必須存在,但馬上我們將發現它存在的另一個目的:爲塊設備驅動添加獲得塊設備物理結構的接口。

對於具有極強鑽研精神的極品讀者來說,大概在第一章中就會自己去看struct block_device_operations結構,然後將發現這個結構其實還挺複雜:
struct block_device_operations {
        int (*open) (struct block_device *, fmode_t);
        int (*release) (struct gendisk *, fmode_t);
        int (*locked_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*direct_access) (struct block_device *, sector_t,
                                                void **, unsigned long *);
        int (*media_changed) (struct gendisk *);
        int (*revalidate_disk) (struct gendisk *);
        int (*getgeo)(struct block_device *, struct hd_geometry *);
        struct module *owner;
};
在前幾章中,我們邂逅過其中的owner成員變量,它用於存儲這個結構的所有者,也就是我們的模塊,因此我們做了如下的賦值:
.owner                = THIS_MODULE,
而這一章中,我們將與它的同胞妹妹------getgeo也親密接觸一下。

我們要做的是:
1:在block_device_operations中增加getgeo成員變量初值的設定,指向我們的“獲得塊設備物理結構”函數。
2:實現我們的“獲得塊設備物理結構”函數。

第一步很簡單,我們暫且爲“獲得塊設備物理結構”函數取個名字叫simp_blkdev_getgeo()吧,也避免了在下文中把這麼一大堆漢字拷來拷去。
在simp_blkdev_fops中添加.getgeo指向simp_blkdev_getgeo,也就是把simp_blkdev_fops結構改成這個樣子:
struct block_device_operations simp_blkdev_fops = {
        .owner                = THIS_MODULE,
        .getgeo                = simp_blkdev_getgeo,
};

第二步難一些,但也難不到哪去,在代碼中的struct block_device_operations simp_blkdev_fops這行之前找個空點的場子,把如下函數插進去:
static int simp_blkdev_getgeo(struct block_device *bdev,
                struct hd_geometry *geo)
{
        /*
         * capacity        heads        sectors        cylinders
         * 0~16M        1        1        0~32768
         * 16M~512M        1        32        1024~32768
         * 512M~16G        32        32        1024~32768
         * 16G~...        255        63        2088~...
         */
        if (SIMP_BLKDEV_BYTES < 16 * 1024 * 1024) {
                geo->heads = 1;
                geo->sectors = 1;

        } else if (SIMP_BLKDEV_BYTES < 512 * 1024 * 1024) {
                geo->heads = 1;
                geo->sectors = 32;
        } else if (SIMP_BLKDEV_BYTES < 16ULL * 1024 * 1024 * 1024) {
                geo->heads = 32;
                geo->sectors = 32;
        } else {
                geo->heads = 255;
                geo->sectors = 63;
        }

        geo->cylinders = SIMP_BLKDEV_BYTES>>9/geo->heads/geo->sectors;

        return 0;
}
因爲這裏我們用到了struct hd_geometry結構,所以還要增加一行#include <linux/hdreg.h>。

這個函數的目的,是選擇適當的物理結構信息裝入struct hd_geometry *geo結構。
當然,爲了克服上一章中只能分成2個區的問題,我們應該儘可能增加磁道的數量。
希望讀者不要理解成分幾個區就需要幾個磁道,這意味着一個磁道一個區,也意味着每個區必須一般大小。
由於分區總是以磁道爲邊界,儘可能增加磁道的數量不僅僅是爲了讓塊設備容納更多的分區,
更重要的是讓分區的實際大小更接近於分區時的指定值,也就是提高實際做出的分區容量的精度。

不過對於設置的物理結構值,還存在一個限制,就是struct hd_geometry中的數值上限。
我們看struct hd_geometry的內容:
struct hd_geometry {
        unsigned char heads;
        unsigned char sectors;
        unsigned short cylinders;
        unsigned long start;
};
unsigned char的磁頭數和每磁道扇區數決定了其255的上限,同樣,unsigned short的磁道數決定了其65535的上限。
這還不算,但在前一章中,我們知道對於現代硬盤,磁頭數和每磁道扇區數通常取的值是255和63,
再組合上這裏的65535的磁道數上限,hd_geometry能夠表示的最大塊設備容量是255*63*65535*512/1024/1024/1024=502G。
顯然目前linux支持的最大硬盤容量大於502G,那麼對於這類塊設備,內核是如何通過hd_geometry結構表示其物理結構的呢?
訣竅不在內核,而在於用戶態程序如fdisk等通過內核調用獲得hd_geometry結構後,
會捨棄hd_geometry.cylinders內容,取而代之的是直接通過hd_geometry中的磁頭數和每磁道扇區數以及硬盤大小去計算磁道數。
因此對於超過502G的硬盤,由於用戶程序得出的磁道數與hd_geometry.cylinders無關,所以我們往往在fdisk中能看到這塊硬盤的磁道數大於65535。

剛纔扯遠了,現在言歸正題,我們決定讓這個函數對於任何尺寸的塊設備,總是試圖返回比較漂亮的物理結構。
漂亮意味着返回的物理結構既要保證擁有足夠多的磁道,也要保證磁頭數和每磁道扇區數不超過255和63,同時最好使用程序員看起來比較順眼的數字,
如:1、2、4、8、16、32、64等。
當然,我們也希望找到某個One Shot公式適用於所有大小的塊設備,但很遺憾目前作者沒找到,因此採用了分段計算的方法:
首先考慮容量很小的塊設備:
  即使磁頭數和每磁道扇區數都是1,磁道數也不夠多時,我們會將磁頭數和每磁道扇區數都固定爲1,以使磁道數儘可能多,以提高分區的精度。
  因此磁道數隨塊設備容量而上升。
  雖然我們已經知道了磁道數其實可以超過unsigned short的65535上限,但在這裏卻沒有必要,因此我們要給磁道數設置一個上限。
  因爲不想讓上限超過65535,同時還希望上限也是一個程序員喜歡的數字,因此這裏選擇了32768。
  當然,當磁道數超過32768時,已經意味着塊設備容量不那麼小了,也就沒有必要使用這種情況中如此苛刻的磁頭數和每磁道扇區數了。
  簡單來說,當塊設備容量小於1個磁頭、每磁道1扇區和32768個磁道對應的容量--也就是16M時,我們將按照這種情況處理。
然後假設塊設備容量已經大於16M了:
  我們希望保證塊設備包含足夠多的磁道,這裏我們認爲1024個磁道應該不少了。
  磁道的最小值發生在塊設備容量爲16M的時候,這時使用1024作爲磁道數,可以計算出磁頭數*每磁道扇區數=32。
  這裏暫且把磁頭數和每磁道扇區數固定爲1和32,而讓磁道數隨着塊設備容量的增大而增加。
  同時,我們還是磁道的上限設置成32768,這時的塊設備容量爲512M。
  總結來說,當塊設備容量在16M和512M之間時,我們把磁頭數和每磁道扇區數固定爲1和32。
然後對於容量大於512M的塊設備:
  與上述處理相似,當塊設備容量在512M和16G之間時,我們把磁頭數和每磁道扇區數固定爲32和32。
最後的一種情況:
  塊設備已經足夠大了,大到即使我們使用磁頭數和每磁道扇區數的上限,
  也能獲得足夠多的磁道數。這時把磁頭數和每磁道扇區數固定爲255和63。
  至於磁道數就算出多少是多少了,即使超過unsigned short的上限也無所謂,反正用不着。

隨着這個函數解說到此結束,我們對代碼的修改也結束了。

現在開始試驗:
編譯和加載:
# make
make -C /lib/modules/2.6.27.4/build SUBDIRS=/mnt/host_test/simp_blkdev/simp_blkdev_step05 modules
make[1]: Entering directory `/mnt/ltt-kernel'
  CC [M]  /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.mod.o
  LD [M]  /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.ko
make[1]: Leaving directory `/mnt/ltt-kernel'
# insmod simp_blkdev.ko
#
用fdisk打開設備文件
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won't be recoverable.

Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)

Command (m for help):
看看設備的物理結構:
Command (m for help): p

Disk /dev/simp_blkdev: 16 MB, 16777216 bytes
1 heads, 32 sectors/track, 1024 cylinders
Units = cylinders of 32 * 512 = 16384 bytes

           Device Boot      Start         End      Blocks   Id  System

Command (m for help):
我們發現,現在的設備有1個磁頭、32扇區每磁道、1024個磁道。
這是符合代碼中的處理的。

本章的內容也不是太難,連同上一章,我們已經休息2章了。
聰明的讀者可能已經猜到作者打算說什麼了。
不錯,下一章會有一個surprise。

<未完,待續>

第6章

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

經歷了內容極爲簡單的前兩章的休息,現在大家一定感到精神百倍了。
作爲已經堅持到現在的讀者,對接下去將要面臨的內容大概應該能夠猜得八九不離十了,
具體的內容猜不出來也無妨,但一定將是具有增加顱壓功效的。

與物理塊設備驅動程序的區別在於,我們的驅動程序使用內存來存儲塊設備中的數據。
到目前爲止,我們一直都是使用這樣一個靜態數組來擔負這一功能的:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];

如果讀者懂得一些模塊的知識,或者現在趕緊去臨時抱佛腳google一些模塊知識,
應該知道模塊其實是加載在非線性映射區域的。
詳細來說,在加載模塊時,根據模塊的ELF信息(天哪,又要去google elf了),確定這個模塊所需的靜態內存大小。
這些內存用來容納模塊的二進制代碼,以及靜態變量。然後申請容納這些數據的一大堆頁面。
當然,這些頁面並不是連續的,而代碼和變量卻不可能神奇到即使被切成一塊一塊的也能正常工作。
因此需要在非線性映射區域中找到一塊連續的地址(現在有要去google非線性映射區域了),用來將剛纔申請到的一塊一塊的內存頁映射到這個地址段中。
最後模塊被請到這段區域中,然後執行模塊的初始化函數......

現在看我們這個模塊中的simp_blkdev_data變量,如果不是現在刻意關注,這個變量看起來顯得那麼得普通。
正如其它的一些名字原先也是那麼的普通,但由於一些突發的事件受到大家的熱烈關注,
比如一段視頻讓我們熟悉了kappa和陸佳妮,比如呼吸稅讓我們認識了蔣有緒。

現在我們開始關注simp_blkdev_data變量了,導火索是剛纔介紹的非線性映射區域。
模塊之所以被加載到非線性映射區域,是因爲很難在線性映射區域中得到加載模塊所需的連續的內存。
但使用非線性映射區域也並非只賺不賠的生意,至少在i386結構中,非線性映射區域實在是太小了。
在物理內存大於896M的i386系統中,整個非線性映射區域不會超過128M。
相反如果物理內存小於896M(不知道該算是幸運還是不幸),非線性映射區域反而會稍微大一些,這種情況我想我們可以不用討論了,畢竟不能爲了加載一個模塊去拔內存。
因此我們的結論是:非線性映射區域是很緊張的資源,我們要節約使用。
而像我們現在這個模塊中的simp_blkdev_data卻是個如假包換的反面典型,居然上來就吃掉了16M!這還是因爲我們沒有把SIMP_BLKDEV_BYTES定義得更大。

現在我們開始列舉simp_blkdev_data的種種罪行:
1:剩餘的非線性映射區域較小時導致模塊加載失敗
2:模塊加載後佔用了大量的非線性映射區域,導致其它模塊加載失敗。
3:模塊加載後佔用了大量的非線性映射區域,影響系統的正常運行。
   這是因爲不光模塊,系統本身的很多功能也依賴非線性映射區域空間。
對於這樣的害羣之馬,我們難道還有留下他的理由嗎?
本章的內容雖然麻煩一些,但想到能夠一了百了地清除這個體大膘肥的simp_blkdev_data,倒也相當值得。
也希望今後能夠看到在對貪官的處理上,能夠也拿出這樣的魄力和勇氣。

現在在清除simp_blkdev_data的問題上,已經不存在什麼懸念了,接下來我們需要關注的是將simp_blkdev_data碎屍萬段後,拿出一個更恰當方法來代替它。
首先,我們決定不用靜態聲明的數組,而改用動態申請的內存。
其次,使用類似vmalloc()的函數可以動態申請大段內存,但其實這段內存佔用的還是非線性映射區域,就好像用一個比較隱蔽的貪官來代替下馬的貪官,我們不會愚蠢在這種地步。
剩下的,就是在線性映射區域申請很多個頁的內存,然後自己去管理。這個方法一了百了地解決了使用大段非線性映射區域的問題,而唯一的問題是由於需要自己管理申請到的頁面,使程序複雜了不少。
但爲了整個系統的利益,這難道不是我們該做的嗎?

申請一個內存頁是很容易的,這裏我們將採用所有容易的方法中最容易的那個:
__get_free_page函數,原型是:
unsigned long __get_free_page(gfp_t gfp_mask);
這個函數用來申請一個頁面的內存。gfp_mask包含一些對申請內存時的指定,比如,要在DMA區域中啦、必須清零等。
我們這裏倒是使用最常見的__get_free_page(GFP_KERNEL)就可以了。

通過__get_free_page申請到了一大堆內存頁,新的問題來了,在讀寫塊設備時,我們得到是塊設備的偏移,如何快速地通過偏移找到對應的內存頁呢?

最簡單的方法是建立一個數組,用來存放偏移到內存的映射,數組中的每項對應一個一個頁:
數組定義如下:
void *simp_blkdev_data[(SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE];
PAGE_SIZE是系統中每個頁的大小,對i386來說,通常是4K,那堆加PAGE_SIZE減1的代碼是考慮到SIMP_BLKDEV_BYTES不是PAGE_SIZE的整數倍時要讓末尾的空間也能訪問。
然後申請內存的代碼大概是:
for (i=0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE; i++) {
        p = (void *)__get_free_page(GFP_KERNEL);
        simp_blkdev_data[i] = p;
}
通過塊設備偏移得到內存中的數據地址的代碼大概是:
mem_addr = simp_blkdev_data[dev_addr/PAGE_SIZE] + dev_addr % PAGE_SIZE;
這種方法實現起來還是比較簡單的,但缺點也不是沒有:存放各個頁面地址的數組雖然其體積比原先那個直接存放數據的數組已經縮小了很多,
但本身畢竟還是在非線性映射區域中。如果塊設備大小爲16M,在i386上,需要4096個頁面,數組大小16K,這不算太大。
但如果某個瘋子打算建立一個2G的虛擬磁盤,數組大小將達到2M,這就不算小了。

或者我們可以不用數組,而用鏈表來存儲偏移到內存頁的映射關係,這樣可以迴避掉數組存在的問題,但在鏈表中查找指定元素卻不是一般的費時,
畢竟我們不希望用戶覺得這是個軟盤。

接下來作者不打斷繼續賣關子了,我們最終選擇使用的是傳說中的基樹。
關於linux中基樹細節的文檔不多,特別是中文文檔更少,更糟的是我們這篇文檔也不打算作詳細的介紹(因爲作者建議去RTFSC)。
但總的來說,相對於二叉平衡樹的紅黑樹來說,基樹是一個n叉(一般爲64叉)非平衡樹,n叉減少了搜索的深度,非平衡減少了複雜的平衡操作。
當然,這兩個特點也不是僅僅帶來優點,但在這裏我們就視而不見了,畢竟我們已經選擇了基樹,因此護短也是自認而然的事情,正如公僕護着王細牛一樣。
從功能上來說,基樹好像是爲我們量身定做的一樣,好用至極。
(其實我們也可以考慮選擇紅黑樹和哈希表來實現這個功能,感興趣的讀者可以瞭解一下)

接下來的代碼中,我們將要用到基樹種的如下函數:
  void INIT_RADIX_TREE((struct radix_tree_root *root, gfp_t mask);
  用來初始化一個基樹的結構,root是基樹結構指針,mask是基樹內部申請內存時使用的標誌。

  int radix_tree_insert(struct radix_tree_root *root, unsigned long index, void *item);
  用來往基樹中插入一個指針,index是指針的索引,item是指針,將來可以通過index從基樹中快速獲得這個指針的值。

  void *radix_tree_delete(struct radix_tree_root *root, unsigned long index);
  用來根據索引從基樹中刪除一個指針,index是指針的索引。

  void *radix_tree_lookup(struct radix_tree_root *root, unsigned long index);
  用來根據索引從基樹中查找對應的指針,index是指針的索引。
其實基樹的功能不僅限於此,比如,還可以給指針設定標誌,詳情還是請去讀linux/lib/radix-tree.c

現在開始改造我們的代碼:
首先刪除那個無恥的數組:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
然後引入它的替代者--一個基樹結構:
static struct radix_tree_root simp_blkdev_data;

然後增加兩個函數,用來申請和釋放塊設備的內存:

申請內存的函數如下:
int alloc_diskmem(void)
{
        int ret;
        int i;
        void *p;

        INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);

        for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> PAGE_SHIFT;
                i++) {
                p = (void *)__get_free_page(GFP_KERNEL);
                if (!p) {
                        ret = -ENOMEM;
                        goto err_alloc;
                }

                ret = radix_tree_insert(&simp_blkdev_data, i, p);
                if (IS_ERR_VALUE(ret))
                        goto err_radix_tree_insert;
        }
        return 0;

err_radix_tree_insert:
        free_page((unsigned long)p);
err_alloc:
        free_diskmem();
        return ret;
}
先初始化基樹結構,然後申請需要的每一個頁面,按照每頁面的次序作爲索引,將指針插入基樹。
代碼中的“>> PAGE_SHIFT”與“/ PAGE_SIZE”作用相同,
if (不明白爲什麼要這樣)
        do_google();

釋放內存的函數如下:
void free_diskmem(void)
{
        int i;
        void *p;

        for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> PAGE_SHIFT;
                i++) {
                p = radix_tree_lookup(&simp_blkdev_data, i);
                radix_tree_delete(&simp_blkdev_data, i);
                /* free NULL is safe */
                free_page((unsigned long)p);
        }
}
遍歷每一個索引,得到頁面的指針,釋放頁面,然後從基樹中釋放這個指針。
由於alloc_diskmem()函數在中途失敗時需要釋放申請過的頁面,因此我們把free_diskmem()函數設計成能夠釋放建立了一半的基樹的形式。
對於只建立了一半的基樹而言,有一部分索引對應的指針還沒來得及插入基樹,對於不存在的索引,radix_tree_delete()函數會返回NULL,幸運的是free_page()函數能夠忽略傳入的NULL指針。

因爲alloc_diskmem()函數需要調用free_diskmem()函數,在代碼中需要把free_diskmem()函數寫在alloc_diskmem()前面,或者在文件頭添加函數的聲明。

然後在模塊的初始化和釋放函數中添加對alloc_diskmem()和free_diskmem()的調用,
也就是改成這個樣子:
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(SIMP_BLKDEV_MAXPARTITIONS);
        if (!simp_blkdev_disk) {
                ret = -ENOMEM;
                goto err_alloc_disk;
        }

        ret = alloc_diskmem();
        if (IS_ERR_VALUE(ret))
                goto err_alloc_diskmem;

        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_diskmem:
        put_disk(simp_blkdev_disk);
err_alloc_disk:
        blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
        return ret;
}

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

最麻煩的放在最後:
我們需要修改simp_blkdev_make_request()函數,讓它適應新的數據結構。

原先的實現中,對於一個bio_vec,我們找到對應的內存中數據的起點,直接傳送bvec->bv_len個字節就大功告成了,比如,讀塊設備時就是:
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
但現在由於容納數據的每個頁面地址是不連續的,因此可能出現bio_vec中的數據跨越頁面邊界的情況。
也就是說,一個bio_vec中的數據的前半段在一個頁面中,後半段在另一個頁面中。
雖然這兩個頁面對應的塊設備地址連續,但在內存中的地址不一定連續,因此像原先那樣簡單使用memcpy看樣子是解決不了問題了。

實際上,雖然bio_vec可能跨越頁面邊界,但它無論如何也不可能跨越2個以上的頁面。
這是因爲bio_vec本身對應的數據最大長度只有一個頁面。
因此如果希望做最簡單的實現,只要在代碼中做一個條件判斷就OK了:
if (沒有跨越頁面) {
        1個memcpy搞定
} else {
        /* 肯定是跨越2個頁面了 */
        2個memcpy搞定
}

但爲了表現出物理設備一次傳送1個扇區數據的處理方式(這種情況下一個bio_vec可能會跨越2個以上的扇區),我們讓代碼支持2個以上頁面的情況。
首先列出修改後的simp_blkdev_make_request()函數:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
        struct bio_vec *bvec;
        int i;
        unsigned long long dsk_offset;

        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_offset = bio->bi_sector << 9;

        bio_for_each_segment(bvec, bio, i) {
                unsigned int count_done, count_current;
                void *iovec_mem;
                void *dsk_mem;

                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;

                count_done = 0;
                while (count_done < bvec->bv_len) {
                        count_current = min(bvec->bv_len - count_done,
                                (unsigned int)(PAGE_SIZE
                                - ((dsk_offset + count_done) & ~PAGE_MASK)));

                        dsk_mem = radix_tree_lookup(&simp_blkdev_data,
                                (dsk_offset + count_done) >> PAGE_SHIFT);
                        if (!dsk_mem) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": search memory failed: %llu\n",
                                        (dsk_offset + count_done)
                                        >> PAGE_SHIFT);
                                kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                                bio_endio(bio, 0, -EIO);
#else
                                bio_endio(bio, -EIO);
#endif
                                return 0;
                        }
                        dsk_mem += (dsk_offset + count_done) & ~PAGE_MASK;

                        switch (bio_rw(bio)) {
                        case READ:
                        case READA:
                                memcpy(iovec_mem + count_done, dsk_mem,
                                        count_current);
                                break;
                        case WRITE:
                                memcpy(dsk_mem, iovec_mem + count_done,
                                        count_current);
                                break;
                        default:
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": unknown value of bio_rw: %lu\n",
                                        bio_rw(bio));
                                kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                                bio_endio(bio, 0, -EIO);
#else
                                bio_endio(bio, -EIO);
#endif
                                return 0;
                        }
                        count_done += count_current;
                }

                kunmap(bvec->bv_page);
                dsk_offset += 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;
}

看樣子長了一些,但不要被嚇着了,因爲讀的時候我們可以對代碼做一些簡化:
1:去掉亂七八糟的出錯處理
2:無視每行80字符限制
3:把比特運算改成等價但更易讀的乘除運算
4:無視礙眼的類型轉換
5:假設內核版本大於2.6.24,以去掉判斷版本的宏
就會變成這樣了:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
        struct bio_vec *bvec;
        int i;
        unsigned long long dsk_offset;

        dsk_offset = bio->bi_sector * 512;

        bio_for_each_segment(bvec, bio, i) {
                unsigned int count_done, count_current;
                void *iovec_mem;
                void *dsk_mem;

                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;

                count_done = 0;
                while (count_done < bvec->bv_len) {
                        count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE);

                        dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) / PAGE_SIZE);
                        dsk_mem += (dsk_offset + count_done) % PAGE_SIZE;

                        switch (bio_rw(bio)) {
                        case READ:
                        case READA:
                                memcpy(iovec_mem + count_done, dsk_mem, count_current);
                                break;
                        case WRITE:
                                memcpy(dsk_mem, iovec_mem + count_done, count_current);
                                break;
                        }
                        count_done += count_current;
                }

                kunmap(bvec->bv_page);
                dsk_offset += bvec->bv_len;
        }

        bio_endio(bio, 0);
        return 0;
}
是不是清楚多了?

dsk_offset用來存儲當前要處理的數據在塊設備上的偏移,初始值是bio->bi_sector * 512,也就是起始扇區對應的偏移,也是第一個bio_vec對應的塊設備偏移。
每處理完成一個bio_vec時,dsk_offset值會被更新:dsk_offset += bvec->bv_len,以指向將要處理的數據在塊設備上的偏移。

在bio_for_each_segment()中代碼的起始和末尾,執行kmap和kunmap開映射當前這個bio_vec的內存,這個知識在前面的章節中已經提到了,
這個處理的結果是iovec_mem指向當前的bio_vec中的緩衝區。

現在在kmap和kunmap之間的代碼的功能已經很明確了,就是完成塊設備上偏移爲dsk_offset、長度爲bvec->bv_len的數據與iovec_mem地址之間的傳送。
假設不考慮bio_vec跨越頁面邊界的情況,這段代碼應該十分寫意:
dsk_mem = radix_tree_lookup(&simp_blkdev_data, dsk_offset / PAGE_SIZE) + dsk_offset % PAGE_SIZE;
switch (bio_rw(bio)) {
case READ:
case READA:
        memcpy(iovec_mem, dsk_mem, bvec->bv_len);
        break;
case WRITE:
        memcpy(dsk_mem, iovec_mem, bvec->bv_len);
        break;
}
首先使用dsk_offset / PAGE_SIZE、也就是塊設備偏移在內存中數據所位於的頁面次序作爲索引,查找該頁的內存起始地址,
然後加上塊設備偏移在該頁內的偏移、也就是dsk_offset % PAGE_SIZE,
就得到了內存中數據的地址,然後就是簡單的數據傳送。
關於塊設備偏移到內存地址的轉換,我們舉個例子:
假使模塊加載時我們分配的第1個頁面的地址爲0xd0000000,用於存放塊設備偏移爲0~4095的數據
第2個頁面的地址爲0xd1000000,用於存放塊設備偏移爲4096~8191的數據
第3個頁面的地址爲0xc8000000,用於存放塊設備偏移爲8192~12287的數據
第4個頁面的地址爲0xe2000000,用於存放塊設備偏移爲12288~16383的數據
對於塊設備偏移爲9000的數據,首先通過9000 / PAGE_SIZE確定它位於第3個頁面中,
然後使用radix_tree_lookup(&simp_blkdev_data, 3)將查找出0xc8000000這個地址。
這是第3個頁面的起始地址,這個地址的數據在塊設備中的偏移是8192,
因此我們還要加上塊設備偏移在頁內的偏移量,也就是9000 % PAGE_SIZE = 808,
得到的纔是塊設備偏移爲9000的數據在內存中的數據地址。

當然,假設終歸是假設,往往大多數情況下是自欺欺人的,就好像彩迷總喜歡跟女友說如果中了500萬,就要怎麼怎麼對她好一樣。
現在回到殘酷的現實,我們還是要去考慮bio_vec跨越頁面邊界的情況。
這意味着對於一個bio_vec,我們將有可能傳送多次。
爲了記錄前幾次中已經完成的數據量,我們引入了一個新的變量,叫做count_done。
在進行bio_vec內的第一次傳送前,count_done的值是0,每完成一次傳送,count_done將加上這次完成的數據量。
當count_done == bvec->bv_len時,就是大功告成的之日。

接着就是邊界的判斷。
當前塊設備偏移所在的內存頁中,塊設備偏移所在位置到頁頭的距離爲:
offset % PAGE_SIZE
塊設備偏移所在位置到頁尾的距離爲:
PAGE_SIZE - offset % PAGE_SIZE
這個距離也就是不超越頁邊界時所能傳送的數據的最大值。

因此在bio_vec內的每一次中,我們使用
count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE);
來確定這一次傳送的數據量。
bvec->bv_len - count_done指的是餘下需要傳送的數據總量,
PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE指的是從當前塊設備偏移開始、不超越頁邊界時所能傳送的數據的最大值。
如果bvec->bv_len - count_done > PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE,說明這一次將傳送從當前塊設備偏移到其所在內存頁的頁尾之間的數據,
餘下的數據位於後續的頁面中,將在接下來的循環中搞定,
如果bvec->bv_len - count_done <= PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE,那麼可喜可賀,這將是當前bio_vec的最後一次傳送,完成後就可以回家洗澡了。

結合以上的說明,我想應該不難看懂simp_blkdev_make_request()的代碼了,而我們的程序也已經大功告成。

現在總結一下修改的位置:
1:把unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];換成static struct radix_tree_root simp_blkdev_data;
2:把本文中的free_diskmem()和alloc_diskmem()函數添加到代碼中,雖然沒有特別意義,但建議插在緊鄰simp_blkdev_init()之前的位置。
   但有特別意義的是free_diskmem()和alloc_diskmem()的順序,如果讀者到這裏還打算提問是什麼順序,作者可要哭了。
3:把simp_blkdev_make_request()、simp_blkdev_init()和simp_blkdev_exit()函數替換成文中的代碼。
   注意不要企圖使用簡化過的simp_blkdev_make_request()函數,否則造成的後果:從程序編譯失敗到讀者被若干美女輪姦,作者都概不負責。

從常理分析,在修改完程序後,我們都將試驗一次修改的效果。
這次也不例外,因爲審判彭宇的王法官也是這麼推斷的。

首先證明我們的模塊至今爲止仍然經得起編譯、能夠加載:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step06 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#

看看模塊的加載時分配的非線性映射區域大小:
# lsmod
Module                  Size  Used by
simp_blkdev             8212  0
...
#
如果這個Size一欄的數字沒有引起讀者的足夠重視的話,我們拿修改前的模塊來對比一下:
# lsmod
Module                  Size  Used by
simp_blkdev         16784392  0
看出區別了沒?

如果本章到這裏還不結束的話,估計讀者要開始閃人了。
好的,我們馬上就結束,希望在這之前閃掉的讀者不要太多。
由於還沒有來得及閃掉而看到這段話的讀者們,作者相信你們具有相當的毅力。
學習是需要毅力的,這時的作者同樣也需要毅力來堅持完成這本教程。

最後還是希望讀者堅持,堅持看完所有的章節,堅持在遇到每一個不明白的問題時都努力尋求答案,
堅持在發現自己感興趣的內容時能夠深入地去了解、探尋、思考。

<未完,待續>

第7章

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

上一章中我們對驅動程序做了很大的修改,單獨分配每一頁的內存,然後使用基樹來進行管理。
這使得驅動程序佔用的非線性映射區域大大減少,讓它看起來朝優秀的代碼又接近了一些。
因爲優秀的代碼是相似的,糟糕的代碼卻各有各的糟糕之處。

本章中我們將討論一些細枝末節的問題,算是對上一章中內容的鞏固,也是爲後面的章節作一些鋪墊。

首先聊一聊低端內存、高端內存和非線性映射區域的問題:
在i386結構中,由於任務使用32位寄存器表示地址,這造成每個任務的最大尋址範圍是4G。
無論任務對應的是用戶程序還是內核代碼,都逃脫不了這個限制。

讓問題更糟糕的是,普通的linux內核又將4G的地址劃分爲2個部分,前3G讓用戶空間程序使用,後1G由內核本身使用。
這又將內核實際使用的空間壓縮了4倍。
不過linux採用這樣的方案倒也不是由於開發者腦癱,因爲這樣一來,內核可以與用戶進程共用同一個頁表,
因而在進行用戶態和內核態的切換時不必刷新頁表,提高了系統的效率。
而帶來的麻煩就是內核只有1G的地址範圍可用。
其實也有一個相當出名的4G+4G的patch,就是採用上述相反的方法,讓內核與用戶進程使用獨立的地址空間,其優缺點也正好與現在的實現相反。
但這畢竟不是標準內核的情況,對大多數系統而言,我們不得不接受內核只有1G的地址範圍可用的現實。

然後我們再來看內核如何使用這1G的地址範圍。
作爲內核,當然需要有能力訪問到所有的物理內存,而在保護模式下,內存需要通過頁表映射到一個虛擬地址上,再進行訪問。
雖然內核可以在訪問任何物理內存時都採用映射->訪問->取消映射的方法,但這很可能將任意一臺機器徹底變成386的速度。
因此,內核一般把儘可能多的物理內存事先映射到它的地址空間中去,這裏的“儘可能多”指的是896M。
原因是內核手頭只有1G的地址空間,而其中的128M還需要留作非線性映射空間。
這樣一來,內核地址空間中的3G~3G+896M便映射了0~896M範圍的物理內存。
這個映射關係在啓動系統時完成,並且在系統啓動後不會改變。
物理內存中0~896M的這段空間是幸運的,因爲它們在內核空間中有固定的住所,
這也使它們能夠方便、快速地被訪問。相對896M以上的物理內存,它們地址是比較低的,
正因爲此,我們通常把這部分內存區域叫做低端內存。

但地址高於896M的物理內存就沒這麼幸運了。
由於它們沒有在啓動時被固定映射到內核空間的地址空間中,我們需要在訪問之前對它們進行映射。
但映射到哪裏呢?幸好內核沒有把整個1G的地址空間都用作映射上面所說的低端內存,好歹還留下128M。
其實這128M還是全都能用,在其開頭和結尾處還有一些區域拿去幹別的事情了(希望讀者去詳細瞭解一下),
所以我們可以用這剩下的接近128M的區域來映射高於896M的物理內存。
明顯可以看出這時是僧多粥少,所以這部分區域最好應該節約使用。
但希望讀者不要把訪問高於896M的物理內存的問題想得過於嚴重,因爲一般來說,內核會傾向於把這部分內存分配給用戶進程使用,而這是不需要佔用內核空間地址的。

其實非線性映射區域還有另一個作用,就是用來作連續地址的映射。
內核採用夥伴系統管理內存,這使得內核程序可以一次申請2的n次冪個頁面。
但如果n比較大時,申請失敗的風險也會隨之增加。正如桑拿時遇到雙胞胎的機會很少、遇到三胞胎的機會更少一樣,
獲得地址連續的空閒頁面的機會總是隨着連續地址長度的增加而減少。
另外,即使能夠幸運地得到地址連續的空閒頁面,可能產生的浪費問題也是不能迴避的。
比如我們需要申請地址連續513K的內存,從夥伴系統中申請時,由於只能選擇申請2的n次冪個頁面,因此我們不得不去申請1M內存。
不過這兩個問題倒是都能夠通過使用非線性映射區域來解決。
我們可以從夥伴系統中申請多個小段的內存,然後把它們映射到非線性映射區域中的連續區域中訪問。
內核中與此相關的函數有vmalloc、vmap等。

其實80前的作者很羨慕80後和90後的新一代,不僅因爲可以在上中學時談戀愛,
還因爲隨着64位系統的流行,上面這些與32位系統如影隨形的問題都將不復存在。
關於64位系統中的內存區域問題就留給有興趣的讀者去鑽研了。

然後我們再談談linux中的夥伴系統。
夥伴系統總是分配出2的n次冪個連續頁面,並且首地址以其長度爲單位對齊。
這增大了將回收的頁與其它空白頁合併的可能性,也就是減少了內存碎片。

我們的塊設備驅動程序需要從夥伴系統中獲得所需的內存。
目前的做法是每次獲得1個頁面,也就是分配頁面時,把2的n次冪中的n指定爲0。
這樣做的好處是隻要系統中存在空閒的頁面,不管空閒的頁面是否連續,分配總是能成功。
但壞處是增加了造就頁面碎片的機率。
當系統中沒有單獨的空閒頁面時,夥伴系統就不得不把原先連續的空閒頁面拆開,再把其中的1個頁面返回給我們的程序。
同時,在夥伴系統中需要使用額外的內存來管理每一組連續的空閒頁面,因此增大頁面碎片也意味着需要更多的內存來管理這些碎片。
這還不算,如果系統中的空閒頁面都以碎片方式存在,那麼真正到了需要分配連續頁面的時候,即使存在空閒的內存,也會因爲這些內存不連續而導致分配失敗。
除了對系統的影響以外,對我們的驅動程序本身而言,由於使用了基樹來管理每一段內存,將內存段定義得越短,意味着需要管理更多的段數,也意味着更大的基樹結構和更慢的操作。

因此我們打算增加單次從夥伴系統中獲得連續內存的長度,比如,每次分配2個、4個、或者8個甚至64個頁,來避免上述的問題。
每次分配更大的連續頁面很明顯擁有不少優勢,但其劣勢也同樣明顯:
當系統中內存碎片較多時,吃虧的就是咱們的驅動程序了。原本分很多次一點一點去系統討要,最終可以要到足夠的內存,但像現在這樣子獅子大開口,卻反而要不到了。
還有就是如果系統中原先就存在不少碎片,原先的分配方式倒是可以把碎片都利用起來,而現在這種挑肥撿瘦的分配會同樣無視那些更小的不連續頁面,反而可能企圖去拆散那些更大的連續頁面。

折中的做法大概就是選擇每次分配一塊不大不小的連續的頁,暫且我們選擇每次分配連續的4個頁。
現在開始修改代碼:
爲簡單起見,我們了以下的4個宏:
#define SIMP_BLKDEV_DATASEGORDER        (2)
#define SIMP_BLKDEV_DATASEGSHIFT        (PAGE_SHIFT + SIMP_BLKDEV_DATASEGORDER)
#define SIMP_BLKDEV_DATASEGSIZE                (PAGE_SIZE << SIMP_BLKDEV_DATASEGORDER)
#define SIMP_BLKDEV_DATASEGMASK                (~(SIMP_BLKDEV_DATASEGSIZE-1))
SIMP_BLKDEV_DATASEGORDER表示我們從夥伴系統中申請內存時使用的order值,把這個值設置爲2時,每次將從夥伴系統中申請連續的4個頁面。
我們暫且把這樣的連續頁面叫做內存段,這樣一來,在i386結構中,每個內存段的大小爲16K,假設塊設備大小還是16M,那麼經歷了本章的修改後,
驅動程序所使用的內存段數量將從原先的4096個減少爲現在的1024個。
SIMP_BLKDEV_DATASEGSHIFT是在偏移量和內存段之間相互轉換時使用的移位值,類似於頁面處理中的PAGE_SHIFT。這裏就不做更詳細地介紹了,畢竟這不是C語言教程。
SIMP_BLKDEV_DATASEGSIZE是以字節爲單位的內存段的長度,在i386和SIMP_BLKDEV_DATASEGORDER=2時它的值是16384。
SIMP_BLKDEV_DATASEGMASK是內存段的屏蔽位,類似於頁面處理中的PAGE_MASK。
其實對於功能而言,我們只需要SIMP_BLKDEV_DATASEGORDER和SIMP_BLKDEV_DATASEGSIZE就足夠了,其它的宏用於快速的乘除和取模等計算。
如果讀者對此感到有些迷茫的話,建議最好還是搞明白,因爲在linux內核的世界中這一類的位操作將隨處可見。

然後要改的是申請和釋放內存代碼。
原先我們使用的是__get_free_page()和free_page()函數,這一對函數用來申請和釋放一個頁面。
這顯然不能滿足現在的要求,我們改用它們的大哥:__get_free_pages()和free_pages()。
它們的原型是:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
可以注意到與__get_free_page()和free_page()函數相比,他們多了個order參數,正是用於指定返回2的多少次冪個連續的頁。
因此原先的free_diskmem()和alloc_diskmem()函數將改成以下這樣:
void free_diskmem(void)
{
        int i;
        void *p;

        for (i = 0; i < (SIMP_BLKDEV_BYTES + SIMP_BLKDEV_DATASEGSIZE - 1)
                >> SIMP_BLKDEV_DATASEGSHIFT; i++) {
                p = radix_tree_lookup(&simp_blkdev_data, i);
                radix_tree_delete(&simp_blkdev_data, i);
                /* free NULL is safe */
                free_pages((unsigned long)p, SIMP_BLKDEV_DATASEGORDER);
        }
}

int alloc_diskmem(void)
{
        int ret;
        int i;
        void *p;

        INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);

        for (i = 0; i < (SIMP_BLKDEV_BYTES + SIMP_BLKDEV_DATASEGSIZE - 1)
                >> SIMP_BLKDEV_DATASEGSHIFT; i++) {
                p = (void *)__get_free_pages(GFP_KERNEL,
                        SIMP_BLKDEV_DATASEGORDER);
                if (!p) {
                        ret = -ENOMEM;
                        goto err_alloc;
                }

                ret = radix_tree_insert(&simp_blkdev_data, i, p);
                if (IS_ERR_VALUE(ret))
                        goto err_radix_tree_insert;
        }
        return 0;

err_radix_tree_insert:
        free_pages((unsigned long)p, SIMP_BLKDEV_DATASEGORDER);
err_alloc:
        free_diskmem();
        return ret;
}
除了用__get_free_pages()和free_pages()代替了原先的__get_free_page()和free_page()函數以外,
還使用剛剛定義的那幾個宏代替了原先的PAGE宏。
這樣一來,所需內存段數的計算方法也完成了修改。

剩下的就是使用內存段的simp_blkdev_make_request()代碼。
實際上,我們只要用剛纔定義的SIMP_BLKDEV_DATASEGSIZE、SIMP_BLKDEV_DATASEGMASK和SIMP_BLKDEV_DATASEGSHIFT替換原先代碼中的PAGE_SIZE、PAGE_MASK和PAGE_SHIFT就大功告成了,
當然,這個結論是作者是經過充分檢查和實驗後才得出的,希望不要誤認爲編程時可以大大咧咧地隨心所欲。作爲程序員,嚴謹的態度永遠都是需要的。

現在,我們的simp_blkdev_make_request()函數變成了這樣:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
        struct bio_vec *bvec;
        int i;
        unsigned long long dsk_offset;

        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_offset = bio->bi_sector << 9;

        bio_for_each_segment(bvec, bio, i) {
                unsigned int count_done, count_current;
                void *iovec_mem;
                void *dsk_mem;

                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;

                count_done = 0;
                while (count_done < bvec->bv_len) {
                        count_current = min(bvec->bv_len - count_done,
                                (unsigned int)(SIMP_BLKDEV_DATASEGSIZE
                                - ((dsk_offset + count_done) &
                                ~SIMP_BLKDEV_DATASEGMASK)));

                        dsk_mem = radix_tree_lookup(&simp_blkdev_data,
                                (dsk_offset + count_done)
                                >> SIMP_BLKDEV_DATASEGSHIFT);
                        if (!dsk_mem) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": search memory failed: %llu\n",
                                        (dsk_offset + count_done)
                                        >> SIMP_BLKDEV_DATASEGSHIFT);
                                kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                                bio_endio(bio, 0, -EIO);
#else
                                bio_endio(bio, -EIO);
#endif
                                return 0;
                        }
                        dsk_mem += (dsk_offset + count_done)
                                & ~SIMP_BLKDEV_DATASEGMASK;

                        switch (bio_rw(bio)) {
                        case READ:
                        case READA:
                                memcpy(iovec_mem + count_done, dsk_mem,
                                        count_current);
                                break;
                        case WRITE:
                                memcpy(dsk_mem, iovec_mem + count_done,
                                        count_current);
                                break;
                        default:
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": unknown value of bio_rw: %lu\n",
                                        bio_rw(bio));
                                kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                                bio_endio(bio, 0, -EIO);
#else
                                bio_endio(bio, -EIO);
#endif
                                return 0;
                        }
                        count_done += count_current;
                }

                kunmap(bvec->bv_page);
                dsk_offset += 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;
}

本章的到這裏就完成了,接下去我們還是打算試驗一下效果。
其實這個實驗不太好做,因爲linux本身也會隨時分配和釋放頁面,這會影響我們看到的結果。
如果讀者看到的現象與預期不同,這也屬於預期。

不過爲了降低試驗受到linux自身活動影響的可能性,建議試驗開始之前儘可能關閉系統中的服務、不要同時做其它的操作、不要在xwindows中做。
然後我們開始試驗:
先編譯模塊:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step07 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step07/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step07/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step07/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
#

現在看看夥伴系統的情況:
# cat /proc/buddyinfo
Node 0, zone      DMA    288     63     34      0      0      0      0      1      1      1      0
Node 0, zone   Normal   9955   1605     24      1      0      1      1      0      0      0      1
Node 0, zone  HighMem   2036    544     13      6      2      1      1      0      0      0      0
#

加載模塊後再看看夥伴系統的情況:
# insmod simp_blkdev.ko
# cat /proc/buddyinfo
Node 0, zone      DMA    337    140      1      1      1      0      0      0      1      0      0
Node 0, zone   Normal  27888   8859     18      0      0      1      0      0      1      0      0
Node 0, zone  HighMem   1583    544     13      6      2      1      1      0      0      0      0
#

釋放模塊後再看看夥伴系統的情況:
# rmmod simp_blkdev
# cat /proc/buddyinfo
Node 0, zone      DMA    337    140     35      0      0      0      0      1      1      1      0
Node 0, zone   Normal  27888   8860    632      7      0      1      1      0      0      0      1
Node 0, zone  HighMem   1583    544     13      6      2      1      1      0      0      0      0
#

首先補充說明一下夥伴系統對每種類型的內存區域分別管理,這在夥伴系統中稱之爲zone。
在i386中,常見的zone有DMA、Normal和HighMem,分別對應0~16M、16~896M和896M以上的物理內存。
DMA zone的特點是老式ISA設備只能使用這段區域進行DMA操作。
Normal zone的特點它被固定映射在內核的地址空間中,我們可以直接使用指針訪問這段內存。(不難看出,DMA zone也有這個性質)
HighMem zone的特點它沒有以上兩種zone的特點。
其實我們在上文中講述的低端內存區域是這裏的DMA和Normal zone,而高端內存區域是這裏的HighMem zone。

/proc/buddyinfo用於顯示夥伴系統的各個zone中剩餘的各個order的內存段個數。

我們的模塊目前使用低端內存來存儲數據,而一般情況下系統會儘可能保留DMA zone的空域內存不被分配出去,
因此我們主要關注/proc/buddyinfo中的Normal行。
行中的各列中的數字表示夥伴系統的這一區域中每個order的剩餘內存數量。
比如:
Node 0, zone   Normal   9955   1605     24      1      0      1      1      0      0      0      1
這一行表示Normal zone中剩餘9955個獨立的內存頁、1605個連續2個頁的內存、24連續4個頁的內存等。

由於我們現在每次申請4個頁的內存,因此最關注的Normal行的第3列。

首先看模塊加載前,Normal行的第3列數字是24,表示系統中剩餘24個連續4頁的內存區域。
然後我們看模塊加載之後的情況,Normal行的第3列從24變爲了18,減少了6個連續4頁的內存區域。
這說明我們的程序只用掉了6個連續4頁的內存區域------明顯不可能。
因爲作爲模塊編者,我們很清楚程序需要使用1024個連續4頁的內存區域。

繼續看這一行的後面,原先處在最末尾的1便成了0。
我們可以數出來最末尾的數字對應order爲10的連續頁面,也就是連續4M的頁面,原來是空閒的,而現在被拆散用掉了。
但即使它被用掉了,也不夠我們的的16M空間,數字的分析變得越來越複雜,是堅持下去還是就此停止?
這一次我們決定停止,因爲真相是現在進行的模塊加載前後的剩餘內存對比確實產生不了什麼結論。

詳細解釋一下,其實我們可以看出在模塊加載之前,Normal區域中order>=2的全部空閒內存加起來也不夠這個模塊使用。
甚至加上DMA區域中order>=2的全部空閒內存也不夠。
雖然剩餘的order<2的一大堆頁面湊起來倒是足夠,但誰讓我們的模塊挑食,只要order=2的頁面呢。
因此這時候系統會試圖釋放出空閒內存。比如:釋放一些塊設備緩衝頁面,或者將用戶進程的內存轉移到swap中,以獲得更多的空閒內存。
很幸運,系統通過釋放內存操作拿到了足夠的空閒內存使我們的模塊得以順利加載,
但同時由於額外增加出的空閒內存使我們對比模塊加載前後的內存差別失去了意義。

其實細心一些的話,剛纔的對比中,我們還是能夠得到一些結論的,比如,
我們可以注意到模塊加載後order爲0和1的兩個數字的暴增,這就是系統釋放頁面的證明。
詳細來說,系統釋放出的頁面既包含order<2的,也包含order>=2的,但由於其中order>=2的頁面多半被我們的程序拿走了,
這就造成模塊加載後的空閒頁面中大量出現order<2的頁面。

既然我們沒有從模塊加載前後的空閒內存變化中拿到什麼有意義的結論,
我們不妨換條路走,去看看模塊釋放前後空閒內存的變化情況:

首先還是看Normal區域:
order爲0和1的頁面數目基本沒有變化,這容易解釋,因爲我們釋放出的都是order=2的連續頁面。
order=2的連續頁面從18增加到632,增加了614個。這應該是模塊卸載時所釋放的內存的一部分。
由於這個模塊在卸載時,會釋放1024個order=2的連續頁面,那麼我們還要繼續找出模塊釋放的內存中其他部分的行蹤。
也就是1024-614=410個order=2的連續頁到哪去了。

回顧上文中的夥伴系統說明,夥伴系統會適時地合併連續頁面,那麼我們假設一部分模塊釋放出的頁面被合併成更大order的連續頁面了。
讓我們計算一下order>2的頁面的增加情況:
order=3的頁面增加了7個,order=6的頁面增加了1個,order=8的頁面減少了1個,order=10的頁面增加了1個。
這分別相當於order=2的頁面增加14個、增加16、減少64個、增加256個,綜合起來就是增加222個。
這就又找到了一部分,剩下的行蹤不明的頁面還有410-222=188個。

我們繼續追查,現在DMA zone區域。
我們的程序所使用的是低端內存,其實也包含0~16M之間的DMA zone。
剛纔我們說過,系統會儘可能不把DMA區域的內存分配出去,以保證真正到必須使用這部分內存時,能夠拿得出來。
但“儘可能”不代表“絕對不”,如果出現內存不足的情況,DMA zone的空閒內存也很難倖免。
但剛纔我們的試驗中,已經遇到了Normal區域內存不足情況,這時把DMA zone中的公主們拿去充當Normal zone的軍妓也是必然的了。
因此我們繼續計算模塊釋放後DMA區域的內存變化。在DMA區域:
order=2的頁面增加了34個,order=3的頁面減少了1個,order=4的頁面減少了1個,order=7的頁面增加了1個,order=9的頁面增加了1個。
這分別相當於order=2的頁面增加34個、減少2、減少4個、增加32個,增加128個,綜合起來就是增加188個。

數字剛好吻合,我們就找到了模塊釋放出的全部頁面的行蹤。
這也驗證了本章中改動的功能符合預期。

然後我們再一次加載和卸載模塊,同時查看夥伴系統中空閒內存的變化:
# insmod simp_blkdev.ko
# cat /proc/buddyinfo
Node 0, zone      DMA    336    141      0      0      0      1      1      0      1      0      0
Node 0, zone   Normal  27781   8866      0      1      0      1      0      0      1      0      0
Node 0, zone  HighMem   1459    544     13      6      2      1      1      0      0      0      0
#
# rmmod simp_blkdev
# cat /proc/buddyinfo
Node 0, zone      DMA    336    141     35      0      0      0      0      1      1      1      0
Node 0, zone   Normal  27781   8867    633      7      0      1      1      0      0      0      1
Node 0, zone  HighMem   1459    544     13      6      2      1      1      0      0      0      0
#

我們可以發現這一次模塊加載前後的內存變化情況與上一輪有些不同,而分析工作就留給有興趣的讀者了。

本章對代碼的改動量不大,主要說明一下與我們程序中出現的linux內存管理知識。
其實上一章的改動中已經涉及到了這部分知識,只是因爲那時的重點不在這個方面,並且作者也不希望在同一章中加入過多的內容,
因此在本章中做個補足。
同時,本章中的說明也給後續章節中將要涉及到的內容做個準備,這樣讀者在將來也可以愜意一些。

不過在開始寫這一章時,作者曾反覆考慮該不該這樣組織本章,
正如我們曾經說過的,希望讀者在遇到不明白的地方時主動去探索教程之外更多的知識,
而不是僅僅讀完這個教程本身。
本教程的目的是牽引出通過實現一個塊設備驅動程序來牽引出相關的linux的各個知識點,
讓讀者們以此爲契機,通過尋求疑問的答案、通過學習更細節的知識來提高自己的能力。
因此教程中對於不少涉及到的知識點僅僅給出簡單的介紹,因爲讀者完全有能力通過google瞭解更詳細的內容,
這也是作者建議的看書方法。
不過本章是個例外,因爲作者最終認爲對這些知識的介紹對於這部教程的整體性是有幫助的。
但這裏的介紹其實仍然只屬於皮毛,因此還是希望讀者進一步瞭解教程以外的更多知識。

<未完,待續>

第8章

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

本章的目的是讓讀者繼續休息,因此決定仍然搞一些簡單的東西。
比如:給我們的驅動程序模塊加上模塊參數,這樣在加載模塊時,可以通過參數設定塊設備的大小。

給我們模塊加參數的工作不難,這牽涉到1個宏:
module_param_named(name, value, type, perm)
        name是參數的名稱
        value是參數在模塊中對應的變量
        type是參數的類型
        perm是參數的權限
如,在模塊中添加
int disk_size = 1024;
module_param_named(size, disk_size, int, S_IRUGO);
可以給模塊加上名稱爲"size"的參數,如果在加載模塊是使用insmod thismodule size=100,那麼在模塊代碼中disk_size的值就是100。
相反,如果加載模塊時沒有指定參數,那麼模塊代碼中disk_size的值仍是默認的1024。
S_IRUGO指定了這個參數的值在模塊加載以後可以被所有人通過/sys/module/[module_name]/parameters/看到,但無法修改。
好了,有關module_param_named就介紹到這裏,細節可以google或者看linux/include/linux/moduleparam.h。

然後我們就要給這個模塊加個參數,用來在加載時指定塊設備的大小。
參數的名字都已經想好了,就叫size吧,類型嘛,32位無符號整數最大能設定到4G,而我們的野心看起來可能更大一些,
爲了讓這個模塊支持4G以上的虛擬磁盤(當然是內存足夠的情況下),我們打算使用64位無符號整型。這樣能夠設定的最大值爲16777216T,應該夠了吧。

然後我們試圖找出module_param_named的參數中與unsigned long long對應的type來。
結果是:google了,沒找到;看linux/include/linux/moduleparam.h了,還是沒找到。
結論是:目前的linux(2.6.28)還不支持unsigned long long類型的模塊參數。
更新一些的內核中會不會有是將來的事,儘快搞定這一章的功能卻是現在面臨的問題。

然後我們就開始找解決方案:
1:給內核打個補丁,看樣子不錯,但至少今天之類完成不了我們的程序了
   並且這樣一來,我們的程序只能在今後的內核中運行,而失去對舊版linux的兼容性。
2:指定設置磁盤大小的單位爲M。這樣可設置的最大的數字就成了4G*1M,也就是4096T。
   這個主意看似不錯。而且看樣子10年內機器的內存應該到不了這個容量。
3:用字符串來指定大小
   這倒是可以解決所有問題,並且我們可以支持16M、1G之類的設定,讓我們的程序看起來比較花哨。
   缺點應該是我們需要在程序中自己去解析傳入的字符串了,幸運的是,實際的解析代碼比想象的容易一些。
因此,我們採用第3個方案,向模塊中添加一個名稱爲size、類型爲字符串的參數,並且支持解析以K,M,G,T爲單位的設定。

第1步:
  向程序中添加以下參數申明。
  static char *simp_blkdev_param_size = "16M";
  module_param_named(size, simp_blkdev_param_size, charp, S_IRUGO);
  char *simp_blkdev_param_size用於存儲設定的磁盤大小,我們把磁盤大小的默認值指定爲16M。
  目前我們不允許用戶在模塊加載後改變磁盤大小,將來嘛,有可能增加這一功能,看起來很眩。
第2步:
  原來的程序使用
  #define SIMP_BLKDEV_BYTES      (16*1024*1024)
  定義磁盤大小,而現在我們不需要這一行了。
  同時,我們需要一個unsigned long long變量來存儲用戶設定的磁盤大小,因此我們增加這個變量:
  static unsigned long long simp_blkdev_bytes;
  然後把程序中所有使用SIMP_BLKDEV_BYTES的位置換成使用simp_blkdev_bytes變量。
第3步:
  在模塊加載時對模塊參數進行解析,設置simp_blkdev_bytes變量的值。
  我們增加一個函數進行解析工作:
int getparam(void)
{
        char unit;
        char tailc;

        if (sscanf(simp_blkdev_param_size, "%llu%c%c", &simp_blkdev_bytes,
                &unit, &tailc) != 2) {
                return -EINVAL;
        }

        if (!simp_blkdev_bytes)
                return -EINVAL;

        switch (unit) {
        case 'g':
        case 'G':
                simp_blkdev_bytes <<= 30;
                break;
        case 'm':
        case 'M':
                simp_blkdev_bytes <<= 20;
                break;
        case 'k':
        case 'K':
                simp_blkdev_bytes <<= 10;
                break;
        case 'b':
        case 'B':
                break;
        default:
                return -EINVAL;
        }

        /* make simp_blkdev_bytes fits sector's size */
        simp_blkdev_bytes = (simp_blkdev_bytes + (1<<9) - 1) & ~((1ULL<<9) - 1);

        return 0;
}
然後在simp_blkdev_init()中調用這個函數:
ret = getparam();
if (IS_ERR_VALUE(ret))
        goto err_getparam;
當然,err_getparam的位置讀者應該能猜出來了。

這樣一來,工作大概就完成了,讓我們看看結果:
使用默認值:
# insmod simp_blkdev.ko
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won't be recoverable.

Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)

Command (m for help): p

Disk /dev/simp_blkdev: 16 MB, 16777216 bytes
1 heads, 32 sectors/track, 1024 cylinders
Units = cylinders of 32 * 512 = 16384 bytes

           Device Boot      Start         End      Blocks   Id  System

Command (m for help): q

#
設定成20M:
# rmmod simp_blkdev
# insmod simp_blkdev.ko size=20M
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won't be recoverable.


The number of cylinders for this disk is set to 1280.
There is nothing wrong with that, but this is larger than 1024,
and could in certain setups cause problems with:
1) software that runs at boot time (e.g., old versions of LILO)
2) booting and partitioning software from other OSs
   (e.g., DOS FDISK, OS/2 FDISK)
Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)

Command (m for help): p

Disk /dev/simp_blkdev: 20 MB, 20971520 bytes
1 heads, 32 sectors/track, 1280 cylinders
Units = cylinders of 32 * 512 = 16384 bytes

           Device Boot      Start         End      Blocks   Id  System

Command (m for help): q

#
變態一下,還是設定成20M,但用k作單位:
# rmmod simp_blkdev
# insmod simp_blkdev.ko size=20480k
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won't be recoverable.


The number of cylinders for this disk is set to 1280.
There is nothing wrong with that, but this is larger than 1024,
and could in certain setups cause problems with:
1) software that runs at boot time (e.g., old versions of LILO)
2) booting and partitioning software from other OSs
   (e.g., DOS FDISK, OS/2 FDISK)
Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)

Command (m for help): p

Disk /dev/simp_blkdev: 20 MB, 20971520 bytes
1 heads, 32 sectors/track, 1280 cylinders
Units = cylinders of 32 * 512 = 16384 bytes

           Device Boot      Start         End      Blocks   Id  System

Command (m for help): q

#

看樣子結果不錯。
這一章中基本上沒有提到什麼比較晦澀的知識,而且看樣子通過這一章的學習,大家也應該休息好了。
如果讀者現在感覺到精神百倍,那麼這一章的目的應該就達到了。

<未完,待續>

第9章

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

在本章中我們來討論一下這個驅動程序的數據安全,
因爲最近的一些事情讓作者愈發地感覺到數據泄漏對當事人來說是麻煩的。

我們開門見山的解釋一下數據安全問題:
內核常常會向用戶態傳遞數據,而作爲內核程序的開發者,我們必須意識到不能把包含意料內容之外的數據隨便透露給用戶態,
因爲如果這些數據不巧被別有用心者利用,就會帶來不少麻煩。
比如陳冠希就犯了這樣的錯誤。新餘市出國考察團也沒有在陳冠希身上吸取教訓,把單據也不當回事兒。
單據對於考察團而言並不是什麼重要的玩意兒,但一旦落到“別有用心”的人手中被加以利用,就不得不當一回事了。
由此我們發現了單據的商業價值。
今後在旅遊公司幹過的員工拿着手頭攢到的大量單據,可能會比KIRA更有前途。
因此公務員確實屬於高風險職業,加薪也是情理當中的了。

對於內核而言,其中的數據也是如此。
即使一些數據對內核而言沒有價值,但也不能隨意地向用戶態傳遞,因爲這段內存中可能不巧包含了不能隨意讓用戶獲取的數據,
比如用戶A使用linux整理他女友的裸照文件,裸照的數據很可能存在於用戶A的進程的虛存中,也可能還存在於文件緩存中,
A的進程結束後,系統回收了進程的內存,這時內存中的數據被系統認定爲無效數據,但系統並沒有清空這段數據。
A打開的文件的緩存也類似,緩存被系統回收後,內存中的數據並沒有被清除。
隨後用戶B使用了我們的塊設備驅動程序。驅動程序初始化時需要獲取足夠的內存以存儲塊設備中的數據,
系統很可能將用戶A使用過的那段包含裸照數據的內存分配給我們的塊設備驅動程序。
這時如果用戶B老老實實分區、創建文件系統、寫入文件,這當然沒事,
但如果用戶B別有用心的上來就直接去讀塊設備中的數據,那麼他可能很幸運的看到不該看的東西。

因此我們咬牙切齒,嫉妒心促使我們修改這個塊設備驅動,我們都沒遇到的好事兒,也決不允許用戶B遇到。
修改的方法很簡單,我們申請內存時使用了__get_free_pages()函數,
這個函數的第一個參數是gfp_mask,原先我們傳遞的是GFP_KERNEL,表示用於內核中的一般情況。
現在我們只要向gfp_mask中添加__GFP_ZERO標誌,以提示需要申請清0後的內存。
這樣驅動程序加載後,塊設備中數據的初始值全爲0,這就避免了上文中提到的安全問題。
詳細來說,就是把alloc_diskmem()函數中的
p = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO,
這一行改成
p = (void *)__get_free_pages(GFP_KERNEL,

安全方面的改動已經完成了,但爲了避免讀者認爲本章偷工減料,我們再多改一些代碼。

塊設備中每扇區的數據長度爲512字節,我們在驅動程序經常遇到與此相關的轉換。
爲了快速運算,我們經常用到9這個常數,比如:
乘以512就是左移9、除以512就是右移9、除以512的餘數就是& ((1ULL<<9) - 1)、
向上對齊到512的倍數就是加上(1<<9) - 1再& ~((1ULL<<9) - 1)。

不過現在我們決定通過定義幾個宏來吧這些操作寫得好看一些。
先定義:
#define SIMP_BLKDEV_SECTORSHIFT        (9)
#define SIMP_BLKDEV_SECTORSIZE        (1ULL<<SIMP_BLKDEV_SECTORSHIFT)
#define SIMP_BLKDEV_SECTORMASK        (~(SIMP_BLKDEV_SECTORSIZE-1))

然後使用這幾個宏來進行扇區相關的轉換工作。

詳細來說,就是把simp_blkdev_make_request()函數中的:
if ((bio->bi_sector << 9) + bio->bi_size > simp_blkdev_bytes) {
改成
if ((bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT) + bio->bi_size
        > simp_blkdev_bytes) {

dsk_offset = bio->bi_sector << 9;
改成
dsk_offset = bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT;

把simp_blkdev_getgeo()函數中的:
geo->cylinders = simp_blkdev_bytes>>9/geo->heads/geo->sectors;
改成
geo->cylinders = simp_blkdev_bytes >> SIMP_BLKDEV_SECTORSHIFT
        / geo->heads / geo->sectors;

把getparam()函數中的:
simp_blkdev_bytes = (simp_blkdev_bytes + (1<<9) - 1) & ~((1ULL<<9) - 1);
改成
simp_blkdev_bytes = (simp_blkdev_bytes + SIMP_BLKDEV_SECTORSIZE - 1)
        & SIMP_BLKDEV_SECTORMASK;

把simp_blkdev_init()函數中的:
set_capacity(simp_blkdev_disk, simp_blkdev_bytes>>9);
改成
set_capacity(simp_blkdev_disk,
        simp_blkdev_bytes >> SIMP_BLKDEV_SECTORSHIFT);

如果運氣不算太背的話,程序應該是能夠運行的,讓我們試試:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step09 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step09/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step09/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step09/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#
看一看驅動程序剛剛加載時裏面的數據:
# hexdump /dev/simp_blkdev -vn512
0000000 0000 0000 0000 0000 0000 0000 0000 0000
0000010 0000 0000 0000 0000 0000 0000 0000 0000
0000020 0000 0000 0000 0000 0000 0000 0000 0000
0000030 0000 0000 0000 0000 0000 0000 0000 0000
0000040 0000 0000 0000 0000 0000 0000 0000 0000
0000050 0000 0000 0000 0000 0000 0000 0000 0000
0000060 0000 0000 0000 0000 0000 0000 0000 0000
0000070 0000 0000 0000 0000 0000 0000 0000 0000
0000080 0000 0000 0000 0000 0000 0000 0000 0000
0000090 0000 0000 0000 0000 0000 0000 0000 0000
00000a0 0000 0000 0000 0000 0000 0000 0000 0000
00000b0 0000 0000 0000 0000 0000 0000 0000 0000
00000c0 0000 0000 0000 0000 0000 0000 0000 0000
00000d0 0000 0000 0000 0000 0000 0000 0000 0000
00000e0 0000 0000 0000 0000 0000 0000 0000 0000
00000f0 0000 0000 0000 0000 0000 0000 0000 0000
0000100 0000 0000 0000 0000 0000 0000 0000 0000
0000110 0000 0000 0000 0000 0000 0000 0000 0000
0000120 0000 0000 0000 0000 0000 0000 0000 0000
0000130 0000 0000 0000 0000 0000 0000 0000 0000
0000140 0000 0000 0000 0000 0000 0000 0000 0000
0000150 0000 0000 0000 0000 0000 0000 0000 0000
0000160 0000 0000 0000 0000 0000 0000 0000 0000
0000170 0000 0000 0000 0000 0000 0000 0000 0000
0000180 0000 0000 0000 0000 0000 0000 0000 0000
0000190 0000 0000 0000 0000 0000 0000 0000 0000
00001a0 0000 0000 0000 0000 0000 0000 0000 0000
00001b0 0000 0000 0000 0000 0000 0000 0000 0000
00001c0 0000 0000 0000 0000 0000 0000 0000 0000
00001d0 0000 0000 0000 0000 0000 0000 0000 0000
00001e0 0000 0000 0000 0000 0000 0000 0000 0000
00001f0 0000 0000 0000 0000 0000 0000 0000 0000
0000200
#
對比一下修改前的效果:
# hexdump /dev/simp_blkdev -vn512
0000000 f300 0800 1200 0000 b804 1200 0000 0500
0000010 501a 6930 1806 246a bf0a 7700 256a bf0b
0000020 1f80 256b bf0b 47a0 266b bf0b 0ff0 246a
0000030 bf0a 1708 ffff 00ff 5028 256b bf0b 00a8
0000040 ffff 00ff 04b8 ffff 00ff 10c8 256b bf0b
0000050 00e8 246a bf0a 0229 ffff 00ff 1339 ffff
0000060 00ff 0059 246a bf0a 1669 ffff 00ff 12a9
0000070 256b bf0b 02c9 ffff 00ff 12d9 246a bf0a
0000080 215a ffff 00ff 302c 256b bf0b 03ac ffff
0000090 00ff 10cc 256b bf0b 03ec 246a bf0a 522d
00000a0 256b bf0b 32bd 2318 266b bf0c 2700 266c
00000b0 bf0c 2730 276c bf0c 1f60 276c bf0d 3580
00000c0 276d bf0d 1bc0 286d bf0d 05e0 286d bf0e
00000d0 04f0 ffff 00ff 07f5 276c bf0d 0186 ffff
00000e0 00ff 1596 276c bf0d 01b6 ffff 00ff 15e6
00000f0 266b bf0c 0708 266b bf0c 0018 ffff 00ff
0000100 0428 ffff 00ff 1038 266c bf0c 0058 ffff
0000110 00ff 3088 ffff 00ff 1219 266c bf0c 0239
0000120 ffff 00ff 1249 276c bf0d 0689 276c bf0d
0000130 02b9 266b bf0c 031c ffff 00ff 103c 266c
0000140 bf0c 035c 276c bf0d 039c ffff 00ff 20ac
0000150 276d bf0d 03dc 286d bf0d 03ec 266b bf0c
0000160 022d 266c bf0c 223d 276c bf0d 12ad 276d
0000170 bf0d 12cd 286d bf0e 02fd 2b18 286d bf0e
0000180 4400 296e bf0e 1450 296e bf0f 4470 2a6e
0000190 bf0f 14c0 2a6f bf0f 04e0 2a6f bf10 04f0
00001a0 ffff 00ff 2005 286d bf0e 1035 ffff 00ff
00001b0 5055 296e bf0f 0ab5 ffff 00ff 30c5 286d
00001c0 bf0e 1006 ffff 00ff 1426 286d bf0e 0946
00001d0 ffff 00ff 1056 296e bf0f 0176 ffff 00ff
00001e0 1186 296e bf0f 14a6 2a6e bf0f 05c6 ffff
00001f0 00ff 16d6 2a6f bf10 05f6 286d bf0e 0007
0000200
#

本章到此結束,讀者是不是感覺我們的教程越來越簡單了?

<未完,待續>

第10章

如果你的linux系統是x86平臺,並且內存大於896M,那麼恭喜你,我們大概可以在這個實驗中搞壞你的系統。
反之如果你的系統不符合這些條件,也不用爲無法搞壞系統而感到失望,本章的內容同樣適合你。
這時作者自然也要申明一下對讀者產生的任何損失概不負責,
因爲這年頭一不小心就可能差點成了被告,比如南京的彭宇和鎮江花山灣的小許姑娘。

在實驗看到的情況會因爲系統的實際狀況不同而稍有區別,但我們需要說明的問題倒是相似的。
但希望讀者不要把這種相似理解成了ATM機取款17.5萬和貪污2.6億在判決上的那種相似。

首先我們來看看目前系統的內存狀況:
# cat /proc/meminfo
MemTotal:      1552532 kB
MemFree:       1529236 kB
Buffers:          2716 kB
Cached:          10124 kB
SwapCached:          0 kB
Active:           8608 kB
Inactive:         7664 kB
HighTotal:      655296 kB
HighFree:       640836 kB
LowTotal:       897236 kB
LowFree:        888400 kB
SwapTotal:      522104 kB
SwapFree:       522104 kB
Dirty:              44 kB
Writeback:           0 kB
AnonPages:        3440 kB
Mapped:           3324 kB
Slab:             2916 kB
SReclaimable:      888 kB
SUnreclaim:       2028 kB
PageTables:        272 kB
NFS_Unstable:        0 kB
Bounce:              0 kB
WritebackTmp:        0 kB
CommitLimit:   1298368 kB
Committed_AS:    10580 kB
VmallocTotal:   114680 kB
VmallocUsed:       392 kB
VmallocChunk:   114288 kB
HugePages_Total:     0
HugePages_Free:      0
HugePages_Rsvd:      0
HugePages_Surp:      0
Hugepagesize:     4096 kB
DirectMap4k:     12288 kB
DirectMap4M:    905216 kB
#
輸出很多,但我們只關心這幾行:
MemFree:       1529236 kB --這說明系統中有接近1.5G的空閒內存
HighFree:       640836 kB --這說明空閒內存中,處在高端的有600M左右
LowFree:        888400 kB --這說明空閒內存中,處在低端的有800M左右

現在加載上一章完成的模塊,我們指定創建800M的塊設備:
# insmod simp_blkdev.ko size=800M
#
成功了,我們再看看內存狀況:
# cat /proc/meminfo
MemFree:        708812 kB
HighFree:       640464 kB
LowFree:         68348 kB
...
#
我們發現高端內存沒怎變,低端內存卻已經被耗得差不多了。
我們一不做二不休,繼續加大塊設備的容量,看看極限能到多少:
# rmmod simp_blkdev
# insmod simp_blkdev.ko size=860M
# cat /proc/meminfo
MemFree:        651184 kB
HighFree:       641972 kB
LowFree:          9212 kB
...
#
系統居然還沒事,這時雖然高端內存還是沒怎麼變,但低端內存剩下的得已經很可憐了。
然後進一步加大塊設備的容量:
# rmmod simp_blkdev
# insmod simp_blkdev.ko size=870M
...
這裏不用再cat /proc/meminfo了,因爲系統已經完蛋了。
如果有些讀者嗜好獨特,對出錯信息情有獨鍾的話,在這裏也滿足一下:
kernel: [ 3588.769050] insmod invoked oom-killer: gfp_mask=0x80d0, order=2, oomkilladj=0
kernel: [ 3588.769516] Pid: 4236, comm: insmod Tainted: G        W 2.6.27.4 #53
kernel: [ 3588.769868]  [<c025e61e>] oom_kill_process+0x42/0x183
kernel: [ 3588.771041]  [<c025ea5c>] out_of_memory+0x157/0x188
kernel: [ 3588.771306]  [<c0260a5c>] __alloc_pages_internal+0x2ab/0x360
kernel: [ 3588.771500]  [<c0260b25>] __get_free_pages+0x14/0x24
kernel: [ 3588.771679]  [<f8865204>] alloc_diskmem+0x45/0xb5 [simp_blkdev]
kernel: [ 3588.771899]  [<f8867054>] simp_blkdev_init+0x54/0xc6 [simp_blkdev]
kernel: [ 3588.772217]  [<c0201125>] _stext+0x3d/0xff
kernel: [ 3588.772393]  [<f8867000>] ? simp_blkdev_init+0x0/0xc6 [simp_blkdev]
kernel: [ 3588.772599]  [<c0235f2f>] ? __blocking_notifier_call_chain+0x40/0x4c
kernel: [ 3588.772845]  [<c0241771>] sys_init_module+0x87/0x19d
kernel: [ 3588.773250]  [<c02038cd>] sysenter_do_call+0x12/0x21
kernel: [ 3588.773884]  =======================
kernel: [ 3588.774237] Mem-Info:
kernel: [ 3588.774241] DMA per-cpu:
kernel: [ 3588.774404] CPU    0: hi:    0, btch:   1 usd:   0
kernel: [ 3588.774582] Normal per-cpu:
kernel: [ 3588.774689] CPU    0: hi:  186, btch:  31 usd:   0
kernel: [ 3588.774870] HighMem per-cpu:
kernel: [ 3588.778602] CPU    0: hi:  186, btch:  31 usd:   0
...

搞壞系統就當是交學費了,但交完學費我們總要學到些東西。
雖然公款出國考察似乎已經斯通見慣,但至少在我們的理解中,學費不是旅遊費,更不是家屬的旅遊費。

我們通過細心觀察、周密推理後得出的結論是:
目前的塊設備驅動程序會一根筋地使用低端內存,即使系統中低端內存很緊缺的時候,
也會直道把系統搞死卻不去動半點的高端內存,這未免也太挑食了,
因此在本章和接下來的幾章中,我們將幫助驅動程序戒掉對低端內存的癮。

相對高端內存而言,低端內存是比較寶貴的,這是因爲它不需要影射就能直接被內核訪問的特性。
而內核中的不少功能都直接使用低端內存,以保證訪問的速度和簡便,
但換句話來說,如果低端內存告急,那麼系統可能離Panic也不遠了。
因此總的來說,對低端內存的使用方法大概應該是:除非有足夠理由,否則就別亂佔着。
詳細來說,就是:
1:不需要使用低端內存的“在內核中不需要映射就能直接訪問”這個特性的功能,應該優先使用高端內存
   如:分配給用戶態進程的內存,和vmalloc的內存
2:需要佔用大量內存的功能,並且也可以通過高端內存實現的,應該優先使用高端內存
   如:我們的程序

與內存有關的知識我們在以前的章節中已經談到,因此這裏不再重複了,
但需要說明的是在高端內存被映射之前,我們是無法通過指針來指向它的。
因爲它不在內核空間的地址範圍以內。

雖然如此,我們卻無論如何都需要找出一種方法來指定一個沒有被映射的高端內存,
這是由於至少在進行映射操作時,我們需要指定去映射誰。
這就像爲一羣猴子取名的時候,如何來說明是正在給哪隻猴子取名一樣。

雖然給猴子取名的問題可能比較容易解決,比如我們可以說,
給哪隻紅屁股的公猴取名叫齊天大聖、給那隻瘦瘦的母猴取名叫白晶晶,
但可惜一塊高端內存即沒有紅屁股,又沒有胖瘦之分,
它們唯一有的就是地址,因此我們也必須通過地址來指定這段高端內存。

剛纔說過,在高端內存被映射之前,他在內核的地址空間中是不存在的,
但雖然如此,它至少存在其物理地址,而我們正是可以通過它的物理地址來指定它。
是的,本質上是這樣的,但在linux中,我們還需要再繞那麼一丁點:
linux在啓動階段爲全部物理內存按頁爲單位建立了的對應的struct page結構,用來管理這些物理內存,
也就是,每個頁的物理內存,都有着1對1的struct page結構,而這些struct page結構是位於低端內存中的,
我們只要使用指向某個struct page結構的指針,就能指定物理內存中的一個頁。
因此,對於沒有被映射到內核空間中的高端內存,我們可以通過對應的struct page結構來指定它。
(如果讀者希望瞭解更詳細的知識,可以考慮從virt_to_page函數一路google下去)

我們在這裏大肆談論高端內存的表示方法,因爲這是讓我們的模塊使用高端內存的前提。

我們的驅動程序使用多段內存來存儲塊設備中的數據。
原先的程序中,我們使用指向這些內存段的指針來指定這些數據的位置,這是沒有問題的,
因爲當時我們是使用__get_free_pages()來申請內存,__get_free_pages()函數只能用來申請低端內存,
因爲這個函數返回的是申請到的內存的指針,而上文中說過,高端內存是不能用這樣的指針表示的。
要申請高端內存,明顯不能使用這樣的函數,因此我們隆重介紹它的代替者出場:
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
這個函數的參數與__get_free_pages()相同,但區別在於,它返回指向struct page的指針,
這個我們在上文中介紹過的指針賦予了alloc_pages()函數申請高端內存的能力。
其實申請一塊高端內存並不難,只要使用__GFP_HIGHMEM參數調用alloc_pages()函數,
就可能返回一塊高端內存,之所以說是“可能”,使因爲在某些情況下,比如高端內存不夠或不存在時,也會但會低端內存充數。

我們的現在的目標是讓驅動程序使用高端內存,這需要:
1:讓驅動程序申請高端內存
2:讓驅動程序使用高端內存

但在這一章中,我們要做的即不是1,也不是2,而是1之前的準備工作。
因爲1和2必須一氣呵成地改完,而爲了讓一氣呵成的時候不要再面臨其他插曲,
我們需要做好充足的準備工作,就像ml前尿尿一樣。
對應到程序的修改工作上,我們打算先讓程序使用struct page *來指定申請到的內存。

要實現這個目的,我們先要改申請內存的函數,也就是alloc_diskmem()。
剛纔我們介紹過alloc_pages(),現在就要用它了:

首先把函數中定義的
void *p;
改成
struct page *page;
因爲我們要使用struct page *來指定申請到的內存,而不是地址了。

然後把
p = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO, SIMP_BLKDEV_DATASEGORDER);
改成
page = alloc_pages(GFP_KERNEL | __GFP_ZERO, SIMP_BLKDEV_DATASEGORDER);
這一行改動的原因大概已經說得很詳細了。

還有那個if(!p)改成if (!page)

然後就是把指針加入基樹的那一行:
ret = radix_tree_insert(&simp_blkdev_data, i, p);
改成
ret = radix_tree_insert(&simp_blkdev_data, i, page);

由於我們使用了struct page *來指定申請到的內存,因此錯誤處理部分也要小改一下:
free_pages((unsigned long)p, SIMP_BLKDEV_DATASEGORDER);
改成
__free_pages(page, SIMP_BLKDEV_DATASEGORDER);
這裏補充介紹一下__free_pages()函數,可能大家已經猜到其作用了,
其實與我們原先使用的free_pages()函數相似,都是用來釋放一段內存,
但__free_pages()使用struct page *來指定要釋放的內存,這也意味着它能夠用來釋放高端內存。

大家應該已經發現我們雖然改用alloc_pages()函數來申請內存,但並沒有指定__GFP_HIGHMEM參數,
這時申請到的仍然是低端內存,因此避免了在這一章中對訪問內存那部分代碼的大肆改動。

改動過的alloc_pages()函數是這樣的:
int alloc_diskmem(void)
{
        int ret;
        int i;
        struct page *page;

        INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);

        for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1)
                >> SIMP_BLKDEV_DATASEGSHIFT; i++) {
                page = alloc_pages(GFP_KERNEL | __GFP_ZERO,
                        SIMP_BLKDEV_DATASEGORDER);
                if (!page) {
                        ret = -ENOMEM;
                        goto err_alloc;
                }

                ret = radix_tree_insert(&simp_blkdev_data, i, page);
                if (IS_ERR_VALUE(ret))
                        goto err_radix_tree_insert;
        }
        return 0;

err_radix_tree_insert:
        __free_pages(page, SIMP_BLKDEV_DATASEGORDER);
err_alloc:
        free_diskmem();
        return ret;
}

相應的,釋放內存用的free_diskmem()函數也需要一些更改,
爲了避免有人說作者唐僧,列出修改後的樣子應該已經足夠了:
void free_diskmem(void)
{
        int i;
        struct page *page;

        for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1)
                >> SIMP_BLKDEV_DATASEGSHIFT; i++) {
                page = radix_tree_lookup(&simp_blkdev_data, i);
                radix_tree_delete(&simp_blkdev_data, i);
                /* free NULL is safe */
                __free_pages(page, SIMP_BLKDEV_DATASEGORDER);
        }
}

隨後是simp_blkdev_make_request()函數:

首先我們不是把void *dsk_mem改成struct page *dsk_page,而是增加一個
struct page *dsk_page;
變量,因爲在訪問內存時,我們還是需要用到dsk_mem變量的。

然後是從基數中獲取指針的代碼,把原先的
dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT);
改成
dsk_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT);
雖然看起來沒什麼太大變化,但我們需要知道,這時基樹返回的指針已經不是直接指向數據所在的內存了。

還有那個判斷是否從基樹中獲取成功的
if (!dsk_mem) {
用腳丫子也能想得出應該改成這樣:
if (!dsk_page) {

還有就是我們需要首先將struct page *dsk_page地址轉換成內存的地址後,才能對這塊內存進行訪問。
這裏我們使用了page_address()函數。
這個函數可以獲得struct page數據結構所對應內存的地址。
這時可能有讀者要問了,如果這個struct page對應的是高端內存,那麼如何返回地址呢?
實際上,這種情況下如果高端內存中的頁面已經被映射到內核的地址空間,那麼函數會返回映射到內核空間中的地址,
而如果沒有映射的話,函數將返回0。
對於我們目前的程序而言,由於使用的是低端內存,因此struct page對應的內存總是處於內核地址空間中的。

對應到代碼中,我們需要在使用dsk_mem之前,也就是
dsk_mem += (dsk_offset + count_done) & ~SIMP_BLKDEV_DATASEGMASK;
這條語句之前,讓dsk_mem指向struct page *dsk_page對應的內存的實際地址。
這是通過如下代碼實現的:

dsk_mem = page_address(dsk_page);
if (!dsk_mem) {
        printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                ": get page's address failed: %p\n",
                dsk_page);
        kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
        bio_endio(bio, 0, -EIO);
#else
        bio_endio(bio, -EIO);
#endif

總的來說,修改後的simp_blkdev_make_request()函數是這樣的:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
        struct bio_vec *bvec;
        int i;
        unsigned long long dsk_offset;

        if ((bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT) + 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_offset = bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT;

        bio_for_each_segment(bvec, bio, i) {
                unsigned int count_done, count_current;
                void *iovec_mem;
                struct page *dsk_page;
                void *dsk_mem;

                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;

                count_done = 0;
                while (count_done < bvec->bv_len) {
                        count_current = min(bvec->bv_len - count_done,
                                (unsigned int)(SIMP_BLKDEV_DATASEGSIZE
                                - ((dsk_offset + count_done) &
                                ~SIMP_BLKDEV_DATASEGMASK)));

                        dsk_page = radix_tree_lookup(&simp_blkdev_data,
                                (dsk_offset + count_done)
                                >> SIMP_BLKDEV_DATASEGSHIFT);
                        if (!dsk_page) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": search memory failed: %llu\n",
                                        (dsk_offset + count_done)
                                        >> SIMP_BLKDEV_DATASEGSHIFT);
                                kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                                bio_endio(bio, 0, -EIO);
#else
                                bio_endio(bio, -EIO);
#endif
                                return 0;
                        }

                        dsk_mem = page_address(dsk_page);
                        if (!dsk_mem) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": get page's address failed: %p\n",
                                        dsk_page);
                                kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                                bio_endio(bio, 0, -EIO);
#else
                                bio_endio(bio, -EIO);
#endif
                        }

                        dsk_mem += (dsk_offset + count_done)
                                & ~SIMP_BLKDEV_DATASEGMASK;

                        switch (bio_rw(bio)) {
                        case READ:
                        case READA:
                                memcpy(iovec_mem + count_done, dsk_mem,
                                        count_current);
                                break;
                        case WRITE:
                                memcpy(dsk_mem, iovec_mem + count_done,
                                        count_current);
                                break;
                        default:
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": unknown value of bio_rw: %lu\n",
                                        bio_rw(bio));
                                kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                                bio_endio(bio, 0, -EIO);
#else
                                bio_endio(bio, -EIO);
#endif
                                return 0;
                        }
                        count_done += count_current;
                }

                kunmap(bvec->bv_page);
                dsk_offset += 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;
}


通過對這3個函數的更改,代碼可以使用struct page *來定位存儲塊設備數據的內存了。
這也爲將來使用高端內存做了一部分準備。

因爲本章修改的代碼在外部功能上沒有發生變動,所以我們就不在這裏嘗試編譯了運行代碼了。
不過感興趣的讀者不妨試一試這段代碼能不能進行編譯和會不會引起死機。

<未完,待續>

第11章

本章中我們仍然爲塊設備驅動程序使用高端內存做準備工作。
這裏要進行的準備工作並不意味着要增加或改變什麼功能,
而是要收拾一部分代碼,因爲它們看起來已經有點複雜了。

有編程經驗的讀者大概能夠意識到,編程時最常做的往往不是輸入程序,而是拷貝-粘貼。
這是由於我們在編程時可能會不斷地發現設計上的問題,或意識到還可以採用更好的結構,然後當然是實現它。
當然,更理想的情況大概是在一開始規劃時就確定一個最佳的結構,以避免將來的更改,
但事實往往會與理想背道而馳,但關鍵是我們發現這種苗頭時要及時糾正,而不是像某些部門一樣去得過且過大事化小來掩蓋問題。
要知道,酒是越陳越香,而垃圾卻是越捂越臭,如果我們無法在最初做出完美的設計,至少我們還擁有糾正的勇氣。

這裏讀者可能已經感覺到了,這裏我們將要修改simp_blkdev_make_request()函數,因爲它顯得有些大了,
以至於在前幾章中對其進行修改時,不得不列出大段的代碼來展示修改結果。
不過這不是主要原因,相對於縮短函數長度來說,我們分割函數時可能更加在意的是提高代碼的可讀性。

其實這裏分割simp_blkdev_make_request()也是爲了將來實現對高端內存的支持,
因爲訪問高端內存無疑將牽涉到頁面映射問題,而頁面映射的處理又牽涉到了這個函數,
因此我們也希望把這部分功能獨立出來,以免動戳就改動這個大函數,
也可能是爲了作者的偏好,因爲作者作者哪怕是改動函數中的一個字符,也會把整個函數從頭到尾檢查一番,
以確定這次改動不會產生其他影響,這就解釋了作者爲什麼更加偏愛簡單一些的函數了。
當然這種偏好也不一定完全是好事,比如前兩天選擇液晶電視時,作者就趨向於顯示器+機頂盒...

對於一直堅持到這一章的讀者而言,應該對simp_blkdev_make_request()函數的功能爛熟於心了,
因此我們直接列出修改後的代碼:

static int simp_blkdev_trans_oneseg(struct page *start_page,
                unsigned long offset, void *buf, unsigned int len, int dir)
{
        void *dsk_mem;

        dsk_mem = page_address(start_page);
        if (!dsk_mem) {
                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                        ": get page's address failed: %p\n", start_page);
                return -ENOMEM;
        }
        dsk_mem += offset;

        if (!dir)
                memcpy(buf, dsk_mem, len);
        else
                memcpy(dsk_mem, buf, len);

        return 0;
}

static int simp_blkdev_trans(unsigned long long dsk_offset, void *buf,
                unsigned int len, int dir)
{
        unsigned int done_cnt;
        struct page *this_first_page;
        unsigned int this_off;
        unsigned int this_cnt;

        done_cnt = 0;
        while (done_cnt < len) {
                /* iterate each data segment */
                this_off = (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK;
                this_cnt = min(len - done_cnt,
                        (unsigned int)SIMP_BLKDEV_DATASEGSIZE - this_off);

                this_first_page = radix_tree_lookup(&simp_blkdev_data,
                        (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
                if (!this_first_page) {
                        printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                ": search memory failed: %llu\n",
                                (dsk_offset + done_cnt)
                                >> SIMP_BLKDEV_DATASEGSHIFT);
                        return -ENOENT;
                }

                if (IS_ERR_VALUE(simp_blkdev_trans_oneseg(this_first_page,
                        this_off, buf + done_cnt, this_cnt, dir)))
                        return -EIO;

                done_cnt += this_cnt;
        }

        return 0;
}

static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
        int dir;
        unsigned long long dsk_offset;
        struct bio_vec *bvec;
        int i;
        void *iovec_mem;

        switch (bio_rw(bio)) {
        case READ:
        case READA:
                dir = 0;
                break;
        case WRITE:
                dir = 1;
                break;
        default:
                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                        ": unknown value of bio_rw: %lu\n", bio_rw(bio));
                goto bio_err;
        }

        if ((bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT) + 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);
                goto bio_err;
        }

        dsk_offset = bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT;

        bio_for_each_segment(bvec, bio, i) {
                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
                if (!iovec_mem) {
                        printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                ": map iovec page failed: %p\n", bvec->bv_page);
                        goto bio_err;
                }

                if (IS_ERR_VALUE(simp_blkdev_trans(dsk_offset, iovec_mem,
                        bvec->bv_len, dir)))
                        goto bio_err;

                kunmap(bvec->bv_page);

                dsk_offset += 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;

bio_err:
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
        bio_endio(bio, 0, -EIO);
#else
        bio_endio(bio, -EIO);
#endif
        return 0;
}


代碼在功能上與原先沒什麼不同,
我們只是從中抽象出處理塊設備與一段連續內存之間數據傳輸的simp_blkdev_trans()函數,
和同樣功能的、但數據長度符合塊設備數據塊長度限制的simp_blkdev_trans_oneseg()函數。

這樣一來,程序的結構就比較明顯了:
simp_blkdev_make_request()負責決定數據傳輸方向、檢查bio請求是否合法、遍歷bio中的每個bvec、映射bvec中的內存頁,
然後把剩餘的工作扔給simp_blkdev_trans(),
而simp_blkdev_trans()函數通過分割請求數據搞定了數據跨越多個塊設備數據塊的問題,並且順便把塊設備數據塊的第一個page給找了出來,
然後邀請simp_blkdev_trans_oneseg()函數出場。
simp_blkdev_trans_oneseg()函數是幸運的,因爲前期的大多數鋪墊工作已經做完了,而它只要像領導種樹一樣裝模作樣的添上最後一剷土,
就可以迎來開熱烈的掌聲。實際上,simp_blkdev_trans_oneseg()拿到page指針對應的內存,然後根據給定的數據方向執行指定長度的數據傳輸。
simp_blkdev_trans_oneseg()不需要關心數據長度是否超出塊設備數據塊邊界的問題,正如領導也不會去管那棵樹的死活一樣。

本章的代碼也同樣不做實驗,因爲我們確實也沒什麼好做的。
至於能不能通過編譯,作者已經試過了,有興趣的讀者大概可以驗證一下前一句話是不是真的。

作爲支持高端內存的前奏,前一章和本章中做了一些可能讓人覺得莫名其妙的改動。
不過到此爲止,準備工作已經做得差不多了,我們的程序已經爲支持高端內存打下堅實的基礎。
下一章將進入正題,我們將實現這一期盼已久的功能。

<未完,待續>

第12章

本章中我們將實現對高端內存的支持。

女孩子相處時,和她聊天,逛街,爬山,看電影,下棋中的每一件事情好像都與結婚扯不上太大的關係,
但經過天天年年的日積月累後,女孩子在潛意識中可能已經把你看成了她生活的一部分,
最終的結果顯得是那麼的自然,甚至連求婚都有些多餘了。

學習也很相似,我們認真學習的的每一樣知識,努力尋求的每一個答案就其本身而言,
都不能讓自己成爲專家,但專家卻無一不是經歷了長時間的認真學習,
努力鑽研和細緻思考的結果。

正如我們的程序,經歷了前幾章中的準備工作,離目標功能的距離大概也不算太遠了。
而現在我們要做得就是實現它。

首先改動alloc_diskmem()函數,給這個函數中申請內存的語句、也就是alloc_pages()的gfp_mask中加上__GFP_HIGHMEM標誌,
這使得申請塊設備的內存塊時,會優先考慮使用高端內存。
修改過的函數如下:
int alloc_diskmem(void)
{
        int ret;
        int i;
        struct page *page;

        INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);

        for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1)
                >> SIMP_BLKDEV_DATASEGSHIFT; i++) {
                page = alloc_pages(GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,
                        SIMP_BLKDEV_DATASEGORDER);
                if (!page) {
                        ret = -ENOMEM;
                        goto err_alloc;
                }

                ret = radix_tree_insert(&simp_blkdev_data, i, page);
                if (IS_ERR_VALUE(ret))
                        goto err_radix_tree_insert;
        }
        return 0;

err_radix_tree_insert:
        __free_pages(page, SIMP_BLKDEV_DATASEGORDER);
err_alloc:
        free_diskmem();
        return ret;
}

不過事情還沒有全部做完,拿到了高端內存,我們還要有能力使用它才行。
這就如同帶回一個身材火爆的mm僅僅是個開始,更關鍵的還在於如何不讓人家半小時後怒火沖天摔門而歸。
因此我們要繼續改造使用內存處的代碼,也就是simp_blkdev_trans_oneseg()函數。

在此之前這個函數很簡單,由於申請的是低端內存,這就保證了這些內存一直是被映射在內核的地址空間中的。
因此只要使用一個page_address()函數就完成了page指針到內存指針的轉換問題。
但對於高端內存就沒有這樣簡單了。

首先,高端內存需要在進行訪問之前被映射到非線性映射區域,還要在訪問之後解除這個映射以免人家罵我們的程序像公僕欠白條,
我們可以使用kmap()和kunmap()函數解決這個問題。

然後我們還要考慮另一個邊界問題,也就是頁面邊界。
由於我們使用的kmap()函數一次只能映射一個物理頁面,當需要訪問的數據在塊設備的內存塊中跨越頁面邊界時,
我們就需要識別這樣的情況,並做出相應的處理,也就是多次調用kmap()和kunmap()函數對依次每個頁面進行訪問。
我們可以採用與先前章節中處理被訪問數據跨越多個塊設備內存塊相似的方法來應對這種情況。

其實對於這種情況,我們還可以選擇另一個方案,就是使用vmap()函數。
我們可以使用它把地址分散的多個物理頁面映射到一段地址連續的區域中,
當然對我們正在用作塊設備存儲空間的這些地址連續的物理頁面更沒有問題。
但問題在於vmap()函數的內部處理比較複雜,這也意味着vmap()函數需要耗費更多的CPU時間,
並且使用vmap()函數時,我們需要一次性映射相當於內存塊長度的所有頁面,
但我們往往不會訪問全部的這些頁面,這意味着另一方面的性能損失。
因此,我們決定選擇使用kmap()函數,而讓程序自己去處理跨頁面的訪問問題。

參照以上的思路,我們寫出了新的simp_blkdev_trans_oneseg()函數:
static int simp_blkdev_trans_oneseg(struct page *start_page,
                unsigned long offset, void *buf, unsigned int len, int dir)
{
        unsigned int done_cnt;
        struct page *this_page;
        unsigned int this_off;
        unsigned int this_cnt;
        void *dsk_mem;

        done_cnt = 0;
        while (done_cnt < len) {
                /* iterate each page */
                this_page = start_page + ((offset + done_cnt) >> PAGE_SHIFT);
                this_off = (offset + done_cnt) & ~PAGE_MASK;
                this_cnt = min(len - done_cnt, (unsigned int)PAGE_SIZE
                        - this_off);

                dsk_mem = kmap(this_page);
                if (!dsk_mem) {
                        printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                ": map device page failed: %p\n", this_page);
                        return -ENOMEM;
                }
                dsk_mem += this_off;

                if (!dir)
                        memcpy(buf + done_cnt, dsk_mem, this_cnt);
                else
                        memcpy(dsk_mem, buf + done_cnt, this_cnt);

                kunmap(this_page);

                done_cnt += this_cnt;
        }

        return 0;
}

其核心是使用kmap()函數將內存頁面映射到內核空間然後再進行訪問,
以實現對高端內存的操作。

到此爲止,經歷了若干章的問題就這樣被解決了。
通過這樣的改變,我們至少得到了兩個好處:
1:避免了爭搶寶貴的低端內存
   作爲內存消耗大戶,霸佔低端內存的行爲不可容忍,
   其理由我們在前些章節中已經論述過。
   今後我們的程序至少不會在這一方面被人鄙視了。
2:增加了塊設備的最大容量
   使用原先的程序,在i386中無論如何也無法建立容量超過896M的塊設備,
   實際上更小,這是由於低端內存不可能全部拿來放塊設備的數據,
   而現在的程序可以使用包括高端內存在內的所有空閒內存,
   這無疑大大增加了塊設備的最大容量。

前些章中沒有進行的試驗憋到現在終於可以開始了。

首先證明這個程序經過了這麼多個章節的折騰後仍然是能編譯的:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step12 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step12/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step12/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step12/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
#

然後瞧瞧目前的內存狀況:
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:       509320 kB
LowTotal:       896356 kB
LowFree:        872612 kB
...
#
我們看到高端內存與低端內存分別剩餘509M和872M。

然後加載現在的模塊,爲了讓模塊吃內存的行爲表現得更加顯眼一些,
我們使用size參數指定了更大的塊設備容量:
# insmod simp_blkdev.ko size=500M
#

現在看看內存的變化情況:
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:         1652 kB
LowTotal:       896356 kB
LowFree:        863696 kB
...
#
結果顯示模塊如我們所料的吃掉了500M左右的高端內存。
雖然低端內存看樣子也少了一些,我們卻不能用模塊本身佔用的內存空間來解釋這一現象,
因爲模塊的代碼和靜態數據佔用的內存無論如何也到不了8.9M,
或許我們解釋爲用作一些文件操作的緩存了,還有就是基樹結構佔用的內存,
這個結構佔用的內存會隨着塊設備容量的增大而增加,或者我們可以計算一下......
不過現在我們並不打算對這個小問題做過多的關注,因爲這是扯淡,
正如鬧得沸沸揚揚的周久耕事件的最後調查結果居然僅僅只是公款買菸。
因此我們不會糾纏在這8.9M的問題中,因爲很明顯大頭是在減少的500多兆高端內存上,
這減少的500M高端內存已經足以證明這幾章中的修改結果了。

我們再移除這個模塊後看看內存的狀況:
# rmmod simp_blkdev
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:       504684 kB
LowTotal:       896356 kB
LowFree:        868480 kB
...
#
剛纔被佔用的高端內存又回來了,
一切都顯得如此的和諧。

作爲最後一步的測試,我們做一件本章之前做不到的事情,
就是申請大於896M的內存。
剛纔我們看到剩餘的低端內存和高端內存總共達到了1.37G,
好吧,我們就申請1.3G:
# insmod simp_blkdev.ko size=1300M
#
這時我們驚喜地發現系統沒有DOWN掉。

再看看這時的內存情況:
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:        41204 kB
LowTotal:       896356 kB
LowFree:         48284 kB
...
#
高端內存與低端內存中的大頭基本上都被吃掉了,
數量上也差不多是1.3G,這符合我們的預期。

老讓模塊佔用着這麼多的內存也不是什麼好主意,
我們放掉:
# rmmod simp_blkdev.
#

隨着本章的結束,圍繞高端內存的討論也終於修成正果了。
不過我們對這個驅動程序的改進還沒有完,因爲我們要發揚做精每一樣事情的精神,
一個民族的振興,不是靠對小學生進行填鴨式的政治思想教育,也不是靠官員及家屬的出國考察,
更不是靠公僕們身先士卒、前仆後繼、以自己的健康爲代價大吃大喝以創造9000億的GDP,
而是靠每一個屁民們的誠實、認真、勤勞、勇敢、創造、奉獻與精益求精。

<未完,待續>

第13章

沒有最好的代碼,是因爲我們總能把代碼改得更好。
因此我們現在打算做一個小的性能改進,這次我們準備拿free_diskmem()函數下刀。

本質上說,這個改進的意義不大,這是因爲free_diskmem()函數僅僅是在模塊卸載時被調用,
而對這種執行次數即少又不在關鍵路徑上的函數來說,最好是儘量讓他簡單以增加可靠性和可讀性,
除非它的耗時已經慢到能讓人有所感覺,否則0.01秒和0.00001秒是差不多的,畢竟在現實中尼奧不太可能用我們的程序。

但我們仍然打算繼續這一改進,一是爲了示範什麼是沒有意義的改進,二是爲了通過這一改進示範使用radix_tree_gang_lookup()函數和page->index的技巧。

首先我們看看原先的free_diskmem()函數:
void free_diskmem(void)
{
        int i;
        struct page *page;

        for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1)
                >> SIMP_BLKDEV_DATASEGSHIFT; i++) {
                page = radix_tree_lookup(&simp_blkdev_data, i);
                radix_tree_delete(&simp_blkdev_data, i);
                /* free NULL is safe */
                __free_pages(page, SIMP_BLKDEV_DATASEGORDER);
        }
}

它遍歷所有的內存塊索引,在基樹中找到這個內存塊的page指針,然後釋放內存,順帶着釋放掉基數中的這個節點。
考慮到這個函數不僅會在模塊卸載時被調用,也會在模塊加載時、申請內存中途掉鏈子時用來擦屁股,因此也需要考慮內存沒有完全申請的情況。
所幸的是這種情況下radix_tree_lookup()函數會返回NULL指針,而radix_tree_delete()和__free_pages()函數都能對NULL指針做出我們最期待的處理:就是什麼也不做。

這段代碼很小很直接,邏輯簡單而清晰,性能也差不到哪裏去,完全符合設計要求,
不幸的是我們還是打算做一些沒必要的優化,藉此還可以順便讀一讀基樹的內核代碼。
首先看radix_tree_lookup()函數,它在基數中查找指定索引對應的指針,爲了獲得這一指針的值,基本上它需要把基樹從上到下找一遍。
而對於free_diskmem()函數而言,我們僅僅是需要遍歷基樹中的所有節點,使用逐一查找的方法進行遍歷未免代價太大了。
就像是我們要給在場的所有同學每人發一個糖果,只需要讓他們排好隊,每人領一個即可,而不需要按照名單找出每個人再發。
爲了實現這一思想,我們跑到linux/lib/radix-tree.c中找函數,找啊找,找到了radix_tree_gang_lookup()函數。

radix_tree_gang_lookup()函數雖然不是我們理想中的遍歷函數,但也有了八九不離十的功能。
就像在酒吧裏找不到D Cup,帶回去個C Cup也總比看A片強。
通過radix_tree_gang_lookup()函數,我們可以一次從基樹中獲取多個節點的信息:
unsigned int radix_tree_gang_lookup(struct radix_tree_root *root, void **results, unsigned long first_index, unsigned int max_items);
具體的參數嘛,RTFSC吧。

這是我們注意到使用這個函數時顧此失彼的一面,雖然我們獲得了一組需要釋放的指針,但卻無法獲得這些指針的索引。
而執行釋放基樹中節點的操作時卻恰恰需要使用索引作參數。
然後就是一個技巧了,我們借用page結構的index成員來存儲這一索引。
之所以可以這樣用,是因爲page結構的index成員在該頁用作頁高速緩存時存儲相對文件起始處的以頁大小爲單位的偏移,
而我們所使用的頁面不會被同時用作頁高速緩存,因此這裏可以借用page.index成員。
按照以上思路,我們寫出了修改後的代碼:
void free_diskmem(void)
{
        unsigned long long next_seg;
        struct page *seglist[64];
        int listcnt;
        int i;

        next_seg = 0;
        do {
                listcnt = radix_tree_gang_lookup(&simp_blkdev_data,
                        (void **)seglist, next_seg, ARRAY_SIZE(seglist));

                for (i = 0; i < listcnt; i++) {
                        next_seg = seglist[i]->index;
                        radix_tree_delete(&simp_blkdev_data, next_seg);
                        __free_pages(seglist[i], SIMP_BLKDEV_DATASEGORDER);
                }

                next_seg++;
        } while (listcnt == ARRAY_SIZE(seglist));
}

當然,alloc_diskmem()函數中也需要加上page->index = i這一行,用於把基樹的索引存入page.index,修改後的代碼如下:
int alloc_diskmem(void)
{
        int ret;
        int i;
        struct page *page;

        INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);

        for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1)
                >> SIMP_BLKDEV_DATASEGSHIFT; i++) {
                page = alloc_pages(GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,
                        SIMP_BLKDEV_DATASEGORDER);
                if (!page) {
                        ret = -ENOMEM;
                        goto err_alloc;
                }

                page->index = i;
                ret = radix_tree_insert(&simp_blkdev_data, i, page);
                if (IS_ERR_VALUE(ret))
                        goto err_radix_tree_insert;
        }
        return 0;

err_radix_tree_insert:
        __free_pages(page, SIMP_BLKDEV_DATASEGORDER);
err_alloc:
        free_diskmem();
        return ret;
}

現在試驗一下修改後的代碼,先看看能不能編譯:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step13 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step13/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step13/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step13/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
#

看看當前系統的內存情況:
# cat /proc/meminfo
HighTotal:     1146816 kB
HighFree:       339144 kB
LowTotal:       896356 kB
LowFree:        630920 kB
...
#
這裏顯示現在剩餘339M高端內存和630M低端內存。

然後加載我們的模塊,讓它吃掉300M內存:
# insmod simp_blkdev.ko size=300M
# cat /proc/meminfo
HighTotal:     1146816 kB
HighFree:       137964 kB
LowTotal:       896356 kB
LowFree:        523900 kB
...
#
正如我們的預期,剩餘內存減少300M左右。

然後看看卸載模塊後的內存情況:
# rmmod simp_blkdev
# cat /proc/meminfo
HighTotal:     1146816 kB
HighFree:       338028 kB
LowTotal:       896356 kB
LowFree:        631044 kB
...
#
我們發現剩餘內存增加了300M,這意味着模塊已經把吃掉的內存吐回來了,
從而可以推斷出我們修改過的free_diskmem()函數基本上是能夠工作的。

本章的改動不大,就算是暫作休整,以留住忍耐至今忍無可忍認爲無需再忍而開始打包收拾行李準備溜之大吉的讀者們。
不過下一章中倒是預備了一個做起來讓人比較有成就感的功能。

<未完,待續>

第14章

在本章中我們要做一個比較大的改進,就是實現內存的推遲分配。

這意味着我們並不是在驅動程序加載時就分配用於容納數據的全部內存,
而是推遲到真正需要用到某塊內存時再進行分配。

詳細來說,我們將在塊設備的某個區域上發生第一次寫請求時分配用於容納被寫入數據的內存,
如果讀者在之前章節的薰陶下養成了細緻的作風和勤于思考的習慣,
應該能發現這裏提到的分配內存的時機是第一次寫,而不是第一次讀寫。
現在可能有些讀者已經悟出了這樣做的道理,讓我們無視他們,依然解釋一下這樣做的目的。
對塊設備而言,只要保證讀出的數據是最近一次寫進的即可。
如果在讀數據之前從來沒有往塊設備的同一塊區域中寫入數據,那麼這時返回任何隨機數據都是正確的。
這意味着對於第一次讀,我們完全可以返回任意的數據給用戶,這時並不需要分配某段內存來存儲它。
對真實的物理設備而言,就像我們買回的新硬盤,出廠時盤片中的數據內容是什麼都無所謂。
在具體的實現中,我們可以不對用以接收被讀出數據的內存進行任何填充,直接告訴上層“已經讀好了”,
這樣做無疑會更加快速,但這會造成2個問題:
1:這塊內存原先的內容最終將被傳送到用戶程序中,這將造成數據安全問題
2:違背了真實設備的一個潛特性,就是即使這個設備沒有寫入任何內容,對同一區域的多次讀操作返回的內容相同。
因此,我們將向接收數據的內存中寫些什麼,最簡單的就是用全0填充了。

實現這一功能的優點在於,塊設備不需要在一開始加載時就佔用全部的內存,這優化了系統資源的使用率。
讓我們假設塊設備自始至終沒有被全部填滿時,通過本章的功能,將佔用更少的內存。
另外,我們甚至可以創建容量遠遠大於機器物理內存的塊設備,只要在隨後的使用中不往這個塊設備中寫入過多的內容即可。

在linux中,類似的思想被廣泛應用。
比如對進程的內存區而言,並不是一開始就爲這段內存區申請和映射全部需要的物理內存,
又如在不少文件系統中,也不會給沒有寫入內容的文件部分分配磁盤的。

現在我們就實現這一功能。
分析代碼,我們發現不太容易找到往什麼地方加代碼。
往往在這種情況下,不如首先看看可以剝掉哪部分不需要的代碼,
正如初次跟一個mm時,如果兩個人都有些害羞,不知道從哪開始、或者正在期待對方打開局面時,
不如先脫下該脫的東西,然後的事情基本上就比較自然了。

現在的代碼中,明顯可以砍掉的是在驅動程序加載時用於申請容納數據的內存的代碼,
也就是alloc_diskmem()函數,把它砍了,沒錯,是全砍了。

還有調用它的代碼,在simp_blkdev_init()函數裏面的這幾行:
ret = alloc_diskmem();
if (IS_ERR_VALUE(ret))
        goto err_alloc_diskmem;
是的,也砍了。

還沒完,既然這個函數的調用都沒了,那麼調用這個函數失敗時的出錯處理也沒用了,也就是:
err_alloc_diskmem:
        put_disk(simp_blkdev_disk);
這兩句,不用猶豫了,砍掉。

經過剛纔的大刀闊斧後,我們發現......剛纔由於砍上癮了,不小心多砍了一條語句,就是對基樹的初始化語句:
INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);
原來它是在alloc_diskmem()函數裏面的,現在alloc_diskmem()函數不在了,我們索性把它放到初始化模塊的simp_blkdev_init()函數中,
放到剛纔原來調用alloc_diskmem()函數的位置就行了。
(注:
其實這裏不添加INIT_RADIX_TREE()宏也行,直接在定義基樹結構時順便初始化掉就行了,也就是把
static struct radix_tree_root simp_blkdev_data;
改成
static struct radix_tree_root simp_blkdev_data = RADIX_TREE_INIT(GFP_KERNEL);
就行了,或者改成讓人更加撞牆的形式:
static RADIX_TREE(simp_blkdev_data, GFP_KERNEL);
也可以,但我們這裏的代碼中,依然沿用原先的方式。
)

這樣一來,simp_blkdev_init()函數變成了這個樣子:

static int __init simp_blkdev_init(void)
{
        int ret;

        ret = getparam();
        if (IS_ERR_VALUE(ret))
                goto err_getparam;

        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(SIMP_BLKDEV_MAXPARTITIONS);
        if (!simp_blkdev_disk) {
                ret = -ENOMEM;
                goto err_alloc_disk;
        }

        INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);

        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 >> SIMP_BLKDEV_SECTORSHIFT);
        add_disk(simp_blkdev_disk);

        return 0;

err_alloc_disk:
        blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
err_getparam:
        return ret;
}

淋漓盡致地大砍一番之後,我們發現下一步的工作清晰多了。
現在在模塊加載時,已經不會申請所需的內存,而我們需要做的就是,
在處理塊設備讀寫操作時,添加不存在相應內存時的處理代碼。

在程序中,查找基數中的一個內存塊是在simp_blkdev_trans()函數內完成的,目前的處理是:
this_first_page = radix_tree_lookup(&simp_blkdev_data,
        (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
if (!this_first_page) {
        printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                ": search memory failed: %llu\n",
                (dsk_offset + done_cnt)
                >> SIMP_BLKDEV_DATASEGSHIFT);
        return -ENOENT;
}
也就是找不到內存塊時直接看作錯誤。
在以前這是正確的,因爲所有的內存塊都在初始化驅動程序時申請了,因此除非電腦的腦子進水了,
運行錯了指令,或者人腦的腦子進水了,編錯了代碼,否則不會發生這種情況。

但現在情況不同了,這時找不到內存塊是正常的,這意味着該位置的數據從未被寫入過,
因此我們需要在這裏做出合理的動作。
也就是在本章開始時所說的,對於讀處理返回全0,對於寫處理給塊設備的這段空間申請內存,並寫入數據。
因此我們把上段代碼改成了這個樣子:

this_first_page = radix_tree_lookup(&simp_blkdev_data,
        (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
if (!this_first_page) {
        if (!dir) {
                memset(buf + done_cnt, 0, this_cnt);
                goto trans_done;
        }

        /* prepare new memory segment for write */
        this_first_page = alloc_pages(
                GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,
                SIMP_BLKDEV_DATASEGORDER);
        if (!this_first_page) {
                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                        ": allocate page failed\n");
                return -ENOMEM;
        }

        this_first_page->index = (dsk_offset + done_cnt)
                >> SIMP_BLKDEV_DATASEGSHIFT;

        if (IS_ERR_VALUE(radix_tree_insert(&simp_blkdev_data,
                this_first_page->index, this_first_page))) {
                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                        ": insert page to radix_tree failed"
                        " seg=%lu\n", this_first_page->index);
                __free_pages(this_first_page,
                        SIMP_BLKDEV_DATASEGORDER);
                return -EIO;
        }
}
對這段代碼的流程幾乎不要解釋了,因爲代碼本身就是最好的說明。
唯一要提一下的就是goto trans_done這句話,因爲前一條語句實質上已經完成了數據讀取,
因此需要直接跳轉到該段數據處理完成的位置,也就是函數中的done_cnt += this_cnt語句之前。
說到這裏猴急的讀者可能已經在done_cnt += this_cnt語句之前添加
trans_done:
這一行了,不錯,正是要加這一行。

改過的simp_blkdev_trans()函數變成了這個樣子:
static int simp_blkdev_trans(unsigned long long dsk_offset, void *buf,
                unsigned int len, int dir)
{
        unsigned int done_cnt;
        struct page *this_first_page;
        unsigned int this_off;
        unsigned int this_cnt;

        done_cnt = 0;
        while (done_cnt < len) {
                /* iterate each data segment */
                this_off = (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK;
                this_cnt = min(len - done_cnt,
                        (unsigned int)SIMP_BLKDEV_DATASEGSIZE - this_off);

                this_first_page = radix_tree_lookup(&simp_blkdev_data,
                        (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
                if (!this_first_page) {
                        if (!dir) {
                                memset(buf + done_cnt, 0, this_cnt);
                                goto trans_done;
                        }

                        /* prepare new memory segment for write */
                        this_first_page = alloc_pages(
                                GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,
                                SIMP_BLKDEV_DATASEGORDER);
                        if (!this_first_page) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": allocate page failed\n");
                                return -ENOMEM;
                        }

                        this_first_page->index = (dsk_offset + done_cnt)
                                >> SIMP_BLKDEV_DATASEGSHIFT;

                        if (IS_ERR_VALUE(radix_tree_insert(&simp_blkdev_data,
                                this_first_page->index, this_first_page))) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": insert page to radix_tree failed"
                                        " seg=%lu\n", this_first_page->index);
                                __free_pages(this_first_page,
                                        SIMP_BLKDEV_DATASEGORDER);
                                return -EIO;
                        }
                }

                if (IS_ERR_VALUE(simp_blkdev_trans_oneseg(this_first_page,
                        this_off, buf + done_cnt, this_cnt, dir)))
                        return -EIO;

trans_done:
                done_cnt += this_cnt;
        }

        return 0;
}

代碼就這樣被莫名其妙地改完了,感覺這次的改動比預想的少,並且也比較集中,
這其實還是託了前些章的福,正是在此之前對程序結構的規劃調整,
在增加可讀性的同時,也給隨後的維護帶來方便。
處於良好維護下的程序代碼結構應該越維護越讓人賞心悅目,而不是越維護越混亂不堪。

現在我們來試驗一下這次修改的效果:
先編譯:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step14 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step14/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step14/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step14/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
#
沒發現問題。

然後看看目前的內存狀況:
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:        87920 kB
LowTotal:       896356 kB
LowFree:        791920 kB
...
#
可以看出高端和低端內存分別剩餘87M和791M。

然後指定size=50M加載模塊後看看內存變化:
# insmod simp_blkdev.ko size=50M
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:        86804 kB
LowTotal:       896356 kB
LowFree:        791912 kB
...
#
在這裏我們發現剩餘內存的變化不大,
這也證明了這次修改的效果,因爲加載模塊時不會申請用於存儲數據的全部內存。
而在原先的代碼中,這一步驟將使機器減少大約50M的剩餘空間。

然後我們來驗證讀取塊設備時也不會導致分配內存:
# dd if=/dev/simp_blkdev of=/dev/null
102400+0 records in
102400+0 records out
52428800 bytes (52 MB) copied, 0.376118 seconds, 139 MB/s
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:        85440 kB
LowTotal:       896356 kB
LowFree:        791888 kB
...
#
剩餘內存幾乎沒有變化,這證明了我們的設想。

然後是寫設備的情況:
# dd if=/dev/zero of=/dev/simp_blkdev
dd: writing to `/dev/simp_blkdev': No space left on device
102401+0 records in
102400+0 records out
52428800 bytes (52 MB) copied, 0.542117 seconds, 96.7 MB/s
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:        34116 kB
LowTotal:       896356 kB
LowFree:        791516 kB
...
#
這時剩餘內存終於減少了大約50M,
這意味着驅動程序申請了大約50M的內存用於存儲寫入的數據。

如果向已寫入的位置再次寫入數據,理論上不應該造成再一次的分配,
讓我們試試:
# dd if=/dev/zero of=/dev/simp_blkdev
dd: writing to `/dev/simp_blkdev': No space left on device
102401+0 records in
102400+0 records out
52428800 bytes (52 MB) copied, 0.644972 seconds, 81.3 MB/s
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:        33620 kB
LowTotal:       896356 kB
LowFree:        791516 kB
...
#
結果與預想一致。

現在卸載模塊:
# rmmod simp_blkdev
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:        84572 kB
LowTotal:       896356 kB
LowFree:        791640 kB
...
#
我們發現被驅動程序使用的內存被釋放回來了。

如果以上的實驗沒有讓讀者過癮的話,我們來繼續一個過分一些的,
也就是創建空間遠遠大於機器物理內存的塊設備。
首先我們看看目前的系統內存狀況:
# cat /proc/meminfo
...

HighTotal:     1146816 kB
HighFree:        77688 kB
LowTotal:       896356 kB
LowFree:        783296 kB
...
#
機器的總內存是2G,目前剩餘的高、低端內存加起來是860M左右。

然後我們加載模塊,注意一下size參數的值:
# insmod simp_blkdev.ko size=10000G
#
命令成功返回,而如果換作原先的代碼,
命令出錯返回......是不太可能的,
最可能的大概是內核直接panic。
這是因爲申請光全部內存的操作將導致申請出錯時運行的用於釋放內存的代碼所需要的內存都無法滿足。

無論我們設置多大的塊設備容量,模塊加載後只要不執行寫操作,
驅動程序都不會申請存儲數據的內存。而這個測試:
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:        75208 kB
LowTotal:       896356 kB
LowFree:        783132 kB
...
#
也證明了這一點。

現在我們看看這時的塊設備情況:
# fdisk -l /dev/simp_blkdev

Disk /dev/simp_blkdev: 10737.4 GB, 10737418240000 bytes
255 heads, 63 sectors/track, 1305416 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes

Disk /dev/simp_blkdev doesn't contain a valid partition table
#
果然是10000G,這可以通過換算10737418240000 bytes得到。
而fdisk顯示10737.4 GB是因爲它是按照1k=1000字節、1M=1000K、1G=1000M來算的,
這種流氓的算法給硬盤廠商的缺斤少兩行爲提供了極好的藉口。

這裏省略fdisk、mkfs、mount、cp等操作,
直接用dd往這個"10000G磁盤"中寫入50M的數據:
# dd if=/dev/zero of=/dev/simp_blkdev bs=1M count=50
50+0 records in
50+0 records out
52428800 bytes (52 MB) copied, 0.324054 seconds, 162 MB/s
# cat /proc/meminfo
...
HighTotal:     1146816 kB
HighFree:        23512 kB
LowTotal:       896356 kB
LowFree:        782884 kB
...
#
現在的內存情況證明我們的"10000G磁盤"爲這些數據申請了50M的內存。

實驗差不多了,我們卸載模塊:
# rmmod simp_blkdev.
#

做完以上的實驗,讀者可能會有一個疑問,如果我們真的向那個"10000G磁盤"中寫入了10000G的數據怎麼樣呢?
回答可能不太如人意,就是系統很可能會panic。
因爲這個操作將迫使驅動程序吃掉全部可能獲得的物理內存,並且在吃光最後那麼一丁點內存之前不會發生錯誤,
這也意味着走到出錯處理這一步的時候,系統已經幾乎無可救藥了。其實在此之前系統就會一次進行:
釋放緩存、試圖把所有的用戶進程的內存換出、殺死全部能夠殺死的進程等操作。
而我們的驅動程序由於被看作是內核的一部分,卻不會被停止,而是在繼續不停的吃掉通過上述方式釋放出的可憐的內存。
試想,一個已經走到這一步的系統還有什麼繼續運行的可能呢?

因此,我們的程序確實需要改善以解決這個問題,因爲世界上總是有一些瘋狂的人在想各種辦法虐待電腦。
但我們並不打算在本教程中解決它,因爲這個教程中的每一章都企圖爲讀者說明一類知識或一種方法,
而不是僅僅爲了這個示例性質的程序的功能本身。
所以這一項改善就當作是留給讀者的練習了。

本章通過改善塊設備驅動程序實現了內存的滯後申請,
其目的在於介紹這種方法,以使它在其他的相似程序中也得以實現。
不過,這並不意味着作者希望讀者把這種方法過分引用,
比如引用成平時不學習,考試前臨時抱佛腳。

<未完,待續>

第15章(最終章)

在上一章中我們對這個塊設備驅動所作的更改使它具備了動態申請內存的能力,
但實際上同時也埋下一個隱患,就是數據訪問衝突。

這裏我們順便嘮叨一下內核開發中的同步問題。
提到數據訪問同步,自然而然會使人想到多進程、多線程、加鎖、解鎖、
信號量、synchronized關鍵字等東西,然後就很頭疼。
對於用戶態程序,網上大量的解釋數據同步概念和方法的文章給人的印象大概是:
同步很危險,編程要謹慎,
處處有機關,問題很難找。

對於第一次進行多線程時編程的人來說,感覺可能是以下兩種:
一種是覺得程序中處處都會有問題,任何一條訪問數據的指令都不安全,
恨不得把程序中所有的數據都加上鎖,甚至打算給鎖本身的數據再加個鎖,
另一種是沒覺得有什麼困難,根本不去理什麼都互斥不互斥,
就按原先的來,編出的程序居然也運行得很順。
然後懷着這兩種想法人通過不斷的學習和實踐掌握了數據同步的知識後認識到,
數據同步其實並不像前一種想法那樣危險,也不像後一種想法那樣簡單。

所幸的是對於不少用戶態程序來說,倒是可以不用考慮數據同步問題。
至少當我們剛開始寫HelloWorld時不用去理這個麻煩。

而對於內核態代碼而言,很不幸,整個兒幾乎都相當於用戶態的多線程。
其實事情也並非原本就是這麼糟的。
在很久很久以前,山是青的,草是綠的,牛奶是能喝的,
見到老人摔跤是敢扶的,作者是純情的,電腦也是單CPU的。
那時的內核環境很靜,很美。除了中斷會時不時地搗搗亂,其餘的都挺詩意。
代碼獨個兒在跑,就像是一輛汽車在荒漠上奔馳,因爲沒有其他妨礙,
幾乎可以毫無顧忌地訪問數據,而不用考慮什麼萬惡的訪問衝突。
唯一要考慮的從天而降的中斷奧特曼,解決的方法倒也不難,禁用了中斷看你還能咋的。

然後隨着作者的成長,目光從書本轉向了美眉,計算機也由單CPU發展成了多CPU。
內核代碼的執行環境終於開始熱鬧起來,由於每個CPU上都在執行任務,
這些任務進入到對應的內核態時會出現多條內核指令流同時執行,
這些指令流對全局數據的訪問很明顯就牽涉到了同步問題,這是開端。
從那時起編程時要考慮其他CPU上的事情了。

然後隨着作者的進一步成長,目光從美眉的臉轉向了胸,
CPU製造商爲了貫徹給程序員找麻煩的精神,搞出了亂序執行。
這一創舉驚醒了多年來還在夢中的諸多程序員,原來,程序不是按程序執行的啊。
正如林高官說的:“我是交通部派來的,級別和你們市長一樣高,敢跟我鬥,
你們這些人算個屁呀!”原來,無職無權的平民百姓就是屁啊。
正當程序員從睡夢中驚醒還沒緩過神時,編譯器又跟着搗亂,
“你CPU都能亂序了,憑什麼不讓我亂序?”
然後熱鬧了,好在我們還有mb()、rmb()、wmb()、barrier()這幾根救命稻草,
事情倒是沒變得太糟。

然後隨着作者的進一步成長,目光從美眉的胸轉向了臀,
內核也從一開始時被動的爲了適應多CPU而不得已半推半就支持多任務並行,
轉向了主動掀起裙角管它一個還是幾個CPU都去多任務了。
從技術面解釋,這就是大名鼎鼎的內核搶佔。
內核的程序員從此不僅要考慮其他CPU,好要提妨自個兒的CPU,
因爲執行代碼的CPU說不定什麼時候就莫名其妙的被調度執行別的任務了。

如果以作者的成長曆程爲主線解釋內核的演化還不至於太混亂的話,
我們還可以考慮再介紹一下spin_lock, mutex_lock, preempt_disable,
atomic_t和rcu等函數,不過作者忍住了這一衝動,還是讓讀者去google吧。

然後回到我們的代碼,現在的代碼是有問題的。
比如simp_blkdev_trans()函數中,假設2個任務同時向塊設備的同一區域寫數據,
而這塊區域在這之前沒有被寫過,也就是說還沒有申請內存,那麼如果運氣夠好的話,
這兩個進程可能幾乎同時運行到:
this_first_page = radix_tree_lookup(&simp_blkdev_data,
                        (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
這句,很明顯這兩個任務得到的this_first_page都是NULL,然後它們爭先恐後的執行
if (!this_first_page)
判斷,從而進入之後的alloc_pages,隨後它們都會爲這個塊設備區域申請內存,並加入基樹結構。
如果運氣爆發的話,這兩個任務radix_tree_insert()的代碼中將有機會近乎同時越過
if (slot != NULL)
        return -EEXIST;
的最後防線,先後將新申請的內存指針賦值給基樹結點。
雖然x86的多處理器對同一塊內存的寫操作是原子的,
這樣至少不會因爲這兩個任務同時賦值基樹指針造成指針指向莫名其妙的值,
但這仍然也解決不了我們的問題,後一個賦值操作將覆蓋前一個操作的結果,
基數節點最終將指向稍後一點執行賦值操作的任務。
這兩個任務最終將運行到radix_tree_insert()函數的結尾,而函數的返回值都是漂亮的0。
剩下的事情扳腳丫子大概也能想出來了,這兩個任務都將自欺欺人地認爲自己正確而成功地爲塊設備分配了內存,
而真相是其中一個任務拿走的內存卻再也沒有機會拿回來了。

至於解決方法嘛,當然是加鎖。
只要我們讓“查找基數中有沒有這個節點”到“分配內存並插入這節點”的過程中沒有其他任務的打攪,
就自然的解決了這個問題。

首先定義一個鎖,因爲是用來鎖simp_blkdev_data的,
就放在static struct radix_tree_root simp_blkdev_data;後面吧:
DEFINE_MUTEX(simp_blkdev_datalock); /* protects the disk data op */

然後根據剛纔的思想給對simp_blkdev_trans()函數中的simp_blkdev_datalock的操作加鎖,
也就是在
this_first_page = radix_tree_lookup(&simp_blkdev_data,
        (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
語句之前添加:
mutex_lock(&simp_blkdev_datalock);

操作結束後被忘了把鎖還回去,否則下次再操作時就成死鎖了,因此在
trans_done:
後面加上
mutex_unlock(&simp_blkdev_datalock);
這一行。

完成了嗎?細心看看就知道還沒完。
simp_blkdev_trans()函數中有一些判斷異常的代碼,這些代碼大多是扔出一條printk就直接return的。
這樣可不行,可千萬別讓它們臨走時把鎖也順回去了。
這意味着我們要在simp_blkdev_trans()函數中的3個故障時return的代碼前完成鎖的釋放。
因此simp_blkdev_trans()函數最後就成了這樣:

static int simp_blkdev_trans(unsigned long long dsk_offset, void *buf,
                unsigned int len, int dir)
{
        unsigned int done_cnt;
        struct page *this_first_page;
        unsigned int this_off;
        unsigned int this_cnt;

        done_cnt = 0;
        while (done_cnt < len) {
                /* iterate each data segment */
                this_off = (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK;
                this_cnt = min(len - done_cnt,
                        (unsigned int)SIMP_BLKDEV_DATASEGSIZE - this_off);

                mutex_lock(&simp_blkdev_datalock);

                this_first_page = radix_tree_lookup(&simp_blkdev_data,
                        (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
                if (!this_first_page) {
                        if (!dir) {
                                memset(buf + done_cnt, 0, this_cnt);
                                goto trans_done;
                        }

                        /* prepare new memory segment for write */
                        this_first_page = alloc_pages(
                                GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,
                                SIMP_BLKDEV_DATASEGORDER);
                        if (!this_first_page) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": allocate page failed\n");
                                mutex_unlock(&simp_blkdev_datalock);
                                return -ENOMEM;
                        }

                        this_first_page->index = (dsk_offset + done_cnt)
                                >> SIMP_BLKDEV_DATASEGSHIFT;

                        if (IS_ERR_VALUE(radix_tree_insert(&simp_blkdev_data,
                                this_first_page->index, this_first_page))) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": insert page to radix_tree failed"
                                        " seg=%lu\n", this_first_page->index);
                                __free_pages(this_first_page,
                                        SIMP_BLKDEV_DATASEGORDER);
                                mutex_unlock(&simp_blkdev_datalock);
                                return -EIO;
                        }
                }

                if (IS_ERR_VALUE(simp_blkdev_trans_oneseg(this_first_page,
                        this_off, buf + done_cnt, this_cnt, dir))) {
                        mutex_unlock(&simp_blkdev_datalock);
                        return -EIO;
                }
trans_done:
                mutex_unlock(&simp_blkdev_datalock);
                done_cnt += this_cnt;
        }

        return 0;
}


這個函數差不多了。
我們再看看代碼中還有什麼地方也對simp_blkdev_data進行操作來着,別漏掉了這些小王八蛋。
查找一下代碼,我們發現free_diskmem()函數中也進行了操作。

其實從理論上說,這裏不加鎖是不會產生問題的,因爲對內核在執行對塊設備設備時,
會鎖住這個設備對應的模塊(天哪,又是鎖,這一章和鎖彪上了),
其結果是在simp_blkdev_trans()函數操作simp_blkdev_data的過程中,
該模塊無法卸載,從而無法不會運行到free_diskmem()函數。

那麼如果同時卸載這個模塊呢,回答是也沒有問題,英勇的模塊鎖也會搞掂這種情況。

這一章由於沒有進行功能增加,就不列出修改後模塊的測試經過了,
不過作爲對讀者的安慰,我們將列出到目前爲止經歷了大大小小修改後的全部模塊代碼。
看到這些代碼,我們能歷歷在目的回憶出讀這篇教程到現在爲止所經受的全部折磨和苦難。
當然也能感受到堅持到現在所得到的知識和領悟。

對於Linux而言,甚至僅僅對於塊設備驅動程序而言,這部教程揭開的也僅僅是冰山一角。
而更多的知識其實離我們很近,在google上,在代碼中,在心中。
學習,是要用心,不斷地去想,同時要有恆心、耐心、要細心,
人應該越學越謙虛,問題應該越學越多,這大概就是作者通過這部教程最想告訴讀者的。

#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>
#include <linux/version.h>

/*
* A simple block device driver based on memory
*
* Copyright 2008 -
*        Zhaolei <[email protected]>
*
* Sample for using:
*   Create device file (first time only):
*     Note: If your system have udev, it can create device file for you in time
*           of lsmod and fdisk automatically.
*           Otherwise you need to create them yourself by following steps.
*     mknod /dev/simp_blkdev  b 72 0
*     mknod /dev/simp_blkdev1 b 72 1
*     mknod /dev/simp_blkdev2 b 72 2
*
*   Create dirs for test (first time only):
*     mkdir /mnt/temp1/ # first time only
*     mkdir /mnt/temp2/ # first time only
*
*   Run it:
*     make
*     insmod simp_blkdev.ko
*     # or insmod simp_blkdev.ko size=numK/M/G/T
*     fdisk /dev/simp_blkdev # create 2 patitions
*     mkfs.ext3 /dev/simp_blkdev1
*     mkfs.ext3 /dev/simp_blkdev2
*     mount /dev/simp_blkdev1 /mnt/temp1/
*     mount /dev/simp_blkdev2 /mnt/temp2/
*     # play in /mnt/temp1/ and /mnt/temp2/
*     umount /mnt/temp1/
*     umount /mnt/temp2/
*     rmmod simp_blkdev.ko
*
*/

#define SIMP_BLKDEV_DEVICEMAJOR        COMPAQ_SMART2_MAJOR
#define SIMP_BLKDEV_DISKNAME        "simp_blkdev"

#define SIMP_BLKDEV_SECTORSHIFT        (9)
#define SIMP_BLKDEV_SECTORSIZE        (1ULL<<SIMP_BLKDEV_SECTORSHIFT)
#define SIMP_BLKDEV_SECTORMASK        (~(SIMP_BLKDEV_SECTORSIZE-1))

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

#define SIMP_BLKDEV_DATASEGORDER        (2)
#define SIMP_BLKDEV_DATASEGSHIFT        (PAGE_SHIFT + SIMP_BLKDEV_DATASEGORDER)
#define SIMP_BLKDEV_DATASEGSIZE                (PAGE_SIZE << SIMP_BLKDEV_DATASEGORDER)
#define SIMP_BLKDEV_DATASEGMASK                (~(SIMP_BLKDEV_DATASEGSIZE-1))

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

static struct radix_tree_root simp_blkdev_data;
DEFINE_MUTEX(simp_blkdev_datalock); /* protects the disk data op */

static char *simp_blkdev_param_size = "16M";
module_param_named(size, simp_blkdev_param_size, charp, S_IRUGO);

static unsigned long long simp_blkdev_bytes;

static int simp_blkdev_trans_oneseg(struct page *start_page,
                unsigned long offset, void *buf, unsigned int len, int dir)
{
        unsigned int done_cnt;
        struct page *this_page;
        unsigned int this_off;
        unsigned int this_cnt;
        void *dsk_mem;

        done_cnt = 0;
        while (done_cnt < len) {
                /* iterate each page */
                this_page = start_page + ((offset + done_cnt) >> PAGE_SHIFT);
                this_off = (offset + done_cnt) & ~PAGE_MASK;
                this_cnt = min(len - done_cnt, (unsigned int)PAGE_SIZE
                        - this_off);

                dsk_mem = kmap(this_page);
                if (!dsk_mem) {
                        printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                ": map device page failed: %p\n", this_page);
                        return -ENOMEM;
                }
                dsk_mem += this_off;

                if (!dir)
                        memcpy(buf + done_cnt, dsk_mem, this_cnt);
                else
                        memcpy(dsk_mem, buf + done_cnt, this_cnt);

                kunmap(this_page);

                done_cnt += this_cnt;
        }

        return 0;
}

static int simp_blkdev_trans(unsigned long long dsk_offset, void *buf,
                unsigned int len, int dir)
{
        unsigned int done_cnt;
        struct page *this_first_page;
        unsigned int this_off;
        unsigned int this_cnt;

        done_cnt = 0;
        while (done_cnt < len) {
                /* iterate each data segment */
                this_off = (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK;
                this_cnt = min(len - done_cnt,
                        (unsigned int)SIMP_BLKDEV_DATASEGSIZE - this_off);

                mutex_lock(&simp_blkdev_datalock);

                this_first_page = radix_tree_lookup(&simp_blkdev_data,
                        (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
                if (!this_first_page) {
                        if (!dir) {
                                memset(buf + done_cnt, 0, this_cnt);
                                goto trans_done;
                        }

                        /* prepare new memory segment for write */
                        this_first_page = alloc_pages(
                                GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,
                                SIMP_BLKDEV_DATASEGORDER);
                        if (!this_first_page) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": allocate page failed\n");
                                mutex_unlock(&simp_blkdev_datalock);
                                return -ENOMEM;
                        }

                        this_first_page->index = (dsk_offset + done_cnt)
                                >> SIMP_BLKDEV_DATASEGSHIFT;

                        if (IS_ERR_VALUE(radix_tree_insert(&simp_blkdev_data,
                                this_first_page->index, this_first_page))) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": insert page to radix_tree failed"
                                        " seg=%lu\n", this_first_page->index);
                                __free_pages(this_first_page,
                                        SIMP_BLKDEV_DATASEGORDER);
                                mutex_unlock(&simp_blkdev_datalock);
                                return -EIO;
                        }
                }

                if (IS_ERR_VALUE(simp_blkdev_trans_oneseg(this_first_page,
                        this_off, buf + done_cnt, this_cnt, dir))) {
                        mutex_unlock(&simp_blkdev_datalock);
                        return -EIO;
                }
trans_done:
                mutex_unlock(&simp_blkdev_datalock);
                done_cnt += this_cnt;
        }

        return 0;
}

static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
        int dir;
        unsigned long long dsk_offset;
        struct bio_vec *bvec;
        int i;
        void *iovec_mem;

        switch (bio_rw(bio)) {
        case READ:
        case READA:
                dir = 0;
                break;
        case WRITE:
                dir = 1;
                break;
        default:
                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                        ": unknown value of bio_rw: %lu\n", bio_rw(bio));
                goto bio_err;
        }

        if ((bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT) + 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);
                goto bio_err;
        }

        dsk_offset = bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT;

        bio_for_each_segment(bvec, bio, i) {
                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
                if (!iovec_mem) {
                        printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                ": map iovec page failed: %p\n", bvec->bv_page);
                        goto bio_err;
                }

                if (IS_ERR_VALUE(simp_blkdev_trans(dsk_offset, iovec_mem,
                        bvec->bv_len, dir)))
                        goto bio_err;

                kunmap(bvec->bv_page);

                dsk_offset += 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;

bio_err:
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
        bio_endio(bio, 0, -EIO);
#else
        bio_endio(bio, -EIO);
#endif
        return 0;
}

static int simp_blkdev_getgeo(struct block_device *bdev,
                struct hd_geometry *geo)
{
        /*
         * capacity        heads        sectors        cylinders
         * 0~16M        1        1        0~32768
         * 16M~512M        1        32        1024~32768
         * 512M~16G        32        32        1024~32768
         * 16G~...        255        63        2088~...
         */
        if (simp_blkdev_bytes < 16 * 1024 * 1024) {
                geo->heads = 1;
                geo->sectors = 1;

        } else if (simp_blkdev_bytes < 512 * 1024 * 1024) {
                geo->heads = 1;
                geo->sectors = 32;
        } else if (simp_blkdev_bytes < 16ULL * 1024 * 1024 * 1024) {
                geo->heads = 32;
                geo->sectors = 32;
        } else {
                geo->heads = 255;
                geo->sectors = 63;
        }

        geo->cylinders = simp_blkdev_bytes >> SIMP_BLKDEV_SECTORSHIFT
                / geo->heads / geo->sectors;

        return 0;
}

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

void free_diskmem(void)
{
        unsigned long long next_seg;
        struct page *seglist[64];
        int listcnt;
        int i;

        next_seg = 0;
        do {
                listcnt = radix_tree_gang_lookup(&simp_blkdev_data,
                        (void **)seglist, next_seg, ARRAY_SIZE(seglist));

                for (i = 0; i < listcnt; i++) {
                        next_seg = seglist[i]->index;
                        radix_tree_delete(&simp_blkdev_data, next_seg);
                        __free_pages(seglist[i], SIMP_BLKDEV_DATASEGORDER);
                }

                next_seg++;
        } while (listcnt == ARRAY_SIZE(seglist));
}

int getparam(void)
{
        char unit;
        char tailc;

        if (sscanf(simp_blkdev_param_size, "%llu%c%c", &simp_blkdev_bytes,
                &unit, &tailc) != 2) {
                return -EINVAL;
        }

        if (!simp_blkdev_bytes)
                return -EINVAL;

        switch (unit) {
        case 'g':
        case 'G':
                simp_blkdev_bytes <<= 30;
                break;
        case 'm':
        case 'M':
                simp_blkdev_bytes <<= 20;
                break;
        case 'k':
        case 'K':
                simp_blkdev_bytes <<= 10;
                break;
        case 'b':
        case 'B':
                break;
        default:
                return -EINVAL;
        }

        /* make simp_blkdev_bytes fits sector's size */
        simp_blkdev_bytes = (simp_blkdev_bytes + SIMP_BLKDEV_SECTORSIZE - 1)
                & SIMP_BLKDEV_SECTORMASK;

        return 0;
}

static int __init simp_blkdev_init(void)
{
        int ret;

        ret = getparam();
        if (IS_ERR_VALUE(ret))
                goto err_getparam;

        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(SIMP_BLKDEV_MAXPARTITIONS);
        if (!simp_blkdev_disk) {
                ret = -ENOMEM;
                goto err_alloc_disk;
        }

        INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);

        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 >> SIMP_BLKDEV_SECTORSHIFT);
        add_disk(simp_blkdev_disk);

        return 0;

err_alloc_disk:
        blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
err_getparam:
        return ret;
}

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

module_init(simp_blkdev_init);
module_exit(simp_blkdev_exit);

MODULE_LICENSE("GPL");



追記:偶然看到剛纔的代碼首部註釋,Copyright後面還是2008年。
大概是從第一章開始一直這樣拷貝過來的。
這部教程從2008年11月斷斷續續的寫到了2009年3月,終於功德圓滿了。
作爲作者寫的第一個如此長度篇幅的教程,炸一眼瞟過來,倒也還像個樣子,
看來寫教程並不是太難高攀的事情,因此如果讀者也時不時地有一些寫起來的衝動,
就不妨開始吧: )

本章以塊設備驅動程序的代碼爲例,說明了內核中的同步概念,
當然,在不少情況下,程序員遇到的同步問題比這裏的要複雜的多,
內核中也採用了很多方法和技巧來處理同步,瞭解和學習這些知識,
收穫的不僅是數據同步本身的解決方法,更是一種思路,
這對於更一般的程序設計都是有很大幫助的,因此有空時google一下,
總能找到自己想了解的知識。

<--全文完,趙磊出品,必屬精品-->

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章