build_embed_linux_system

块设备驱动和RAM模拟实现

块设备驱动时操作系统中用于管理以数据块为单位进行数据读写的设备的软件模块。常见的块设备包含NAND FLASH、EMMC、SD卡、固态硬盘(SSD)等。块设备驱动作为字符设备,网络设备并列的三大驱动之一;也是操作系统中最复杂的一类驱动。对于块设备,在开发中通常使用内核中已经实现的驱动,除原厂外很少有单独开发实现的机会。不过从产品角度,理解块设备有助于开发问题的定位和排查,值得花时间去理解学习。

硬盘,SD卡等都是数据存储的硬件。以SD卡为例,内部提供了类似EMMC的数据存储空间,保存数据。SDCard为了方便管理数据,将数据固定的格式划分存储;通过表的方法查询管理资源,这就是分区机制。对于SD卡或者EMMC来说,常见的系统格式有FAT32,EXT-4等;其中分区数据数据信息保存在硬件的固定位置,也就是MBR/GPT分区表。对于日常中硬盘的损坏,往往是分区表的异常导致系统无法解析;文件往往还存在于系统指定位置,并未损坏,这也是数据能够恢复的原因。

和字符设备不同,块设备只能以块为单位接收输入和返回输出;读写带缓冲区,支持顺序或随机访问,不过顺序访问往往会有更快的速度。块设备驱动从原理上来说分成以下部分。

  1. 访问硬件设备的驱动接口,如何通过SPI,SDIO,PCIE,USB等接口读取具体的硬件设备;硬件上支持某款设备,大都在设备树种添加相应控制器节点、
  2. 块设备在内核的注册,同时管理访问硬件设备的驱动接口,将读写操作关联到具体硬件的处理。
  3. 设备内部文件系统的接口,从硬件中读取数据后,按照具体的系统格式如FAT32,EXT-4进行解析;挂载到应用层目录,进行具体数据读写的访问。

具体结构参考下图所示。

image

可以看到,在块设备框架下,物理层连接块设备驱动的接口总类繁多。如果按照外部器件、硬件接口、块设备驱动、上层应用等去理解,就要先掌握对应器件的手册以及相应接口的知识,在去学习块设备驱动。当这类设备通过设备树或者其它机制注册到系统后,会执行相应的驱动关联对器件的访问,这部分内容是比较困难的。例如常用的SDCard,掌握就需要理解sdio接口、SD命令交互、SD数据通讯;这部分进行说明,一篇文章都不够了,理解起来十分复杂。另外类似的块设备物理层器件还有很多种类型,接口也各异;杂糅一起理解起来就更加困难。

对于块设备种物理层部分分散到相应接口去说明,这里主要讲解块设备内核注册和应用实践的部分;为了简化操作,对于物理层使用内部的存储来模拟,降低理解的难度。

块设备支持两种操作方式:队列模式和直接模式。

  1. 队列模式是块设备驱动中常见的操作方式,它会将多个 I/O 请求收集到一个请求队列里,然后由调度器对这些请求进行排序和优化,以此提升I/O性能。
  2. 直接模式是一种特殊的操作方式,它会直接将 I/O 请求发送给块设备驱动,而不会将其放入请求队列中。这种方式通常用于一些特殊的场景,例如需要立即执行I/O操作的情况。

按照上述框架,目录如下所示。

no_queue_mode

非队列模式相当于队列模式属于简单的情况;当应用层有请求时,会直接调用驱动提供的接口,读写数据。这里先了解下非对列模式注册的相关接口。

注意: 对于Linux内核来说,不同大版本的块设备内核接口差异很大;也就是kernel-4.x,kernel-5.x, kernel-6.x中块设备驱动接口是不一致的,本文以Kernel-6.x为例,如果其它版本,需要参考内核中驱动接口进行相应修改。

no_queue_interface

非队列模式的接口主要包括以下内容。

// 向系统申请块设备号
// @major: 主设备号,为0则由硬件分配
// @name: 块设备名称
// @probe: 块设备探测函数,用于在块设备注册后进行初始化操作(可选)
// 返回值: 成功返回申请的主设备号,失败返回负数
int register_blkdev(unsigned int major, const char *name,
    void (*probe)(dev_t devt));
int __register_blkdev(unsigned int major, const char *name,
    void (*probe)(dev_t devt));
#define register_blkdev(major, name) \
    register_blkdev(major, name, NULL)

// 注销已经申请的块设备号
// @major: 主设备号
// @name: 块设备名称
// 返回值: 无
void unregister_blkdev(unsigned int major, const char *name);

// 分配一个块设备结构体
// @node_id: 节点ID
// @key: 锁类键
// 返回值: 成功返回分配的块设备结构体指针,失败返回NULL
struct gendisk *__blk_alloc_disk(int node, struct lock_class_key *lkclass)
#define blk_alloc_disk(node_id)                        \
({                                    \
    static struct lock_class_key __key;                \
                                    \
    __blk_alloc_disk(node_id, &__key);                \
})

