免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1032 | 回复: 0
打印 上一主题 下一主题

LINUX块设备驱动 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2008-12-02 17:30 |只看该作者 |倒序浏览
第6章
+---------------------------------------------------+
|                 写一个块设备驱动                  |
+---------------------------------------------------+
| 作者:赵磊                                        |
| email: zhaoleidd@hotmail.com                      |
+---------------------------------------------------+
| 文章版权归原作者所有。                            |
| 大家可以自由转载这篇文章,但原版权信息必须保留。  |
| 如需用于商业用途,请务必与原作者联系,若因未取得  |
| 授权而收起的版权争议,由侵权者自行负责。          |
+---------------------------------------------------+
经历了内容极为简单的前两章的休息,现在大家一定感到精神百倍了。
作为已经坚持到现在的读者,对接下去将要面临的内容大概应该能够猜得八九不离十了,
具体的内容猜不出来也无妨,但一定将是具有增加颅压功效的。
与物理块设备驱动程序的区别在于,我们的驱动程序使用内存来存储块设备中的数据。
到目前为止,我们一直都是使用这样一个静态数组来担负这一功能的:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
如果读者懂得一些模块的知识,或者现在赶紧去临时抱佛脚google一些模块知识,
应该知道模块其实是加载在非线性映射区域的。
详细来说,在加载模块时,根据模块的ELF信息(天哪,又要去google elf了),确定这个模块所需的静态内存大小。
这些内存用来容纳模块的二进制代码,以及静态变量。然后申请容纳这些数据的一大堆页面。
当然,这些页面并不是连续的,而代码和变量却不可能神奇到即使被切成一块一块的也能正常工作。
因此需要在非线性映射区域中找到一块连续的地址(现在有要去google非线性映射区域了),用来将刚才申请到的一块一块的内存页映射到这个地址段中。
最后模块被请到这段区域中,然后执行模块的初始化函数......
现在看我们这个模块中的simp_blkdev_data变量,如果不是现在刻意关注,这个变量看起来显得那么得普通。
正如其它的一些名字原先也是那么的普通,但由于一些突发的事件受到大家的热烈关注,
比如一段视频让我们熟悉了kappa和陆佳妮,比如呼吸税让我们认识了蒋有绪。
现在我们开始关注simp_blkdev_data变量了,导火索是刚才介绍的非线性映射区域。
模块之所以被加载到非线性映射区域,是因为很难在线性映射区域中得到加载模块所需的连续的内存。
但使用非线性映射区域也并非只赚不赔的生意,至少在i386结构中,非线性映射区域实在是太小了。
在物理内存大于896M的i386系统中,整个非线性映射区域不会超过128M。
相反如果物理内存小于896M(不知道该算是幸运还是不幸),非线性映射区域反而会稍微大一些,这种情况我想我们可以不用讨论了,毕竟不能为了加载一个模块去拔内存。
因此我们的结论是:非线性映射区域是很紧张的资源,我们要节约使用。
而像我们现在这个模块中的simp_blkdev_data却是个如假包换的反面典型,居然上来就吃掉了16M!这还是因为我们没有把SIMP_BLKDEV_BYTES定义得更大。
现在我们开始列举simp_blkdev_data的种种罪行:
1:剩余的非线性映射区域较小时导致模块加载失败
2:模块加载后占用了大量的非线性映射区域,导致其它模块加载失败。
3:模块加载后占用了大量的非线性映射区域,影响系统的正常运行。
   这是因为不光模块,系统本身的很多功能也依赖非线性映射区域空间。
