Linux驱动开发学习笔记-块设备驱动

<块设备驱动>
块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。

块设备驱动相比字符设备驱动的主要区别如下:
①块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。
   字符设备是以字节为单位进行数据传输的,不需要缓冲。
②块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,
   等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。

块设备结构不同其 I/O 算法也会不同,比如对于 EMMC、SD 卡、NAND Flash 这类没有任何机械设备的存储设备
就可以任意读写任何的扇区(块设备物理存储单元)。

对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,
将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能。linux 里面针对不同的存储设备实现了不同的 I/O 调度算法。


1. 块设备驱动框架
   1). block_device 结构体:
       linux 内核使用 block_device 结构体表示块设备,定义在include/linux/fs.h 文件中。
       struct block_device {
    dev_t bd_dev;
    int bd_openers;
    ......
    ......
    struct gendisk *bd_disk;
    ......
    ......
       };
       对于 block_device 结构体,需要重点关注 bd_disk 成员变量,此成员变量为 gendisk 结构体指针类型。
       内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。

   2). 注册/注销块设备:
        和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev:
        int register_blkdev(unsigned int major, const char *name)
        参数 major 在 1~255 之间的话表示自定义主设备号,major 为 0 的话表示由系统自动分配主设备号(1~255)。

        注销块设备函数为unregister_blkdev:
        void unregister_blkdev(unsigned int major, const char *name)

   3). gendisk 结构体:
       linux 内核使用 gendisk 结构体来描述一个磁盘设备,定义在 include/linux/genhd.h中。
       struct gendisk {
    int major;                    //磁盘设备的主设备号
    int first_minor;                //磁盘的第一个次设备号。
    int minors;                //磁盘的次设备号数量,也就是磁盘的分区数量
    char disk_name[DISK_NAME_LEN];        //磁盘名
    ......
    struct disk_part_tbl __rcu *part_tbl;        //磁盘对应的分区表        
    ......
    const struct block_device_operations *fops;    //块设备操作集
    ......
    struct request_queue *queue;            //磁盘对应的请求队列,针对该磁盘设备的请求都放到此队列中,
                        //驱动程序需要处理此队列中的所有请求
    ......
    ......
      };

   4). 编写块设备驱动的时候需要分配并初始化一个 gendisk,linux 内核提供了一组 gendisk 操作函数:
        a. 申请 gendisk
                struct gendisk *alloc_disk(int minors)

        b. 删除 gendisk
                void del_gendisk(struct gendisk *gp)

        c. 将 gendisk 添加到内核(将申请到的gendisk 添加到内核后系统才能使用)
                void add_disk(struct gendisk *disk)

        d. 设置 gendisk 容量(参数size是扇区数量,其大小等于块设备实际物理容量除以512 字节)
                void set_capacity(struct gendisk *disk, sector_t size)

        e. 调整 gendisk 引用计数
    truct kobject *get_disk(struct gendisk *disk)    //增加 gendisk 的引用计数
    void put_disk(struct gendisk *disk)        //减少 gendisk 的引用计数

   5). block_device_operations 结构体:
        和字符设备的 file _operations 一样,块设备也有操作集,为结构体 block_device_operations,此结构体定义在 include/linux/blkdev.h。