// 释放一个块设备结构体
// @disk: 块设备结构体指针
// 返回值: 无
void put_disk(struct gendisk *disk)

// 设置块设备的容量
// @disk: 块设备结构体指针
// @size: 块设备的大小
// 返回值: 无
void set_capacity(struct gendisk *disk, sector_t size)

//  向系统中添加块设备
// @disk: 块设备结构体指针
// 返回值: 成功返回0,失败返回负数
static inline int __must_check add_disk(struct gendisk *disk)
{
    return device_add_disk(NULL, disk, NULL);
}
int __must_check device_add_disk(struct device *parent, struct gendisk *disk,
                 const struct attribute_group **groups);

// 从系统中删除块设备
// @disk: 块设备结构体指针
// 返回值: 无
void del_gendisk(struct gendisk *gp);

对于上述接口,其核心操作的结构体就是”struct gendisk *“,该结构体用于描述一个块设备,包含了块设备的各种属性和操作函数。

// 表示块设备的结构体
struct gendisk {
    int major;          // 块设备的主设备号,用于标识一类设备
    int first_minor;    // 块设备的起始次设备号,区分同一类设备中的不同设备
    int minors;         // 块设备支持的次设备号数量,表明该块设备能够包含多少个子设备

    char disk_name[DISK_NAME_LEN];  //块设备的名称,例如/dev/sda里的 sda

    unsigned short events;          //块设备的事件标志,记录设备产生的事件
    unsigned short event_flags;     //块设备的事件标志,用于控制事件的处理

    struct xarray part_tbl;         // 块设备的分区表,用来管理块设备的所有分区信息
    struct block_device *part0;     // 指向分区 0 的块设备指针

    const struct block_device_operations *fops; //指向块设备操作函数集的指针,包含了对块设备进行读写、打开、关闭等操作的函数
    struct request_queue *queue;    // 指向请求队列的指针,用于管理对块设备的 I/O 请求
    void *private_data;             // 指向私有数据的指针,驱动开发者可用于存储与该块设备相关的私有信息

    struct bio_set bio_split;       //用于存储拆分的 BIO(Block I/O)的集合

    int flags;                      //块设备的标志,包含多种状态和控制信息
    unsigned long state;            //块设备的状态,通过宏定义来表示不同的状态
#define GD_NEED_PART_SCAN        0
#define GD_READ_ONLY            1
#define GD_DEAD                2
#define GD_NATIVE_CAPACITY        3
#define GD_ADDED            4
#define GD_SUPPRESS_PART_SCAN        5
#define GD_OWNS_QUEUE            6

    struct mutex open_mutex;        //用于保护块设备的打开和关闭操作的互斥锁,避免多个进程同时操作设备。
    unsigned open_partitions;       //打开的分区数量,记录当前有多少个分区处于打开状态

    struct backing_dev_info *bdi;   //指向块设备信息的指针,包含设备的缓存、I/O 调度等信息。
    struct kobject *slave_dir;      //指向从设备目录的指针,用于设备模型的管理

#ifdef CONFIG_BLOCK_HOLDER_DEPRECATED
    struct list_head slave_bdevs;   //指向块设备持有者的指针

#endif

    struct timer_rand_state *random;    //指向随机数生成器状态的指针
    atomic_t sync_io;                   //同步 I/O 操作的计数,用于跟踪同步 I/O 操作的次数
    struct disk_events *ev;             //指向磁盘事件的指针,记录磁盘发生的事件

#ifdef  CONFIG_BLK_DEV_INTEGRITY
    struct kobject integrity_kobj;      //如果配置了块设备完整性,指向块设备完整性的对象。

#endif    /* CONFIG_BLK_DEV_INTEGRITY */

#ifdef CONFIG_BLK_DEV_ZONED
    unsigned int        nr_zones;               // 块设备的分区数量
    unsigned int        max_open_zones;         // 最大打开的分区数量
    unsigned int        max_active_zones;       // 最大活动的分区数量
    unsigned long        *conv_zones_bitmap;    // 块设备分区的位图
    unsigned long        *seq_zones_wlock;      // 块设备分区的写锁

#endif

#if IS_ENABLED(CONFIG_CDROM)
    struct cdrom_device_info *cdi;              //如果配置了光驱设备,指向光盘设备信息的指针。

#endif
    int node_id;                                //节点 ID,用于多节点系统中标识设备所在的节点
    struct badblocks *bb;                       //指向坏块信息的指针,记录设备上的坏块信息
    struct lockdep_map lockdep_map;             //用于锁依赖分析的映射,帮助调试锁相关的问题
    u64 diskseq;                                //磁盘序列号,唯一标识该磁盘设备

    struct blk_independent_access_ranges *ia_ranges; //独立访问范围,用于描述设备的独立访问区域。
};

