请求队列描述符

来源:互联网 发布:ping域名找不到主机 编辑:程序博客网 时间:2024/04/30 11:49

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调度层了。