一步一步走進塊驅動之第七章

第七章

本教程修改自趙磊的網上的一系列教程.本人覺得該系列教程寫的非常不錯.以風趣幽默的語言將塊驅動寫的非常詳細,對於入門教程,應該屬於一份經典了. 本人在這對此係列教程最後附上對Linux 2.6.36版本的代碼.並編譯運行成功. 該教程所有版權仍歸作者趙磊所有,本人只做附錄代碼的添加,併爲對原文修改.有不懂的地方,可以聯繫我 [email protected] 或者給我留言  

+---------------------------------------------------+

|                 寫一個塊設備驅動                  

+---------------------------------------------------+

作者:趙磊                                        

| 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以上的物理內存,它們地址是比較低的,

正因爲此,我們通常把這部分(0-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瞭解更詳細的內容,

這也是作者建議的看書方法。

不過本章是個例外,因爲作者最終認爲對這些知識的介紹對於這部教程的整體性是有幫助的。

但這裏的介紹其實仍然只屬於皮毛,因此還是希望讀者進一步瞭解教程以外的更多知識。

<未完,待續>

#include <linux/init.h>
#include <linux/module.h>
#include <linux/genhd.h>		//add_disk
#include <linux/blkdev.h>		//struct block_device_operations
#include <linux/hdreg.h>

#define _DEBUG_

#define BLK_PAGE_ORDER		2
#define	BLK_PAGE_SIZE	(PAGE_SIZE << BLK_PAGE_ORDER)
#define BLK_PAGE_SHIFT		(PAGE_SHIFT + BLK_PAGE_ORDER)
#define BLK_PAGE_MASK		(~(BLK_PAGE_SIZE - 1))

#define BLK_DISK_NAME 		"block_name"
#define BLKDEV_DEVICEMAJOR    COMPAQ_SMART2_MAJOR
#define BLKDEV_BYTES        	(16*1024*1024)
#define MAX_PARTITIONS		64
static int MAJOR_NR = 0;

static struct gendisk *g_blkdev_disk;
static struct request_queue *g_blkdev_queue;
//unsigned char blkdev_data[BLKDEV_BYTES];
struct radix_tree_root blk_dev_data;

static int blkdev_make_request(struct request_queue *q, struct bio *bio)
{
	struct bio_vec *bvec;
	int i;
	unsigned long long disk_offset = 0;

	if ((bio->bi_sector << 9) + bio->bi_size > BLKDEV_BYTES) {
		printk(KERN_ERR BLK_DISK_NAME
				": bad request: block=%llu, count=%u\n",
				(unsigned long long)bio->bi_sector, bio->bi_size);
		bio_endio(bio, -EIO);
		return 0;
	}

	disk_offset = bio->bi_sector << 9;

	bio_for_each_segment(bvec, bio, i) {
		unsigned int count_current = 0;
		unsigned int count_done = 0;
		void *iovec_mem;
		void *dsk_mem = NULL;
		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)(BLK_PAGE_SIZE - ((disk_offset + count_done) & ~BLK_PAGE_MASK)));
			//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的最後一次傳送,完成後就可以回家洗澡了。
			dsk_mem = radix_tree_lookup(&blk_dev_data,(disk_offset + count_done) >> BLK_PAGE_SHIFT);
			//(disk_offset + count_done) >> PAGE_SHIFT 如果在傳送的數據是跨頁的情況下,
			//那麼這個disk_offset + count_done 正好爲頁對齊地址,也就是下一面的地址.
			//當前要傳送的數據的後半段數據,放到了下一個頁的的前面,正好完成了數據的後半段搬移
			if(!dsk_mem){
				printk(KERN_ERR BLK_DISK_NAME
						": search memory failed: %llu\n",
                        (disk_offset + count_done) >> BLK_PAGE_SHIFT);
                		kunmap(bvec->bv_page);
				bio_endio(bio,-EIO);
				return 0;
			}
			dsk_mem += (disk_offset + count_done) & ~BLK_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;
			}
			count_done += count_current;
			//當前完成的字節數,這次必須加上,一方面是根據此值來獲得min中計算數據,另一方面,那就是hang住啦~~while的循環條件的原因
		}
		kunmap(bvec->bv_page);
		disk_offset += bvec->bv_len;
	}		
	
	bio_endio(bio, 0);

	return 0;
}

