第十三章
本教程修改自趙磊的網上的一系列教程.本人覺得該系列教程寫的非常不錯.以風趣幽默的語言將塊驅動寫的非常詳細,對於入門教程,應該屬於一份經典了. 本人在這對此係列教程最後附上對Linux 2.6.36版本的代碼.並編譯運行成功. 該教程所有版權仍歸作者趙磊所有,本人只做附錄代碼的添加,併爲對原文修改.有不懂的地方,可以聯繫我 [email protected] 或者給我留言.
+---------------------------------------------------+
| 寫一個塊設備驅動
+---------------------------------------------------+
| 作者:趙磊
| email: [email protected]
+---------------------------------------------------+
| 文章版權歸原作者所有。
| 大家可以自由轉載這篇文章,但原版權信息必須保留。
| 如需用於商業用途,請務必與原作者聯繫,若因未取得
| 授權而收起的版權爭議,由侵權者自行負責。
+---------------------------------------------------+
沒有最好的代碼,是因爲我們總能把代碼改得更好。
因此我們現在打算做一個小的性能改進,這次我們準備拿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()函數基本上是能夠工作的。
本章的改動不大,就算是暫作休整,以留住忍耐至今忍無可忍認爲無需再忍而開始打包收拾行李準備溜之大吉的讀者們。
不過下一章中倒是預備了一個做起來讓人比較有成就感的功能。
<未完,待續>
#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_SECTOR_SHIFT 9
#define BLK_SECTOR_SIZE (1ULL << BLK_SECTOR_SHIFT)
#define BLK_SECTOR_MASK (~(BLK_SECTOR_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;
struct radix_tree_root blk_dev_data;
static unsigned long long g_blk_size = 1;
static char* defaule_size = "16M";
int getsize(void)
{
char flag;
char tail = 'N';
#ifdef _DEBUG_
printk(KERN_WARNING "parameter = %s\n",defaule_size);
#endif
if(sscanf(defaule_size,"%llu%c%c",&g_blk_size,&flag,&tail) != 2){
return -EINVAL;
}
if(!g_blk_size)
return -EINVAL;
switch(flag){
case 'g':
case 'G':
g_blk_size <<= 30;
break;
case 'm':
case 'M':
g_blk_size <<= 20;
break;
case 'k':
case 'K':
g_blk_size <<= 10;
break;
}
g_blk_size = (g_blk_size + BLK_SECTOR_SIZE - 1) & BLK_SECTOR_MASK;
//此處爲字節對齊.(1<<9 - 1) = 255 = 0xFF
#ifdef _DEBUG_
printk(KERN_WARNING "size = %llu tail = %c\n",g_blk_size,tail);
#endif
return 0;
}
/*
function: blk_make_once
description:
該函數完成一次數據操作,將數據讀/寫到指定的page頁面.
parameter:
struct page *page, :要讀/寫的頁面
unsigned int offset, :頁面的相對偏移長度
void *iovec_men, :真實的數據地址
unsigned int len, :數據長度
int dir :數據方向
*/
static int blk_make_once(struct page *page,unsigned int offset,void *iovec_men,unsigned int len,int dir)
{
void *dsk_mem = NULL;
unsigned int count_current = 0;
unsigned int count_done = 0;
unsigned int count_offset = 0;
struct page *this_page = NULL;
while(count_done < len){
count_offset = (offset + count_done) & ~PAGE_MASK;
/*
count_offset:計算當前地址到頁首的偏移
*/
count_current = min(len - count_done,
(unsigned int)(PAGE_SIZE - count_offset));
/*
count_current:計算當前傳輸數據的長度,若跨頁,則取此頁到頁尾的長度
(PAGE_SIZE - count_offset):計算當前偏移到頁尾的長度
*/
this_page = page + ((offset + count_done) >> PAGE_SHIFT);
/*
this_page:獲取片面,將頁面映射至高端內存
*/
dsk_mem = kmap(this_page);
if(!dsk_mem){
printk(KERN_ERR BLK_DISK_NAME
": get memory page address failed: %p\n",
page);
return -EIO;
}
dsk_mem += count_offset;
if(!dir){
//read
memcpy(iovec_men + count_done,dsk_mem,count_current);
}else{
//write
memcpy(dsk_mem,iovec_men + count_done,count_current);
}
kunmap(this_page);
count_done += count_current;
}
return 0;
}
/*
function: blk_make_transition
description:
該函數完成一次映射好的becv數據操作
parameter:
unsigned long long disk_offset, :要讀寫的虛擬磁盤的地址
void *iovec_men, :映射好的bvec操作的數據地址
unsigned int len, :數據長度
int dir :數據方向
*/
static int blk_make_transition(unsigned long long disk_offset,void *iovec_men,unsigned int len,int dir)
{
unsigned int count_current = 0;
unsigned int count_done = 0;
unsigned int count_offset = 0;
struct page *page = NULL;
count_done = 0;
while(count_done < len){
count_offset = (disk_offset + count_done) & ~BLK_PAGE_MASK;
/*
count_offset:表示當前頁對應頁對齊的地址的偏移長度
*/
count_current = min(len - count_done,
(unsigned int)(BLK_PAGE_SIZE - count_offset));
/*
BLK_PAGE_SIZE - count_offset:表示當前頁最後能存儲的長度.
*/
page = (struct page*)radix_tree_lookup(&blk_dev_data,(disk_offset + count_done) >> BLK_PAGE_SHIFT);
if(!page){
printk(KERN_ERR BLK_DISK_NAME
": search page index failed: %d\n",
count_offset);
return -EIO;
}
//進行一次數據讀寫
if(IS_ERR_VALUE(blk_make_once(page,count_offset,iovec_men + count_done,count_current,dir))){
return -EIO;
}
count_done += count_current;
}
return 0;
}
static int blkdev_make_request(struct request_queue *q, struct bio *bio)
{
struct bio_vec *bvec;
int i;
int dir = 0;
unsigned long long disk_offset = 0;
if ((bio->bi_sector << BLK_SECTOR_SHIFT) + bio->bi_size > g_blk_size) {
printk(KERN_ERR BLK_DISK_NAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)bio->bi_sector, bio->bi_size);
goto bio_err;
}
switch(bio_rw(bio)){
case READ:
case READA:
dir = 0;
break;
case WRITE:
dir = 1;
break;
}
disk_offset = bio->bi_sector << BLK_SECTOR_SHIFT;
bio_for_each_segment(bvec, bio, i) {
void *iovec_mem;
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
if(!iovec_mem){
printk(KERN_ERR BLK_DISK_NAME
": kmap page faile: %p\n",
bvec->bv_page);
goto bio_err;
}
//進行一次bvec的操作
if(IS_ERR_VALUE(blk_make_transition(disk_offset,iovec_mem,bvec->bv_len,dir))){
kunmap(bvec->bv_page);
goto bio_err;
}
kunmap(bvec->bv_page);
disk_offset += bvec->bv_len;
}
bio_endio(bio, 0);
return 0;
bio_err:
bio_endio(bio, -EIO);
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 (g_blk_size < 16 * 1024 * 1024) {
phd_geo->heads = 1;
phd_geo->sectors = 1;
} else if (g_blk_size < 512 * 1024 * 1024) {
phd_geo->heads = 1;
phd_geo->sectors = 32;
} else if (g_blk_size < 16ULL * 1024 * 1024 * 1024) {
phd_geo->heads = 32;
phd_geo->sectors = 32;
} else {
phd_geo->heads = 255;
phd_geo->sectors = 63;
}
phd_geo->cylinders = g_blk_size >> BLK_SECTOR_SHIFT / 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;
int page_count = 0;
int next_count = 0;
struct page *pagelist[64] = {NULL};
page_count = 0;
next_count = 0;
do{
page_count = radix_tree_gang_lookup(&blk_dev_data, (void**)pagelist,next_count,ARRAY_SIZE(pagelist));
for(i = 0;i < page_count;i++){
next_count = pagelist[i]->index;
radix_tree_delete(&blk_dev_data,next_count);
__free_pages(pagelist[i],BLK_PAGE_ORDER);
}
next_count++;
}while(page_count == ARRAY_SIZE(pagelist));
/*
for(i = 0; i < (g_blk_size + BLK_PAGE_SIZE - 1) / BLK_PAGE_SIZE; i++){
page = radix_tree_lookup(&blk_dev_data,i);
radix_tree_delete(&blk_dev_data,i);
__free_pages(page,BLK_PAGE_ORDER);
}
*/
}
int alloc_diskmem(void)
{
int ret = 0;
int i = 0;
struct page *page = NULL;
INIT_RADIX_TREE(&blk_dev_data,GFP_KERNEL);
for(i = 0; i < (g_blk_size + BLK_PAGE_SIZE - 1) / BLK_PAGE_SIZE; i++){
page = (void*)alloc_pages(GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,BLK_PAGE_ORDER);
if(NULL == page){
ret = -ENOMEM;
goto err_get_page;
}
/*
page->index成員,在頁面被用作高速緩存時,記錄相對偏移,而此處我們不會將頁面用作高速緩存,
也就是該成員沒有被使用,所以這裏可以用來表示頁的index.
以便在釋放的時候,可以用作index來釋放基樹節點
*/
page->index = i;
ret = radix_tree_insert(&blk_dev_data,i,(void*)page);
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(page,BLK_PAGE_ORDER);
err_get_page:
delete_diskmem();
return ret;
}
static int __init initialization_function(void)
{
int ret = 0;
getsize();
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, g_blk_size>>BLK_SECTOR_SHIFT);
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); //指定模塊卸載函數
//到處函數.導出名size,對應變量defaule_size
module_param_named(size, defaule_size, charp, S_IRUGO);
//模塊信息及許可證
MODULE_AUTHOR("LvApp"); //作者
MODULE_LICENSE("Dual BSD/GPL"); //許可證
MODULE_DESCRIPTION("A simple block module"); //描述
MODULE_ALIAS("block"); //別名
本人是在參考教程之後修改的教程內容.如有不同.可能有遺漏沒有修改.造成對讀者的迷惑,在此致歉~~