一切皆文件之块设备驱动(一)

块设备驱动简介

在linux系统中,有3大驱动类型,分别是:字符设备驱动、块设备驱动、网络设备驱动。块设备驱动与文件系统有着密不可分的关系,块设备是文件系统实际的数据传输单位,通常存储设备有eMMC,Nand/Nor flash,机械硬盘,固态硬盘等,这里所说的块设备驱动,实际就是这些存储设备驱动。块设备驱动与字符设备驱动有较大差异,块设备驱动是以块位单位进行读写,而字符设备驱动以字节为单位进行传输。块设备驱动可以进行随机访问,块设备驱动一般都有缓冲区来暂存数据,当对块设备进行写入时会先将数据写到缓冲区中,累计到一定数据量后才一次性刷到设备中,这样既可以提高读写的速度也可以提高快设备驱动的寿命;相对字符设备数据的操作都是字节流的方式,字符设备没有缓冲区。

关键数据结构

block_device

linux系统中使用strcut block_device来表示块设备,可以表示磁盘或一个特定的分区。下面我们看一下strcut block_device数据结构,该数据结构表示块设备。

struct block_device {
    sector_t        bd_start_sect;
    struct disk_stats __percpu *bd_stats;
    unsigned long       bd_stamp;
    bool            bd_read_only;   /* read-only policy */
    dev_t           bd_dev;
    int         bd_openers;
    struct inode *      bd_inode;   /* will die */
    struct super_block *    bd_super;
    void *          bd_claiming;
    struct device       bd_device;
    void *          bd_holder;
    int         bd_holders;
    bool            bd_write_holder;
    struct kobject      *bd_holder_dir;
    u8          bd_partno;
    spinlock_t      bd_size_lock; /* for bd_inode->i_size updates */
    struct gendisk *    bd_disk;

    /* The counter of freeze processes */
    int         bd_fsfreeze_count;
    /* Mutex for freeze */
    struct mutex        bd_fsfreeze_mutex;
    struct super_block  *bd_fsfreeze_sb;

    struct partition_meta_info *bd_meta_info;
#ifdef CONFIG_FAIL_MAKE_REQUEST
    bool            bd_make_it_fail;
#endif

} __randomize_layout;
  • bd_dev: 该块设备(分区)的设备号
  • bd_inode: 设备的文件inode,bdev文件系统将通过该inode来标识块设备。
  • bd_disk: 指向描述整个设备的gendisk。

block_device是bdevfs伪文件系统对块设备或设备分区的抽象,它与设备号唯一对应。

gendisk

linux系统中,struct gendisk是通用存储设备描述,跟具体的硬件设备关联,一个具体的硬件存储设备可以分为多个分区,而每个分区的对应用block_device描述。

struct gendisk {
    /* major, first_minor and minors are input parameters only,
     * don't use directly.  Use disk_devt() and disk_max_parts().
     */
    int major;          /* major number of driver */磁盘主设备号
    int first_minor;    磁盘第一个次设备
    int minors;                     /* maximum number of minors, =1 for 次设备的数量,也是分区数量
                                         * disks that can't be partitioned. */

    char disk_name[DISK_NAME_LEN];  /* name of major driver */

    unsigned short events;      /* supported events */
    unsigned short event_flags; /* flags related to event processing */

    struct xarray part_tbl; 对应分区表,每一项对应要给分区信息
    struct block_device *part0;

    const struct block_device_operations *fops;磁盘操作函数集
    struct request_queue *queue; 磁盘对应的请求队列,对磁盘操作的请求都放在这个队列中
    void *private_data;

    int flags;
    unsigned long state;
#define GD_NEED_PART_SCAN       0
#define GD_READ_ONLY            1
#define GD_DEAD             2

    struct mutex open_mutex;    /* open/close mutex */
    unsigned open_partitions;   /* number of open partitions */ 

    struct backing_dev_info *bdi;
    struct kobject *slave_dir;
#ifdef CONFIG_BLOCK_HOLDER_DEPRECATED
    struct list_head slave_bdevs;
#endif
    struct timer_rand_state *random;
    atomic_t sync_io;       /* RAID */
    struct disk_events *ev;
#ifdef  CONFIG_BLK_DEV_INTEGRITY
    struct kobject integrity_kobj;
#endif  /* CONFIG_BLK_DEV_INTEGRITY */
#if IS_ENABLED(CONFIG_CDROM)
    struct cdrom_device_info *cdi;
#endif
    int node_id;
    struct badblocks *bb;
    struct lockdep_map lockdep_map;
    u64 diskseq;
}
  • major: 存储设备的主设备号
  • first_minor: 存储设备的第一个次设备号
  • minors:存储设备的次设备号数量,也对应存储设备的分区数量。
  • part_tbl:存储设备对应的分区表
  • fops:存储设备块操作集合,与字符设备的file_operations一样。
  • queue:存储设备对应的请求队列,对磁盘的请求都会放到该队列中。

下面是磁盘的操作函数集合block_device_operations,与字符设备驱动file_operations类似。

struct block_device_operations {
    blk_qc_t (*submit_bio) (struct bio *bio);
    int (*open) (struct block_device *, fmode_t);
    void (*release) (struct gendisk *, fmode_t);
    int (*rw_page)(struct block_device *, sector_t, struct page *, unsigned int);
    int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
    int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
    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);
    /* this callback is with swap_lock and sometimes page table lock held */
    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);
    struct module *owner;
    const struct pr_ops *pr_ops;

    /*
     * Special callback for probing GPT entry at a given sector.
     * Needed by Android devices, used by GPT scanner and MMC blk
     * driver.
     */
    int (*alternative_gpt_sector)(struct gendisk *disk, sector_t *sector);
};
  • submit_bio: 对存储设备读写操作,但不一定会使用submit_bio来传输,而是通过请求队列来完成。
  • open: 打开指定的块设备
  • rw_page:读写指定的页
  • ioctl:块设备的I/O控制

在block_device_operations结构中不像字符设备驱动有对应的read和write函数,有些内核版本甚至没有submit_bio,对于块设备的读写操作,主要是通过request_queue,request和bio来实现的。

  • request_queue:内核对存储设备的读写操作会先发送到request_queue中,该队列中包含有一系列的request,在request中具体的基本单元是bio,bio保存了对存储设备读写的实际数据,包括从存储设备的那个地址读写,具体的长度等信息。
  • request: 存储设备的请求队列中,包含多个内核对存储设备的多个request,每个请求包含多个bio。
  • bio:内核对存储设备读写会构造一个或者多个的bio,bio数据结构包含了对存储设备具体的读写位置和长度等信息。

接下来将单独对三者关系进行深入描述。

bio,request,request_queue

Generic Block layer

文件系统向下就是Generic Block Layer,文件系统请求读写的文件位置需要被转换到对应的存储介质的位置(如磁盘的扇区号)。如下是文件系统写过程,将写请求根据文件的pos位转化为bio。

int __block_write_begin(struct page *page, loff_t pos, unsigned len,
        get_block_t *get_block)

    -->return __block_write_begin_int(page, pos, len, get_block, NULL);

        -->ll_rw_block(int op, int op_flags,  int nr, struct buffer_head *bhs[])

            -->int submit_bh(int op, int op_flags, struct buffer_head *bh)

                    -->submit_bio(bio)

bio是块设备数据传输最小单元,下面是struct bio的数据结构。

  • bio_vec:标识存储设备中数据缓存在page的位置,描述IO请求在内存段的数据位置属性(page地址,页内偏移,长度)。
  • bvec_iter:标识操作数据在存储设备的位置,描述IO请求在存储器段的数据位置属性(起始sector,长度)。

I/O Scheduler Layer

从submit_bio调用开始,bio被block层抽象为request进行管理,request会被组织到request queue中。IO调度器的目的是在现有请求下,让尽可能少操作存储设备,提高存储设备的读写效率。在IO调度器中,从文件系统提交下来的bio被构造成request结构,一个request结构包含了多个bio,而物理存储设备都会有对应的request queue,里面存放着相关的request,新的bio可能被合并到request queue现有的request结构中,也有可能生成新的request,如何合并、插入等取决于设备驱动选择的IO调度算法。

I/O请求

早期阶段,只有一个单队列single-queue,随着多核体系结构的发展,单队列的暴露了较多劣势,如多核请求队列时,需要使用spinlock来做同步,锁的竞争带来比较高的额外开销,因此后来引入了multi-queue,将单个队列请求锁的竞争分散到多个队列中,这样就极大提高了并发IO的处理能力。

multi-queue结构分为两层队列设计,分别是Per-CPU级别的软件暂存队列(Software staging queue,Per CPU)和存储设备硬件派发队列(Hardware Dispatch queue,Per Disk Per channel);

  • 软件暂存队列(ctx):对应的数据结构blk_mq_ctx,为每个cpu分配一个软件队列,将bio生成一个新的request或合并到已有的request中。bio的提交/完成处理、IO请求合并/排序/标记、调度记账等block layer操作都在该队列中进行。队列是Per CPU,所以每个CPU io请求并发的时候不存在锁竞争问题。

  • 硬件派发队列(hctx):对应的数据结构blk_mq_hw_ctx,每个存储设备的每个硬件队列(通常为一个)分配一个硬件派发队列,用于处理上层软件暂存队列下发的io请求。在存储设备驱动初始化时,blk-mq会将一个或多个软件暂存队列固定映射到一个硬件派发队列,后续软件队列上的io请求就会直接往映射的硬件派发队列下发,最终下发到硬件存储设备。

https://blog.csdn.net/morecrazylove/article/details/128712522

https://blog.csdn.net/juS3Ve/article/details/79890688