- 論壇徽章:
- 0
|
第6章
+---------------------------------------------------+
| 寫一個塊設備驅動 |
+---------------------------------------------------+
| 作者:趙磊 |
| email: [email]zhaoleidd@hotmail.com[/email] |
+---------------------------------------------------+
| 文章版權歸原作者所有。 |
| 大家可以自由轉載這篇文章,但原版權信息必須保留。 |
| 如需用于商業(yè)用途,請務必與原作者聯系,若因未取得 |
| 授權而收起的版權爭議,由侵權者自行負責。 |
+---------------------------------------------------+
經歷了內容極為簡單的前兩章的休息,現在大家一定感到精神百倍了。
作為已經堅持到現在的讀者,對接下去將要面臨的內容大概應該能夠猜得八九不離十了,
具體的內容猜不出來也無妨,但一定將是具有增加顱壓功效的。
與物理塊設備驅動程序的區(qū)別在于,我們的驅動程序使用內存來存儲塊設備中的數據。
到目前為止,我們一直都是使用這樣一個靜態(tài)數組來擔負這一功能的:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
如果讀者懂得一些模塊的知識,或者現在趕緊去臨時抱佛腳google一些模塊知識,
應該知道模塊其實是加載在非線性映射區(qū)域的。
詳細來說,在加載模塊時,根據模塊的ELF信息(天哪,又要去google elf了),確定這個模塊所需的靜態(tài)內存大小。
這些內存用來容納模塊的二進制代碼,以及靜態(tài)變量。然后申請容納這些數據的一大堆頁面。
當然,這些頁面并不是連續(xù)的,而代碼和變量卻不可能神奇到即使被切成一塊一塊的也能正常工作。
因此需要在非線性映射區(qū)域中找到一塊連續(xù)的地址(現在有要去google非線性映射區(qū)域了),用來將剛才申請到的一塊一塊的內存頁映射到這個地址段中。
最后模塊被請到這段區(qū)域中,然后執(zhí)行模塊的初始化函數......
現在看我們這個模塊中的simp_blkdev_data變量,如果不是現在刻意關注,這個變量看起來顯得那么得普通。
正如其它的一些名字原先也是那么的普通,但由于一些突發(fā)的事件受到大家的熱烈關注,
比如一段視頻讓我們熟悉了kappa和陸佳妮,比如呼吸稅讓我們認識了蔣有緒。
現在我們開始關注simp_blkdev_data變量了,導火索是剛才介紹的非線性映射區(qū)域。
模塊之所以被加載到非線性映射區(qū)域,是因為很難在線性映射區(qū)域中得到加載模塊所需的連續(xù)的內存。
但使用非線性映射區(qū)域也并非只賺不賠的生意,至少在i386結構中,非線性映射區(qū)域實在是太小了。
在物理內存大于896M的i386系統(tǒng)中,整個非線性映射區(qū)域不會超過128M。
相反如果物理內存小于896M(不知道該算是幸運還是不幸),非線性映射區(qū)域反而會稍微大一些,這種情況我想我們可以不用討論了,畢竟不能為了加載一個模塊去拔內存。
因此我們的結論是:非線性映射區(qū)域是很緊張的資源,我們要節(jié)約使用。
而像我們現在這個模塊中的simp_blkdev_data卻是個如假包換的反面典型,居然上來就吃掉了16M!這還是因為我們沒有把SIMP_BLKDEV_BYTES定義得更大。
現在我們開始列舉simp_blkdev_data的種種罪行:
1:剩余的非線性映射區(qū)域較小時導致模塊加載失敗
2:模塊加載后占用了大量的非線性映射區(qū)域,導致其它模塊加載失敗。
3:模塊加載后占用了大量的非線性映射區(qū)域,影響系統(tǒng)的正常運行。
這是因為不光模塊,系統(tǒng)本身的很多功能也依賴非線性映射區(qū)域空間。
對于這樣的害群之馬,我們難道還有留下他的理由嗎?
本章的內容雖然麻煩一些,但想到能夠一了百了地清除這個體大膘肥的simp_blkdev_data,倒也相當值得。
也希望今后能夠看到在對貪官的處理上,能夠也拿出這樣的魄力和勇氣。
現在在清除simp_blkdev_data的問題上,已經不存在什么懸念了,接下來我們需要關注的是將simp_blkdev_data碎尸萬段后,拿出一個更恰當方法來代替它。
首先,我們決定不用靜態(tài)聲明的數組,而改用動態(tài)申請的內存。
其次,使用類似vmalloc()的函數可以動態(tài)申請大段內存,但其實這段內存占用的還是非線性映射區(qū)域,就好像用一個比較隱蔽的貪官來代替下馬的貪官,我們不會愚蠢在這種地步。
剩下的,就是在線性映射區(qū)域申請很多個頁的內存,然后自己去管理。這個方法一了百了地解決了使用大段非線性映射區(qū)域的問題,而唯一的問題是由于需要自己管理申請到的頁面,使程序復雜了不少。
但為了整個系統(tǒng)的利益,這難道不是我們該做的嗎?
申請一個內存頁是很容易的,這里我們將采用所有容易的方法中最容易的那個:
__get_free_page函數,原型是:
unsigned long __get_free_page(gfp_t gfp_mask);
這個函數用來申請一個頁面的內存。gfp_mask包含一些對申請內存時的指定,比如,要在DMA區(qū)域中啦、必須清零等。
我們這里倒是使用最常見的__get_free_page(GFP_KERNEL)就可以了。
通過__get_free_page申請到了一大堆內存頁,新的問題來了,在讀寫塊設備時,我們得到是塊設備的偏移,如何快速地通過偏移找到對應的內存頁呢?
最簡單的方法是建立一個數組,用來存放偏移到內存的映射,數組中的每項對應一個一個頁:
數組定義如下:
void *simp_blkdev_data[(SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE];
PAGE_SIZE是系統(tǒng)中每個頁的大小,對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;
這種方法實現起來還是比較簡單的,但缺點也不是沒有:存放各個頁面地址的數組雖然其體積比原先那個直接存放數據的數組已經縮小了很多,
但本身畢竟還是在非線性映射區(qū)域中。如果塊設備大小為16M,在i386上,需要4096個頁面,數組大小16K,這不算太大。
但如果某個瘋子打算建立一個2G的虛擬磁盤,數組大小將達到2M,這就不算小了。
或者我們可以不用數組,而用鏈表來存儲偏移到內存頁的映射關系,這樣可以回避掉數組存在的問題,但在鏈表中查找指定元素卻不是一般的費時,
畢竟我們不希望用戶覺得這是個軟盤。
接下來作者不打斷繼續(xù)賣關子了,我們最終選擇使用的是傳說中的基樹。
關于linux中基樹細節(jié)的文檔不多,特別是中文文檔更少,更糟的是我們這篇文檔也不打算作詳細的介紹(因為作者建議去RTFSC)。
但總的來說,相對于二叉平衡樹的紅黑樹來說,基樹是一個n叉(一般為64叉)非平衡樹,n叉減少了搜索的深度,非平衡減少了復雜的平衡操作。
當然,這兩個特點也不是僅僅帶來優(yōu)點,但在這里我們就視而不見了,畢竟我們已經選擇了基樹,因此護短也是自認而然的事情,正如公仆護著王細牛一樣。
從功能上來說,基樹好像是為我們量身定做的一樣,好用至極。
(其實我們也可以考慮選擇紅黑樹和哈希表來實現這個功能,感興趣的讀者可以了解一下)
接下來的代碼中,我們將要用到基樹種的如下函數:
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個字節(jié)就大功告成了,比如,讀塊設備時就是:
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
但現在由于容納數據的每個頁面地址是不連續(xù)的,因此可能出現bio_vec中的數據跨越頁面邊界的情況。
也就是說,一個bio_vec中的數據的前半段在一個頁面中,后半段在另一個頁面中。
雖然這兩個頁面對應的塊設備地址連續(xù),但在內存中的地址不一定連續(xù),因此像原先那樣簡單使用memcpy看樣子是解決不了問題了。
實際上,雖然bio_vec可能跨越頁面邊界,但它無論如何也不可能跨越2個以上的頁面。
這是因為bio_vec本身對應的數據最大長度只有一個頁面。
因此如果希望做最簡單的實現,只要在代碼中做一個條件判斷就OK了:
if (沒有跨越頁面) {
1個memcpy搞定
} else {
/* 肯定是跨越2個頁面了 */
2個memcpy搞定
}
但為了表現出物理設備一次傳送1個扇區(qū)數據的處理方式(這種情況下一個bio_vec可能會跨越2個以上的扇區(qū)),我們讓代碼支持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,也就是起始扇區(qū)對應的偏移,也是第一個bio_vec對應的塊設備偏移。
每處理完成一個bio_vec時,dsk_offset值會被更新:dsk_offset += bvec->bv_len,以指向將要處理的數據在塊設備上的偏移。
在bio_for_each_segment()中代碼的起始和末尾,執(zhí)行kmap和kunmap開映射當前這個bio_vec的內存,這個知識在前面的章節(jié)中已經提到了,
這個處理的結果是iovec_mem指向當前的bio_vec中的緩沖區(qū)。
現在在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,說明這一次將傳送從當前塊設備偏移到其所在內存頁的頁尾之間的數據,
余下的數據位于后續(xù)的頁面中,將在接下來的循環(huán)中搞定,
如果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
#
看看模塊的加載時分配的非線性映射區(qū)域大。
# lsmod
Module Size Used by
simp_blkdev 8212 0
...
#
如果這個Size一欄的數字沒有引起讀者的足夠重視的話,我們拿修改前的模塊來對比一下:
# lsmod
Module Size Used by
simp_blkdev 16784392 0
看出區(qū)別了沒?
如果本章到這里還不結束的話,估計讀者要開始閃人了。
好的,我們馬上就結束,希望在這之前閃掉的讀者不要太多。
由于還沒有來得及閃掉而看到這段話的讀者們,作者相信你們具有相當的毅力。
學習是需要毅力的,這時的作者同樣也需要毅力來堅持完成這本教程。
最后還是希望讀者堅持,堅持看完所有的章節(jié),堅持在遇到每一個不明白的問題時都努力尋求答案,
堅持在發(fā)現自己感興趣的內容時能夠深入地去了解、探尋、思考。
<未完,待續(xù)>
[[i] 本帖最后由 OstrichFly 于 2008-11-26 09:31 編輯 [/i]] |
|