1.块设备驱动

块设备驱动是用来操作硬盘类的存储设备的
在这里插入图片描述

1.1什么是块设备驱动

  1. 块设备驱动的概念:系统中能够随机访问固定大小(1block 512byte)数据片的设备被称之为块设备。块设备文件一般都是以安装文件系统的方式使用,这也是块设备通常的访问方式。块设备的方式访问方式是随机的。
  2. 块设备中最小的可寻址单位是扇区,扇区大小一般是2的整数倍。最常见的大小是512字节。块是文件系统的一种抽象,只能基于块来访问文件系统。物理磁盘寻址是按照扇区的级别进行的,内核访问的所有磁盘操作又都是按照块进行的。扇区是设备的最小可寻址单位,所以块不能比扇区还小,只能数倍与扇区大小。
  3. 内核对块大小的要求是:必须是扇区大小的整数倍,并且小于页面的大小,所以块的大小通常是512字节、1K或者4K。

1.2块设备驱动和字符设备驱动的对比

  1. 块设备接口相对复杂,不如字符设备明晰易用
  2. 块设备驱动程序对整个系统的性能影响较大,速度和效率是设计块设备驱动程要重点考虑的问题
  3. 系统中使用缓冲区与访问请求的优化管理(合并与重新排序)来提高系统性能
    在这里插入图片描述

1.3块设备驱动的相关知识简介

磁头:一个磁盘有多少个面就有多少个磁头

磁道:在一个磁头上可以有很多环,这些环就叫做磁道

扇区:磁道上访问数据的最小的单位就是扇区,一个扇区的大小就是512字节

1block = 512字节 1024字节 2048字节 4096字节

1扇区 = 512字节

块设备的能存储的数据=磁头*磁道*扇区*512
在这里插入图片描述

1.4块设备驱动的框架图

在这里插入图片描述

  1. 虚拟文件系统(VFS):隐藏了各种硬件的具体细节,为用户操作不同的硬件提供了一个统一的接口。其基于不同的文件系统格式,比如EXT,FAT等。用户程序对设备的操作都通过VFS来完成,在VFS上面就是诸如open、close、write和read的函数API。
  2. Disk Cache:硬盘的告诉缓存,用户缓存最近访问的文件数据,如果能在高速缓存中找到,就不必去访问硬盘,毕竟硬盘的访问速度慢很多。
  3. 映射层(mapping layer):这一层主要用于确定文件系统的block size,然后计算所请求的数据包含多少个block。同时调用具体文件系统函数来访问文件的inode,确定所请求的数据在磁盘上面的逻辑地址。
  4. Generic Block Layer:通用块层,Linux内核把块设备看做是由若干个扇区组成的数据空间,上层的读写请求在通用块层被构造成一个或多个bio结构
  5. I/O Scheduler Layer :I/O调度层,负责将通用块层的块I/O操作进行(电梯调度算法)调度、插入、暂存、排序、合并、分发等操作,对磁盘的操作更为高效。负责将通用块层的块I/O操作进行
  6. 块设备驱动层:在块系统架构的最底层,由块设备驱动根据排序好的请求,对硬件进行数据访问。
user:
	open     read    write    close
-------------------(io请求)-----------------------------------
kernel	|中间层: (block_device)
	    |	将用户的io请求转化成BIO(block,input ,output),
	    |	在物理内存上连续的bio会被合成request,这个request
	    |	会被放到内核的一个队列上。
	    |---------------------------------------------------------
	    |driver:gendisk
	    |	1.分配对象
	    |	2.对象初始化
	    |	3.初始化一个队列  head----request(read)----request(write)---...
	    |	//4.硬盘设备的初始化
	    |	5.注册、注销
------------------------------------------------------------------		
haredware :   分配的内存(模拟真实的设备)(1M)

1.5块设备驱动的API

1.gendisk的结构体对象
    struct gendisk {   
        int major;   //块设备的主设备号
        int first_minor; //起始的次设备号
        int minors; //设备的个数,分区的个数
        char disk_name[DISK_NAME_LEN]; //磁盘的名字
        struct disk_part_tbl  *part_tbl;
        //磁盘的分区表的首地址
        struct hd_struct part0;
        //part0分区的描述

        const struct block_device_operations *fops;
        //块设备的操作方法结构体

        struct request_queue *queue;
        //队列(重要)

        void *private_data;
        //私有数据
    };

    分区的结构体
    struct hd_struct {
        sector_t start_sect; //起始的扇区号
        sector_t nr_sects;   //扇区的个数                                                                                             
        int  partno;        //分区号
    };

    //块设备的操作方法结构体
    struct block_device_operations {
        int (*open) (struct block_device *, fmode_t);
        int (*release) (struct gendisk *, fmode_t);
        int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*getgeo)(struct block_device *, struct hd_geometry *);	
        //设置磁盘的磁头,磁道,扇区的个数的。hd_geometry
    }	