上述结构体包含了一个块设备的所有属性和操作函数,全部理解起来并不简单。不过很多参数都是内核用于管理和访问块设备的状态和标志,对于驱动实现来说,主要涉及的是如下参数。

  1. major,定义块设备的主设备号,用于标识一类设备;使用register_blkdev申请的主设备号。
  2. first_minor,设置第一个分区的次设备号,一般为0。
  3. fops,指定块设备的操作函数集,包含了对块设备进行读写、打开、关闭等操作的函数;是连接应用到底层的接口。

其中最关键的就是fops结构体,是关联应用层和底层硬件的接口;结构体struct block_device_operations的详细信息如下。

//上述结构体中,通过const struct block_device_operations指定设备的操作函数
struct block_device_operations {
    void (*submit_bio)(struct bio *bio);                            //提交一个块I/O请求(struct bio)到块设备驱动进行处理
    int (*poll_bio)(struct bio *bio, struct io_comp_batch *iob,     //对bio请求的完成状态进行轮询
            unsigned int flags);
    int (*open) (struct block_device *, fmode_t);                   //打开块设备,在用户程序调用open()系统调用访问块设备时触发
    void (*release) (struct gendisk *, fmode_t);                    //关闭块设备,在用户程序调用close()系统调用关闭块设备时触发
    int (*rw_page)(struct block_device *, sector_t, struct page *, enum req_op);    //对指定块设备的页面进行读写操作
    int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);         //执行特定的设备控制命令,用户程序调用ioctl触发
    int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);  //执行特定的设备控制命令,用于兼容旧版本的ioctl调用
    unsigned int (*check_events) (struct gendisk *disk,             //检查块设备的事件
                      unsigned int clearing);
    void (*unlock_native_capacity) (struct gendisk *);              //解锁块设备的原生容量
    int (*getgeo)(struct block_device *, struct hd_geometry *);     //获取块设备的几何信息,如柱面数、磁头数、扇区数等
    int (*set_read_only)(struct block_device *bdev, bool ro);       //设置块设备为只读或可写状态
    void (*free_disk)(struct gendisk *disk);                        //释放块设备资源
    void (*swap_slot_free_notify) (struct block_device *, unsigned long);   // 当交换空间槽被释放时调用此函数
    int (*report_zones)(struct gendisk *, sector_t sector,          //报告块设备的区域信息
            unsigned int nr_zones, report_zones_cb cb, void *data);
    char *(*devnode)(struct gendisk *disk, umode_t *mode);          //创建块设备节点的名称
    int (*get_unique_id)(struct gendisk *disk, u8 id[16],           //获取块设备的唯一标识符
            enum blk_unique_id id_type);
    struct module *owner;                                           //指向实现这些操作函数的模块的指针,用于引用计数和模块管理
    const struct pr_ops *pr_ops;                                    //指向性能报告操作结构体的指针,用于性能相关的操作

    int (*alternative_gpt_sector)(struct gendisk *disk, sector_t *sector);  //获取块设备的备用 GPT 扇区号
};

上述结构体可以说功能繁多,不过对于驱动中,起始比较关注的是两个参数。

  1. submit_bio, 格式”struct bio”,提交一个块I/O请求(struct bio)到块设备驱动进行处理;也就是我们常说的读写函数。
  2. getgeo, 格式”struct hd_geometry”,获取块设备的几何信息,如柱面数、磁头数、扇区数等;应用层处理依赖这些信息。
// struct bio表示块设备I/O请求的结构体
struct bio {
    struct bio              *bi_next;       // 指向下一个bio结构体的指针,用于将多个bio链接成链表
    struct block_device     *bi_bdev;       // 指向关联的块设备的指针
    blk_opf_t               bi_opf;         // 块操作标志,定义了I/O操作的类型
    unsigned short          bi_flags;       // 用于表示bio的各种状态和控制标志的短整型变量
    unsigned short          bi_ioprio;      // I/O操作的优先级
    blk_status_t            bi_status;      // 块操作的状态,用于表示操作的结果
    atomic_t                __bi_remaining; // 原子类型变量,表示剩余需要处理的I/O数据量
    struct bvec_iter        bi_iter;        // 用于遍历bio中的数据块向量的结构体
    blk_qc_t                bi_cookie;      // 块I/O请求的cookie,用于跟踪和标识请求
    bio_end_io_t            *bi_end_io;     // 指向I/O操作完成时调用的回调函数的指针
    void                    *bi_private;    // 指向私有数据的指针,用于存储与I/O操作相关的额外信息
#ifdef CONFIG_BLK_CGROUP
    struct blkcg_gq        *bi_blkg;        // 如果配置了块设备cgroup,指向块设备cgroup组队列的指针
    struct bio_issue        bi_issue;       // 如果配置了块设备cgroup,用于表示bio的分发信息
#ifdef CONFIG_BLK_CGROUP_IOCOST
    u64                     bi_iocost_cost; // 如果配置了块设备cgroup I/O成本核算,用于记录I/O操作的成本
#endif
#endif

#ifdef CONFIG_BLK_INLINE_ENCRYPTION
    // 如果配置了块设备内联加密,指向bio加密上下文的指针
    struct bio_crypt_ctx    *bi_crypt_context; 
#endif

