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

第八章

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

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

|                 寫一個塊設備驅動                  

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

作者:趙磊                                        

| email: [email protected]                      

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

文章版權歸原作者所有。                            

大家可以自由轉載這篇文章,但原版權信息必須保留。  

如需用於商業用途,請務必與原作者聯繫,若因未取得  

授權而收起的版權爭議,由侵權者自行負責。          

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


本章的目的是讓讀者繼續休息,因此決定仍然搞一些簡單的東西。
比如:給我們的驅動程序模塊加上模塊參數,這樣在加載模塊時,可以通過參數設定塊設備的大小。


給我們模塊加參數的工作不難,這牽涉到1個宏:
module_param_named(name, value, type, perm)
        name是參數的名稱
        value是參數在模塊中對應的變量
        type是參數的類型
        perm是參數的權限
如,在模塊中添加
int disk_size = 1024;
module_param_named(size, disk_size, int, S_IRUGO);
可以給模塊加上名稱爲"size"的參數,如果在加載模塊是使用insmod thismodule size=100,那麼在模塊代碼中disk_size的值就是100。
相反,如果加載模塊時沒有指定參數,那麼模塊代碼中disk_size的值仍是默認的1024。
S_IRUGO指定了這個參數的值在模塊加載以後可以被所有人通過/sys/module/[module_name]/parameters/看到,但無法修改。
好了,有關module_param_named就介紹到這裏,細節可以google或者看linux/include/linux/moduleparam.h。


然後我們就要給這個模塊加個參數,用來在加載時指定塊設備的大小。
參數的名字都已經想好了,就叫size吧,類型嘛,32位無符號整數最大能設定到4G,而我們的野心看起來可能更大一些,
爲了讓這個模塊支持4G以上的虛擬磁盤(當然是內存足夠的情況下),我們打算使用64位無符號整型。這樣能夠設定的最大值爲16777216T,應該夠了吧。


然後我們試圖找出module_param_named的參數中與unsigned long long對應的type來。
結果是:google了,沒找到;看linux/include/linux/moduleparam.h了,還是沒找到。
結論是:目前的linux(2.6.28)還不支持unsigned long long類型的模塊參數。
更新一些的內核中會不會有是將來的事,儘快搞定這一章的功能卻是現在面臨的問題。


然後我們就開始找解決方案:
1:給內核打個補丁,看樣子不錯,但至少今天之類完成不了我們的程序了
   並且這樣一來,我們的程序只能在今後的內核中運行,而失去對舊版linux的兼容性。
2:指定設置磁盤大小的單位爲M。這樣可設置的最大的數字就成了4G*1M,也就是4096T。
   這個主意看似不錯。而且看樣子10年內機器的內存應該到不了這個容量。
3:用字符串來指定大小
   這倒是可以解決所有問題,並且我們可以支持16M、1G之類的設定,讓我們的程序看起來比較花哨。
   缺點應該是我們需要在程序中自己去解析傳入的字符串了,幸運的是,實際的解析代碼比想象的容易一些。
因此,我們採用第3個方案,向模塊中添加一個名稱爲size、類型爲字符串的參數,並且支持解析以K,M,G,T爲單位的設定。


第1步:
  向程序中添加以下參數申明。
  static char *simp_blkdev_param_size = "16M";
  module_param_named(size, simp_blkdev_param_size, charp, S_IRUGO);
  char *simp_blkdev_param_size用於存儲設定的磁盤大小,我們把磁盤大小的默認值指定爲16M。
  目前我們不允許用戶在模塊加載後改變磁盤大小,將來嘛,有可能增加這一功能,看起來很眩。
第2步:
  原來的程序使用
  #define SIMP_BLKDEV_BYTES      (16*1024*1024)
  定義磁盤大小,而現在我們不需要這一行了。
  同時,我們需要一個unsigned long long變量來存儲用戶設定的磁盤大小,因此我們增加這個變量:
  static unsigned long long simp_blkdev_bytes;
  然後把程序中所有使用SIMP_BLKDEV_BYTES的位置換成使用simp_blkdev_bytes變量。