在这里插入图片描述

2.结构体对象的初始化
    struct gendisk *mydisk;

    struct gendisk *alloc_disk(int minors)
    //void put_disk(struct gendisk *disk)
    //归还引用计数
    功能:分配gendisk的内存,然后完成必要的初始化
    参数:
        @minors:分区的个数
    返回值:成功返回分配到的内存的首地址,失败返回NULL


    int register_blkdev(unsigned int major, const char *name)
    //void unregister_blkdev(unsigned int major, const char *name)
    功能:申请设备设备驱动的主设备号
    参数:
        @major : 0:自动申请
                  >0 :静态指定
        @name  :名字  cat /proc/devices
    返回值:	
            major=0 ;成功返回主设备号,失败返回错误码
            major>0 :成功返回0 ,失败返回错误码

    void set_capacity(struct gendisk *disk, sector_t size)
    功能:设置磁盘的容量

    struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,const struct blk_mq_ops 
    *ops,unsigned int queue_depth,unsigned int set_flags)
    //void blk_cleanup_queue(struct request_queue *q)
     功能:用于在给定队列深度的情况下使用mq ops设置队列的助手,以及通过mq ops标志传递的助手
     参数:
     @被初始化的tag对象,tag被上层使用,里面包含硬件队列的个数,队列的操作方法结构体,标志位等
     @放入到tag中的操作方法结构体
     @ tag中指定支持的队列深度
     @将tag中队列的处理标志位,例如BLK_MQ_F_SHOULD_MERGE, BLK_MQ_F_BLOCKING等
     返回值:成功返回队列指针,失败返回错误码指针 

3.注册、注销
    void add_disk(struct gendisk *disk)
    //注册
    void del_gendisk(struct gendisk *disk)
    //注销

1.6块设备驱动的实例

#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/init.h>
#include <linux/module.h>

#define BLKSIZE (1 * 1024 * 1024)
#define BLKNAME "mydisk"
struct gendisk* disk;
int major;
char *dev_addr;
int mydisk_open(struct block_device* blkdev, fmode_t mode)
{
    return 0;
}
int mydisk_getgeo(struct block_device* blkdev,
    struct hd_geometry* hd)
{
    //磁盘的大小 = 磁头*磁道*扇区个数*512
    //     struct hd_geometry {
    //       unsigned char heads; //磁头的个数
    //       unsigned char sectors; //扇区个数
    //       unsigned short cylinders; //磁道的个数
    //       unsigned long start;   //0
    // };

    hd->heads = 4;
    hd->cylinders = 16;
    hd->sectors = BLKSIZE / hd->heads / hd->cylinders / 512;
    return 0;
}
void mydisk_close(struct gendisk* dk, fmode_t mode)
{
}
struct block_device_operations fops = {
    .open = mydisk_open,
    .getgeo = mydisk_getgeo,
    .release = mydisk_close,
};
static int __init mydisk_init(void)
{
    // 1.分配对象
    disk = alloc_disk(4);
    if (disk == NULL) {
        printk("alloc disk memory error\n");
        return -ENOMEM;
    }
    // 2.初始化对象
    major = register_blkdev(0, BLKNAME);
    if (major < 0) {
        printk("register blk device error\n");
        return major;
    }
    disk->major = major;
    disk->first_minor = 0;
    strcpy(disk->disk_name, BLKNAME);
    set_capacity(disk, BLKSIZE / 512); //将磁盘默认先全部给part0
    disk->fops = &fops;
    //disk->queue = 

    //3申请内存当成硬盘使用
    dev_addr = vmalloc(BLKSIZE);
    if(dev_addr == NULL){
        printk("alloc dev memory error\n");
        return -ENOMEM;
    }
    // 4注册
    add_disk(disk);

    return 0;
}
static void __exit mydisk_exit(void)
{
    del_gendisk(disk);
    vfree(dev_addr);
    unregister_blkdev(major,BLKNAME);
    put_disk(disk);
}
module_init(mydisk_init);
module_exit(mydisk_exit);
MODULE_LICENSE("GPL");

1.7队列处理相关的结构体、关系、相关函数