    union {
#if defined(CONFIG_BLK_DEV_INTEGRITY)
    // 如果配置了块设备完整性,指向bio完整性负载的指针
    struct bio_integrity_payload *bi_integrity;    
#endif
    };

    unsigned short          bi_vcnt;                // 表示bio中的数据块向量数量
    unsigned short          bi_max_vecs;            // 表示bio中允许的最大数据块向量数量
    atomic_t                __bi_cnt;               // 原子类型变量,表示bio的引用计数
    struct bio_vec          *bi_io_vec;             // 指向bio中的数据块向量数组的指针
    struct bio_set          *bi_pool;               // 指向bio池的指针,用于管理bio结构体的分配
    struct bio_vec          bi_inline_vecs[];       // 用于存储内联数据块向量的数组
};

// struct hd_geometry 硬盘信息
struct hd_geometry {
      unsigned char heads;                          // 硬盘的磁头数量, 磁头是硬盘中用于读写数据的部件,每个盘面通常有一个磁头.
      unsigned char sectors;                        // 表示每个磁道上的扇区数量
      unsigned short cylinders;                     // 表示硬盘的柱面数量
      unsigned long start;                          // 表示硬盘的起始扇区号,通常用于指定分区或逻辑卷在硬盘上的起始位置
};

对于struct hd_geometry,是和硬件挂钩的结构;这里以机械硬盘为例进行说明。

  1. heads说明,磁盘是由多个盘片叠加的构成的结构,每个盘片都由一个磁头进行寻址,多个盘片就要由多个磁头寻址,磁头数目就对应这里的heads。
  2. sectors说明,sector是磁盘访问硬件的最小单位,磁盘上为了访问读写效率,会将多个相邻扇区组合划分为一个磁道,这个磁道是单个盘面中的一组扇区的组合;sectors则表明磁道中扇区的数目。
  3. cyliners说明,在早期磁道是以圈进行访问的,对应到多个盘片,就形成柱面;不过对于较新的硬盘,采用逻辑分区的模式,柱面不在表示物理的柱形结构,但逻辑上继承下来,柱面和磁道的数目是一致的。
  4. start说明,起始的扇区号,通常是0。

对于固态硬盘,虽然原理上不依赖于机械硬盘的磁头读取机制;不过为了兼容性,也是逻辑上划分为扇区、柱面、磁头进行统一管理。

no_queue_driver

上述接口讲述了非队列驱动设备申请,注册的接口,下面使用这些接口进行驱动的实现。

块设备作为驱动设备的一员,也要实现驱动加载和注册的接口;具体如下所示。

// 驱动加载时执行的函数
static int __init ram_blk_init(void)
{
    //......
}

// 驱动移除时执行的函数
static void __exit ram_blk_exit(void)
{
    //......
}

// 驱动信息
module_init(ram_blk_init);
module_exit(ram_blk_exit);
MODULE_AUTHOR("wzdxf");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("platform driver for nvmem");
MODULE_ALIAS("ram disk");
static int __init ram_blk_init(void)
{
    int err = -ENOMEM;

    // 注册一个块设备,返回值为主设备号
    disk_data.ram_blk_major = register_blkdev(0, "ram_blk");
    if (disk_data.ram_blk_major < 0 ) {
        printk(KERN_ERR"ram disk register failed!\n");
        goto out;
    }
    
    // 申请gendisk结构,进行后续注册操作
    disk_data.ram_gendisk = blk_alloc_disk(NUMA_NO_NODE);
    if(!disk_data.ram_gendisk)
    {
        printk(KERN_ERR"alloc_disk failed!\n");
        goto out_unregister_blkdev;
    }

    strcpy(disk_data.ram_gendisk->disk_name, "ram_blk");
    disk_data.ram_gendisk->major = disk_data.ram_blk_major;     //设置主设备号
    disk_data.ram_gendisk->first_minor = 0;                     //设置第一个分区的次设备号
    disk_data.ram_gendisk->minors = 1;                          //设置分区个数: 1
    disk_data.ram_gendisk->fops = &ram_blk_ops;                 //指定块设备ops集合
    set_capacity(disk_data.ram_gendisk, 20480);                 //设置扇区数量:10MiB/512B=20480
    spin_lock_init(&disk_data.ram_blk_lock);                    //初始化自旋锁
    
    disk_data.ram_blk_addr = (char*)vmalloc(RAM_CAPACITY);      //分配10M空间作为硬盘,每个扇区512,对应20480个扇区
    if(disk_data.ram_blk_addr == NULL) {
        printk(KERN_ERR"alloc memory failed!\n");
        goto out_cleanup_disk;
    }
    disk_data.ram_blk_sursor = disk_data.ram_blk_addr;
    
    err = add_disk(disk_data.ram_gendisk);                      //添加硬盘
    if (err) {
        printk(KERN_ERR"add_disk failed!\n");
        goto out_freemem;
    }

    printk(KERN_INFO"ram disk register success!\n");

    return 0;

out_freemem:
    vfree(disk_data.ram_blk_addr);
out_cleanup_disk:
    put_disk(disk_data.ram_gendisk);
out_unregister_blkdev:
    unregister_blkdev(disk_data.ram_blk_major, "ram_blk");
out:
    return err;
}