第3步:
  在模塊加載時對模塊參數進行解析,設置simp_blkdev_bytes變量的值。
  我們增加一個函數進行解析工作:

int getparam(void)
{
        char unit;
        char tailc;


        if (sscanf(simp_blkdev_param_size, "%llu%c%c", &simp_blkdev_bytes,
                &unit, &tailc) != 2) {
                return -EINVAL;
        }


        if (!simp_blkdev_bytes)
                return -EINVAL;


        switch (unit) {
        case 'g':
        case 'G':
                simp_blkdev_bytes <<= 30;
                break;
        case 'm':
        case 'M':
                simp_blkdev_bytes <<= 20;
                break;
        case 'k':
        case 'K':
                simp_blkdev_bytes <<= 10;
                break;
        case 'b':
        case 'B':
                break;
        default:
                return -EINVAL;
        }


        /* make simp_blkdev_bytes fits sector's size */
        simp_blkdev_bytes = (simp_blkdev_bytes + (1<<9) - 1) & ~((1ULL<<9) - 1);


        return 0;
}
然後在simp_blkdev_init()中調用這個函數:
ret = getparam();
if (IS_ERR_VALUE(ret))
        goto err_getparam;
當然,err_getparam的位置讀者應該能猜出來了。


這樣一來,工作大概就完成了,讓我們看看結果:
使用默認值:
# insmod simp_blkdev.ko
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won't be recoverable.


Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)


Command (m for help): p


Disk /dev/simp_blkdev: 16 MB, 16777216 bytes
1 heads, 32 sectors/track, 1024 cylinders
Units = cylinders of 32 * 512 = 16384 bytes


           Device Boot      Start         End      Blocks   Id  System


Command (m for help): q


#
設定成20M:
# rmmod simp_blkdev
# insmod simp_blkdev.ko size=20M
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won't be recoverable.




The number of cylinders for this disk is set to 1280.
There is nothing wrong with that, but this is larger than 1024,
and could in certain setups cause problems with:
1) software that runs at boot time (e.g., old versions of LILO)
2) booting and partitioning software from other OSs
   (e.g., DOS FDISK, OS/2 FDISK)
Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)


Command (m for help): p


Disk /dev/simp_blkdev: 20 MB, 20971520 bytes
1 heads, 32 sectors/track, 1280 cylinders
Units = cylinders of 32 * 512 = 16384 bytes


           Device Boot      Start         End      Blocks   Id  System


Command (m for help): q


#
變態一下,還是設定成20M,但用k作單位:
# rmmod simp_blkdev
# insmod simp_blkdev.ko size=20480k
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won't be recoverable.




The number of cylinders for this disk is set to 1280.
There is nothing wrong with that, but this is larger than 1024,
and could in certain setups cause problems with:
1) software that runs at boot time (e.g., old versions of LILO)
2) booting and partitioning software from other OSs
   (e.g., DOS FDISK, OS/2 FDISK)
Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)


Command (m for help): p


Disk /dev/simp_blkdev: 20 MB, 20971520 bytes
1 heads, 32 sectors/track, 1280 cylinders
Units = cylinders of 32 * 512 = 16384 bytes


           Device Boot      Start         End      Blocks   Id  System


Command (m for help): q


#


看樣子結果不錯。
這一章中基本上沒有提到什麼比較晦澀的知識,而且看樣子通過這一章的學習,大家也應該休息好了。
如果讀者現在感覺到精神百倍,那麼這一章的目的應該就達到了。


<未完,待續>

#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;
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 + (1<<9) - 1) & ~((1ULL<<9) - 1);
	//此處爲字節對齊.(1<<9 - 1) = 255 = 0xFF
	
#ifdef _DEBUG_
	printk(KERN_WARNING "size = %llu tail = %c\n",g_blk_size,tail);
#endif			
	return 0;
}


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 > 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);
		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 (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 >> 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 < (g_blk_size + 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);
	//(g_blk_size + PAGE_SIZE - 1) / PAGE_SIZE 防止頁面不對齊的情況下,增加一頁.這樣剩餘的數據也能用.不然不對齊超過部分不能使用了
	for(i = 0; i < (g_blk_size + 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;
	
	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>>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_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");					    //別名


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

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