请求队列描述符

    技术2024-09-28  61

    1.4.4 请求队列描述符

    make_request_fn方法属于块设备I/O调度层的内容,要继续往下走,需要介绍一下通用块层的体系架构,这里需要从磁盘和磁盘分区开始说起。磁盘是一个由通用块层处理的逻辑块设备,是块设备驱动中最重要的一个概念。通常一个磁盘对应一个硬件块设备,例如硬盘、软盘或光盘。但是,磁盘也可以是一个虚拟设备,可以建立在几个物理磁盘分区之上或一些RAM专用页中的内存区上。在任何情形中,借助通用块层提供的服务,上层内核组件可以以同样的方式工作在所有的磁盘上。

     

    磁盘是由gendisk对象描述的,其中各字段如下所示。

     

    struct gendisk {

           int major;                     /* Major磁盘主设备号 */

           int first_minor;    //与磁盘关联的第一个次设备号

           int minors;                 /* 与磁盘关联的次设备号范围 */

           char disk_name[32];             /* 磁盘的标准名称(通常是相应设备文件的规范名称) */

           struct hd_struct **part;     /* 磁盘的分区描述符数组 */

           int part_uevent_suppress;

           struct block_device_operations *fops;  //指向块设备操作表的指针

           struct request_queue *queue;   //指向磁盘请求队列的指针

           void *private_data;             //块设备驱动程序的私有数据

           sector_t capacity;            //磁盘内存区的大小(扇区数目)

     

           int flags;                   //描述磁盘类型的标志

           struct device *driverfs_dev;   //指向磁盘的硬件设备的device对象的指针

           struct kobject kobj;          //内嵌的kobject结构

           struct kobject *holder_dir;

           struct kobject *slave_dir;

     

           struct timer_rand_state *random; //该指针指向的这个数据结构记录磁盘中断的定时;

    //由内核内置的随机数发生器使用

           int policy;  //如果磁盘是只读的,则置为1(写操作禁止),否则为0

     

           atomic_t sync_io;          /* 写入磁盘的扇区数计数器,仅为RAID使用 */

           unsigned long stamp;   //统计磁盘队列使用情况的时间戳

           int in_flight;          //正在进行的I/O操作数

    #ifdef      CONFIG_SMP

           struct disk_stats *dkstats;

    #else

           struct disk_stats dkstats;  //统计每个CPU使用磁盘的情况

    #endif

    };

     

    flags字段存放了关于磁盘的信息。其中最重要的标志是GENHD_FL_UP:如果设置它,那么磁盘将被初始化并可以使用。另一个相关的标志是GENHD_FL_REMOVABLE,如果是诸如软盘或光盘这样可移动的磁盘,那么就要设置该标志。

     

    gendisk对象的fops字段指向一个表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;

    };

     

    通常硬盘被划分成几个逻辑分区。每个块设备文件要么代表整个磁盘,要么代表磁盘中的某一个分区。例如,一个主设备号为3、次设备号为0的设备文件/dev/hda代表的可能是一个主EIDE磁盘;该磁盘中的前两个分区分别由设备文件/dev/hda1/dev/hda2代表,它们的主设备号都是3,而次设备号分别为12。一般而言,磁盘中的分区是由连续的次设备号来区分的。

     

    如果将一个磁盘分成了几个分区,那么其分区表保存在hd_struct结构的数组中,该数组的地址存放在gendisk对象的part字段中。通过磁盘内分区的相对索引对该数组进行索引。hd_struct描述符中的字段如下所示:

     

    struct hd_struct {

           sector_t start_sect;  //磁盘中分区的起始扇区

           sector_t nr_sects;   //分区的长度(扇区数)

           struct kobject kobj;  //内嵌的kobject

           struct kobject *holder_dir;

           unsigned ios[2], sectors[2];    /* 对分区发出的读写操作次数和从分区读写的扇区数 */

           int policy, partno;  //磁盘中分区的相对索引

    };

     

    当内核发现系统中一个新的磁盘时(在启动阶段,或将一个可移动介质插人一个驱动器中时,或在运行期附加一个外置式磁盘时),就调用alloc_disk()函数,该函数分配并初始化一个新的gendisk对象,如果新磁盘被分成了几个分区,那么alloc_disk()还会分配并初始化一个适当的hd_struct类型的数组。然后,内核调用add_disk ()函数将新的gendisk对象插入到通用块层的数据结构中。

     

    不管怎样,整个gendisk- hd_struct体系中,最重要的是请求队列结构。请求队列描述符是由一个大的数据结构request_queue表示的,其字段如下表所示:

     

    struct request_queue

    {

           struct list_head       queue_head; //待处理请求的链表

           struct request         *last_merge; //指向队列中首先可能合并的请求描述符

           elevator_t             *elevator; //指向elevator对象的指针(电梯算法)

     

           struct request_list   rq; //为分配请求描述符所使用的数据结构

     

           request_fn_proc            *request_fn; //实现驱动程序的策略例程入口点的方法,

    //策略例程方法来处理请求队列中的下一个请求

           merge_request_fn   *back_merge_fn; //检查是否可能将bio合并到请求队列的最后一个请求中的方法

           merge_request_fn   *front_merge_fn; //检查是否可能将bio合并到队列的第一个请求中的方法

           merge_requests_fn  *merge_requests_fn; //试图合并请求队列中两个相邻请求的方法

           make_request_fn           *make_request_fn; //将一个新请求插入请求队列时调用的方法

           prep_rq_fn            *prep_rq_fn; //该方法把这个处理请求的命令发送给硬件设备

           unplug_fn              *unplug_fn; //去掉块设备的方法

           merge_bvec_fn             *merge_bvec_fn; //当增加一个新段时,

    //该方法返回可插人到某个已存在的bio结构中的字节数(通常未定义)

           activity_fn             *activity_fn; //将某个请求加入请求队列时调用的方祛通常未定义

           issue_flush_fn        *issue_flush_fn; //刷新请求队列时调用的方法

    //通过连续处理所有的请求清空队列

           prepare_flush_fn    *prepare_flush_fn;

           softirq_done_fn             *softirq_done_fn;

     

           /*

            * Dispatch queue sorting

            */

           sector_t          end_sector;

           struct request         *boundary_rq;

     

           /*

            * Auto-unplugging state

            */

           struct timer_list      unplug_timer; //插入设备时使用的动态定时器

           int                 unplug_thresh; /* 如果请求队列中待处理请求数大于该值,

    将立即去掉请求设备(缺省值是4*/

           unsigned long        unplug_delay; /* 去掉设备之前的时间延迟(缺省值是3ms*/

           struct work_struct   unplug_work; //去掉设备时使用的操作队列

     

           struct backing_dev_info backing_dev_info;

     

           void               *queuedata; //指向块设备驱动程序的私有数据的指针

     

           void               *activity_data; //activity_fn方法使用的私有数据

     

           unsigned long        bounce_pfn; //在大于该页框号时必须使用缓冲区回弹

           gfp_t                     bounce_gfp; //回弹缓冲区的内存分配标志

     

           unsigned long        queue_flags; //描述请求队列状态的标志

     

           spinlock_t              __queue_lock; //请求队列锁

           spinlock_t              *queue_lock; //指向请求队列锁的指针

     

           struct kobject kobj; //请求队列的内嵌kobject结构

     

           unsigned long        nr_requests;    /* 请求队列中允许的最大请求数s */

           unsigned int           nr_congestion_on; //如果待处理请求数超出了该闭值,

    //则认为该队列是拥挤的

           unsigned int           nr_congestion_off;//如果待处理请求数在这个闭值的范围内,

    //则认为该队列是不拥挤的

           unsigned int           nr_batching; //即使队列已满,

    //仍可以由特殊进程“batcher”提交的待处理请求的最大值(通常为32

     

           unsigned int           max_sectors; //单个请求所能处理的最大扇区数(可调的)

           unsigned int           max_hw_sectors; //单个请求所能处理的最大扇区数(硬约束)

           unsigned short        max_phys_segments; //单个请求所能处理的最大物理段数

           unsigned short        max_hw_segments; //单个请求所能处理的最大硬段数(分散-聚集DMA操作中的最大不同内存区数)

           unsigned short        hardsect_size; //扇区中以字节为单位的大小

           unsigned int           max_segment_size; //物理段的最大长度(以字节为单位)

     

           unsigned long        seg_boundary_mask; //段合并的内存边界屏蔽字

           unsigned int           dma_alignment; //DMA缓冲区的起始地址和长度的对齐位图(缺省值是511)

     

           struct blk_queue_tag      *queue_tags; //空闲/忙标记的位图(用于带标记的请求)blk_queue_tag *

     

           unsigned int           nr_sorted; //请求队列的引用计数器

           unsigned int           in_flight; //请求队列中待处理请求数

     

           unsigned int           sg_timeout; //用户定义的命令超时(仅由SCSI通用块设备使用)

           unsigned int           sg_reserved_size; //基本上没有使用

           int                 node;

     

           struct blk_trace      *blk_trace;

     

           unsigned int           ordered, next_ordered, ordseq;

           int                 orderr, ordcolor;

           struct request         pre_flush_rq, bar_rq, post_flush_rq;

           struct request         *orig_bar_rq;

           unsigned int           bi_size;

     

           struct mutex           sysfs_lock;

    };

     

    请求队列是一个双向链表,其元素就是请求描述符(也就是request数据结构,下面马上谈到)。请求队列描述符中的queue_head字段存放链表的头(第一个伪元素),而请求描述符中queuelist字段的指针把任一请求链接到链表的前一个和后一个元素之间。

     

    队列链表中元素的排序方式对每个块设备驱动程序是特定的;然而,I/O调度程序提供了几种预先确定好的元素排序方式,牵涉到“I/O调度算法”的概念,后面会提到。

     

    backing_dev_info字段是一个backing_dev_info类型的小对象,它存放了关于基本的硬件块设备的I/O数据流量的信息。例如,它保存了关于预读以及关于请求队列拥塞状态的信息。

     

    每个块设备的待处理请求都是用一个请求描述符来表示的,请求描述符存放在如下所示的request数据结构中:

     

    struct request {

           struct list_head queuelist;   /* 请求队列链表的指针 */

           struct list_head donelist;

     

           unsigned long flags;              /* 请求标志 */

     

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

           unsigned long nr_sectors;      /* 整个请求中要传送的扇区数 */

           /* no. of sectors left to submit in the current segment */

           unsigned int current_nr_sectors;  //当前bio的当前段中要传送的扇区数

     

           sector_t hard_sector;                 /* 要传送的下一个扇区号 */

           unsigned long hard_nr_sectors;     /* 整个请求中要传送的扇区数(由通用块层更新)*/

           /* no. of sectors left to complete in the current segment */

           unsigned int hard_cur_sectors; //当前bio的当前段中要传送的扇区数(由通用块层更新)

     

           struct bio *bio;         //请求中第一个没有完成传送操作的bio

           struct bio *biotail;      //请求链表中末尾的bio

     

           void *elevator_private;  //指向I/O调度程序私有数据的指针

           void *completion_data; 

     

           int rq_status;   /* 请求状态:实际上,或者是RQ_ACTIVE,或者是RQ_INACTIVE */

           int errors;   //用于记录当前传送中发生的I/O失败次数的计数器

           struct gendisk *rq_disk;  //请求所引用的磁盘描述符

           unsigned long start_time; //请求的起始时间(用jiffies表示)

     

           unsigned short nr_phys_segments; //请求的物理段数

     

           unsigned short nr_hw_segments; //请求的硬段数

     

           unsigned short ioprio;

     

           int tag; //与请求相关的标记(只适合支持多次数据传送的硬件设备)

     

           int ref_count; //请求的引用计数器

           request_queue_t *q; //指向包含请求的请求队列描述符的指针

           struct request_list *rl; //指向request_list结构的指针

     

           struct completion *waiting; //等待数据传送终止的Completion结构

           void *special; //对硬件设备发出“特殊”命令的请求所使用的数据的指针

           char *buffer; //指向当前数据传送的内存缓冲区的指针(如果缓冲区是高端内存区,则为NULL

     

           unsigned int cmd_len; //cmd字段中命令的长度

           unsigned char cmd[BLK_MAX_CDB]; //由请求队列的prep_rq_fn方法准备好的预先内置命令所在的缓冲区后面还会详细谈到

     

           unsigned int data_len; //通常,由data字段指向的缓冲区中数据的长度

           unsigned int sense_len; //sense字段指向的缓冲区的长度(如果senseNULL,则为0)

           void *data; //设备驱动程序为了跟踪所传送的数据而使用的指针

           void *sense; //指向输出sense命令的缓冲区的指针

     

           unsigned int timeout; //请求的超时

           int retries;

     

           rq_end_io_fn *end_io;

           void *end_io_data;

    };

     

    每个request请求包含一个或多个bio结构。最初,通用块层创建一个仅包含一个bio结构的请求。然后,I/O调度程序要么向初始的bio中增加一个新段,要么将另一个bio结构链接到请求中,从而“扩展”该请求,原因是可能存在新数据与请求中已存在的数据物理相邻的情况。请求描述符的bio字段指向请求中的第一个bio结构,而biotail字段则指向最后一个bio结构。rq_for_each_bio宏执行一个循环,从而遍历一个请求(而不是请求队列)中的所有bio结构。

     

    请求描述符中的几个字段值可能是动态变化的。例如,一旦bio中引用的数据块全部传送完毕,bio字段立即更新从而指向请求链表中的下一个bio。在此期间,另一个新的bio可能被加人到请求链表的尾部,所以biotail的值也可能改变。

     

    当磁盘数据块正在传送时,请求描述符的其它几个字段的值由I/O调度程序或设备驱动程序修改。例如,nr_sectors存放整个请求还需传送的扇区数,current_nr_sectors存放当前bio结构中还需传送的扇区数。

     

    flags中存放了很多标志,如下表中所示。到目前为止,最重要的一个标志是REQ_RW,它确定数据传送的方向。

     

    REQ_RW       数据传送的方向:READ0)或WRITE1

    REQ_FAILFAST    万一出错请求申明不再重试I/O操作

    REQ_SOFTBARRIER   请求相当于I/O调度程序的屏障

    REQ_HARDBARRIER  请求相当于1/O调度程序和设备驱动程序的屏障—应当在旧请求与新请求之间处理该请求

    REQ_CMD     包含一个标准的读或写I/O数据传送的请求

    REQ_NOMERGE  不允许扩展或与其它请求合并的请求

    REQ_STARTED    正处理的请求

    REQ_DONTPREP 不调用请求队列中的prep_rq_fn方法预先准备把命令发选项发给硬件设备

    REQ_QUEUED     请求被标记——也就是说,与该请求相关的硬件设备可以同时管理很多未完成数据的传送

    REQ_PC 请求包含发送给硬件设备的直接命令

    REQ_BLOCK_PC  与前一个标志功能相同,但发送的命令包含在bio结构中

    REQ_SENSE 请求包含一个“sense”请求命令(SCSIATAPI设备使用)

    REQ_FAILED       当请求中的sensedirect命令的操作与预期的不一致时设置该标志

    REQ_QUIET  万一I/O操作出错请求申明不产生内核消息

    REQ_SPECIAL     请求包含对硬件设备的特殊命令(例如,重设驱动器)

    REQ_DRIVE_CMD      请求包含对IDE磁盘的特殊命令

    REQ_DRIVE_TASK     请求包含对IDE磁盘的特殊命令

    REQ_DRIVE_TASKFILE     请求包含对IDE磁盘的特殊命令

    REQ_PREEMPT    请求取代位于请求队列前面的请求(仅对IDE磁盘而言)

    REQ_PM_SUSPEND    请求包含一个挂起硬件设备的电源管理命令

    REQ_PM_RESUME     请求包含一个唤醒硬件设备的电源管理命令

    REQ_PM_SHUTDOWN       请求包含一个切断硬件设备的电源管理命令

    REQ_BAR_PREFLUSH       请求包含一个要发送给磁盘控制器的“刷新队列”命令

    REQ_BAR_POSTFLUSH     请求包含一个已发送给磁盘控制器的“刷新队列”命令

     

    介绍完了这些来自ULK-3上的基础知识,我们通过一个图把前面的知识串联起来:

    前面提到generic_make_request调用q->make_request_fn方法将bio请求插入请求队列q中。那么接下来的工作就交给I/O调度层了。

    最新回复(0)