对于这样的害群之马,我们难道还有留下他的理由吗?
本章的内容虽然麻烦一些,但想到能够一了百了地清除这个体大膘肥的simp_blkdev_data,倒也相当值得。
也希望今后能够看到在对贪官的处理上,能够也拿出这样的魄力和勇气。
现在在清除simp_blkdev_data的问题上,已经不存在什么悬念了,接下来我们需要关注的是将simp_blkdev_data碎尸万段后,拿出一个更恰当方法来代替它。
首先,我们决定不用静态声明的数组,而改用动态申请的内存。
其次,使用类似vmalloc()的函数可以动态申请大段内存,但其实这段内存占用的还是非线性映射区域,就好像用一个比较隐蔽的贪官来代替下马的贪官,我们不会愚蠢在这种地步。
剩下的,就是在线性映射区域申请很多个页的内存,然后自己去管理。这个方法一了百了地解决了使用大段非线性映射区域的问题,而唯一的问题是由于需要自己管理申请到的页面,使程序复杂了不少。
但为了整个系统的利益,这难道不是我们该做的吗?
申请一个内存页是很容易的,这里我们将采用所有容易的方法中最容易的那个:
__get_free_page函数,原型是:
unsigned long __get_free_page(gfp_t gfp_mask);
这个函数用来申请一个页面的内存。gfp_mask包含一些对申请内存时的指定,比如,要在DMA区域中啦、必须清零等。
我们这里倒是使用最常见的__get_free_page(GFP_KERNEL)就可以了。
通过__get_free_page申请到了一大堆内存页,新的问题来了,在读写块设备时,我们得到是块设备的偏移,如何快速地通过偏移找到对应的内存页呢?
最简单的方法是建立一个数组,用来存放偏移到内存的映射,数组中的每项对应一个一个页:
数组定义如下:
void *simp_blkdev_data[(SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE];
PAGE_SIZE是系统中每个页的大小,对i386来说,通常是4K,那堆加PAGE_SIZE减1的代码是考虑到SIMP_BLKDEV_BYTES不是PAGE_SIZE的整数倍时要让末尾的空间也能访问。
然后申请内存的代码大概是:
for (i=0; i > PAGE_SHIFT;
                i++) {
                p = (void *)__get_free_page(GFP_KERNEL);
                if (!p) {
                        ret = -ENOMEM;
                        goto err_alloc;
                }
                ret = radix_tree_insert(&simp_blkdev_data, i, p);
                if (IS_ERR_VALUE(ret))
                        goto err_radix_tree_insert;
        }
        return 0;
err_radix_tree_insert:
        free_page((unsigned long)p);
err_alloc:
        free_diskmem();
        return ret;
}
先初始化基树结构,然后申请需要的每一个页面,按照每页面的次序作为索引,将指针插入基树。
代码中的“>> PAGE_SHIFT”与“/ PAGE_SIZE”作用相同,
if (不明白为什么要这样)
        do_google();
