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,而次设备号分别为1和2。一般而言,磁盘中的分区是由连续的次设备号来区分的。
如果将一个磁盘分成了几个分区,那么其分区表保存在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字段指向的缓冲区的长度(如果sense是NULL,则为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 数据传送的方向:READ(0)或WRITE(1)
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”请求命令(SCSI和ATAPI设备使用)
REQ_FAILED 当请求中的sense或direct命令的操作与预期的不一致时设置该标志
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调度层了。