第十三章
本教程修改自赵磊的网上的一系列教程.本人觉得该系列教程写的非常不错.以风趣幽默的语言将块驱动写的非常详细,对于入门教程,应该属于一份经典了. 本人在这对此系列教程最后附上对Linux 2.6.36版本的代码.并编译运行成功. 该教程所有版权仍归作者赵磊所有,本人只做附录代码的添加,并为对原文修改.有不懂的地方,可以联系我 97164811@qq.com 或者给我留言.
+---------------------------------------------------+
| 写一个块设备驱动
+---------------------------------------------------+
| 作者:赵磊
| email: zhaoleidd@hotmail.com
+---------------------------------------------------+
| 文章版权归原作者所有。
| 大家可以自由转载这篇文章,但原版权信息必须保留。
| 如需用于商业用途,请务必与原作者联系,若因未取得
| 授权而收起的版权争议,由侵权者自行负责。
+---------------------------------------------------+
没有最好的代码,是因为我们总能把代码改得更好。
因此我们现在打算做一个小的性能改进,这次我们准备拿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"); //别名
本人是在参考教程之后修改的教程内容.如有不同.可能有遗漏没有修改.造成对读者的迷惑,在此致歉~~