static void __exit ram_blk_exit(void)
{
    //执行注销的操作
    del_gendisk(disk_data.ram_gendisk);                     //删除硬盘
    put_disk(disk_data.ram_gendisk);                        //注销设备管理结构 blk_alloc_disk
    unregister_blkdev(disk_data.ram_blk_major, "ram_blk");  //注销块设备
    vfree(disk_data.ram_blk_addr);                          //移除申请的memory
}
#define DISK_HEADS              1               // 磁盘头数
#define DISK_CYLINDERS          2048            // 磁盘柱面数(柱面数和磁道数相同)
#define DISK_SECTORS            10              // 磁盘每个磁道的扇区数
#define DISK_SECTOR_BLOCK       512             // 扇区大小

struct ram_disk_data disk_data;

// 进行块数据传输
static void ram_blk_transfer(unsigned long sector, unsigned long nsect, char *buffer, int write)
{
    unsigned long offset = sector << SECTOR_SHIFT;
    unsigned long nbytes = nsect << SECTOR_SHIFT;

    spin_lock(&disk_data.ram_blk_lock);

    // 将游标移动到操作的memory地址
    disk_data.ram_blk_sursor = disk_data.ram_blk_addr + offset;
    READ_ONCE(*buffer);

    // 检查游标是否有效
    if (disk_data.ram_blk_sursor) {
        if (write) {
            memcpy(disk_data.ram_blk_sursor, buffer, nbytes);
        } else {
            memcpy(buffer, disk_data.ram_blk_sursor, nbytes);
        }
    } else {
        printk(KERN_ERR"ram_blk_transfer sursor null:0x%x, 0x%x\n",
            (unsigned int)disk_data.ram_blk_addr, (unsigned int)disk_data.ram_blk_sursor);
    }

    spin_unlock(&disk_data.ram_blk_lock);
}

static void ram_blk_submit_bio(struct bio *bio)
{
    struct bio_vec bvec;
    struct bvec_iter iter;

    // 获取起始的扇区
    sector_t sector = bio->bi_iter.bi_sector;

    // 遍历bio结构的所有段
    bio_for_each_segment(bvec, bio, iter) {
        
        //获取缓冲取地址
        char *buffer = kmap_atomic(bvec.bv_page) + bvec.bv_offset;
        unsigned len = bvec.bv_len >> SECTOR_SHIFT;

        if (buffer) {
            ram_blk_transfer(sector, len, buffer, bio_data_dir(bio) == WRITE);
            sector += len;
            kunmap_atomic(buffer);
        } else {
            printk(KERN_ERR"ram_blk_submit_bio: buffer null:%d\n", len);
            break;
        }
    }
    bio_endio(bio);
}

static int ram_blk_open(struct block_device *bdev, fmode_t mode)
{
    return 0;
}

static void ram_blk_release(struct gendisk *disk, fmode_t mode)
{
}

static int ram_blk_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
    // 磁盘参数
    geo->heads = DISK_HEADS;            // 磁盘头数
    geo->cylinders = DISK_CYLINDERS;    // 磁盘柱面数
    geo->sectors = DISK_SECTORS;        // 磁盘每个磁道的扇区数
    geo->start = 0;                     // 磁盘起始扇区
    return 0;
}

static int ram_blk_ioctl(struct block_device *bdev, fmode_t mode, unsigned int cmd, unsigned long arg)
{
    return 0;
}

static const struct block_device_operations ram_blk_ops = {
    .owner = THIS_MODULE,
    .submit_bio = ram_blk_submit_bio,           // 提交bio队列,具体的读写函数
    .open = ram_blk_open,                       // 打开设备执行的函数
    .release = ram_blk_release,                 // 关闭设备执行的函数
    .getgeo = ram_blk_getgeo,                   // 返回磁盘信息,fdisk命令执行会读取
    .ioctl = ram_blk_ioctl,                     // 磁盘执行ioctl操作的命令
};

关于非队列模式的块设备驱动讲解完毕,详细代码可参考:非队列模式块设备驱动代码

queue_mode