释放内存的函数如下:
void free_diskmem(void)
{
        int i;
        void *p;
        for (i = 0; i > PAGE_SHIFT;
                i++) {
                p = radix_tree_lookup(&simp_blkdev_data, i);
                radix_tree_delete(&simp_blkdev_data, i);
                /* free NULL is safe */
                free_page((unsigned long)p);
        }
}
遍历每一个索引,得到页面的指针,释放页面,然后从基树中释放这个指针。
由于alloc_diskmem()函数在中途失败时需要释放申请过的页面,因此我们把free_diskmem()函数设计成能够释放建立了一半的基树的形式。
对于只建立了一半的基树而言,有一部分索引对应的指针还没来得及插入基树,对于不存在的索引,radix_tree_delete()函数会返回NULL,幸运的是free_page()函数能够忽略传入的NULL指针。
因为alloc_diskmem()函数需要调用free_diskmem()函数,在代码中需要把free_diskmem()函数写在alloc_diskmem()前面,或者在文件头添加函数的声明。
然后在模块的初始化和释放函数中添加对alloc_diskmem()和free_diskmem()的调用,
也就是改成这个样子:
static int __init simp_blkdev_init(void)
{
        int ret;
        simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
        if (!simp_blkdev_queue) {
                ret = -ENOMEM;
                goto err_alloc_queue;
        }
        blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
        simp_blkdev_disk = alloc_disk(SIMP_BLKDEV_MAXPARTITIONS);
        if (!simp_blkdev_disk) {
                ret = -ENOMEM;
                goto err_alloc_disk;
        }
        ret = alloc_diskmem();
        if (IS_ERR_VALUE(ret))
                goto err_alloc_diskmem;
        strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
        simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;
        simp_blkdev_disk->first_minor = 0;
        simp_blkdev_disk->fops = &simp_blkdev_fops;
        simp_blkdev_disk->queue = simp_blkdev_queue;
        set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
        add_disk(simp_blkdev_disk);
        return 0;
err_alloc_diskmem:
        put_disk(simp_blkdev_disk);
err_alloc_disk:
        blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
        return ret;
}
static void __exit simp_blkdev_exit(void)
{
        del_gendisk(simp_blkdev_disk);
        free_diskmem();
        put_disk(simp_blkdev_disk);
        blk_cleanup_queue(simp_blkdev_queue);
}
最麻烦的放在最后:
我们需要修改simp_blkdev_make_request()函数,让它适应新的数据结构。
原先的实现中,对于一个bio_vec,我们找到对应的内存中数据的起点,直接传送bvec->bv_len个字节就大功告成了,比如,读块设备时就是:
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
但现在由于容纳数据的每个页面地址是不连续的,因此可能出现bio_vec中的数据跨越页面边界的情况。
也就是说,一个bio_vec中的数据的前半段在一个页面中,后半段在另一个页面中。
虽然这两个页面对应的块设备地址连续,但在内存中的地址不一定连续,因此像原先那样简单使用memcpy看样子是解决不了问题了。
实际上,虽然bio_vec可能跨越页面边界,但它无论如何也不可能跨越2个以上的页面。
这是因为bio_vec本身对应的数据最大长度只有一个页面。
因此如果希望做最简单的实现,只要在代码中做一个条件判断就OK了:
if (没有跨越页面) {
        1个memcpy搞定
} else {
        /* 肯定是跨越2个页面了 */
        2个memcpy搞定
}
但为了表现出物理设备一次传送1个扇区数据的处理方式(这种情况下一个bio_vec可能会跨越2个以上的扇区),我们让代码支持2个以上页面的情况。
首先列出修改后的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 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 bi_sector bv_page) + bvec->bv_offset;
                count_done = 0;
                while (count_done bv_len) {
                        count_current = min(bvec->bv_len - count_done,
                                (unsigned int)(PAGE_SIZE
                                - ((dsk_offset + count_done) & ~PAGE_MASK)));
                        dsk_mem = radix_tree_lookup(&simp_blkdev_data,
                                (dsk_offset + count_done) >> PAGE_SHIFT);
                        if (!dsk_mem) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": search memory failed: %llu\n",
                                        (dsk_offset + count_done)
                                        >> PAGE_SHIFT);
                                kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE bv_page);
#if LINUX_VERSION_CODE bv_page);
                dsk_offset += bvec->bv_len;
        }
#if LINUX_VERSION_CODE bi_size, 0);
#else
        bio_endio(bio, 0);
#endif
        return 0;
}
看样子长了一些,但不要被吓着了,因为读的时候我们可以对代码做一些简化:
1:去掉乱七八糟的出错处理
2:无视每行80字符限制
3:把比特运算改成等价但更易读的乘除运算
4:无视碍眼的类型转换
5:假设内核版本大于2.6.24,以去掉判断版本的宏
就会变成这样了:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
        struct bio_vec *bvec;
        int i;
        unsigned long long dsk_offset;
        dsk_offset = bio->bi_sector * 512;
        bio_for_each_segment(bvec, bio, i) {
                unsigned int count_done, count_current;
                void *iovec_mem;
                void *dsk_mem;
                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
                count_done = 0;
                while (count_done bv_len) {
                        count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE);
                        dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) / PAGE_SIZE);
                        dsk_mem += (dsk_offset + count_done) % PAGE_SIZE;
                        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;
                }
                kunmap(bvec->bv_page);
                dsk_offset += bvec->bv_len;
&

本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/1595/showart_1675967.html
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP