Ubuntu16.04 一个内存虚拟磁盘块设备驱动的例子

本文深入解析块设备的基本概念,包括块设备、扇区、块和段等,并详细介绍了通用块设备层的工作原理及其数据结构。此外,还展示了不同内核版本下的请求处理函数实现,并提供了一段完整的块设备驱动代码。

一、基本概念:

  • 块设备(block device):能随机访问固定大小数据片(chunk)的设备,如硬盘,通常以安装文件系统的方式使用;

  • 扇区(sector):块设备中的最小可寻址单元,一般为512Byte(如果大于512Byte,则底层驱动负责相应的转换),数据在块设备上的位置由块索引和块内偏移决定。

  • 块(blocks):对VFS(虚拟文件系统),块是基本数据传输单元。当内核访问文件数据时,首先从磁盘上读取一个块,这个块有文件的inode,该块对应磁盘上一个或多个扇区。块的大小一般为扇区大小的整数倍,但同时要不能超过一页的长度。

  • 段(segment):
    简单DMA操作,只能传输磁盘上相邻的扇区的数据到连续的内存区域中。
    但在“分散/聚合”DMA操作模式下,传输可以在多个非连续的内存区域中进行。例如在读操作中,控制器从磁盘相邻扇区上读取数据,然后将数据分散存储在内存的不同区域中。一个段(segment)就是一个内存页面或内存页面的一部分,它包含磁盘相邻扇区上的部分数据。
    这里写图片描述

二、通用块设备层

    通用块设备层(generic block layer)处理系统所有对块设备的请求。有了它之后,内核可以:
    1)磁盘数据直接拷贝到用户地址空间中,不需要先拷贝到内核地址空间。实际上是,内核进行I/O传输的数据页面被映射到用户进程的地址空间中。

    2)管理逻辑卷,如LVM(Logical volume manager)和RAID(磁盘阵列)。

gendisk结构:
    gendisk是一个物理磁盘或分区在内核中的描述。

Request_queue结构:
    每一个gendisk对象都有一个request_queue对象,表示针对一个gendisk对象的所有请求的队列。

请求request:
    每个request结构都代表了一个块设备的I/O请求.
    在分散-聚合DMA模式下,读请求操作中,控制器从磁盘相邻扇区上读取数据,然后将数据分散存储在内存的不同区域中;写请求操作中,控制器读取分散在内存不同区域的数据,然后写入磁盘相邻扇区中。
请求被表示为一系列段(segment),每个段对应内存中的一个缓冲区。从本质上讲,一个request结构是作为一个bio结构的链表实现的。

bio结构:
    bio用来描述单一的块设备I/O请求。它代表了正在活动的以段(segment)链表形式组织的块I/O操作。一个段是一小块内存缓冲区,这些段代表的小块缓冲区在内存中不一定连续,但通过段链表组织在一起就在逻辑上合并成了一个完整的缓冲区。这样的向量I/O称为分散-聚合I/O。
核心成员:

unsigned short bi_vcnt;// 这个bio包含的bio_vec的数目;
struct bio_vec  *bi_io_vec;// 指向一个bio_vec结构体数组,该结构体数组包含了一个特定I/O操作所需要使用的所有段(segment)

注:bio_vec描述指定page中的一块连续的区域,也就是一个page中的一个”段”(segment)。
这里写图片描述
更多细节参考链接:https://www.cnblogs.com/xiaojiang1025/p/6500557.html



版本1(Linux2.6.32之前的版本)的请求处理函数

static void simp_blkdev_do_request(struct request_queue *q){
    struct request *req;
    // elv_next_request从请求队列中拿出一条请求
    // end_rquest结束一个请求,1:成功,0:失败
    while((req = elv_next_request(q)) != NULL){
        if((req->setor + req->current_nr_sectors)<<9 > SIMP_BLKDEV_BYTES){
            printk(KERN_ERR_SIMP_BLKDEV_DISKNAME
                   ":bad request:block=%llu, count=%u\n",
                   (unsigned long long)req->sector,
                   req->current_nr_sectors);
            end_request(req, 0);
            continue;
        }
        // rq_data_dir返回请求的方向(读/写)
        // void *memcpy(void*dest, const void *src, size_t n);
        // 由src指向地址为起始地址的连续n个字节的数据复制到以destin指向地址为起始地址的空间内。
        switch(rq_data_dir(req)){
        case READ:
            memcpy(req->buffer,
                   simp_blkdev_disk+(req->sector<<9),
                   req->current_nr_sectors<<9);
            end_request(req, 1);
            break;
        case WRITE:
            memcpy(simp_blkdev_disk+(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;
        }
    }
}

当使用高于Linux2.6.32版本的内核编译以上函数时,会报错:request结构体不存在buffer成员。



版本2(Linux3.x.x版本)的请求处理函数:

        相比于Linux2.6版本的内核,Linux2.6.32之后的版本内核有了很大的改动,例如request结构体的buffer成员被抛弃,无法直接利用memcpy函数对块设备的物理内存进行读写操作;在2.6.32内核中:

原成员转变新成员
request -> sectors变为blk_rq_pos(request)
request -> nr_sectors变为blk_rq_nr_sectors(request)
elv_next_request(request)变为blk_fetch_request(request)
end_request(request, error)变为__blk_end_request_all(req, err))
static void simp_blkdev_do_request(struct request_queue *q)
{
    struct request *req;
    struct req_iterator ri;
    struct bio_vec *bvec;
    char *disk_mem;
    char *buffer;

    //依次从队列中获取request
    while ((req = blk_fetch_request(q)) != NULL) {
       //判断当前request是否合法
        if ((blk_rq_pos(req) << 9) + blk_rq_cur_bytes(req)
            > SIMP_BLKDEV_BYTES) {
            printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                ": bad request: block=%llu, count=%u\n",
                (unsigned long long)blk_rq_pos(req),
                blk_rq_cur_bytes(req));
                blk_end_request_all(req, -EIO);
            continue;
        }
        //获取需要操作的内存位置
        disk_mem = simp_blkdev_data + (blk_rq_pos(req) << 9);
        switch (rq_data_dir(req)) {  //判断请求的类型
        case READ:    
            rq_for_each_segment(bvec, req, ri)
            {
                buffer = kmap(bvec->bv_page) + bvec->bv_offset;
                memcpy(buffer, disk_mem, bvec->bv_len);
                kunmap(bvec->bv_page);
                disk_mem += bvec->bv_len;
            }
            __blk_end_request_all(req, 0);
            break;
        case WRITE:        
            rq_for_each_segment(bvec, req, ri)
            {
                buffer = kmap(bvec->bv_page) + bvec->bv_offset;
                memcpy(disk_mem, buffer, bvec->bv_len);
                kunmap(bvec->bv_page);
                disk_mem += bvec->bv_len;
            }
            __blk_end_request_all(req, 0);
            break;
        default:
            /* No default because rq_data_dir(req) is 1 bit */
            break;
        }
    }
}

使用Linux4.13.0版本进行编译出现的错误:
这里写图片描述
原因:函数rq_for_each_segment(bvec, req, ri)参数类型出错。
参考链接:https://www.cnblogs.com/chengxuyuancc/p/3550047.html



版本3(Linux4.13.0内核)的请求处理函数:

        为了纠正版本2中函数参数类型不兼容问题,不再使用rq_for_each_segment函数遍历req->bio结构中的段(segment)链表。
        bio->bi_io_vec指向一个bio_vec结构体数组,该结构体数组包含了一个特定块I/O操作所需要使用的所有段(segment)。每个bio_vec结构体都是一个形式为(page, offset , len)的向量,它描述一个特定的段:段所在的物理页,段在物理页中的偏移量,从给定偏移量开始的段的长度。于是整个bi_io_vec结构体数组就可以表示一个完整的缓冲区。
        根据以上数据结构的分析,定义一个while循环体来遍历当前正在处理的request请求的bio结构体链表,在while循环体内部定义一个for循环来遍历bio结构体中的bio_vec结构体数组:

/******************************************************
*
*   磁盘块设备数据请求的处理函数
*
******************************************************/
static void simp_blkdev_do_request(struct request_queue *q){
    struct request *req;// 正在处理的请求队列中的请求
    struct bio *req_bio;// 当前请求的bio
    struct bio_vec *bvec;// 当前请求的bio的段(segment)链表
    char *disk_mem;      // 需要读/写的磁盘区域
    char *buffer;        // 磁盘块设备的缓冲区
    int i = 0;

    while((req = blk_fetch_request(q)) != NULL){
        // 判断当前req是否合法
        if((blk_rq_pos(req)<<SECTOR_SIZE_SHIFT) + blk_rq_bytes(req) > SIMP_BLKDEV_BYTES){
            printk(KERN_ERR SIMP_BLKDEV_DISKNAME":bad request:block=%llu, count=%u\n",(unsigned long long)blk_rq_pos(req),blk_rq_sectors(req));
            blk_end_request_all(req, -EIO);
            continue;
        }
        //获取需要操作的内存位置
        disk_mem = simp_blkdev_data + (blk_rq_pos(req) << SECTOR_SIZE_SHIFT);
        req_bio = req->bio;// 获取当前请求的bio

        switch (rq_data_dir(req)) {  //判断请求的类型
        case READ:
            // 遍历req请求的bio链表
            while(req_bio != NULL){
                // for循环处理bio结构中的bio_vec结构体数组(bio_vec结构体数组代表一个完整的缓冲区)
                for(i=0; i<req_bio->bi_vcnt; i++){
                    bvec = &(req_bio->bi_io_vec[i]);
                    buffer = kmap(bvec->bv_page) + bvec->bv_offset;
                    memcpy(buffer, disk_mem, bvec->bv_len);
                    kunmap(bvec->bv_page);
                    disk_mem += bvec->bv_len;
                }
                req_bio = req_bio->bi_next;
            }
            __blk_end_request_all(req, 0);
            break;
        case WRITE:
            while(req_bio != NULL){
                for(i=0; i<req_bio->bi_vcnt; i++){
                    bvec = &(req_bio->bi_io_vec[i]);
                    buffer = kmap(bvec->bv_page) + bvec->bv_offset;
                    memcpy(disk_mem, buffer, bvec->bv_len);
                    kunmap(bvec->bv_page);
                    disk_mem += bvec->bv_len;
                }
                req_bio = req_bio->bi_next;
            }
            __blk_end_request_all(req, 0);
            break;
        default:
            /* No default because rq_data_dir(req) is 1 bit */
            break;
        }
    }
}




三、完整代码:

simp_blkdev.c 文件的内容:

#include <linux/module.h>
#include <linux/blkdev.h>

#define SIMP_BLKDEV_DISKNAME "simp_blkdev"          //块设备名
#define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR //主设备号
#define SIMP_BLKDEV_BYTES (50*1024*1024)            // 块设备大小为50MB
#define SECTOR_SIZE_SHIFT 9

static struct gendisk *simp_blkdev_disk;// gendisk结构表示一个简单的磁盘设备
static struct block_device_operations simp_blkdev_fops = { //块设备操作,gendisk的一个属性
    .owner = THIS_MODULE,
};
static struct request_queue *simp_blkdev_queue;//指向块设备请求队列的指针
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];// 虚拟磁盘块设备的存储空间