队列模式和非队列模式处理相似,不过需要维护队列进行数据处理,这里首先进行相应接口的说明。

queue_interface

// 申请块设备管理队列
// @set: 队列管理结构
// @返回: 0 申请成功,其他值失败
int blk_mq_alloc_tag_set(struct blk_mq_tag_set *set)

// 申请队列管理的gendisk结构
// @set: 队列管理结构
// @queuedata: 队列数据
// @lkclass: 锁类,可以为NULL
// @返回: 申请的gendisk结构
struct gendisk *__blk_mq_alloc_disk(struct blk_mq_tag_set *set, void *queuedata,
    struct lock_class_key *lkclass)

对于队列模式,关键时管理信息的结构体struct blk_mq_tag_set,如下所示。

// struct blk_mq_tag_set,队列管理结构
struct blk_mq_tag_set {
    struct blk_mq_queue_map    map[HCTX_MAX_TYPES]; // 块设备多队列(Multi-Queue)的队列映射数组,数组大小为 HCTX_MAX_TYPES
    unsigned int        nr_maps;                    // 队列映射的数量,即 map 数组中有效元素的个数
    const struct blk_mq_ops    *ops;                // 指向块设备多队列操作函数集的指针,定义了队列的相关操作
    unsigned int        nr_hw_queues;               // 硬件队列的数量,代表实际的硬件处理队列数目
    unsigned int        queue_depth;                // 队列深度,即每个队列可以容纳的最大请求数量
    unsigned int        reserved_tags;              // 预留的标签数量,这些标签不会被正常请求使用
    unsigned int        cmd_size;                   // 每个命令的大小,单位为字节
    int            numa_node;                       // NUMA 节点编号,用于非统一内存访问架构,指定该队列所属的节点
    unsigned int        timeout;                    // 超时时间,单位为毫秒,用于设置请求的超时时间
    unsigned int        flags;                      // 标志位,用于配置队列的各种特性和行为
    void            *driver_data;                   // 驱动私有数据指针,供驱动开发者存储自定义数据

    struct blk_mq_tags    **tags;                   // 指向块设备多队列标签数组的指针,每个元素指向一个 blk_mq_tags 结构

    struct blk_mq_tags    *shared_tags;             // 指向共享标签的指针,多个队列可以共享这些标签

    struct mutex        tag_list_lock;              // 用于保护标签列表的互斥锁,防止并发访问导致的数据竞争
    struct list_head    tag_list;                   // 标签列表,用于管理所有的标签
};

// 队列管理操作 struct blk_mq_ops
struct blk_mq_ops {
    blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *,
                 const struct blk_mq_queue_data *);         // 将 I/O 请求加入硬件队列

    void (*commit_rqs)(struct blk_mq_hw_ctx *);             // 交硬件队列中的所有请求,通知硬件开始处理
    void (*queue_rqs)(struct request **rqlist);             // 将一组请求加入请求队列
    int (*get_budget)(struct request_queue *);              // 获取请求队列的预算,通常用于控制并发请求数量
    void (*put_budget)(struct request_queue *, int);        // 归还请求队列的预算
    void (*set_rq_budget_token)(struct request *, int);     // 为请求设置预算令牌
    int (*get_rq_budget_token)(struct request *);           // 取请求的预算令牌
    enum blk_eh_timer_return (*timeout)(struct request *);  // 处理请求超时事件
    int (*poll)(struct blk_mq_hw_ctx *, struct io_comp_batch *);    // 轮询硬件上下文,检查 I/O 操作是否完成
    void (*complete)(struct request *);                             // 标记请求处理完成,释放相关资源
    int (*init_hctx)(struct blk_mq_hw_ctx *, void *, unsigned int); // 初始化块设备多队列硬件上下文
    void (*exit_hctx)(struct blk_mq_hw_ctx *, unsigned int);        // 退出块设备多队列硬件上下文,清理相关资源
    int (*init_request)(struct blk_mq_tag_set *set, struct request *,
                unsigned int, unsigned int);                        // 初始化请求
    void (*exit_request)(struct blk_mq_tag_set *set, struct request *,
                 unsigned int);                                     // 退出请求处理,清理相关资源
    void (*cleanup_rq)(struct request *);                           // 清理请求,释放请求占用的资源
    bool (*busy)(struct request_queue *);                           // 检查请求队列是否忙碌
    void (*map_queues)(struct blk_mq_tag_set *set);                 // 映射硬件队列和 CPU 队列的关系

#ifdef CONFIG_BLK_DEBUG_FS
    void (*show_rq)(struct seq_file *m, struct request *rq);        // 在调试文件系统中显示请求信息
#endif
};

queue_driver

和非队列模式实现一致,队列模式驱动也包含如下流程完成注册。

static int __init ram_blk_init(void)
{
    //......
}

//执行注销的操作
static void __exit ram_blk_exit(void)
{
    //......
}