struct  request_queue 
{
    /*双向链表数据结构,将所有加入到队列的IO请求组建成一个双向链表*/
    struct  list_head  queue_head; 
    struct list_head    requeue_list; //request队列
    spinlock_t      requeue_lock;     //队列自旋锁
    unsigned long     nr_requests;     /* 最大的请求数量 */
    unsigned long     queue_flags;/*当前请求队列的状QUEUE_FLAG_STOPPED*/};
struct  request
{
    struct list_head queuelist;/* 请求对象中的链表元素*/
    struct request_queue *q;	/* 指向存放当前请求的请求队列*/
    unsigned int __data_len;	/* 当前请求要求数据传输的总的数据量 */
    sector_t __sector;         /* 当前请求要求数据传输的块设备的起始扇区 */
    struct bio *bio;		/* bio对象所携带的信息转存至请求对象中*/
    struct bio *biotail;	/* bio链表*/};
通常一个request请求可以包含多个bio,一个bio对应一个I/O请求  
 struct bio {		
    struct bio *bi_next;	 /* 指向当前bio的下一个对象*/	
    unsigned long  bi_flags; 	 /* 状态、命令等 */	
    unsigned long bi_rw; 	 /* 表示READ/WRITE*/	
    struct block_device	*bi_bdev;    /* 与请求相关联的块设备对象指针*/	
    unsigned short bi_vcnt;	 /* bi_io_vec数组中元素个数 */	
    unsigned short bi_idx;	 /* 当前处理的bi_io_vec数组元素索引 */
    unsigned int bi_size;	 /* 本次传输需要传输的数据总量,byte(扇区大小整数倍) */	
    struct bio_vec *bi_io_vec;/* 指向一个IO向量的数组,数组中的内各元素对应一个物理页的page对象 */
  };
struct bio_vec {		
    struct page  *bv_page; //指向用于数据传输的页面所对应的struct page对象
    unsigned int bv_len;   //表示当前要传输的数据大小		
    unsigned int bv_offset;//表示数据在页面内的偏移量	
};

reque_queue和request及bio的关系
在这里插入图片描述
在这里插入图片描述

队列处理相关的函数

blk_mq_start_request(rq); //开始处理队列
blk_mq_end_request(rq, BLK_STS_OK); //结束队列处理
rq_for_each_segment(bvec, rq, iter) //从request->bio_vec
void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset; //将页地址转换为线性地址(内地址)
rq_data_dir(rq))   //从request获取本次读写的方向  WRITE 1   READ 0
dev_addr+(rq->__sector *512) //磁盘设备的地址

1.8虚拟块设备驱动的实例

mydisk.c

#include <linux/blk-mq.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/init.h>
#include <linux/module.h>

#define BLKSIZE (1 * 1024 * 1024)
#define BLKNAME "mydisk"
struct gendisk* disk;
int major;
struct request_queue* queue;
char* dev_addr;
struct blk_mq_tag_set tag;

int mydisk_open(struct block_device* blkdev, fmode_t mode)
{
    return 0;
}
int mydisk_getgeo(struct block_device* blkdev,
    struct hd_geometry* hd)
{
    //磁盘的大小 = 磁头*磁道*扇区个数*512
    //     struct hd_geometry {
    //       unsigned char heads; //磁头的个数
    //       unsigned char sectors; //扇区个数
    //       unsigned short cylinders; //磁道的个数
    //       unsigned long start;   //0
    // };

