块设备驱动初探
发布日期:2021-06-30 18:44:49 浏览次数:3 分类:技术文章

本文共 7592 字,大约阅读时间需要 25 分钟。

前言

研究IO也很久了,一直无法串联bio和块设备驱动,只知道bio经过IO调度算法传递到块设备驱动,怎么过去的,IO调度算法在哪里发挥作用,一直没有完全搞明白,查看了很多资料,终于对块设备驱动有所理解,也打通了bio到块设备。

一、传统块设备

我们先来实现一个基于内存的传统块设备驱动。

1.1 初始化一些东西

//暂时使用COMPAQ_SMART2_MAJOR作为主设备号,防止设备号冲突#define SIMP_BLKDEV_DEVICEMAJOR   COMPAQ_SMART2_MAJOR//块设备名#define SIMP_BLKDEV_DISKNAME "simp_blkdev"//用一个数组来模拟一个物理存储#define SIMP_BLKDEV_BYTES (16*1024*1024)unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];static struct request_queue *simp_blkdev_queue;//请求队列static struct gendisk *simp_blkdev_disk;//块设备struct block_device_operations simp_blkdev_fops = {//块设备的操作函数    .owner = THIS_MODULE, };

1.2 加载驱动

整个过程

1.创建request_queue(每个块设备一个队列),绑定函数simp_blkdev_do_request
2.创建一个gendisk(每个块设备就是一个gendisk)
3.将request_queue和gendisk绑定
4.注册gendisk

