写一个块设备驱动 3

来源:互联网 发布:淘宝开店照片要求 编辑:程序博客网 时间:2024/05/16 15:13

第 3章

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

----------------------- Page 16-----------------------

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

上一章中我们讨论了mm的衣服问题,并成功地为她换上了一件轻如鸿毛、关键是薄如蝉翼的新衣服

而这一章中,我们打算稍稍再前进一步,也就是:给她脱光

目的是更加符合我们的审美观、并且能够更加深入地了解该mm(喜欢制服皮草的读者除外)

付出的代价是这一章的内容要稍稍复杂一些

虽然 noop调度器确实已经很简单了,简单到比我们的驱动程序还简单,在 2.6.27中的 12 行代码量已

经充分说明了这个问题

但显而易见的是,不管它多简单,只要它存在,我们就把它看成累赘

这里我们不打算再次去反复磨嘴皮子论证不使用 I/O调度器能给我们的驱动程序带来什么样的好处、面

临的困难、以及如何与国际接轨的诸多事宜,

毕竟现在不是在讨论汽油降价,而我们也不是中石油。我们更关心的是实实在在地做一些对驱动程序有

益的事情

不过 I/O调度器这层遮体衣服倒也不是这么容易脱掉的,因为实际上我们还使用了它捆绑的另一个功能 ,

就是请求队列

因此我们在前两章中的程序才如此简单

从细节上来说,请求队列request_queue中有个make_request_fn成员变量,我们看它的定义:
struct request_queue
{
         ...
        make_request_fn         *make_request_fn;
         ...
}

它实际上是:

typedef int (make_request_fn) (struct request_queue *q, struct bio *bio);

也就是一个函数的指针

如果上面这段话让读者感到莫名其妙,那么请搬个板凳坐下,Let's Begin the Story

对通用块层的访问,比如请求读某个块设备上的一段数据,通常是准备一个 bio ,然后调用
generic_make_request()函数来实现的
调用者是幸运的,因为他往往不需要去关心 generic_make_request()函数如何做的,只需要知道这个
神奇的函数会为他搞定所有的问题就 OK了
而我们却没有这么幸运,因为对一个块设备驱动的设计者来说,如果不知道generic_make_request()

函数的内部情况,很可能会让驱动的使用者得不到安全感

了解generic_make_request()内部的有效方法还是 RTFSC ,但这里会给出一些提示
我们可以在 generic_make_request()中找到__generic_make_request(bio)这么一句,
然后在__generic_make_request()函数中找到 ret = q->make_request_fn(q, bio)这么一行

偷懒省略掉解开谜题的所有关键步骤后,这里可以得出一个作者相信但读者不一定相信的正确结论:

----------------------- Page 17-----------------------

generic_make_request()最终是通过调用 request_queue.make_request_fn函数完成 bio所描述

的请求处理的

Story到此结束,现在我们可以解释刚才为什么列出那段莫名其妙的数据结构的意图了
对于块设备驱动来说,正是 request_queue.make_request_fn函数负责处理这个块设备上的所有请

也就是说,只要我们实现了 request_queue.make_request_fn ,那么块设备驱动的Primary 
Mission就接近完成了

在本章中,我们要做的就是:

1 :让request_queue.make_request_fn指向我们设计的 make_request函数
2 :把我们设计的 make_request函数写出来

如果读者现在已经意气风发地拿起键盘跃跃欲试了,作者一定会假装谦虚地问读者一个问题:

你的钻研精神遇到城管了 ?

如果这句话问得读者莫名其妙的话,作者将补充另一个问题:

前两章中明显没有实现make_request函数,那时的驱动程序倒是如何工作的 ?

然后就是清清嗓子自问自答

前两章确实没有用到 make_request函数,但当我们使用 blk_init_queue()获得 request_queue时,

万能的系统知道我们搞 IT的都低收入,因此救济了我们一个,这就是大名鼎鼎的__make_request()函