/******************************************************
*
*   磁盘块设备数据请求的处理函数
*
******************************************************/
static void simp_blkdev_do_request(struct request_queue *q){
    struct request *req;// 正在处理的请求队列中的请求
    struct bio *req_bio;// 当前请求的bio
    struct bio_vec *bvec;// 当前请求的bio的段(segment)链表
    char *disk_mem;      // 需要读/写的磁盘区域
    char *buffer;        // 磁盘块设备的请求在内存中的缓冲区
    int i = 0;

    while((req = blk_fetch_request(q)) != NULL){
        // 判断当前req是否合法
        if((blk_rq_pos(req)<<SECTOR_SIZE_SHIFT) + blk_rq_bytes(req) > SIMP_BLKDEV_BYTES){
            printk(KERN_ERR SIMP_BLKDEV_DISKNAME":bad request:block=%llu, count=%u\n",(unsigned long long)blk_rq_pos(req),blk_rq_sectors(req));
            blk_end_request_all(req, -EIO);
            continue;
        }
        //获取需要操作的内存位置
        disk_mem = simp_blkdev_data + (blk_rq_pos(req) << SECTOR_SIZE_SHIFT);
        req_bio = req->bio;// 获取当前请求的bio

        switch (rq_data_dir(req)) {  //判断请求的类型
        case READ:
            // 遍历req请求的bio链表
            while(req_bio != NULL){
                // for循环处理bio结构中的bio_vec结构体数组(bio_vec结构体数组代表一个完整的缓冲区)
                for(i=0; i<req_bio->bi_vcnt; i++){
                    bvec = &(req_bio->bi_io_vec[i]);
                    buffer = kmap(bvec->bv_page) + bvec->bv_offset;
                    memcpy(buffer, disk_mem, bvec->bv_len);
                    kunmap(bvec->bv_page);
                    disk_mem += bvec->bv_len;
                }
                req_bio = req_bio->bi_next;
            }
            __blk_end_request_all(req, 0);
            break;
        case WRITE:
            while(req_bio != NULL){
                for(i=0; i<req_bio->bi_vcnt; i++){
                    bvec = &(req_bio->bi_io_vec[i]);
                    buffer = kmap(bvec->bv_page) + bvec->bv_offset;
                    memcpy(disk_mem, buffer, bvec->bv_len);
                    kunmap(bvec->bv_page);
                    disk_mem += bvec->bv_len;
                }
                req_bio = req_bio->bi_next;
            }
            __blk_end_request_all(req, 0);
            break;
        default:
            /* No default because rq_data_dir(req) is 1 bit */
            break;
        }
    }
}


/******************************************************
*
*   模块的入口函数
*
******************************************************/
static int __init simp_blkdev_init(void){
    int ret;

    //1.添加设备之前,先申请设备的资源
    simp_blkdev_disk = alloc_disk(1);
    if(!simp_blkdev_disk){
        ret = -ENOMEM;
        goto err_alloc_disk;
    }

    //2.设置设备的有关属性(设备名,设备号,fops指针,请求队列,512B的扇区数)
    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;
    // 将块设备请求处理函数的地址传入blk_init_queue函数,初始化一个请求队列
    simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
    if(!simp_blkdev_queue){
        ret = -ENOMEM;
        goto err_init_queue;
    }
    simp_blkdev_disk->queue = simp_blkdev_queue;
    set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);

    //3.入口处添加磁盘块设备
    add_disk(simp_blkdev_disk);
    return 0;

    err_alloc_disk:
        return ret;
    err_init_queue:
        return ret;
}


/******************************************************
*
*   模块的出口函数
*
******************************************************/
static void __exit simp_blkdev_exit(void){
    del_gendisk(simp_blkdev_disk);// 释放磁盘块设备
    put_disk(simp_blkdev_disk);   // 释放申请的设备资源
    blk_cleanup_queue(simp_blkdev_queue);// 清除请求队列
}


module_init(simp_blkdev_init);// 声明模块的入口
module_exit(simp_blkdev_exit);// 声明模块的出口

Makefile 文件的内容:

ifeq ($(KERNELRELEASE),)
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
        $(MAKE) -C $(KDIR) M=$(PWD) modules
modules_install:
        $(MAKE) -C $(KDIR) M=$(PWD) modules_install
clean:
        rm -rf *.o *.ko .depend *.mod.o *.mod.c Module.* modules.*
.PHONY:modules modules_install clean
else
        obj-m := simp_blkdev.o
endif

版本3在Linux4.13.0版本下编译通过:
这里写图片描述
1.    块设备驱动模块插入成功:
这里写图片描述
这里写图片描述
2.    在块设备上创建ext3文件系统:
这里写图片描述
3.    将该块设备挂载到/mnt/temp1目录下:
这里写图片描述
4.    向该块设备中存入一些文件:
这里写图片描述
      添加文件之后,查看块设备的存储资源使用情况:
这里写图片描述
删除块设备中的所有文件,块设备存储资源使用量变小:
这里写图片描述
5.    取消块设备的挂载之后,对该块设备的引用从1变为0:
这里写图片描述
6.    移除该块设备驱动模块:
这里写图片描述

评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值