int gendisk_getgeo(struct block_device *pblk_dev, struct hd_geometry *phd_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 (BLKDEV_BYTES < 16 * 1024 * 1024) {
		phd_geo->heads = 1;
		phd_geo->sectors = 1;
	} else if (BLKDEV_BYTES < 512 * 1024 * 1024) {
		phd_geo->heads = 1;
		phd_geo->sectors = 32;
	} else if (BLKDEV_BYTES < 16ULL * 1024 * 1024 * 1024) {
		phd_geo->heads = 32;
		phd_geo->sectors = 32;
	} else {
		phd_geo->heads = 255;
		phd_geo->sectors = 63;
	}

	phd_geo->cylinders = BLKDEV_BYTES >> 9 / phd_geo->heads / phd_geo->sectors;
	
	return 0;
}

struct block_device_operations fop = {
	.owner = THIS_MODULE,
	.getgeo = gendisk_getgeo,
};

void delete_diskmem(void)
{
	int i = 0;
	void *p = NULL;
	for(i = 0; i < (BLKDEV_BYTES + BLK_PAGE_SIZE - 1) / BLK_PAGE_SIZE; i++){
		p = radix_tree_lookup(&blk_dev_data,i);
		radix_tree_delete(&blk_dev_data,i);
		free_pages((unsigned long)p,BLK_PAGE_ORDER);
	}
}

int alloc_diskmem(void)
{
	int ret = 0;
	int i = 0;
	void *p = NULL;
	//初始化基樹,基樹一般用於內存頁管理
	INIT_RADIX_TREE(&blk_dev_data,GFP_KERNEL);
	//(BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE 防止頁面不對齊的情況下,增加一頁.這樣剩餘的數據也能用.不然不對齊超過部分不能使用了
	for(i = 0; i < (BLKDEV_BYTES + BLK_PAGE_SIZE - 1) / BLK_PAGE_SIZE; i++){
		p = (void*)__get_free_pages(GFP_KERNEL,BLK_PAGE_ORDER);
		if(NULL == p){
			ret = -ENOMEM;
			goto err_get_page;
		}
		ret = radix_tree_insert(&blk_dev_data,i,p);
		if(IS_ERR_VALUE(ret)){
			goto err_insert;
		}
	}
#ifdef _DEBUG_
	printk(KERN_WARNING "page size = %d\n",i);
#endif	
	return 0;

err_insert:
	free_pages((unsigned long)p,BLK_PAGE_ORDER);	//插入失敗,先釋放該頁面
err_get_page:
	delete_diskmem();
	return ret;
}

static int __init initialization_function(void)
{
	int ret = 0;
	
	MAJOR_NR = register_blkdev(0, BLK_DISK_NAME);
	if(MAJOR_NR < 0)
	{		
		return -1;
	}
	
	g_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
	if(NULL == g_blkdev_queue){
		ret = -ENOMEM;		
		goto err_alloc_queue;
	}
	
	blk_queue_make_request(g_blkdev_queue, blkdev_make_request);
	
	g_blkdev_disk = alloc_disk(MAX_PARTITIONS);
	if(NULL == g_blkdev_disk){
		ret = -ENOMEM;		
		goto err_alloc_disk;
	}
	
	//申請頁面空間
	ret = alloc_diskmem();
	if(IS_ERR_VALUE(ret)){
		goto err_alloc_disk;
	}
	
	strcpy(g_blkdev_disk->disk_name,BLK_DISK_NAME);
	g_blkdev_disk->major = MAJOR_NR;
	g_blkdev_disk->first_minor = 0;
	g_blkdev_disk->fops = &fop;
	g_blkdev_disk->queue = g_blkdev_queue;
	
	set_capacity(g_blkdev_disk, BLKDEV_BYTES>>9);
	
	add_disk(g_blkdev_disk);
#ifdef _DEBUG_
	printk(KERN_WARNING "ok\n");
#endif
	return ret;
	
err_alloc_disk:
	blk_cleanup_queue(g_blkdev_queue);
err_alloc_queue:
	unregister_blkdev(MAJOR_NR, BLK_DISK_NAME);
	return ret;
}

static void __exit cleanup_function(void)
{
	del_gendisk(g_blkdev_disk);						//->add_disk
	delete_diskmem();								//->alloc_diskmem
	put_disk(g_blkdev_disk);						//->alloc_disk
	blk_cleanup_queue(g_blkdev_queue);					//->blk_init_queue
	unregister_blkdev(MAJOR_NR, BLK_DISK_NAME);
}

//註冊模塊加載卸載函數
module_init(initialization_function);					//指定模塊加載函數
module_exit(cleanup_function);						//指定模塊卸載函數

//模塊信息及許可證
MODULE_AUTHOR("LvApp");								//作者
MODULE_LICENSE("Dual BSD/GPL");						//許可證
MODULE_DESCRIPTION("A simple block module");				//描述
MODULE_ALIAS("block");						   //別名

本人是在參考教程之後修改的教程內容.如有不同.可能有遺漏沒有修改.造成對讀者的迷惑,在此致歉~~ 

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