第 10 章
+---------------------------------------------------+
| 寫一個塊設備驅動 |
+---------------------------------------------------+
| 作者:趙磊 |
| email: [email protected] |
+---------------------------------------------------+
| 文章版權歸原作者所有。 |
| 大家可以自由轉載這篇文章,但原版權信息必須保留。 |
----------------------- Page 70-----------------------
| 如需用於商業用途,請務必與原作者聯繫,若因未取得 |
| 授權而收起的版權爭議,由侵權者自行負責。 |
+---------------------------------------------------+
如果你的 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: 8884 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
----------------------- Page 71-----------------------
VmallocChunk: 114288 kB
HugePages_Total:
HugePages_Free:
HugePages_Rsvd:
HugePages_Surp:
Hugepagesize: 4096 kB
DirectMap4k: 12288 kB
DirectMap4M: 905216 kB
#
輸出很多,但我們只關心這幾行:
MemFree: 1529236 kB --這說明系統中有接近1.5G的空閒內存
HighFree: 640836 kB --這說明空閒內存中,處在高端的有 6 M左右
LowFree: 8884 kB --這說明空閒內存中,處在低端的有 8 M左右
現在加載上一章完成的模塊,我們指定創建 8 M的塊設備:
# insmod simp_blkdev.ko size=8 M
#
成功了,我們再看看內存狀況:
# 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=
----------------------- Page 72-----------------------
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/0x36
kernel: [ 3588.7715 ] [<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] [<f8867 >] ? 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:
kernel: [ 3588.774582] Normal per-cpu:
kernel: [ 3588.774689] CPU 0: hi: 186, btch: 31 usd:
kernel: [ 3588.774870] HighMem per-cpu:
kernel: [ 3588.778602] CPU 0: hi: 186, btch: 31 usd:
...
搞壞系統就當是交學費了,但交完學費我們總要學到些東西
雖然公款出國考察似乎已經斯通見慣,但至少在我們的理解中,學費不是旅遊費,更不是家屬的旅遊費
我們通過細心觀察、周密推理後得出的結論是:
目前的塊設備驅動程序會一根筋地使用低端內存,即使系統中低端內存很緊缺的時候,
也會直道把系統搞死卻不去動半點的高端內存,這未免也太挑食了,
因此在本章和接下來的幾章中,我們將幫助驅動程序戒掉對低端內存的癮
相對高端內存而言,低端內存是比較寶貴的,這是因爲它不需要影射就能直接被內核訪問的特性
而內核中的不少功能都直接使用低端內存,以保證訪問的速度和簡便,
但換句話來說,如果低端內存告急,那麼系統可能離Panic也不遠了
因此總的來說,對低端內存的使用方法大概應該是:除非有足夠理由,否則就別亂佔着
詳細來說,就是:
1 :不需要使用低端內存的“在內核中不需要映射就能直接訪問”這個特性的功能,應該優先使用高端內存
如:分配給用戶態進程的內存,和 vmalloc的內存
2 :需要佔用大量內存的功能,並且也可以通過高端內存實現的,應該優先使用高端內存
如:我們的程序
與內存有關的知識我們在以前的章節中已經談到,因此這裏不再重複了,
但需要說明的是在高端內存被映射之前,我們是無法通過指針來指向它的
----------------------- Page 73-----------------------
因爲它不在內核空間的地址範圍以內
雖然如此,我們卻無論如何都需要找出一種方法來指定一個沒有被映射的高端內存,
這是由於至少在進行映射操作時,我們需要指定去映射誰
這就像爲一羣猴子取名的時候,如何來說明是正在給哪隻猴子取名一樣
雖然給猴子取名的問題可能比較容易解決,比如我們可以說,
給哪隻紅屁股的公猴取名叫齊天大聖、給那隻瘦瘦的母猴取名叫白晶晶,
但可惜一塊高端內存即沒有紅屁股,又沒有胖瘦之分,
它們唯一有的就是地址,因此我們也必須通過地址來指定這段高端內存
剛纔說過,在高端內存被映射之前,他在內核的地址空間中是不存在的,
但雖然如此,它至少存在其物理地址,而我們正是可以通過它的物理地址來指定它
是的,本質上是這樣的,但在 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之前的準備工作
----------------------- Page 74-----------------------
因爲 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;
----------------------- Page 75-----------------------
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);
}
}
----------------------- Page 76-----------------------
隨後是 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對應的是高端內存,那麼如何返回地址呢?
實際上,這種情況下如果高端內存中的頁面已經被映射到內核的地址空間,那麼函數會返回映射到內核
空間中的地址,
而如果沒有映射的話,函數將返回
對於我們目前的程序而言,由於使用的是低端內存,因此 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);
----------------------- Page 77-----------------------
#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)));
----------------------- Page 78-----------------------
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,
----------------------- Page 79-----------------------
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 *來定位存儲塊設備數據的內存了
這也爲將來使用高端內存做了一部分準備
因爲本章修改的代碼在外部功能上沒有發生變動,所以我們就不在這裏嘗試編譯了運行代碼了
不過感興趣的讀者不妨試一試這段代碼能不能進行編譯和會不會引起死機
<未完,待續>