Block层的请求在device的queue里会发生reorder与merge以提高效率,然而,在进入device的queue之前也会做同样的努力,这就是plug机制。
增大merge的机会 (1)
通常,减小per-request开销(例如HDD的寻道时间)的有效方式是merge:把几个小的request合并成一个连续的、大的request。每个device都会有一个(对于single-queue而言)queue;IO scheduler在这个queue上做各种reorder和merge,以提高效率。这里的merge有一些缺点:
- 当device比较快,或者device虽然不快但比较闲,那么merge的机会就会比较小。这不是大问题,虽然从整体上看device的效率不高,但是能满足请求;
- device的queue需要lock来保证一致性,这在多CPU的情况下会增大竞争开销;
Linux 2.6.39之前也有plug机制,但它是在device的queue上进行的,所以只能解决第一点。Linux 2.6.39引入per-process的plug机制。本文讨论的是后者,所引用的代码来自linux 3.19.8。
per-process plug (2)
文件系统(或者block device的其它客户端)提交一批请求之前:
- 调用
blk_start_plug()
在task_struct
上安装一个list; - 多次调用
generic_make_request()
提交这一批请求;请求在list中reorder与merge; - 调用
blk_finish_plug()
把list中的合并得到的大请求flush到device的queue;调用schedule()
也会触发这样的flush;
下图简单的表示了这个过程,其中mq_list
用于multi-queue(blk-mq),cb_list
用于md,暂时忽略,只看list
。
在task_struct
中维护这个list有一个好处:进程在调用blk_start_plug()
和blk_finish_plug()
之间若发生block,即调用schedule()
,可以在block住之前方便地找到pending的请求(就在task_struct
的list中),并flush它们。在进程block之前flush掉pending的请求非常重要:
- 提高性能:IO请求不会被delay到block之后;
- 避免死锁:比如,进程block就是为了等待pending的请求;再如,进程block是为了分配内存,进而需要reclaim,而reclaim又要等待pending的请求占用的内存页。这些情况下,block之前若不把pending的请求发下去就会死锁。
开始plug (2.1)
代码引自linux 3.19.8:
1 | void blk_start_plug(struct blk_plug *plug) |
blk_start_plug()
非常简单,把一个struct blk_plug
对象安装到struct task_struct
中。另外从代码上看,plug还可以嵌套的(注意只有最底层的plug
才会安装到struct task_struct
):
- 调用
blk_start_plug(plug1)
- 提交请求req1,req1被pending在plug1上;
- 调用
blk_start_plug(plug2)
- 提交请求req2,req2被pending在plug2上;
- 调用
blk_finish_plug(plug2)
flush pending在plug2上的req2; - 提交请求req3,req3被pending在plug1上;
- 调用
blk_finish_plug(plug1)
flush pending在plug1上的req1和req3;
pending请求 (2.2)
1 | generic_make_request(bio) |
两点值得注意一下:
- 当plug中pending的请求数到达
BLK_MAX_REQUEST_COUNT
(默认是16)时,就会触发flush;然后重新开始新一轮的plug; - plug开始和结束(即flush)的时候,分别生成
P(plug)
和U(unplug)
事件。见blktrace.
结束plug (2.3)
结束plug时,会flush掉pending的请求。如前所述,大致有3个flush时机(没有找到timeout导致的flush,per-process的plug机制没有这种方式?):
- 文件系统(或者block device的其它客户端)显式地调用
blk_finish_plug()
; - pending的请求数到达
BLK_MAX_REQUEST_COUNT
; - 进程block,即调用
schedule()
;
其中第2种前面已经展示。blk_finish_plug()
代码如下(linux 3.19.8):
1 | void blk_finish_plug(struct blk_plug *plug) |
schedule()
在block之前(__schedule()
)的调用关系是这样的:
1 | asmlinkage __visible void __sched schedule(void) |
可见,flush是由blk_flush_plug_list()
函数完成的,其中第2个参数表明flush是不是schedule触发的,若是,则以异步的方式处理pending的请求。
1 | void blk_flush_plug_list(struct blk_plug *plug, bool from_schedule) |
运行device的queue的代码如下:
1 | static void queue_unplugged(struct request_queue *q, unsigned int depth, |
这里运行queue就是调用queue的request_fn()
函数来处理queue中的请求。request_fn
是一个函数指针,指向driver的请求处理函数,对scsi driver而言,就是scsi_request_fn
。