Deadline算法的核心在于保证每个IO请求在一定的时间内一定要被服务到,以此来避免某个请求饥饿。
1.1 原理
Deadline 这种调度器对读写 request 进行了分类管理,并且在调度处理的过程中读请求具有较高优先级。这主要是因为读请求往往是同步操作,对延迟时间比较敏感,而写操作往往是异步操作,可以尽可能的将相邻访问地址的请求进行合并,但是,合并的效率越高,延迟时间会越长。因此,为了区别对待读写请求类型, deadline 采用两条链表对读写请求进行分类管理。但是,引入分类管理之后,在读优先的情况下,写请求如果长时间得到不到调度,会出现饿死的情况,因此, deadline 算法考虑了写饿死的情况,从而保证在读优先调度的情况下,写请求不会被饿死。
Deadline 这种调度算法的基本思想可以采用下图进行描述:
读写请求被分成了两个队列,并且采用两种方式将这些 request 管理起来。一种是采用红黑树( RB tree )的方式将所有 request 组织起来,通过 request 的访问地址作为索引;另一种方式是采用队列的方式将 request 管理起来,所有的 request 采用先来后到的方式进行排序,即 FIFO 队列。每个 request 会被分配一个 time stamp ,这样就可以知道这个 request 是否已经长时间没有得到调度,需要优先处理。在请求调度的过程中,读队列是优先得到处理的,除非写队列长时间没有得到调度,存在饿死的状况。
在请求处理的过程中, deadline 算法会优先处理那些访问地址临近的请求,这样可以最大程度的减少磁盘抖动的可能性。只有在有些 request 即将被饿死的时候,或者没有办法进行磁盘顺序化操作的时候, deadline 才会放弃地址优先策略,转而处理那些即将被饿死的 request 。
总体来讲, deadline 算法对 request 进行了优先权控制调度,主要表现在如下几个方面:
1) 读写请求分离,读请求具有高优先调度权,除非写请求即将被饿死的时候,才会去调度处理写请求。这种处理可以保证读请求的延迟时间最小化。
2) 对请求的顺序批量处理。对那些地址临近的顺序化请求, deadline 给予了高优先级处理权。例如一个写请求得到调度后,其临近的 request 会在紧接着的调度过程中被处理掉。这种顺序批量处理的方法可以最大程度的减少磁盘抖动。
3) 保证每个请求的延迟时间。每个请求都赋予了一个最大延迟时间,如果达到延迟时间的上限,那么这个请求就会被提前处理掉,此时,会破坏磁盘访问的顺序化特征,回影响性能,但是,保证了每个请求的最大延迟时间。
1.2 源码分析
Deadline调度器需要处理的核心数据结构是deadline_data,该结构描述如下:
struct deadline_data {
/*
* run time data
*/
/*
* requests(deadline_rq s) are present on both sort_list and fifo_list
*/
/* 采用红黑树管理所有的request,请求地址作为索引值 */
struct rb_rootsort_list[2];
/* 采用FIFO队列管理所有的request,所有请求按照时间先后次序排列 */
struct list_headfifo_list[2];
/*
* next in sortorder. read, write or both are NULL
*/
/* 批量处理请求过程中,需要处理的下一个request */
struct request*next_rq[2];
/* 计数器:统计当前已经批量处理完成的request */
unsigned int batching; /* number ofsequential requests made */
sector_tlast_sector; /* head position */
/* 计数器:统计写队列是否即将饿死 */
unsigned int starved; /* timesreads have starved writes */
/*
* settings thatchange how the i/o scheduler behaves
*/
/* 配置信息:读写请求的超时时间值 */
int fifo_expire[2];
/* 配置信息:批量处理的request数量 */
int fifo_batch;
/* 配置信息:写饥饿值 */
int writes_starved;
int front_merges;
};
sort_list:读写请求的红黑树,以请求的起始扇区来排序
fifo_list:读写请求的链表,以请求的响应期限来排序
next_rq:下一个读(写)请求,当确定一个批量传输时,通过该指针直接获取下一个请求
batching:批量传输的当前值
last_sector:处理的rq的末尾扇区号
starved: 标识着当前是第starved批读请求传输
fifo_expire:读写请求的期限值
fifo_batch:批量传输的请求数
writes_starved:写请求的饿死线,传输了writes_starved批读请求后,必须传输写请求
front_merges:是否使能frontmerge的检查
DeadlineScheduler的定义:
static struct elevator_type iosched_deadline = {
.ops = {
.elevator_merge_fn= deadline_merge,
.elevator_merged_fn= deadline_merged_request,
.elevator_merge_req_fn= deadline_merged_requests,
.elevator_dispatch_fn= deadline_dispatch_requests,
.elevator_add_req_fn= deadline_add_request,
.elevator_queue_empty_fn= deadline_queue_empty,
.elevator_former_req_fn= elv_rb_former_request,
.elevator_latter_req_fn= elv_rb_latter_request,
.elevator_init_fn= deadline_init_queue,
.elevator_exit_fn= deadline_exit_queue,
},
.elevator_attrs= deadline_attrs,
.elevator_name= "deadline",
.elevator_owner= THIS_MODULE,
};
初始化函数deadline_init_queue()用于初始化struct deadline_data中的数据,没有太多好说的,先来看检查一个bio是否能合并到request中的函数deadline_merge()
static int
deadline_merge(struct request_queue *q, structrequest **req, struct bio *bio)
{
structdeadline_data *dd = q->elevator->elevator_data;
structrequest *__rq;
int ret;
/*
* check for front merge
*/
if(dd->front_merges) {//在deadline scheduler使能了front_merges的情况下才会进行front merge的检查
sector_tsector = bio->bi_sector + bio_sectors(bio);//取bio的最后一个扇区
//从红黑树中查找起始扇区号与sector相同的request
__rq =elv_rb_find(&dd->sort_list[bio_data_dir(bio)], sector);
if(__rq) {//查找成功
BUG_ON(sector!= blk_rq_pos(__rq));
if(elv_rq_merge_ok(__rq, bio)) {//各项属性的检查,确定bio可以插入
ret= ELEVATOR_FRONT_MERGE;//设置状态
gotoout;
}
}
}
returnELEVATOR_NO_MERGE;
out:
*req =__rq;
returnret;//将检查的结果返回给通用层
}
函数deadline_merged_request进行bio插入的善后工作。。主要是考虑前插入改变了原红黑树节点的值,所以要将节点删除再重新进行插入
static void deadline_merged_request(structrequest_queue *q,
struct request *req, int type)
{
structdeadline_data *dd = q->elevator->elevator_data;
/*
* if the merge was a front merge, we need toreposition request
*/
if (type== ELEVATOR_FRONT_MERGE) {//如果是是将bio插入request的bio链表的前面则要进行request的重定位
elv_rb_del(deadline_rb_root(dd,req), req);//将request从红黑树中删除
deadline_add_rq_rb(dd,req);//重新添加至红黑树
}
}
在通用层进行request的合并后,deadline_merged_requests()函数负责善后,注意合并时都是保留前request,舍弃后request
static void
deadline_merged_requests(struct request_queue *q,struct request *req,
struct request *next)
{
/*
* if next expires before rq, assign its expiretime to rq
* and move into next position (next will bedeleted) in fifo
*/
/*首先要保证两个请求的所属的队列不为空,然后根据req和next的响应期限时间长短,来选择保留哪个,如果
后者比前者的期限时间短,也就是先响应,那就要将next的期限时间赋给req,并且将req放置到next在fifo的
位置,因为next将要被删除*/
if(!list_empty(&req->queuelist) && !list_empty(&next->queuelist)){
//如果next的期限时间小于req
if(time_before(rq_fifo_time(next), rq_fifo_time(req))) {
list_move(&req->queuelist,&next->queuelist);//调整req在fifo的位置
rq_set_fifo_time(req,rq_fifo_time(next));//重置req的期限时间
}
}
/*
* kill knowledge of next, this one is a goner
*/
//将next从链表和红黑树中删除
deadline_remove_request(q,next);
}
deadline_add_request()用于将一个新请求添加至调度器,主要是插入各个数据结构,并设置期限值
static void
deadline_add_request(struct request_queue *q,struct request *rq)
{
structdeadline_data *dd = q->elevator->elevator_data;
const intdata_dir = rq_data_dir(rq);//获取request的读写方向
deadline_add_rq_rb(dd,rq);//将rq插入红黑树
/*
* set expire time and add to fifo list
*/
//设置期限时间
rq_set_fifo_time(rq,jiffies + dd->fifo_expire[data_dir]);
//将rq插入fifo_list
list_add_tail(&rq->queuelist,&dd->fifo_list[data_dir]);
}
最后要分析的一个函数,也是最重要的一个--deadline_dispatch_requests 调度器如何选择request,分派给request_queue:
static intdeadline_dispatch_requests(struct request_queue *q, intforce)
{
struct deadline_data*dd = q->elevator->elevator_data;
const intreads = !list_empty(&dd->fifo_list[READ]);
const intwrites = !list_empty(&dd->fifo_list[WRITE]);
struct request*rq;
int data_dir;
/*
* batches are currently reads XOR writes
请求批量处理入口
*/
if (dd->next_rq[WRITE])
rq = dd->next_rq[WRITE];
else
rq =dd->next_rq[READ];
/* 如果批量请求处理存在,并且还没有达到批量请求处理的上限值,那么继续请求的批量处理 */
if (rq && dd->batching <dd->fifo_batch)
/* we have a next request are still entitled to batch */
gotodispatch_request;
/*
* at this point weare not running a batch. select the appropriate
* data direction(read / write)
*/
/* 优先处理读请求队列 */
if (reads) {
BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[READ]));
/* 如果写请求队列存在饿死的现象,那么优先处理写请求队列 */
if (writes && (dd->starved++ >=dd->writes_starved))
goto dispatch_writes;
data_dir= READ;
gotodispatch_find_request;
}
/*
* there are eitherno reads or writes have been starved
*/
/* 没有读请求需要处理,或者写请求队列存在饿死现象 */
if (writes) {
dispatch_writes:
BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[WRITE]));
dd->starved= 0;
data_dir= WRITE;
gotodispatch_find_request;
}
return 0;
dispatch_find_request:
/*
* we are notrunning a batch, find best request for selected data_dir
*/
if (deadline_check_fifo(dd, data_dir) ||!dd->next_rq[data_dir]) {
/* 如果请求队列中存在即将饿死的request,或者不存在需要批量处理的请求,那么从FIFO队列头获取一个request */
/*
* A deadline hasexpired, the last request was in the other
* direction, or wehave run out of higher-sectored requests.
* Start again fromthe request with the earliest expiry time.
*/
rq =rq_entry_fifo(dd->fifo_list[data_dir].next);
} else {
/* 继续批量处理,获取需要批量处理的下一个request */
/*
* The last req wasthe same dir and we have a next request in
* sort order. Noexpired requests so continue on from here.
*/
rq =dd->next_rq[data_dir];
}
dd->batching= 0;
dispatch_request:
/* 将request从调度器中移出,发送至设备 */
/*
* rq is theselected appropriate request.
*/
dd->batching++;
deadline_move_request(dd,rq);
return 1;
}
总体而言,Noop和Deadline算法实现是比较简单的