    hd->heads = 4;
    hd->cylinders = 16;
    hd->sectors = BLKSIZE / hd->heads / hd->cylinders / 512;
    return 0;
}
void mydisk_close(struct gendisk* dk, fmode_t mode)
{
}
struct block_device_operations fops = {
    .open = mydisk_open,
    .getgeo = mydisk_getgeo,
    .release = mydisk_close,
};
blk_status_t disk_queue_rq(struct blk_mq_hw_ctx* ctx,
    const struct blk_mq_queue_data* data)
{
    blk_status_t status = BLK_STS_OK;
    struct request* rq = data->rq;
    struct bio_vec bvec; //循环取到的成员
    struct req_iterator iter; //迭代器(依次取下一个成员),内核使用的
    loff_t pos = blk_rq_pos(rq) << SECTOR_SHIFT;

    //队列处理过程在这里完成
    blk_mq_start_request(rq);

    rq_for_each_segment(bvec, rq, iter)
    {
        //长度 每个片段的长度
        unsigned long b_len = bvec.bv_len;

        //内核的地址
        void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset;

        //校验越界
        if ((pos + b_len) > BLKSIZE)
            b_len = (unsigned long)(BLKSIZE - pos);

        if (rq_data_dir(rq)) // WRITE
            //用户想向磁盘写数据
            //目的:磁盘地址 dev_addr+pos
            //源: 用户地址 b_buf
            //长度:b_len
            memcpy(dev_addr+pos, b_buf, b_len);
        else // READ
            //用户想从磁盘读数据
            //目的:用户地址 b_buf
            //源: 磁盘地址 dev_addr+pos
            //长度:b_len
            memcpy(b_buf,dev_addr+pos, b_len);

        pos += b_len;
    }

    blk_mq_end_request(rq, status);

    return status;
}
struct blk_mq_ops qops = {
    .queue_rq = disk_queue_rq,
};
static int __init mydisk_init(void)
{
    // 1.分配对象
    disk = alloc_disk(4);
    if (disk == NULL) {
        printk("alloc disk memory error\n");
        return -ENOMEM;
    }
    // 2.初始化对象
    major = register_blkdev(0, BLKNAME);
    if (major < 0) {
        printk("register blk device error\n");
        return major;
    }
    queue = blk_mq_init_sq_queue(&tag, &qops, 2, BLK_MQ_F_SHOULD_MERGE);
    if (IS_ERR(queue)) {
        printk("blk queue get error\n");
        return PTR_ERR(queue);
    }
    disk->major = major;
    disk->first_minor = 0;
    strcpy(disk->disk_name, BLKNAME);
    set_capacity(disk, BLKSIZE / 512); //将磁盘默认先全部给part0
    disk->fops = &fops;
    disk->queue = queue;

    // 3申请内存当成硬盘使用
    dev_addr = vmalloc(BLKSIZE);
    if (dev_addr == NULL) {
        printk("alloc dev memory error\n");
        return -ENOMEM;
    }
    // 4注册
    add_disk(disk);

    return 0;
}
static void __exit mydisk_exit(void)
{
    del_gendisk(disk);
    vfree(dev_addr);
    blk_cleanup_queue(queue);
    unregister_blkdev(major, BLKNAME);
    put_disk(disk);
}
module_init(mydisk_init);
module_exit(mydisk_exit);
MODULE_LICENSE("GPL");

1.9块设备驱动测试方法

1.安装块设备驱动
    sudo insmod mydisk.ko
2.查看设备节点
   	ls -l /dev/mydisk
    brw-rw---- 1 root disk 252, 0 622 14:14 /dev/mydisk
3.查看磁盘相关的命令
    sudo  fdisk -l
    Disk /dev/mydisk:1 MiB,1048576 字节,2048 个扇区
    单元:扇区 / 1 * 512 = 512 字节
    扇区大小(逻辑/物理)512 字节 / 512 字节
    I/O 大小(最小/最佳)512 字节 / 512 字节  
4.分区
    sudo fdisk /dev/mydisk
    m 获取帮助
    d   删除分区
    p   打印分区表
    n   添加新分区
    w   将分区表写入磁盘并退出
    q   退出而不保存更改

    在dev下产生/dev/mydisk1节点
5.分区格式化
    sudo mkfs.ext2 /dev/mydisk1
        
6.挂载硬盘到某个目录下
     sudo mount -t ext2 /dev/mydisk1  ~/udisk
        
7.向磁盘中存放文件
     sudo cp ~/work/day10/02mydisk/mydisk.c .
8.取消挂载
     cd
     sudo umount udisk
9.将块设备驱动的数据读取到ram.bin文件中
  	sudo cat /dev/mydisk1 > ram.bin
        
10.重新挂载查看
    sudo mount -o loop ram.bin ~/udisk
    去udisk目录下查看是否有刚才拷贝的文件,如果有就说明成功了
p   打印分区表
n   添加新分区
w   将分区表写入磁盘并退出
q   退出而不保存更改

在dev下产生/dev/mydisk1节点

5.分区格式化
sudo mkfs.ext2 /dev/mydisk1

6.挂载硬盘到某个目录下
sudo mount -t ext2 /dev/mydisk1 ~/udisk

7.向磁盘中存放文件
sudo cp ~/work/day10/02mydisk/mydisk.c .
8.取消挂载
cd
sudo umount udisk
9.将块设备驱动的数据读取到ram.bin文件中
sudo cat /dev/mydisk1 > ram.bin

10.重新挂载查看
sudo mount -o loop ram.bin ~/udisk
去udisk目录下查看是否有刚才拷贝的文件,如果有就说明成功了

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