request_queue.make_request_fn指向了__make_request()函数,因此对块设备的所有请求被导
向了__make_request()函数中

__make_request()函数不是吃素的,马上喊上了他的兄弟,也就是I/O调度器来帮忙,结果就是bio
请求被I/O调度器处理了
同时,__make_request()自身也没闲着,它把bio这条咸鱼嗅了嗅,舔了舔,然后放到嘴里嚼了嚼,

把鱼刺鱼鳞剔掉,

然后情意绵绵地通过 do_request函数(也就是 blk_init_queue的第一个参数)喂到驱动程序作者的口

这就解释了前两章中我们如何通过 simp_blkdev_do_request()函数处理块设备请求的

我们理解__make_request()函数本意不错,它把bio这条咸鱼嚼成 request_queue喂给
do_request函数,能让我们的到如下好处:
1 :request.buffer不在高端内存

   这意味着我们不需要考虑映射高端内存到虚存的情况

2 :request.buffer的内存是连续的
   因此我们不需要考虑request.buffer对应的内存地址是否分成几段的问题

这些好处看起来都很自然,正如某些行政不作为的“有关部门”认为老百姓纳税养他们也自然,

但不久我们就会看到不很自然的情况

----------------------- Page 18-----------------------

如果读者是 mm ,或许会认为一个摔锅把咸鱼嚼好了含情脉脉地喂过来是一件很浪 的事情 (也希望这位
读者与作者联系) ,
但对于大多数男性IT工作者来说,除非取向问题,否则......
因此现在我们宁可把__make_request()函数一脚踢飞,然后自己去嚼bio这条咸鱼
当然,踢飞__make_request()函数也意味着摆脱了 I/O调度器的处理

踢飞__make_request()很容易,使用 blk_alloc_queue()函数代替blk_init_queue()函数来获取
request_queue就行了

也就是说,我们把原先的

simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);

改成了

simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);

这样

至于嚼人家口水渣的 simp_blkdev_do_request()函数,我们也一并扔掉:
把simp_blkdev_do_request()函数从头到尾删掉

同时,由于现在要脱光,所以上一章中我们费好大劲换上的那件薄内衣也不需要了,

也就是把上一章中增加的 elevator_init()这部分的函数也删了,也就是删掉如下部分:
old_e = simp_blkdev_queue->elevator;
if (IS_ERR_VALUE(elevator_init(simp_blkdev_queue, "noop")))
        printk(KERN_WARNING "Switch elevator failed, using default\n");
else
        elevator_exit(old_e);

到这里我们已经成功地让__make_request()升空了,但要自己嚼bio ,还需要添加一些东西:
首先给request_queue指定我们自己的 bio处理函数,这是通过blk_queue_make_request()函数
实现的,把这面这行加在 blk_alloc_queue()之后:
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
然后实现我们自己的 simp_blkdev_make_request()函数,

然后编译

如果按照上述的描述修改出的代码让读者感到信心不足,我们在此列出修改过的 simp_blkdev_init()

函数:

static int __init simp_blkdev_init(void)
{
        int ret;

        simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
        if (!simp_blkdev_queue) {
                ret = -ENOMEM;

----------------------- Page 19-----------------------

                goto err_alloc_queue;
        }
        blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);

        simp_blkdev_disk = alloc_disk(1);
        if (!simp_blkdev_disk) {
                ret = -ENOMEM;
                goto err_alloc_disk;
        }

        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_disk:
        blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
        return ret;
}
这里还把err_init_queue也改成了 err_alloc_queue ,希望读者不要打算就这一点进行提问

正如本章开头所述,这一章的内容可能要复杂一些,而现在看来似乎已经做到了

而现在的进度大概是 ......一半!
不过值得安慰的是,余下的内容只有我们的 simp_blkdev_make_request()函数了

首先给出函数原型:

static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio);
该函数用来处理一个 bio请求
函数接受struct request_queue *q和 struct bio *bio作为参数,与请求有关的信息在 bio参数

