• 14、块设备驱动程序框架分析


    (对于需要使用块设备驱动程序框架的设备,比如磁盘,假如一个操作需要读写读三个不同扇区,这个时候如果采用字符驱动程序来读写,对于机械磁盘的磁头需要跳来跳去执行,这样时间会花费很长)

    (比如nand flash产品,在写多个字节的时候,先读整块到buffer,修改buffer中的扇区(即页),在擦除整块,在烧写整块,因此如果是分别读写读写操作,也会很慢)

    NAND flash以页为单位读写数据,而以块为单位擦除数据

    NOR flash采用位读写,因为它具有sram的接口,有足够的引脚来寻址

    所有块设备:

      1、把“读写”放入队列

      2、优化后在执行

    框架:

    app: open,read,write "1.txt"
    --------------------------------------------- 文件的读写
    文件系统: vfat, ext2, ext3, yaffs2, jffs2 (把文件的读写转换为扇区的读写)
    -----------------ll_rw_block----------------- 扇区的读写(ll_rw_block做下面两步,在块设备驱动程序提供的处理队列的函数和参数的帮助下)
                1. 把"读写"放入队列
                2. 调用队列的处理函数(优化/调顺序/合并)
    块设备驱动程序
    ---------------------------------------------
    硬件: 硬盘,flash


    <LINUX内核源代码情景分析>

    分析ll_rw_block(int rw, int nr, struct buffer_head *bhs[])//数据传输三要素:源、目的、长度 放在bhs中,数组有多少项:nr
      for (i = 0; i < nr; i++) {
        struct buffer_head *bh = bhs[i];
        submit_bh(rw, bh);
          struct bio *bio; // 使用bh来构造bio (block input/output)
          submit_bio(rw, bio);
            // 通用的构造请求: 使用bio来构造请求(request)
            generic_make_request(bio);
              //__generic_make_request(bio);
                request_queue_t *q = bdev_get_queue(bio->bi_bdev); // 找到队列

                // 调用队列的"构造请求函数"
                ret = q->make_request_fn(q, bio);
                  // 默认的构造请求函数是__make_request   在4.3.2内核中这个函数是blk_queue_bio,其不仅是默认,在blk_init_queue分配队列的时候,也会把make_request_fn复制blk_queue_bio
                  __make_request   
                    // 先尝试合并
                    elv_merge(q, &req, bio);

                    // 如果合并不成,使用bio构造请求
                    init_request_from_bio(req, bio);

                    // 把请求放入队列,3.4.2 是add_acct_request(q, req, where);
                    add_request(q, req);

                    // 执行队列,3.4.2是__blk_run_queue
                    __generic_unplug_device(q);
                      // 调用队列的"处理函数"
                      q->request_fn(q);

    怎么写块设备驱动程序呢?
    1. 分配gendisk: alloc_disk
    2. 设置
    2.1 分配/设置队列: request_queue_t // 它提供读写能力
    blk_init_queue
    2.2 设置gendisk其他信息 // 它提供属性: 比如容量
    3. 注册: add_disk

    add_disk

      register_disk

        bdev = bdget_disk(disk, 0);//为gendisk分配block_device结构体,作为gendisk在bdevfs中的抽象

         blkdev_get()//以只读方式打开该设备,进行分区扫描,并设置block_device与gendisk、hd_struct之间的关联,以及gendisk的block_device与hd_struct的block_device之间的关联

          __blkdev_get

            bdev->bd_queue = disk->queue;//把gendisk的队列赋给block_device的队列

            

    参考:
    driverslockxd.c   //查看init函数可以发现  通过register_blkdev来注册一个块设备
    driverslockz2ram.c

    测试3th,4th:
    在开发板上:
    1. insmod ramblock.ko(安装之后会提示不识别的分区,因为没有格式化,里面什么信息都没,必须把磁盘格式化为某种系统)
    2. 格式化: mkdosfs /dev/ramblock
    3. 挂接: mount /dev/ramblock /tmp/
    4. 读写文件: cd /tmp, 在里面vi文件
    5. cd /; umount /tmp/
    6. cat /dev/ramblock > /mnt/ramblock.bin
    7. 在PC上查看ramblock.bin
    sudo mount -o loop ramblock.bin /mnt( -o loop表示把一个普通文件当做块设备来挂接)

    在程序4的测试中可以发现执行cp /etc/inittab /tmp时只打印了read,过了好大一会才write,执行sync命令可以立刻读写

    测试5th:
    1. insmod ramblock.ko
    2. ls /dev/ramblock*
    3. fdisk /dev/ramblock(分区指令,前面几个程序都不能分区,因为没有一些参数,比如柱面、磁头,这些事通过block_device_operations结构体里面的getgeo函数提供的)

    4、安装测试4th格式化

    说明:linux2.6之后的内核,elv_next_reques改为blk_fetch_request

    2.6版本之前的内核中,块设备这一部分的代码有些过于简单并且冗余,2.5版本后的内核里这些代码被重新写过了。

    以前,获取I/O请求队列中下一个请求的函数是:
         struct request *elv_next_request(struct request_queue *queue);

    这个函数返回经过I/O调度器优化过后的下一个请求。但是这个函数有一个特点,它会把这个请求保留在请求队列中,这样的话,如果两个elv_next_request()在非常短的时间间隔内被执行,函数就会返回同一个请求。当然,我们可以用blkdev_dequeue_request()来把一个请求从请求队列中移除,但是完全没必要这么做,因为一旦块设备驱动通知内核这个请求被完成了,内核同样会把这个请求从请求队列中移除。

    把请求保留在请求队列中是以前的一个做法,那时候,内核每次只能处理一个请求————比如每次只操作一个扇区。这样做有一个缺点:就像上面讲的那样,内核不知道一个请求何时真正开始被处理,也就没有办法操作一些对处理时间有要求的请求。

    况且,现在的处理器足够强大,使得内核能够同时处理多个请求,同时,也要求设备驱动要亲自移除这些请求并对它们保持追踪。所以,这种 process-on-queue模型要被抛弃,Tejun更改了代码,引入了新方法,具体可以查看:

    新方法的想法就是在原来驱动的基础上增加“把请求从请求队列中移除”这么一个功能,也就是在合适的地方增加 blkdev_dequeue_request() 这么一个函数,有些地方的修改(比如IDE子系统)没有这么简单,但是大多数地方都是这样修改的。

    更改之后,块设备驱动的一些旧的API随之就发生改动:函数elv_next_request(); 不再存在,替代它的是

        struct request *blk_peek_request(struct request_queue *queue);

    这个函数同样不移除请求,移除请求的函数是:
       
        void blk_start_request(struct request *req);
       
    它取代了blkdev_dequeue_request()。除了从请求队列中移除请求,blk_start_request() 同时启动一个定时器,用超时的办法防止这个请求没有响应。

    大部分的情况下,我们只需要调用:

        struct request *blk_fetch_request(struct request_queue *q);

    这个函数包含了blk_peek_request()和 blk_start_request()。

    另外,新的API也需要注意:尝试完成还在一个请求队列里面的请求会引起系统错误。

    1.字符设备与块设备IO操做的区别

    1)块设备只能以块为单位接收输入返回输出,而字符设备则以byte为单位.大多数设备是字符设备,他们不需要缓冲并且不以固定块大小进行操作.

    2)块设备对于IO请求有对应的缓冲区,所以他们可以选择以什么顺序进行响应.字符设备无须缓冲且被直接读写.

    3)字符设备只能被顺序读写,块设备可以随机访问.

     

     

     

     

    2.block_device_operations结构体

    block_device_operations描述了对块设备的操作的集合

        struct block_device_operations {

            int (*open) (struct inode *, struct file *);/*打开*/

            int (*release) (struct inode *, struct file *);/*释放*/

            int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long);

            long (*unlocked_ioctl) (struct file *, unsigned, unsigned long);

            long (*compat_ioctl) (struct file *, unsigned, unsigned long);

            int (*direct_access) (struct block_device *, sector_t, unsigned long *);

            int (*media_changed) (struct gendisk *);/*介质被改变?*/

            int (*revalidate_disk) (struct gendisk *);/*使介质改变*/

            int (*getgeo)(struct block_device *, struct hd_geometry *);/*填充驱动器信息*/

            struct module *owner;/*模块拥有者,一般初始化为THIS_MODULE*/

        };

    关于block_device_operations的操作:

        //open and release

        int (* open)(struct inode*, struct file*);

        int (* release)(struct inode*, struct file*);

        //io contrl

        //系统调用实现,块设备包含大量的标准请求,由设备层处理,所以此函数一般相当短

        int (* ioctl)(struct inode*,struct file*,unsigned int,unsigned long);

        //media changed

        //如果改变返回非0,否则返回0

        int (*media_changed)(struct gendisk*);

        //revalidate media

        //用于相应一个介质的改变,给驱动一个机会做准备工作

        int (* revalidate_disk)(struct gendisk*);

        //get driver informaiton

        //根据驱动器的几何信息填充hd_geometry,包含磁头,柱面,扇区等信息.

        int (* getgeo)(struct block_device*, struct hd_geometry*);

     

     

     

     

    3.gendisk结构体

    使用gendisk结果提来描述一个独立的磁盘设备或分区.

        //gendisk structure

        struct gendisk{

            /*前三个元素共同表征了一个磁盘的主,次设备号,同一个磁盘的各个分区共享一个主设备号*/

            int major;/*主设备号*/

            int first_minor;/*第一个次设备号*/

            int minors;/*最大的次设备数,如果不能分区,则为*/

            char disk_name[32];

            struct hd_struct** part;/*磁盘上的分区信息*/

            struct block_device_operations* fops;/*块设备操作,block_device_operations*/

            struct request_queue* queue;/*请求队列,用于管理该设备IO请求队列的指针*/

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

            sector_t capacity;/*扇区数,512字节为个扇区,描述设备容量*/

            //......

        };

     关于gendisk的操作:

        /*分配一个gendisk结构体,此结构体是由内核动态分配的*/

        struct gendisk* alloc_disk(int minors);

        /*增加gendisk,来注册该设备,此动作应该在设备驱动初始化完毕,并能响应磁盘请求之后*/

        void add_disk(struct gendisk* gd);

        /*释放一个不再需要的磁盘*/

        /**

        ***gendisk引用计数器:gendisk包含一个kobject成员.通过get_disk()&put_disk()函数来操作引用

        ***计数,此操作不需要驱动亲自完成.通常调用del_gendisk()会去掉gendisk的最终引用计数,但不是必 

        ***须的.因此在del_gendisk()gendisk结构体可能继续存在.

        **/

        /*设置gendisk容量*/

        void set_capacity(struct gendisk* disk, sector_t size);

    块设备中,最小的可寻址单元就扇区,常见扇区大小是512字节.扇区的大小是设备的物理属性,是所有块设备的基本单元,块设备无法对比扇区小的单元进行寻址和操作.不过许多块设备能够一次传输多个扇区.不管物理设备的真实扇区是多少,内核与块设备交互的扇区均以512字节为单位.所以set_capcity()函数以512字节为单位.

    块驱动相关的结构体及相关操作

    1)请求request

    requestrequest_queue结构体:Linux块设备驱动中,使用request结构体来表征等待进行的IO请求;并用request_queue来表征一个块IO请求队列.两个结构体的定义如下:

    request结构体

        struct request{

            struct list_head queuelist;

            unsigned long flags;

     

            sector_t sector;/*要传输的下一个扇区*/

            unsigned long nr_sectors;/*要传送的扇区数目*/

            unsigned int current_nr_sector;/*当前要传送的扇区*/

     

            sector_t hard_sector;/*要完成的下一个扇区*/

            unsigned long hard_nr_sectors;/*要被完成的扇区数目*/

            unsigned int hard_cur_sectors;/*当前要被完成的扇区数目*/

     

            struct bio* bio;/*请求的bio结构体的链表*/

            struct bio* biotail;/*请求的bio结构体的链表尾*/

            

            /*请求在屋里内存中占据的不连续的段的数目*/

            unsigned short nr_phys_segments;

            unsigned short nr_hw_segments;

     

            int tag;

            char* buffer;/*传送的缓冲区,内核的虚拟地址*/

            int ref_count;/*引用计数*/

            ...

        };

    说明:

    request结构体的主要成员包括:

            sector_t hard_sector;/*要完成的下一个扇区*/

            unsigned long hard_nr_sectors;/*要被完成的扇区数目*/

            unsigned int hard_cur_sectors;/*当前要被完成的扇区数目*/

            /*

             * 上述三个成员依次是第一个尚未传输的扇区,尚待完成的扇区数,当前IO操作中待完成的扇区数

             * 但驱动中一般不会用到他们.而是下面的一组成员.

             */

            sector_t sector;/*要传输的下一个扇区*/

            unsigned long nr_sectors;/*要传送的扇区数目*/

            unsigned int current_nr_sector;/*当前要传送的扇区*/

            /* 

             * 这三个成员,以字节为单位.如果硬件的扇区大小不是512字节.如字节,则在开始对硬件进行操作之

             * 前,应先用4来除起始扇区号.前三个成员,与后三个成员的关系可以理解为"副本".

             */

    关于unsigned short nr_phys_segments:该成员表示相邻的页被合并后,这个请求在物理内存中的段的数目.如果该设备支持SG(分散/聚合,scatter/gather),可根据该字段申请sizeof(scatterlist*) nr_phys_segments的内存,并使用下面的函数进行DMA映射:

        int blk_rq_map_sg(request_queue_t* q, struct request* rq, struct scatterlist *sg);

    该函数与dma_map_sg()类似,返回scatterlist列表入口的数量.

    关于struct list_head queuelist:该成员用于链接这个请求到请求队列的链表结构,函数blkdev_ dequeue_request()可用于从队列中移除请求.rq_data_dir(struct request* req)可获得数据传送方向.返回0表示从设备读取,否则表示写向设备.

     

    2)request_queue请求队列

        struct request_queue{

            ...

            /*自旋锁,保护队列结构体*/

            spinlock_t __queue_lock;

            spinlock_t* queue_lock;

            struct kobject kobj;/*队列kobject*/

            /*队列设置*/

            unsigned long nr_requests;/*最大的请求数量*/

            unsigned int  nr_congestion_on;

            unsigned int  nr_congestion_off;

            unsigned int  nr_batching;

            unsigned short max_sectors;/*最大扇区数*/

            unsigned short max_hw_sectors;

            unsigned short max_phys_sectors;/*最大的段数*/

            unsigned short max_hw_segments;

            unsigned short hardsect_size;/*硬件扇区尺寸*/

            unsigned int max_segment_size;/*最大的段尺寸*/

            unsigned long seg_boundary_mask;/*段边界掩码*/

            unsigned int dma_alignment;/*DMA传送内存对齐限制*/

            struct blk_queue_tag* queue_tags;

            atomic_t refcnt;/*引用计数*/

            unsigned int in_flight;

            unsigned int sg_timeout;

            unsigned int sg_reserved_size;

            int node;

            struct list_head drain_list;

            struct request* flush_rq;

            unsigned char ordered;

        };

    说明:请求队列跟踪等候的块IO请求,它存储用于描述这个设备能够支持的请求的类型信息,他们的最大大小,多少不同的段可以进入一个请求,硬件扇区大小,对齐要求等参数.其结果是:如果请求队列被配置正确了,它不会交给该设备一个不能处理的请求.

    请求队列还要实现一个插入接口,这个接口允许使用多个IO调度器,IO调度器以最优性能的方式向驱动提交IO请求.大部分IO调度器是积累批量的IO请求,并将其排列为递增/递减的块索引顺序后,提交给驱动.另外,IO调度器还负责合并邻近的请求,当一个新的IO请求被提交给调度器后,它会在队列里搜寻包含邻近的扇区的请求.如果找到一个,并且请求合理,调度器会将这两个请求合并.

    Linux2.6的四个IO调度器,他们分别是No-op/Anticipatory/Deadline/CFQ IO scheduler.

    关于request_queu结构体的操作:

        //初始化请求队列

        kernel elevator = deadline;/*kernel添加启动参数*/

        request_queue_t* blk_init_queue(request_fn_proc* rfn, spinlock_t* lock);

            /*

             * 两个参数分别是请求处理函数指针和控制队列访问权限的自旋锁.

             * 此函数会发生内存分配的行为,需要检查其返回值.一般在加载函数中调用.

             */

        //清除请求队列

        void blk_cleanup_queue(request_queue_t* q);

            /* 

             * 此函数完成将请求队列返回给系统的任务,一般在卸载函数中调用.

             * 此函数即bld_put_queue()的宏定义#define blk_put_queue(q) blk_cleanup_queue((q))

             */

        //分配"请求队列"

        request_queue_t* blk_alloc_queue(int gfp_mask);

        void blk_queue_make_request(request_queue_t* q, make_request_fn* mfn);

            /*

             * 前一个函数用于分配一个请求队列,后一个函数是将请求队列和"制造函数"进行绑定

             * 但函数blk_alloc_queue实际上并不包含任何请求.

             */

        //提取请求

        struct request* elv_next_request(request_queue_t* q);

        //去除请求

        void blkdev_dequeue_request(struct request* req);

        void elv_requeue_request(request_queue_t* queue, struct request* req);

        //启停请求

        void blk_stop_queue(request_queue_t* queue);

        void blk_start_queue(request_queue_t* queue);

        //参数设置

        void blk_queue_max_sectors(request_queue_t* q, unsigned short max);

            /*请求可包含的最大扇区数.默认255*/

        void blk_queue_max_phys_segments(request_queue_t* q, unsigned short max);

        void blk_queue_max_hw_segments(request_queue_t* q, unsigned short max);

            /*这两个函数设置一个请求可包含的最大物理段数(系统内存中不相邻的区),缺省是128*/

        void blk_queue_max_segment_size(request_queue_t* q, unsigned int max);

            /*告知内核请求短的最大字节数,默认2^16 = 65536*/

        //通告内核

        void blk_queue_bounce_limit(request_queue_t* queue, u64 dma_addr);

            /*

             * 此函数告知内核设备执行DMA,可使用的最高物理地址dma_addr,常用的宏如下:

             * BLK_BOUNCE_HIGH:对高端内存页使用反弹缓冲(缺省)

             * BLK_BOUNCE_ISA:驱动只可以在MBISA区执行DMA

             * BLK_BOUNCE_ANY:驱动可在任何地方执行DMA

             */

        blk_queue_segment_boundary(request_queue_t* queue, unsigned long mask);

            /*这个函数在设备无法处理跨越一个特殊大小内存边界的请求时,告知内核这个边界.*/

        void blk_queue_dma_alignment(request_queue_t* q, int mask);

            /*告知内核设备加于DMA传送的内存对齐限制*/

        viod blk_queue_hardsect_size(request_queue_t* q, unsigned short max);

           /*此函数告知内核块设备硬件扇区大小*/

     

    3)块I/O

    通常一个bio对应一个IO请求.IO调度算法可将连续的bio合并成一个请求.所以一个请求包含多个bio.

        struct bio{

            sector_t bi_sector;/*要传送的第一个扇区*/

            struct bio* bi_next;/*下一个bio*/

            struct block_device* bi_bdev;

            unsigned long bi_flags;

            /*如果是一个写请求,最低有效位被置位,可使用bio_data_dir(bio)宏来获取读写方向*/

     

            unsigned long bi_rw;/*地位表示R/W方向,高位表示优先级*/

     

            unsigned short bi_vcnt;/*bio_vec数量*/

            unsigned short bi_idx; /*当前bvl_vec索引*/

     

            unsigned short bi_phys_segments;/*不相邻的物理段的数目*/

            unsigned short bi_hw_segments;/*物理合并和DMA remap合并后不相邻的物理扇区*/

     

            unsigned int bi_size;

            /*被传送的数据大小(byte),bio_sector(bio)获取扇区为单位的大小*/

     

            /*为了明了最大的hw尺寸,考虑bio中第一个和最后一个虚拟的可合并的段的尺寸*/

            unsigned int bi_hw_front_size;

            unsigned int bi_hw_back_size;

     

            unsigned int bi_max_vecs;/*能持有的最大bvl_vecs*/

     

            struct bio_vec* bio_io_vec;/*实际的vec列表*/

            bio_end_io_t* bio_end_io;

            atomic_t bi_cnt;

            void* bi_private;

            bio_destructor_t* bi_destructor;

        };

     

        //结构体包含三个成员

        struct bio_vec{

            struct page* bv_page;//页指针

            unsigned int bv_len;//传送的字节数

            unsigned int bv_offset;//偏移位置

        };

    /*一般不直接访问biobio_vec成员,而使用bio_for_each_segment()宏进行操作.

     *该宏循环遍历整个bio中的每个段.

     */

        #define __bio_for_each_segment(bvl, bio, i, start_idx)

                 for(

                    bvl = bio_iovec_idx((bio),(start_idx)),i = (start_idx);

                    i <(bio)->bi_vcnt;

                    bvl++, i++

                 )

        #define bio_for_each_segment(bvl, bio, i)

                  __bio_for_each_segment(bvl, bio, i, (bio)->bi_idx)

    在内核中,提供了一组函数()用于操作bio:

        int bio_data_dir(struct bio* bio);

        该函数用于获得数据传送方向.

        struct page* bio_page(struct bio* bio);

        该函数用于获得目前的页指针.

        int bio_offset(struct bio* bio);

        该函数返回操作对应的当前页的页内偏移,通常块IO操作本身就是页对齐的.

        int bio_cur_sectors(struct bio* bio);

        该函数返回当前bio_vec要传输的扇区数.

        char* bio_data(struct bio* bio);

        该函数返回数据缓冲区的内核虚拟地址.

        char* bvec_kmap_irq(struct bio_vec* bvec, unsigned long* offset);

        该函数也返回一个内核虚拟地址此地址可用于存取被给定的bio_vec入口指向的数据缓冲区.同时会屏蔽中断并返回一个原子kmap,因此,在此函数调用之前,驱动不应该是睡眠状态.

        void bvec_kunmap_irq(char* buffer, unsigned long flags);

        该函数撤销函数bvec_kmap_irq()创建的内存映射.

        char* bio_kmap_irq(struct bio* bio, unsigned long* flags);

        该函数是对bvec_kmap_irq函数的封装,它返回给定的比偶的当前bio_vec入口的映射.

        char* __bio_kmap_atomic(struct bio* bio, int i, enum km_type type);

        该函数是通过kmap_atomic()获得返回给定bio的第i个缓冲区的虚拟地址.

        void __bio_kunmap_atomic(char* addr, enum km_type type);

        该函数返还由函数__bio_kmap_atomic()获得的内核虚拟地址给系统.

        void bio_get(struct bio* bio);

        void bio_put(struct bio* bio);

        上面两个函数分别完成对bio的引用和引用释放.

    下图可以体现出bio/request/request_queue/bio_vec四个结构体之间的关系.

     

     

     

    5.块设备驱动注册于注销

    块设备驱动的第一个任务就是将他们自己注册到内核中,其函数原型如下:

        int register_blkdev(unsigned int major, const char* name);

    major参数是块设备要使用的主设备号,name为设备名,它会在/proc/devices中被现实.如果major0,内核会自动分配一个新的主设备号,并由该函数返回.如果返回值为负值,则说明设备号分派失败.

    与register_blkdev对应的注销函数是unregister_blkdev(),原型如下:

        int unreister_blkdev(unsigned int major, const char* name);

    这里unreister_blkdev与register_blkdev的参数必须匹配,否则这个函数会返回-EINVAL.

    Linux2.6,register_blkdev的调用是可选的.register_blkdev这个调用在Linux2.6中只完成了两件事情:①如果需要,分派一个主设备号;②在/proc/devices中创建一个入口.

  • 相关阅读:
    软件工程课堂二
    软件工程第二周总结
    软件工程第一周开课博客
    软件工程课堂一
    开学第一次考试感想
    以Function构造函数方式声明函数
    document.scrollingElement
    标识符
    变量声明语句的提升
    用that代替this
  • 原文地址:https://www.cnblogs.com/liusiluandzhangkun/p/8596755.html
Copyright © 2020-2023  润新知