电梯算法简介
电梯调度算法主要适用于LINUX I/O磁盘请求调度。
磁盘结构如下图所示,磁盘主要由盘面和磁头组成。
磁盘每次进读写请求时,需要给磁盘驱动器一个地址,磁盘驱动器根据给定地址计算出相应的扇区,然后将磁头移动到需要访问的扇区,开始进行读写。
读写磁盘时,转动磁头实际上很耗费时间,如果不采用调度策略,磁头直接根据请求进行直接访问,则必定会造成时间的浪费。
比如以下这种访问方式,访问序列为 盘面1的10,70,20,30,50,如果顺序访问,则是10-》70,70-》20,20-》30,30-》50,这样磁头实际上转了2圈,势必造成一种浪费。
采用电梯调度策略,访问顺序变成这样10->20->30->50->70,磁头转动一圈就可以满足所有的访问请求,从而节约I/O访问时间,提高效率。
基础原理
电梯调度策略实际上提供了一种思路,在进行数据访问的同时,如果有新的请求到来,直接将其按照升序插入到等待队列中。
磁盘每次直接从等待队列中取出相应的逻辑地址,直接进行磁盘访问。当然这种调度策略也存在缺陷,如果在某一块地址区域磁盘访问特别频繁,可能会造成处在等待序列末尾的请求“饥饿”,也就是队尾的请求需要等待很长时间才能满足。
调度策略的本质是一种排序算法,对排序队列的一种操作,这种队列采用链表实现比较容易。
基础函数梳理
elevator_init
elv_merge
__elv_add_request
elv_next_request/blk_peek_request
数据结构
struct elevator_type
{
/* managed by elevator core */
struct kmem_cache *icq_cache;
/* fields provided by elevator implementation */
struct elevator_ops ops;
size_t icq_size; /* see iocontext.h */
size_t icq_align; /* ditto */
struct elv_fs_entry *elevator_attrs;
char elevator_name[ELV_NAME_MAX];
struct module *elevator_owner;
/* managed by elevator core */
char icq_cache_name[ELV_NAME_MAX + 5]; /* elvname + "_io_cq" */
struct list_head list;
};
struct elevator_ops
{
elevator_merge_fn *elevator_merge_fn;
elevator_merged_fn *elevator_merged_fn;
elevator_merge_req_fn *elevator_merge_req_fn;
elevator_allow_merge_fn *elevator_allow_merge_fn;
elevator_bio_merged_fn *elevator_bio_merged_fn;
elevator_dispatch_fn *elevator_dispatch_fn;
elevator_add_req_fn *elevator_add_req_fn;
elevator_activate_req_fn *elevator_activate_req_fn;
elevator_deactivate_req_fn *elevator_deactivate_req_fn;
elevator_completed_req_fn *elevator_completed_req_fn;
elevator_request_list_fn *elevator_former_req_fn;
elevator_request_list_fn *elevator_latter_req_fn;
elevator_init_icq_fn *elevator_init_icq_fn; /* see iocontext.h */
elevator_exit_icq_fn *elevator_exit_icq_fn; /* ditto */
elevator_set_req_fn *elevator_set_req_fn;
elevator_put_req_fn *elevator_put_req_fn;
elevator_may_queue_fn *elevator_may_queue_fn;
elevator_init_fn *elevator_init_fn;
elevator_exit_fn *elevator_exit_fn;
elevator_registered_fn *elevator_registered_fn;
};
电梯初始化过程
blk_init_queue–》blk_init_queue_node–》blk_init_allocated_queue–》elevator_init
电梯算法noop
static struct elevator_type elevator_noop = {
.ops = {
.elevator_merge_req_fn = noop_merged_requests,
.elevator_dispatch_fn = noop_dispatch,
.elevator_add_req_fn = noop_add_request,
.elevator_former_req_fn = noop_former_request,
.elevator_latter_req_fn = noop_latter_request,
.elevator_init_fn = noop_init_queue,
.elevator_exit_fn = noop_exit_queue,
},
.elevator_name = "noop",
.elevator_owner = THIS_MODULE,
};
直接看源码,整套看下来没问题,前提是要知道,电梯上层是什么时候调用什么接口,然后这些接口是做什么用的。
然后就没有然后了,算法就是一个list,没有其他任何杂质。
电梯算法deadline
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_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,
};
这个算法是很有意思的,
按照我的看法,应该是造成read和write相互饥饿,导致切换。
deadline的理论原理是,某一个操作的饥饿,导致累积,这样可以成块的处理这一操作。
初始化
static int deadline_init_queue(struct request_queue *q, struct elevator_type *e)
{
struct deadline_data *dd;
struct elevator_queue *eq;
eq = elevator_alloc(q, e);
if (!eq)
return -ENOMEM;
dd = kzalloc_node(sizeof(*dd), GFP_KERNEL, q->node);
if (!dd) {
kobject_put(&eq->kobj);
return -ENOMEM;
}
eq->elevator_data = dd;
INIT_LIST_HEAD(&dd->fifo_list[READ]);
INIT_LIST_HEAD(&dd->fifo_list[WRITE]);
dd->sort_list[READ] = RB_ROOT;
dd->sort_list[WRITE] = RB_ROOT;
dd->fifo_expire[READ] = read_expire;
dd->fifo_expire[WRITE] = write_expire;
dd->writes_starved = writes_starved;
dd->front_merges = 1;
dd->fifo_batch = fifo_batch;
spin_lock_irq(q->queue_lock);
q->elevator = eq;
spin_unlock_irq(q->queue_lock);
return 0;
}
前面的开辟节点空间的,等等的就不说了。主要看下参数的意义。
1、sort_list 有两个,一个读一个写,这就意味着,有两个红黑树。
2、fifo_expire 也哟两个,表示到期的时间
static const int read_expire = HZ / 2; /* max time before a read is submitted. */
static const int write_expire = 5 * HZ; /* ditto for writes, these limits are SOFT! */
HZ 不知道是多少,如果一秒一次来说的话,就是500ms 和 5s。但总之写的时间是读的10倍
3、writes_starved 表示读这个操作可以超过写几次。
static const int writes_starved = 2; /* max times reads can starve a write */
这里赋值两次后面再运用的时候是这样的
if (writes && (dd->starved++ >= dd->writes_starved))
goto dispatch_writes;
所以这边还有个starved表示读取的次数,是从0开始的。这个条件判断就是,读两次,写一次。
4、front_merges 前置合并,这个前置和后置一直没有疏通是什么意思。
5、fifo_batch 批量处理的个数,
static const int fifo_batch = 16; /* # of sequential requests treated as one
这边是16个。后面我们会看到,具体算法实现中是怎么运用这个参数的。
算法详解
1、从add开始
static void
deadline_add_request(struct request_queue *q, struct request *rq)
{
struct deadline_data *dd = q->elevator->elevator_data;
const int data_dir = rq_data_dir(rq);
deadline_add_rq_rb(dd, rq);
/*
* set expire time and add to fifo list
*/
rq->fifo_time = jiffies + dd->fifo_expire[data_dir];
list_add_tail(&rq->queuelist, &dd->fifo_list[data_dir]);
}
加入红黑树
加入fifo队列,是有时间参数的。
so,很简单
2、merge请求
static int
deadline_merge(struct request_queue *q, struct request **req, struct bio *bio)
{
struct deadline_data *dd = q->elevator->elevator_data;
struct request *__rq;
int ret;
/*
* check for front merge
*/
if (dd->front_merges) {
sector_t sector = bio_end_sector(bio);
__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)) {
ret = ELEVATOR_FRONT_MERGE;
goto out;
}
}
}
return ELEVATOR_NO_MERGE;
out:
*req = __rq;
return ret;
}
根据bio 的sector 从rb里面找到一个相应位置的rq。
struct request *elv_rb_find(struct rb_root *root, sector_t sector)
{
struct rb_node *n = root->rb_node;
struct request *rq;
while (n) {
rq = rb_entry(n, struct request, rb_node);
if (sector < blk_rq_pos(rq))
n = n->rb_left;
else if (sector > blk_rq_pos(rq))
n = n->rb_right;
else
return rq;
}
return NULL;
}
那么看这个逻辑应该返回的是最后叶子的父节点,我们用图示来表示。
对照这个图,如果9是根节点,然后来了个3,最终它会到4的左边节点,所以返回的是rq。
static void
deadline_merged_requests(struct request_queue *q, struct request *req,
struct request *next)
{
if (!list_empty(&req->queuelist) && !list_empty(&next->queuelist)) {
if (time_before(next->fifo_time, req->fifo_time)) {
list_move(&req->queuelist, &next->queuelist);
req->fifo_time = next->fifo_time;
}
}
deadline_remove_request(q, next);
}
这merged个有点意思了, req是刚才返回的4,而next是我们自己生成的rq,也就是将来入住菱形的部分。
merged太难研究
3.分发流程
/*
* deadline_dispatch_requests selects the best request according to
* read/write expire, fifo_batch, etc
*/
static int deadline_dispatch_requests(struct request_queue *q, int force)
{
struct deadline_data *dd = q->elevator->elevator_data;
const int reads = !list_empty(&dd->fifo_list[READ]);
const int writes = !list_empty(&dd->fifo_list[WRITE]);
struct request *rq;
int data_dir;
if (dd->next_rq[WRITE])
rq = dd->next_rq[WRITE];
else
rq = dd->next_rq[READ];
if (rq && dd->batching < dd->fifo_batch)
goto dispatch_request;
if (reads) {
BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[READ]));
if (writes && (dd->starved++ >= dd->writes_starved))
goto dispatch_writes;
data_dir = READ;
goto dispatch_find_request;
}
if (writes) {
dispatch_writes:
BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[WRITE]));
dd->starved = 0;
data_dir = WRITE;
goto dispatch_find_request;
}
return 0;
dispatch_find_request:
if (deadline_check_fifo(dd, data_dir) || !dd->next_rq[data_dir]) {
rq = rq_entry_fifo(dd->fifo_list[data_dir].next);
} else {
rq = dd->next_rq[data_dir];
}
dd->batching = 0;
dispatch_request:
dd->batching++;
deadline_move_request(dd, rq);
return 1;
}
分发流程是整个算法的核心,我们分两种情况看:
第一种批量饥饿
流程我们简化代码
static int deadline_dispatch_requests(struct request_queue *q, int force)
{
if (dd->next_rq[WRITE])
rq = dd->next_rq[WRITE];
else
rq = dd->next_rq[READ];
if (rq && dd->batching < dd->fifo_batch)
goto dispatch_request;
dispatch_request:
dd->batching++;
deadline_move_request(dd, rq);
return 1;
}
就是很简单了,dispatch_request 然后到deadline_move_request,batching++,会到16.
但是这里前面的,精髓还不是这样,你单单看这些是看不出来的,要真正的理解必须结合整个算法的逻辑。
这里再if判断 是会保持方向的,也就是说如果是read,就会联系发16个read,之前我也一直看不懂这个函数的逻辑,
为什么说会联系16个保持方向呢?要看最后一个函数
static void
deadline_move_request(struct deadline_data *dd, struct request *rq)
{
const int data_dir = rq_data_dir(rq);
dd->next_rq[READ] = NULL;
dd->next_rq[WRITE] = NULL;
dd->next_rq[data_dir] = deadline_latter_request(rq);
dd->last_sector = rq_end_sector(rq);
deadline_move_to_dispatch(dd, rq);
}
看后面两个是分发的,没什么好说的,看前面这里把制空,这个时候
dd->next_rq[data_dir] = deadline_latter_request(rq);
再通过rq获取一个,所以这个就是保持,deadline_latter_request(rq);这个会是同一个类型的rq吗?后面看了红黑树,因为初始化的时候,
我们看到是有两种红黑树,他们是分开的。所以是保持一致的。
第二种切换饥饿
如第一种批量到16 就要下来判断是否进行reads或者write切换
if (reads) {
if (writes && (dd->starved++ >= dd->writes_starved))
goto dispatch_writes;
data_dir = READ;
goto dispatch_find_request;
}
if (writes) {
dispatch_writes:
BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[WRITE]));
dd->starved = 0;
data_dir = WRITE;
goto dispatch_find_request;
}
dispatch_find_request:
if (deadline_check_fifo(dd, data_dir) || !dd->next_rq[data_dir]) {
rq = rq_entry_fifo(dd->fifo_list[data_dir].next);
} else {
rq = dd->next_rq[data_dir];
}
如果确定某一种,这边判断fifo是否有时间线是饥饿的,如果有就用fifo里面的。
还有一种情况,就是刚才dd->next_rq 不是制空了吗?那就也是用fifo里面的。