2. 块设备 I/O 请求过程
    1). 请求队列 request_queue
        内核将对块设备的读写都发送到请求队列 request_queue 中,request_queue 中是大量的request(请求结构体),
        而 request 又包含了 bio,bio 保存了读写相关数据。每个磁盘(gendisk)都分配了一个 request_queue。

        ①初始化请求队列
           首先需要申请并初始化一个 request_queue,然后在初始化 gendisk 的时候将这个request_queue 地址赋值给 gendisk 的 queue 成员变量。
           使用 blk_init_queue 函数来完成request_queue 的申请与初始化:
    request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
           
           其中,rfn为请求处理函数指针,每个 request_queue 对应一个请求处理函数,
           lock为自旋锁指针。这两个参数都需要自定义实现。

        ②删除请求队列
           当卸载块设备驱动时,需要删除掉申请到的 request_queue。删除请求队列使用函数 blk_cleanup_queue:
    void blk_cleanup_queue(struct request_queue *q)

        ③分配请求队列并绑定制造请求函数
           blk_init_queue 函数完成了请求队列的申请以及请求处理函数的绑定,这个一般用于像机械硬盘的存储设备,需要 I/O 调度器来优化数据读写过程。
           对于 EMMC、SD 卡这样的非机械设备,不需要复杂的 I/O 调度器。
           对于非机械设备可以先申请 request_queue,然后将申请到的 request_queue 与“制造请求”函数绑定在一起。

           非机械设备的 request_queue 申请函数 blk_alloc_queue:
    struct request_queue *blk_alloc_queue(gfp_t gfp_mask)    //gfp_mask一般为 GFP_KERNEL    

           需要为 blk_alloc_queue 函数申请到的请求队列绑定一个“制造请求”函数,用到函数 blk_queue_make_request:
    void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)    //mfn需要自定义实现

    2). 请求 request
         需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,
         最后根据 bio 的描述将数据写入到块设备,或者从块设备中读取数据。

        ①获取请求
           使用blk_peek_request函数从request_queue中依次获取每个request:
    request *blk_peek_request(struct request_queue *q)

        ②开启请求
          使用 blk_start_request函数开始处理获取到的request:
    void blk_start_request(struct request *req)

        ③一步到位处理请求
           可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启:
    struct request *blk_fetch_request(struct request_queue *q)

        ④其他和请求有关的函数
    blk_end_request()         //请求中指定字节数据被处理完成。
    blk_end_request_all()     //请求中所有数据全部处理完成。
    blk_end_request_cur()     //当前请求中的 chunk。
    blk_end_request_err()     //处理完请求,直到下一个错误产生。
    __blk_end_request()     //和 blk_end_request 函数一样,但是需要持有队列锁。
    __blk_end_request_all()     //和 blk_end_request_all 函数一样,但是需要持有队列锁。
    __blk_end_request_cur()     //和 blk_end_request_cur 函数一样,但是需要持有队列锁。
    __blk_end_request_err()     //和 blk_end_request_err 函数一样,但是需要持有队列锁。

    3). bio 结构
         上层应用程序对于块设备的读写会被构造成一个或多个 bio 结构。
         上层会将 bio 提交给 I/O 调度器,I/O 调度器会将这些 bio 构造成 request 结构,而一个物理存储设备对应一个 request_queue。
         新产生的 bio 可能被合并到 request_queue 里现有的 request 中,也可能产生新的 request,
         然后插入到 request_queue 中合适的位置,这一切都是由 I/O 调度器来完成的。

         bio 是个结构体,定义在 include/linux/blk_types.h 中。
         struct bio {
    struct bio *bi_next;         /* 请求队列的下一个 bio */
    struct block_device *bi_bdev; /* 指向块设备 */
    unsigned long bi_flags;     /* bio 状态等信息 */
    unsigned long bi_rw;     /* I/O 操作,读或写 */
    struct bvec_iter bi_iter;     /* I/O 操作,读或写 */
    ......
    ......
    struct bio_vec *bi_io_vec;     /* bio_vec 列表 */
    ......
        };

        其中,bvec_iter 结构体描述物理存储设备地址信息,比如要操作的扇区地址等。
        struct bvec_iter {
    sector_t bi_sector;          /* I/O 请求的设备起始扇区(512 字节) */
    unsigned int bi_size;     /* 剩余的 I/O 数量 */
    unsigned int bi_idx;         /* blv_vec 中当前索引 */
    unsigned int bi_bvec_done;     /* 当前 bvec 中已经处理完成的字节数 */
        };    

        bio_vec 结构体描述了 RAM 数据信息,比如页地址、页偏移以及长度。
        struct bio_vec { 
    struct page *bv_page;     /* 页 */
    unsigned int bv_len;     /* 长度 */
    unsigned int bv_offset;     /* 偏移 */
        };

        ①遍历请求中的 bio
           遍历请求中的 bio 使用函数__rq_for_each_bio,这是一个宏:
    #define __rq_for_each_bio(_bio, rq)  if ((rq->bio)) for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)

        ②遍历 bio 中的所有段(操作数据)
           用到bio_for_each_segment 函数,此函数也是一个宏:
    #define bio_for_each_segment(bvl, bio, iter)  __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)

        ③通知 bio 处理结束
           如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,
           在 bio 处理完成以后要通过内核 bio 处理完成,使用 bio_endio 函数:
    bvoid bio_endio(struct bio *bio, int error)    //error:bio 处理成功就直接填 0,失败的话就填个负值,比如-EIO。

你可能感兴趣的:(linux,驱动开发,学习)