中,

而 struct request_queue *q并没有经过__make_request()的处理,这也意味着我们不能用前几章
那种方式使用 q
因此这里我们关注的是:bio

关于 bio和 bio_vec的格式我们仍然不打算在这里做过多的解释,理由同样是因为我们要避免与
google出的一大堆文章撞衫

----------------------- Page 20-----------------------

这里我们只说一句话:

bio对应块设备上一段连续空间的请求,bio中包含的多个 bio_vec用来指出这个请求对应的每段内存

因此 simp_blkdev_make_request()本质上是在一个循环中搞定 bio中的每个 bio_vec

这个神奇的循环是这样的:

dsk_mem = simp_blkdev_data + (bio->bi_sector << 9);

bio_for_each_segment(bvec, bio, i) {
        void *iovec_mem;

        switch (bio_rw(bio)) {
        case READ:
        case READA:
                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
                memcpy(iovec_mem, dsk_mem, bvec->bv_len);
                kunmap(bvec->bv_page);
                break;
        case WRITE:
                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
                memcpy(dsk_mem, iovec_mem, bvec->bv_len);
                kunmap(bvec->bv_page);
                break;
        default:
                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                         ": unknown value of bio_rw: %lu\n",
                        bio_rw(bio));
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                bio_endio(bio, 0, -EIO);
#else
                bio_endio(bio, -EIO);
#endif
                return 0;
        }
        dsk_mem += bvec->bv_len;
}
bio请求的块设备起始扇区和扇区数存储在 bio.bi_sector和 bio.bi_size中,
我们首先通过 bio.bi_sector获得这个 bio请求在我们的块设备内存中的起始部分位置,存入
dsk_mem
然后遍历bio中的每个 bio_vec ,这里我们使用了系统提供的 bio_for_each_segment宏

循环中的代码看上去有些眼熟,无非是根据请求的类型作相应的处理                                        READA意味着预读,精心设计的

----------------------- Page 21-----------------------

预读请求可以提高I/O效率,
这有点像内存中的 prefetch() ,我们同样不在这里做更详细的介绍,因为这本身就能写一整篇文章,

对于我们的基于内存的块设备驱动,

只要按照READ请求同样处理就 OK了

在很眼熟的 memcpy前后,我们发现了kmap和 kunmap这两个新面孔

这也证明了咸鱼要比烂肉难啃的道理

bio_vec中的内存地址是使用 page *描述的,这也意味着内存页面有可能处于高端内存中而无法直接访

这种情况下,常规的处理方法是用 kmap映射到非线性映射区域进行访问,当然,访问完后要记得把映射

的区域还回去,

不要仗着你内存大就不还,实际上在 i386结构中,你内存越大可用的非线性映射区域越紧张
关于高端内存的细节也请自行 google ,反正在我的印象中 intel总是有事没事就弄些硬件限制给程序

员找麻烦以帮助程序员的就业

所幸的是逐渐流行的 64位机的限制应该不那么容易突破了,至少我这么认为

switch中的 default用来处理其它情况,而我们的处理却很简单,抛出一条错误信息,然后调用
bio_endio()告诉上层这个 bio错了
不过这个万恶的 bio_endio()函数在 2.6.24中改了,如果我们的驱动程序是内核的一部分,那么我们
只要同步更新调用 bio_endio()的语句就行了,
但现在的情况显然不是,而我们又希望这个驱动程序能够同时适应2.6.24之前和之后的内核,因此这里

使用条件编译来比较内核版本

同时,由于使用到了 LINUX_VERSION_CODE和 KERNEL_VERSION宏,因此还需要增加#include 
<linux/version.h>

循环的最后把这一轮循环中完成处理的字节数加到 dsk_mem中,这样 dsk_mem指向在下一个 bio_vec

对应的块设备中的数据

读者或许开始耐不住性子想这一章怎么还不结束了,是的,马上就结束,不过我们还要在循环的前后加

