- 请求队列
块设备将挂起的块IO请求保存在请求队列中,该队列由request_queue结构体表示:
- 在<Blkdev.h(include/linux)>中
- struct request_queue
- {
- /*
- * Together with queue_head for cacheline sharing
- */
- struct list_head queue_head;
- struct request *last_merge;
- elevator_t *elevator;
- /*
- * the queue request freelist, one for reads and one for writes
- */
- struct request_list rq;
- request_fn_proc *request_fn;
- make_request_fn *make_request_fn;
- prep_rq_fn *prep_rq_fn;
- unplug_fn *unplug_fn;
- merge_bvec_fn *merge_bvec_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; /* After this many requests */
- unsigned long unplug_delay; /* After this many jiffies */
- struct work_struct unplug_work;
- struct backing_dev_info backing_dev_info;
- /*
- * The queue owner gets to use this for whatever they like.
- * ll_rw_blk doesn't touch it.
- */
- void *queuedata;
- /*
- * queue needs bounce pages for pages above this limit
- */
- unsigned long bounce_pfn;
- gfp_t bounce_gfp;
- /*
- * various queue flags, see QUEUE_* below
- */
- unsigned long queue_flags;
- /*
- * protects queue structures from reentrancy. ->__queue_lock should
- * _never_ be used directly, it is queue private. always use
- * ->queue_lock.
- */
- spinlock_t __queue_lock;
- spinlock_t *queue_lock;
- /*
- * queue kobject
- */
- struct kobject kobj;
- /*
- * queue settings
- */
- unsigned long nr_requests; /* Max # of requests */
- unsigned int nr_congestion_on;
- unsigned int nr_congestion_off;
- unsigned int nr_batching;
- unsigned int max_sectors;
- unsigned int max_hw_sectors;
- unsigned short max_phys_segments;
- unsigned short max_hw_segments;
- unsigned short hardsect_size;
- unsigned int max_segment_size;
- unsigned long seg_boundary_mask;
- unsigned int dma_alignment;
- struct blk_queue_tag *queue_tags;
- unsigned int nr_sorted;
- unsigned int in_flight;
- /*
- * sg stuff
- */
- unsigned int sg_timeout;
- unsigned int sg_reserved_size;
- int node;
- #ifdef CONFIG_BLK_DEV_IO_TRACE
- struct blk_trace *blk_trace;
- #endif
- /*
- * reserved for flush operations
- */
- 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结构体表示:
- /*
- * try to put the fields that are referenced together in the same cacheline
- */
- struct request {
- struct list_head queuelist;
- struct list_head donelist;
- request_queue_t *q;
- unsigned int cmd_flags;
- enum rq_cmd_type_bits cmd_type;
- /* Maintain bio traversal state for part by part I/O submission.
- * hard_* are block layer internals, no driver should touch them!
- */
- sector_t sector; /* next sector to submit */
- sector_t hard_sector; /* next sector to complete */
- unsigned long nr_sectors; /* no. of sectors left to submit */
- unsigned long hard_nr_sectors; /* no. of sectors left to complete */
- /* no. of sectors left to submit in the current segment */
- unsigned int current_nr_sectors;
- /* no. of sectors left to complete in the current segment */
- unsigned int hard_cur_sectors;
- struct bio *bio;
- struct bio *biotail;
- struct hlist_node hash; /* merge hash */
- /*
- * The rb_node is only used inside the io scheduler, requests
- * are pruned when moved to the dispatch queue. So let the
- * completion_data share space with the rb_node.
- */
- union {
- struct rb_node rb_node; /* sort/lookup */
- void *completion_data;
- };
- /*
- * two pointers are available for the IO schedulers, if they need
- * more they have to dynamically allocate it.
- */
- void *elevator_private;
- void *elevator_private2;
- struct gendisk *rq_disk;
- unsigned long start_time;
- /* Number of scatter-gather DMA addr+len pairs after
- * physical address coalescing is performed.
- */
- unsigned short nr_phys_segments;
- /* Number of scatter-gather addr+len pairs after
- * physical and DMA remapping hardware coalescing is performed.
- * This is the number of scatter-gather entries the driver
- * will actually have to deal with after DMA mapping is done.
- */
- unsigned short nr_hw_segments;
- unsigned short ioprio;
- void *special;
- char *buffer;
- int tag;
- int errors;
- int ref_count;
- /*
- * when request is used as a packet command carrier
- */
- unsigned int cmd_len;
- unsigned char cmd[BLK_MAX_CDB];
- unsigned int data_len;
- unsigned int sense_len;
- void *data;
- void *sense;
- unsigned int timeout;
- int retries;
- /*
- * completion callback.
- */
- rq_end_io_fn *end_io;
- void *end_io_data;
- };
因为一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个bio结构体组成。注意,虽然磁盘上的块必须连续,但在内存中这些块并不一定要连续,每个bio结构体都可以描述多个片段,而内阁请求也可以包含多个bio结构体。
- IO调度程序
为了优化寻址操作,内核不会简单的按请求接收次序或者立即将请求提交给磁盘,它会在提交前,先执行名为合并与排序的预操作。这种预操作可以极大第提高系统的整体性能。在内核中负责提交IO请求的子系统被称为IO调度程序。
IO调度程序将磁盘IO资源分配给系统中所以挂起的块IO请求。这种资源分配是通过将请求队列中挂起的请求合并和排序来完成的。
进程调度程序和IO调度程序都是将一个资源虚拟给多个对象。进程调度程序的作用是将处理器资源分配给系统中的运行进程。处理器被虚拟并被系统中的运行进程共享。这种虚拟提供给用户的就是多任务和分时操作系统。IO调度程序虚拟块设备给多个磁盘请求,以便降低磁盘寻址时间,确保磁盘性能的最优化。
- IO调度程序的工作
IO调度程序的工作是管理块设备的请求队列。它决定队列中的请求排列顺序以及什么时候派发请求到块设备。
IO调度程序通过两种方法减少磁盘寻址时间:合并和排序。合并指将两个或多个请求结和成一个新请求。通过合并请求,IO调度程序将多次请求的开销压缩成一次请求的开销。更重要的是,请求合并后只需要传递给磁盘一条寻址命令,就可以访问到请求合并前必须多次寻址才能访问完的磁盘区域了,因此合并请求显然可以减少系统开销和磁盘寻址次数。
IO调度程序将整个请求队列按扇区增长方向有序排列。使所有请求按硬盘上扇区的排列顺序有序排列的目的不仅是为了缩短单独一次请求的寻址时间,更重要的优化在于,通过保存磁盘头以直线方向移动,缩短了所以请求的磁盘寻址的时间。该排序算法类似于电梯调度----电梯不能随意的从一层跳到另一层,它只向一个方向移动,当抵达了同一方向的最后一层后,再掉头向另一个方向移动。所以IO调度程序被称为电梯调度。
- Linus电梯
Linus电梯能执行合并与排序预处理。当有新的请求加入队列时,它首先检查其他每一个挂起的请求是否可以和新请求合并。Linus电梯可以执行向前和向后合并。如果新请求正好连在一个现存的请求前,就是向前合并;如果请求直接连接在一个现存的请求后,就是向后合并。
如果合并失败,就需要寻找可能的插入点(新请求在队列中的位置必须符合以扇区方向有序排序的原则)。如果找到,新请求就被插入到该点;如果没有合适的位置,那么新请求就被加入到队列尾部。另外,如果发现队列中有驻留时间过长的请求,那么新请求就被加入到队列尾部,即使插入后还要排序。这样可以避免访问相近磁盘的请求太多而造成访问磁盘其他位置请求难以得到执行。
但是这种检测方法并不是很有效,因为它并非是给等待了一段时间的请求提供实质性服务----它仅仅是在经过了一定时间后停止插入----排序请求,虽然改善了等待时间但最终还是会导致请求饥饿现象的发生。
- 最终期限IO调度程序
最终期限IO调度程序是为了解决Linus电梯所带来的饥饿问题而提出的。
在最后期限IO调度程序中,每个请求都有一个超时时间。默认情况下,读请求的超时时间是500毫秒,写请求的超时时间是5秒。最后期限IO调度程序请求类似与Linus电梯,也以磁盘物理位置为次序维护请求队列,该队列被称为排序队列。当一个新请求递交给排序队列时,最后期限IO调度程序合并和插入请求,同时会以请求类型为依据将它们插入到额外队列中。读请求按次序被插入到特定的读FIFO队列中,写请求被插入到特定的写FIFO队列中。普通队列以磁盘扇区为序进行排列,以FIFO形式组织,新队列总是被加入到队列尾部。对普通操作来说,最后期限IO调度程序将请求从排序队列的头部取下,再推入到派发队列中,派发队列然后将请求提交给磁盘驱动,从而保证了最小化的请求寻址。
如果在写FIFO队列头,或是在读FIFO队列头的请求超时,那么最后期限IO调度程序便从FIFO队列中提取请求进行服务。
由于读请求给定的超时时间要比写请求短,所以最后期限IO调度程序确保了写请求不会因为堵塞读请求而使读请求发生饥饿现象。
最后期限IO调度程序的实现在文件drivers/block/deadline-iosched.c中。
- 预测IO调度程序
预测(anticipatory)IO调度程序的目标是在保持良好的读响应的同时也能提供良好的全局吞吐量。
该调度程序是以最后期限IO调度程序为基础的。也实现了三个队列和一个派发队列,并为每个请求设置了超时时间。它最主要的改进是它增加了预测启发能力。预测IO调度程序试图减少在进行IO操作期间,处理新到的读请求所带来的寻址数量。预测IO调度程序与最后期限IO调度不同之处在于,请求提交后并不直接返回处理其他请求,而是会有意空闲片刻。这几毫秒对应用程序来说是个提交其他读请求的好机会,任何对相邻磁盘位置操作的请求都会立刻得到处理。在等待时间结束后,预测IO调度程序重新返回原来的位置,继续执行以前剩余的请求。
预测IO调度程序所能带来的优势取决于能否正确的预测应用程序和文件系统的行为 。这种预测依靠一系列的启发和统计工作。预测IO调度程序跟踪并统计每个应用程序块IO操作的习惯行为,以便正确预测应用程序的未来行为。
预测IO调度程序的实现在内核源代码树的drivers/block/as-iosched.c中,它是Linux内核中默认的IO调度程序。
- 完全公正的排队IO调度程序
完全公正的排队IO调度程序(Complete Fair Queuing,简称CFQ)是为专有工作负荷设计的。
CFQ IO调度程序把进入的IO请求放入特定的队列中,这种队列是根据引起IO请求的进程组织的。例如,来自foo进程的IO请求进入foo队列,来自bar进程的IO请求进入bar队列。在每个队列中,刚进来的请求与相邻请求合并在一起,并进行插入分类。对列由此按扇区方式分类。CFQ IO调度程序与其他调度的差异在于每一个提交IO的进程都有自己的队列。
CFQ IO调度程序以时间片轮转调度队列,从每个队列中选取请求数,然后进行下一轮调度。这就在进程级提供了公平,确保每个进程接收公平的磁盘带宽片段。预定的工作负荷是多媒体。
CFQ IO调度程序的实现在文件drivers/block/cfq-iosched.c中。
- 空操作的IO调度程序
最后一种IO调度程序是空操作(Noop)的IO调度程序。
空操作的IO调度程序不进行排序,也不进行预寻道操作,但是要执行请求的合并。当一个新的请求提交到队列时,就把它与任一相邻的请求合并,维护请求队列以近似FIFO的顺序排列,块设备驱动程序可以从这种队列中摘取请求。
如果块设备没有一点寻道的负担(真正的随机访问设备),那么就没有必要对进入的请求进行插入排序。
空操作的IO调度程序的实现在文件drivers/block/noop-iosched.c中,它是专门为随机访问设备而设计的。
- IO调度程序的选择
内核中,块设备默认使用预测IO调度程序。在启动时,可以覆盖默认,通过命令行选项elevator=某个有效的激活的IO调度程序。
给定elevator选项的参数
参数 | IO调度程序 |
as | 预测 |
cfq | 完全公正的排队 |
deadline | 最终期限 |
noop | 空操作 |