[046]块设备驱动初探

前言

研究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_queue

struct 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.1
struct 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.2
int 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.3
void 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

四、完整代码

没有在内核中编译过,运行过

4.1 传统块设备

#define SIMP_BLKDEV_DEVICEMAJOR   COMPAQ_SMART2_MAJOR//暂时使用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, 
};

static int __init simp_blkdev_init(void)
{
    int ret;
    elevator_t *old_e;
    //初始化请求队列
    simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
    if (!simp_blkdev_queue) {
       ret = -ENOMEM;
       goto err_init_queue;
    } 
    old_e = simp_blkdev_queue->elevator;//检查默认的调度器
    if (IS_ERR_VALUE(elevator_init(simp_blkdev_queue, "noop")))//检查切换调度器"noop"是否成功
        printk(KERN_WARNING "Switch elevator failed, using default\n"); 
    else
        elevator_exit(old_e);//释放老的调度器
    simp_blkdev_disk = alloc_disk(1);//申请simp_blkdev_disk
    if (!simp_blkdev_disk) {
        ret = -ENOMEM;
        goto err_alloc_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_init_queue:
    return ret;
}


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);//释放请求队列
}

//负责处理块设备的请求
static void simp_blkdev_do_request(struct request_queue *q) 
{
    struct request *req;
    while ((req = elv_next_request(q)) != NULL) {//根据调度算法获得下一个request
        //sector有点类似于起始地址,有点类似于请求的块数
        if ((req->sector + req->current_nr_sectors) << 9 > SIMP_BLKDEV_BYTES) {
            printk(KERN_ERR SIcurrent_nr_sectorsMP_BLKDEV_DISKNAME": bad request: block=%llu, count=%u\n", (unsigned long long)req->sector, req->current_nr_sectors);
            end_request(req, 0);
            continue; 
        }
        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;
        }
}

module_init(simp_blkdev_init); 
module_exit(simp_blkdev_exit);

4.2 超高速块设备

#define SIMP_BLKDEV_DEVICEMAJOR   COMPAQ_SMART2_MAJOR//暂时使用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, 
};

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(1);//申请simp_blkdev_disk
    if (!simp_blkdev_disk) {
        ret = -ENOMEM;
        goto err_alloc_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;
}


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);//释放请求队列
}

//将bio放到request_queue中
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;
    }
    //获得块设备内存的起始地址,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;
}

module_init(simp_blkdev_init); 
module_exit(simp_blkdev_exit);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容