- 論壇徽章:
- 0
|
第10章
+---------------------------------------------------+
| 寫一個塊設(shè)備驅(qū)動 |
+---------------------------------------------------+
| 作者:趙磊 |
| email: zhaoleidd@hotmail.com |
+---------------------------------------------------+
| 文章版權(quán)歸原作者所有。 |
| 大家可以自由轉(zhuǎn)載這篇文章,但原版權(quán)信息必須保留。 |
| 如需用于商業(yè)用途,請務(wù)必與原作者聯(lián)系,若因未取得 |
| 授權(quán)而收起的版權(quán)爭議,由侵權(quán)者自行負責。 |
+---------------------------------------------------+
如果你的linux系統(tǒng)是x86平臺,并且內(nèi)存大于896M,那么恭喜你,我們大概可以在這個實驗中搞壞你的系統(tǒng)。
反之如果你的系統(tǒng)不符合這些條件,也不用為無法搞壞系統(tǒng)而感到失望,本章的內(nèi)容同樣適合你。
這時作者自然也要申明一下對讀者產(chǎn)生的任何損失概不負責,
因為這年頭一不小心就可能差點成了被告,比如南京的彭宇和鎮(zhèn)江花山灣的小許姑娘。
在實驗看到的情況會因為系統(tǒng)的實際狀況不同而稍有區(qū)別,但我們需要說明的問題倒是相似的。
但希望讀者不要把這種相似理解成了ATM機取款17.5萬和貪污2.6億在判決上的那種相似。
首先我們來看看目前系統(tǒng)的內(nèi)存狀況:
# 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
#
輸出很多,但我們只關(guān)心這幾行:
MemFree: 1529236 kB --這說明系統(tǒng)中有接近1.5G的空閑內(nèi)存
HighFree: 640836 kB --這說明空閑內(nèi)存中,處在高端的有600M左右
LowFree: 888400 kB --這說明空閑內(nèi)存中,處在低端的有800M左右
現(xiàn)在加載上一章完成的模塊,我們指定創(chuàng)建800M的塊設(shè)備:
# insmod simp_blkdev.ko size=800M
#
成功了,我們再看看內(nèi)存狀況:
# cat /proc/meminfo
MemFree: 708812 kB
HighFree: 640464 kB
LowFree: 68348 kB
...
#
我們發(fā)現(xiàn)高端內(nèi)存沒怎變,低端內(nèi)存卻已經(jīng)被耗得差不多了。
我們一不做二不休,繼續(xù)加大塊設(shè)備的容量,看看極限能到多少:
# rmmod simp_blkdev
# insmod simp_blkdev.ko size=860M
# cat /proc/meminfo
MemFree: 651184 kB
HighFree: 641972 kB
LowFree: 9212 kB
...
#
系統(tǒng)居然還沒事,這時雖然高端內(nèi)存還是沒怎么變,但低端內(nèi)存剩下的得已經(jīng)很可憐了。
然后進一步加大塊設(shè)備的容量:
# rmmod simp_blkdev
# insmod simp_blkdev.ko size=870M
...
這里不用再cat /proc/meminfo了,因為系統(tǒng)已經(jīng)完蛋了。
如果有些讀者嗜好獨特,對出錯信息情有獨鐘的話,在這里也滿足一下:
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
...
搞壞系統(tǒng)就當是交學費了,但交完學費我們總要學到些東西。
雖然公款出國考察似乎已經(jīng)斯通見慣,但至少在我們的理解中,學費不是旅游費,更不是家屬的旅游費。
我們通過細心觀察、周密推理后得出的結(jié)論是:
目前的塊設(shè)備驅(qū)動程序會一根筋地使用低端內(nèi)存,即使系統(tǒng)中低端內(nèi)存很緊缺的時候,
也會直道把系統(tǒng)搞死卻不去動半點的高端內(nèi)存,這未免也太挑食了,
因此在本章和接下來的幾章中,我們將幫助驅(qū)動程序戒掉對低端內(nèi)存的癮。
相對高端內(nèi)存而言,低端內(nèi)存是比較寶貴的,這是因為它不需要影射就能直接被內(nèi)核訪問的特性。
而內(nèi)核中的不少功能都直接使用低端內(nèi)存,以保證訪問的速度和簡便,
但換句話來說,如果低端內(nèi)存告急,那么系統(tǒng)可能離Panic也不遠了。
因此總的來說,對低端內(nèi)存的使用方法大概應(yīng)該是:除非有足夠理由,否則就別亂占著。
詳細來說,就是:
1:不需要使用低端內(nèi)存的“在內(nèi)核中不需要映射就能直接訪問”這個特性的功能,應(yīng)該優(yōu)先使用高端內(nèi)存
如:分配給用戶態(tài)進程的內(nèi)存,和vmalloc的內(nèi)存
2:需要占用大量內(nèi)存的功能,并且也可以通過高端內(nèi)存實現(xiàn)的,應(yīng)該優(yōu)先使用高端內(nèi)存
如:我們的程序
與內(nèi)存有關(guān)的知識我們在以前的章節(jié)中已經(jīng)談到,因此這里不再重復(fù)了,
但需要說明的是在高端內(nèi)存被映射之前,我們是無法通過指針來指向它的。
因為它不在內(nèi)核空間的地址范圍以內(nèi)。
雖然如此,我們卻無論如何都需要找出一種方法來指定一個沒有被映射的高端內(nèi)存,
這是由于至少在進行映射操作時,我們需要指定去映射誰。
這就像為一群猴子取名的時候,如何來說明是正在給哪只猴子取名一樣。
雖然給猴子取名的問題可能比較容易解決,比如我們可以說,
給哪只紅屁股的公猴取名叫齊天大圣、給那只瘦瘦的母猴取名叫白晶晶,
但可惜一塊高端內(nèi)存即沒有紅屁股,又沒有胖瘦之分,
它們唯一有的就是地址,因此我們也必須通過地址來指定這段高端內(nèi)存。
剛才說過,在高端內(nèi)存被映射之前,他在內(nèi)核的地址空間中是不存在的,
但雖然如此,它至少存在其物理地址,而我們正是可以通過它的物理地址來指定它。
是的,本質(zhì)上是這樣的,但在linux中,我們還需要再繞那么一丁點:
linux在啟動階段為全部物理內(nèi)存按頁為單位建立了的對應(yīng)的struct page結(jié)構(gòu),用來管理這些物理內(nèi)存,
也就是,每個頁的物理內(nèi)存,都有著1對1的struct page結(jié)構(gòu),而這些struct page結(jié)構(gòu)是位于低端內(nèi)存中的,
我們只要使用指向某個struct page結(jié)構(gòu)的指針,就能指定物理內(nèi)存中的一個頁。
因此,對于沒有被映射到內(nèi)核空間中的高端內(nèi)存,我們可以通過對應(yīng)的struct page結(jié)構(gòu)來指定它。
(如果讀者希望了解更詳細的知識,可以考慮從virt_to_page函數(shù)一路google下去)
我們在這里大肆談?wù)摳叨藘?nèi)存的表示方法,因為這是讓我們的模塊使用高端內(nèi)存的前提。
我們的驅(qū)動程序使用多段內(nèi)存來存儲塊設(shè)備中的數(shù)據(jù)。
原先的程序中,我們使用指向這些內(nèi)存段的指針來指定這些數(shù)據(jù)的位置,這是沒有問題的,
因為當時我們是使用__get_free_pages()來申請內(nèi)存,__get_free_pages()函數(shù)只能用來申請低端內(nèi)存,
因為這個函數(shù)返回的是申請到的內(nèi)存的指針,而上文中說過,高端內(nèi)存是不能用這樣的指針表示的。
要申請高端內(nèi)存,明顯不能使用這樣的函數(shù),因此我們隆重介紹它的代替者出場:
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
這個函數(shù)的參數(shù)與__get_free_pages()相同,但區(qū)別在于,它返回指向struct page的指針,
這個我們在上文中介紹過的指針賦予了alloc_pages()函數(shù)申請高端內(nèi)存的能力。
其實申請一塊高端內(nèi)存并不難,只要使用__GFP_HIGHMEM參數(shù)調(diào)用alloc_pages()函數(shù),
就可能返回一塊高端內(nèi)存,之所以說是“可能”,使因為在某些情況下,比如高端內(nèi)存不夠或不存在時,也會但會低端內(nèi)存充數(shù)。
我們的現(xiàn)在的目標是讓驅(qū)動程序使用高端內(nèi)存,這需要:
1:讓驅(qū)動程序申請高端內(nèi)存
2:讓驅(qū)動程序使用高端內(nèi)存
但在這一章中,我們要做的即不是1,也不是2,而是1之前的準備工作。
因為1和2必須一氣呵成地改完,而為了讓一氣呵成的時候不要再面臨其他插曲,
我們需要做好充足的準備工作,就像ml前尿尿一樣。
對應(yīng)到程序的修改工作上,我們打算先讓程序使用struct page *來指定申請到的內(nèi)存。
要實現(xiàn)這個目的,我們先要改申請內(nèi)存的函數(shù),也就是alloc_diskmem()。
剛才我們介紹過alloc_pages(),現(xiàn)在就要用它了:
首先把函數(shù)中定義的
void *p;
改成
struct page *page;
因為我們要使用struct page *來指定申請到的內(nèi)存,而不是地址了。
然后把
p = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO, SIMP_BLKDEV_DATASEGORDER);
改成
page = alloc_pages(GFP_KERNEL | __GFP_ZERO, SIMP_BLKDEV_DATASEGORDER);
這一行改動的原因大概已經(jīng)說得很詳細了。
還有那個if(!p)改成if (!page)
然后就是把指針加入基樹的那一行:
ret = radix_tree_insert(&simp_blkdev_data, i, p);
改成
ret = radix_tree_insert(&simp_blkdev_data, i, page);
由于我們使用了struct page *來指定申請到的內(nèi)存,因此錯誤處理部分也要小改一下:
free_pages((unsigned long)p, SIMP_BLKDEV_DATASEGORDER);
改成
__free_pages(page, SIMP_BLKDEV_DATASEGORDER);
這里補充介紹一下__free_pages()函數(shù),可能大家已經(jīng)猜到其作用了,
其實與我們原先使用的free_pages()函數(shù)相似,都是用來釋放一段內(nèi)存,
但__free_pages()使用struct page *來指定要釋放的內(nèi)存,這也意味著它能夠用來釋放高端內(nèi)存。
大家應(yīng)該已經(jīng)發(fā)現(xiàn)我們雖然改用alloc_pages()函數(shù)來申請內(nèi)存,但并沒有指定__GFP_HIGHMEM參數(shù),
這時申請到的仍然是低端內(nèi)存,因此避免了在這一章中對訪問內(nèi)存那部分代碼的大肆改動。
改動過的alloc_pages()函數(shù)是這樣的:
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;
}
相應(yīng)的,釋放內(nèi)存用的free_diskmem()函數(shù)也需要一些更改,
為了避免有人說作者唐僧,列出修改后的樣子應(yīng)該已經(jīng)足夠了:
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()函數(shù):
首先我們不是把void *dsk_mem改成struct page *dsk_page,而是增加一個
struct page *dsk_page;
變量,因為在訪問內(nèi)存時,我們還是需要用到dsk_mem變量的。
然后是從基數(shù)中獲取指針的代碼,把原先的
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);
雖然看起來沒什么太大變化,但我們需要知道,這時基樹返回的指針已經(jīng)不是直接指向數(shù)據(jù)所在的內(nèi)存了。
還有那個判斷是否從基樹中獲取成功的
if (!dsk_mem) {
用腳丫子也能想得出應(yīng)該改成這樣:
if (!dsk_page) {
還有就是我們需要首先將struct page *dsk_page地址轉(zhuǎn)換成內(nèi)存的地址后,才能對這塊內(nèi)存進行訪問。
這里我們使用了page_address()函數(shù)。
這個函數(shù)可以獲得struct page數(shù)據(jù)結(jié)構(gòu)所對應(yīng)內(nèi)存的地址。
這時可能有讀者要問了,如果這個struct page對應(yīng)的是高端內(nèi)存,那么如何返回地址呢?
實際上,這種情況下如果高端內(nèi)存中的頁面已經(jīng)被映射到內(nèi)核的地址空間,那么函數(shù)會返回映射到內(nèi)核空間中的地址,
而如果沒有映射的話,函數(shù)將返回0。
對于我們目前的程序而言,由于使用的是低端內(nèi)存,因此struct page對應(yīng)的內(nèi)存總是處于內(nèi)核地址空間中的。
對應(yīng)到代碼中,我們需要在使用dsk_mem之前,也就是
dsk_mem += (dsk_offset + count_done) & ~SIMP_BLKDEV_DATASEGMASK;
這條語句之前,讓dsk_mem指向struct page *dsk_page對應(yīng)的內(nèi)存的實際地址。
這是通過如下代碼實現(xiàn)的:
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()函數(shù)是這樣的:
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個函數(shù)的更改,代碼可以使用struct page *來定位存儲塊設(shè)備數(shù)據(jù)的內(nèi)存了。
這也為將來使用高端內(nèi)存做了一部分準備。
因為本章修改的代碼在外部功能上沒有發(fā)生變動,所以我們就不在這里嘗試編譯了運行代碼了。
不過感興趣的讀者不妨試一試這段代碼能不能進行編譯和會不會引起死機。
<未完,待續(xù)> |
|