上一丁点:

1 :循环之前的变量声明:
   struct bio_vec *bvec;
   int i;
   void *dsk_mem;
2 :循环之前检测访问请求是否超越了块设备限制:
   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);

----------------------- Page 22-----------------------

   #else
           bio_endio(bio, -EIO);
   #endif
           return 0;
   }
3 :循环之后结束这个 bio ,并返回成功:
   #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
   bio_endio(bio, bio->bi_size, 0);
   #else
   bio_endio(bio, 0);
   #endif
   return 0;
   bio_endio用于返回这个对 bio请求的处理结果,在 2.6.24之后的内核中,第一个参数是被处理的
bio指针,第二个参数成功时为                    ,失败时为-ERRNO
   在 2.6.24之前的内核中,中间还多了个 unsigned int bytes_done ,用于返回搞定了的字节数

现在可以长长地舒一口气了,我们完工了

还是附上 simp_blkdev_make_request()的完成代码:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
        struct bio_vec *bvec;
        int i;
        void *dsk_mem;

        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_mem = simp_blkdev_data + (bio->bi_sector << 9);

        bio_for_each_segment(bvec, bio, i) {
                void *iovec_mem;

                switch (bio_rw(bio)) {

----------------------- Page 23-----------------------

                case READ:
                case READA:
                        iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
                        memcpy(iovec_mem, dsk_mem, bvec->bv_len);
                        kunmap(bvec->bv_page);
                        break;
                case WRITE:
                        iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
                        memcpy(dsk_mem, iovec_mem, bvec->bv_len);
                        kunmap(bvec->bv_page);
                        break;
                default:
                        printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                 ": unknown value of bio_rw: %lu\n",
                                bio_rw(bio));
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                        bio_endio(bio, 0, -EIO);
#else
                        bio_endio(bio, -EIO);
#endif
                        return 0;
                }
                dsk_mem += 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;
}

读者可以直接用本章的 simp_blkdev_make_request()函数替换掉上一章的
simp_blkdev_do_request()函数,
然后用本章的 simp_blkdev_init()函数替换掉上一章的同名函数,再在文件头部增加#include 
<linux/version.h> ,

就得到了本章的最终代码

在结束本章之前,我们还是试验一下:

首先还是编译和加载:

----------------------- Page 24-----------------------

# make
make -C /lib/modules/2.6.18-53.el5/build 
SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step3 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#
然后使用上一章中的方法看看 sysfs中的这个设备的信息:
# ls /sys/block/simp_blkdev
dev  holders  range  removable  size  slaves  stat  subsystem  uevent
#
我们发现我们的驱动程序在 sysfs目录中的 queue子目录不见了

这并不奇怪,否则就要抓狂了

本章中我们实现自己的 make_request函数来处理 bio ,以此摆脱了 I/O调度器和通用的
__make_request()对 bio的处理
由于我们的块设备中的数据都是存在于内存中,不牵涉到 DMA操作、并且不需要寻道,因此这应该是最

适合这种形态的块设备的处理方式

在 linux中类似的驱动程序大多使用了本章中的处理方式,但对大多数基于物理磁盘的块设备驱动来说 ,
使用适合的 I/O调度器更能提高性能
同时,__make_request()中包含的回弹机制对需要进行 DMA操作的块设备驱动来说,也能提供不错帮

虽然说量变产生质变,通常质变比量变要复杂得多

同理,相比前一章,把mm衣服脱光也比让她换一件薄一些的衣服要困难得多
不过无论如何,我们总算连哄带骗地让mm脱下来了,而付出了满头大汗的代价:

本章内容的复杂度相比前一章大大加深了

如果本章的内容不幸使读者感觉头部体积有所增加的话,作为弥补,我们将宣布一个好消息:

因为根据惯例,随后的 1、2章将会出现一些轻松的内容让读者得到充分休息

<未完,待续>


原创粉丝点击