static int __init simp_blkdev_init(void){    int ret;    //初始化请求队列    simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);//这个方法将会在1.5仔细分析    simp_blkdev_disk = alloc_disk(1);//申请simp_blkdev_disk      //初始化simp_blkdev_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;     //设置块设备的大小,大小是扇区的数量,一个扇区是512B    set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);    add_disk(simp_blkdev_disk);//注册simp_blkdev_disk    return 0;}

1.3 simp_blkdev_do_request

1.调用调度算法的elv_next_request方法获得下一个处理的request

2.如果是读,将simp_blkdev_data拷贝到request.buffer,
3.如果是写,将request.buffer拷贝到simp_blkdev_data
4.调用end_request通知完成

static void simp_blkdev_do_request(struct request_queue *q) {    struct request *req;    while ((req = elv_next_request(q)) != NULL) {//根据调度算法获得下一个request        switch (rq_data_dir(req)) {//判断读还是写        case READ:            memcpy(req->buffer, simp_blkdev_data + (req->sector << 9),             req->current_nr_sectors << 9);            end_request(req, 1);//完成通知            break;        case WRITE:            memcpy(simp_blkdev_data + (req->sector << 9),req->buffer,             req->current_nr_sectors << 9);             end_request(req, 1);//完成通知            break;        default:             /* No default because rq_data_dir(req) is 1 bit */             break;        }}

1.4 卸载驱动

static void __exit simp_blkdev_exit(void){    del_gendisk(simp_blkdev_disk);//注销simp_blkdev_disk    put_disk(simp_blkdev_disk);//释放simp_blkdev_disk    blk_cleanup_queue(simp_blkdev_queue);//释放请求队列}

千万别忘记下面代码

module_init(simp_blkdev_init);module_exit(simp_blkdev_exit);

1.5 blk_init_queue

看了上面的代码,可能还是无法清晰的了解request_queue如何串联bio和块设备驱动,我们深入看一下

simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);//调用blk_init_queuestruct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock){    return blk_init_queue_node(rfn, lock, NUMA_NO_NODE);//跳转1.5.1}EXPORT_SYMBOL(blk_init_queue);//1.5.1struct request_queue *blk_init_queue_node(request_fn_proc *rfn, spinlock_t *lock, int node_id){    struct request_queue *q;    q = blk_alloc_queue_node(GFP_KERNEL, node_id, lock);    if (!q)        return NULL;    q->request_fn = rfn;//也就是simp_blkdev_do_request    if (blk_init_allocated_queue(q) < 0) {//转1.5.2        blk_cleanup_queue(q);        return NULL;    }    return q;}EXPORT_SYMBOL(blk_init_queue_node);//1.5.2int blk_init_allocated_queue(struct request_queue *q){    ...    blk_queue_make_request(q, blk_queue_bio);//转1.5.3    if (elevator_init(q))//初始化IO调度算法        goto out_exit_flush_rq;    return 0;    ...}EXPORT_SYMBOL(blk_init_allocated_queue);//1.5.3void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn){    ...    q->make_request_fn = mfn;//mfn也就是blk_queue_bio    ...}EXPORT_SYMBOL(blk_queue_make_request);static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)//完成bio如何插入到request_queue{    //IO调度算法发挥作用的地方}
整个调用完成之后,会绑定当前块设备的request_queue两个重要方法
q->make_request_fn = blk_queue_bio;//linux默认实现q->request_fn = simp_blkdev_do_request;//驱动自己实现
1.5.1 make_request_fn(struct request_queue *q, struct bio *bio)

submit_bio会调用make_request_fn将bio封装成request插入到request_queue,默认会使用linux系统实现的blk_queue_bio。如果我们替换make_request_fn,会导致IO调度算法失效,一般不会去改。

1.5.2 request_fn(struct request_queue *q)

这个方法一般是驱动实现,也就是simp_blkdev_do_request,从request_queue中取出合适的request进行处理,一般会调用调度算法的elv_next_request方法,获得一个推荐的request。

1.5.3 bio-块设备

通过make_request_fn和request_fn,我们将bio和块设备驱动串联起来了。

而且IO调度算法会在这两个函数发挥作用。

给自己挖了两个坑

1.整个过程中受到了IO调度算法,IO调度算法如何发挥作用?
2.make_request_fn之后如何触发request_fn?

二、超高速块设备

传统块设备访问是通过磁头,IO调度算法可以优化多个IO请求的时候移动磁头的顺序。

IO调度算法

假如你是图书管理员,十个人找你借十本书,在图书馆的不同角落,你肯定会选择一条最短的线路去拿这十本书。其实这就是IO调度算法

超高速块设备

假如这个图书馆只有一个窗口,借书的人只要说出书名,书就会从窗口飞出来,这样子还需要什么管理员,更不需要什么IO调度算法,这个图书馆就是超高速块设备。

上面写的基于内存的块设备不就是一个超高速块设备嘛,我们能不能写一个没有中间商的驱动

2.1 simp_blkdev_init

我们需要重写一下init代码,不调用blk_init_queue。直接用下面的2.1.1和2.1.2的方法。

init之后,我们会将make_request_fn设置成simp_blkdev_make_request

static int __init simp_blkdev_init(void){    int ret;    //初始化请求队列    simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);//2.1.1    //将simp_blkdev_make_request绑定到request_queue的make_request_fn。    blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);//2.1.2    simp_blkdev_disk = alloc_disk(1);//申请simp_blkdev_disk    //初始化simp_blkdev_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);//设置块设备的大小,大小是扇区的数量,一个扇区是512B    add_disk(simp_blkdev_disk);//注册simp_blkdev_disk    return 0;err_alloc_disk:    blk_cleanup_queue(simp_blkdev_queue);err_alloc_queue:    return ret;}

2.2 simp_blkdev_make_request

跳过中间商,直接将simp_blkdev_data拷贝到bio的page,调用bio_endio通知读写完成,

从头到尾request_queue和request就没有用到

static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio) {    struct bio_vec *bvec;    int i;    void *dsk_mem;    //获得块设备内存的起始地址,bi_sector代表起始扇区    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:                //page代表高端内存无法直接访问,需要通过kmap映射到线性地址                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;}

2.2 没有中间商

因为我们直接把数据的访问实现在make_request_fn,也就是simp_blkdev_make_request。

这样子就摆脱了request_queue和IO调度算法。没有中间商,访问速度杠杠的。

kernel中的zram设备就是基于内存没有中间商赚差价的块设备,代码很类似,有兴趣的可以看一下。

三、总结

经过那么长时间的学习,捅破层层的窗户纸,终于把IO打通了,但是文件系统,IO调度算法,每一模块都是值得我深入仔细研究,真正的挑战才刚刚开始。

代码参考

写一个块设备驱动.pdf

资料参考

《Linux内核设计与实现》

《Linux内核完全注释》
Linux.Generic.Block.Layer.pdf
https://zhuanlan.zhihu.com/c_132560778

阅读原文获得完整代码

  回复「 篮球的大肚子」进入技术群聊

回复「1024」获取1000G学习资料

转载地址:https://linus.blog.csdn.net/article/details/105236305 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:MIPI白皮书
下一篇:人应该活成什么样子?该以什么方式活着?

发表评论

最新留言

路过,博主的博客真漂亮。。
[***.116.15.85]2024年04月15日 04时55分39秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章