module_init(ram_blk_init);
module_exit(ram_blk_exit);
MODULE_AUTHOR("wzdxf");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("platform driver");
static int __init ram_blk_init(void)
{
    int err = -ENOMEM;

    // 注册一个块设备,返回值为主设备号
    disk_data.ram_blk_major = register_blkdev(0, DEVICE_NAME);
    if (disk_data.ram_blk_major < 0 ) {
        printk(KERN_ERR"ram disk register failed!\n");
        goto out;
    }

    // 初始化使用空间和自旋锁
    disk_data.ram_blk_addr = (char*)vmalloc(RAM_CAPACITY);      //分配10M空间作为硬盘
    if(!disk_data.ram_blk_addr) {
        printk(KERN_ERR"alloc memory failed!\n");
        goto out_unregister_blkdev;
    }
    disk_data.ram_blk_sursor = disk_data.ram_blk_addr;
    spin_lock_init(&disk_data.ram_blk_lock);                    //初始化自旋锁

    // 初始化并申请tag_set,用于队列管理
    disk_data.tag_set.ops = &ram_mq_ops;
    disk_data.tag_set.nr_hw_queues = 1;
    disk_data.tag_set.nr_maps = 1;
    disk_data.tag_set.queue_depth = 16;
    disk_data.tag_set.numa_node = NUMA_NO_NODE;
    disk_data.tag_set.flags = BLK_MQ_F_SHOULD_MERGE;
    err = blk_mq_alloc_tag_set(&disk_data.tag_set);
    if(err) {
        printk(KERN_ERR"blk_mq_alloc_tag_set failed!\n");
        goto out_freemem;
    }

    // 申请gendisk结构,进行后续注册操作
    disk_data.ram_gendisk = blk_mq_alloc_disk(&disk_data.tag_set, NULL);;
    if(!disk_data.ram_gendisk)
    {
        printk(KERN_ERR"alloc_disk failed!\n");
        goto out_free_tag_set;
    }

    strcpy(disk_data.ram_gendisk->disk_name, DEVICE_NAME);
    disk_data.ram_gendisk->major = disk_data.ram_blk_major;     //设置主设备号
    disk_data.ram_gendisk->first_minor = 0;                     //设置第一个分区的次设备号
    disk_data.ram_gendisk->minors = 1;                          //设置分区个数: 1
    disk_data.ram_gendisk->fops = &ram_blk_ops;                 //指定块设备ops集合
    set_capacity(disk_data.ram_gendisk, DISK_SECTORS_TOTAL);    //设置扇区数量:10MiB/512B=20480
    disk_data.ram_size = RAM_CAPACITY;

    err = add_disk(disk_data.ram_gendisk);                      //添加硬盘
    if (err) {
        printk(KERN_ERR"add_disk failed!\n");
        goto out_cleanup_disk;
    }

    printk(KERN_INFO"ram disk register success!\n");

    return 0;

out_cleanup_disk:
    put_disk(disk_data.ram_gendisk);
out_free_tag_set:
    blk_mq_free_tag_set(&disk_data.tag_set);
out_freemem:
    vfree(disk_data.ram_blk_addr);
out_unregister_blkdev:
    unregister_blkdev(disk_data.ram_blk_major, DEVICE_NAME);
out:
    return err;
}

//执行注销的操作
static void __exit ram_blk_exit(void)
{
    del_gendisk(disk_data.ram_gendisk);                         //删除硬盘
    put_disk(disk_data.ram_gendisk);                            //注销设备管理结构 blk_alloc_disk
    blk_mq_free_tag_set(&disk_data.tag_set);                    //注销tag_set
    vfree(disk_data.ram_blk_addr);                              //释放内存
    unregister_blkdev(disk_data.ram_blk_major, DEVICE_NAME);    //注销块设备
}

这部分是队列和非队列模式的最大差异,非队列模式由gendisk接口进行管理,通过block_device_operations中的接口进行管理;队列模式这个接口只提供硬盘的信息,具体的读写操作由队列中的struct blk_mq_ops进行管理; 不过block_device_operations仍然存在,用户提供硬盘的基本信息。

// struct block_device_operations
static int ram_blk_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
    // 磁盘参数
    geo->heads = DISK_HEADS;            // 磁盘头数
    geo->cylinders = DISK_CYLINDERS;    // 磁盘柱面数
    geo->sectors = DISK_SECTORS;        // 磁盘每个磁道的扇区数
    geo->start = 0;                     // 磁盘起始扇区
    return 0;
}

static const struct block_device_operations ram_blk_ops = {
    .owner = THIS_MODULE,
    .getgeo = ram_blk_getgeo,
};

