LINUX块设备驱动6

来源:互联网 发布:莱恩打碟软件mac 编辑:程序博客网 时间:2024/05/16 03:14
第6章

+---------------------------------------------------+
|                 写一个块设备驱动                  |
+---------------------------------------------------+
| 作者:赵磊                                        |
| email: [email]zhaoleidd@hotmail.com[/email]                      |
+---------------------------------------------------+
| 文章版权归原作者所有。                            |
| 大家可以自由转载这篇文章,但原版权信息必须保留。  |
| 如需用于商业用途,请务必与原作者联系,若因未取得  |
| 授权而收起的版权争议,由侵权者自行负责。          |
+---------------------------------------------------+

经历了内容极为简单的前两章的休息,现在大家一定感到精神百倍了。
作为已经坚持到现在的读者,对接下去将要面临的内容大概应该能够猜得八九不离十了,
具体的内容猜不出来也无妨,但一定将是具有增加颅压功效的。

与物理块设备驱动程序的区别在于,我们的驱动程序使用内存来存储块设备中的数据。
到目前为止,我们一直都是使用这样一个静态数组来担负这一功能的:
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 < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE; i++) {
        p = (void *)__get_free_page(GFP_KERNEL);
        simp_blkdev_data[i] = p;
}
通过块设备偏移得到内存中的数据地址的代码大概是:
mem_addr = simp_blkdev_data[dev_addr/PAGE_SIZE] + dev_addr % PAGE_SIZE;
这种方法实现起来还是比较简单的,但缺点也不是没有:存放各个页面地址的数组虽然其体积比原先那个直接存放数据的数组已经缩小了很多,
但本身毕竟还是在非线性映射区域中。如果块设备大小为16M,在i386上,需要4096个页面,数组大小16K,这不算太大。
但如果某个疯子打算建立一个2G的虚拟磁盘,数组大小将达到2M,这就不算小了。

或者我们可以不用数组,而用链表来存储偏移到内存页的映射关系,这样可以回避掉数组存在的问题,但在链表中查找指定元素却不是一般的费时,
毕竟我们不希望用户觉得这是个软盘。

接下来作者不打断继续卖关子了,我们最终选择使用的是传说中的基树。
关于linux中基树细节的文档不多,特别是中文文档更少,更糟的是我们这篇文档也不打算作详细的介绍(因为作者建议去RTFSC)。
但总的来说,相对于二叉平衡树的红黑树来说,基树是一个n叉(一般为64叉)非平衡树,n叉减少了搜索的深度,非平衡减少了复杂的平衡操作。
当然,这两个特点也不是仅仅带来优点,但在这里我们就视而不见了,毕竟我们已经选择了基树,因此护短也是自认而然的事情,正如公仆护着王细牛一样。
从功能上来说,基树好像是为我们量身定做的一样,好用至极。
(其实我们也可以考虑选择红黑树和哈希表来实现这个功能,感兴趣的读者可以了解一下)

接下来的代码中,我们将要用到基树种的如下函数:
  void INIT_RADIX_TREE((struct radix_tree_root *root, gfp_t mask);
  用来初始化一个基树的结构,root是基树结构指针,mask是基树内部申请内存时使用的标志。

  int radix_tree_insert(struct radix_tree_root *root, unsigned long index, void *item);
  用来往基树中插入一个指针,index是指针的索引,item是指针,将来可以通过index从基树中快速获得这个指针的值。

  void *radix_tree_delete(struct radix_tree_root *root, unsigned long index);
  用来根据索引从基树中删除一个指针,index是指针的索引。

  void *radix_tree_lookup(struct radix_tree_root *root, unsigned long index);
  用来根据索引从基树中查找对应的指针,index是指针的索引。
其实基树的功能不仅限于此,比如,还可以给指针设定标志,详情还是请去读linux/lib/radix-tree.c

现在开始改造我们的代码:
首先删除那个无耻的数组:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
然后引入它的替代者--一个基树结构:
static struct radix_tree_root simp_blkdev_data;

然后增加两个函数,用来申请和释放块设备的内存:

申请内存的函数如下:
int alloc_diskmem(void)
{
        int ret;
        int i;
        void *p;

        INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);

        for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> 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 < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> 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 << 9) + 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 << 9;

        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 < bvec->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 < KERNEL_VERSION(2, 6, 24)
                                bio_endio(bio, 0, -EIO);
#else
                                bio_endio(bio, -EIO);
#endif
                                return 0;
                        }
                        dsk_mem += (dsk_offset + count_done) & ~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;
                        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;
}

看样子长了一些,但不要被吓着了,因为读的时候我们可以对代码做一些简化:
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 < bvec->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;
&
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 面试没有正装怎么办 退烧药服用过量怎么办 小孩喝洗衣粉水怎么办 小孩误食沐浴露怎么办 三个月宝宝腿弯怎么办 宝宝背带裤老掉怎么办 羽绒服洗完结块怎么办 天猫保证金被骗怎么办 飞机杯发霉了怎么办 背带裤裆太大了怎么办 宝宝开裆裤裆太大怎么办 a字裙太大怎么办 棉衣服缩水了怎么办 百褶裙子大了怎么办 皮鞋有黑色划痕怎么办 天猫搜索不了怎么办 帆布鞋穿着脚臭怎么办 运动鞋磨脚踝骨怎么办 运动鞋挂烂了怎么办 网状运动鞋烂了怎么办 运动鞋臭怎么办快速去除 天猫预售退货怎么办 肯德基兑换券过期了怎么办 直通车上10之后怎么办 淘宝没有评论过怎么办 爱上街虚假发货怎么办 天猫差评被置顶了一天怎么办 天猫跨店满减其中订单退款怎么办 天猫618津贴不够怎么办 鼻子上长大包怎么办 净水器滤芯漏水怎么办 京东忘记用户名怎么办 详情页图片模糊怎么办 打印图片字体模糊怎么办 淘宝的图片模糊怎么办 余额宝转出限额怎么办 淘宝店论文诈骗怎么办 淘宝号有感叹号怎么办 被同行做关键词怎么办 淘宝店铺被监管怎么办 淘宝视频看不了怎么办