// ram_queue_rq
static blk_status_t ram_queue_rq(struct blk_mq_hw_ctx *hctx,
    const struct blk_mq_queue_data *bd)
{
    int remaining;
    struct request *current_req = bd->rq;
    struct bio_vec bv;
    struct req_iterator iter;
    unsigned int size, offset;

    // 开始处理当前请求
    blk_mq_start_request(current_req);
    remaining = blk_rq_sectors(current_req) << 9;   // 计算当前请求的大小
    offset = blk_rq_pos(current_req) << 9;          // 计算当前请求的起始位置
    
    // 检查当前请求是否超出了 RAM 磁盘的容量
    if (remaining + offset > disk_data.ram_size) {
        blk_mq_end_request(current_req, BLK_STS_IOERR);
        printk(KERN_ERR"out of memory,remaining:0x%x, offset:0x%x, totol size:%lu!\n"
                , remaining, offset, disk_data.ram_size);
        return BLK_STS_IOERR;
    }
    
    spin_lock_irq(&disk_data.ram_blk_lock);         // 加锁以保护对 RAM 磁盘的并发访问
    disk_data.ram_blk_sursor = disk_data.ram_blk_addr + offset; //  计算当前请求在RAM中的起始位置

    // 遍历当前请求的所有bio_vec
    rq_for_each_segment(bv, current_req, iter) {
        // 如果没有剩余空间,则退出循环
        if (!remaining) {
            break;
        }

        // 计算当前 bio_vec 的大小
        size = bv.bv_len;
        size = min_t(int, size, remaining);

        // 从当前 bio_vec 中读取或写入数据
        if (rq_data_dir(current_req) == READ) {
            memcpy_to_bvec(&bv, disk_data.ram_blk_sursor);
        } else {
            memcpy_from_bvec(disk_data.ram_blk_sursor, &bv);
        }

        // 更新剩余空间和当前位置
        remaining -= size;
        disk_data.ram_blk_sursor += size;
    }

    spin_unlock_irq(&disk_data.ram_blk_lock);       // 解锁以允许其他请求访问 RAM 磁盘
    blk_mq_end_request(current_req, BLK_STS_OK);    // 结束当前请求
    return BLK_STS_OK;
}

static const struct blk_mq_ops ram_mq_ops = {
    .queue_rq = ram_queue_rq,
};

关于非队列模式的块设备驱动讲解完毕,详细代码可参考:队列模式块设备驱动代码

block_app

# 进行硬盘分区
fdisk /dev/ram_blk 

# Device contains neither a valid DOS partition table, nor Sun, SGI, OSF or GPT disklabel
# Building a new DOS disklabel. Changes will remain in memory only,
# until you decide to write them. After that the previous content
# won't be recoverable.

##################################################################
Command (m for help): n
Partition type
   p   primary partition (1-4)
   e   extended
p
Partition number (1-4): 1
First sector (63-20479, default 63): 
Using default value 63
Last sector or +size{,K,M,G,T} (63-20479, default 20479): 
Using default value 20479

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table
[   98.408029]  ram_blk: ram_blk1
################################################################

# 格式化分区
mke2fs /dev/ram_blk1

# 挂载分区
mount /dev/ram_blk1 /mnt/ram_blk1

# 卸载分区
umount /dev/ram_blk1

具体流程如下所示。

image

image

image

image

summary

本节中以队列和非队列两种模式,分别讲解了RAM模拟实现的块设备驱动;脱离了硬件框架,块设备其实是提供了应用层按照数据块格式读写的接口,用户层在这个读写接口上,读写内部存储的数据;这些数据要特殊格式存储,这个格式就是我们常说的硬盘,如fat32、ext2、ext4等。完整的理解块设备如何访问是挺困难的事,数据总线和硬件访问接口(SDHC、USB、QSPI等)、块设备驱动注册和读写接口、硬盘数据格式、应用层分区、格式化、挂载和文件存储读写,可以说都是难点;我在接触时也困难重重,不过在仔细梳理后,以存储硬件EMMC为例。

image

上图具体说明如下。

  1. 对于EMMC来说,其本质上就是能够固化存储0/1的数据块,然后以SDHC相关的接口连接到SOC上;SDHC驱动正是实现物理上读写数据的接口,只要读写数据即可,不关心内部数据的格式。
  2. SDHC驱动读取数据后,还要以应用可以访问的接口进行读写,块设备驱动正是进行转换的步骤;将应用层的读写请求,转换为对硬件设备的读写请求。块设备同样不关心数据格式,只提供应用层读写的接口。
  3. 应用层则分为两种情况,一种是新的硬盘,内部不存在数据;此时只能进行基础的读写,不能够执行mount挂载。这时就可以执行fdisk进行分区,以及mke2fs进行格式化,此时分区就可以挂载。另一种是硬盘已经存在数据,此时直接使用mount,会按照指定的格式进行解析数据即可。

至此,关于块设备的初步讲解说明完毕;作为系统中最复杂设备之一,基于一篇文章讲解是困难的,需要更多的实践总结进行说明。

next_chapter

返回目录

直接开始下一节说明: 块设备和SD卡驱动解析