Noop调度算法是内核中最简单的IO调度算法。
1.1 原理
Noop调度算法也叫作电梯调度算法,它将IO请求放入到一个FIFO队列中,然后逐个执行这些IO请求,当然对于一些在磁盘上连续的IO请求,Noop算法会适当做一些合并。这个调度算法特别适合那些不希望调度器重新组织IO请求顺序的应用。
1.2 优势
这种调度算法在以下场景中优势比较明显:
1)在IO调度器下方有更加智能的IO调度设备。如果您的Block Device Drivers是Raid,或者SAN,NAS等存储设备,这些设备会更好地组织IO请求,不用IO调度器去做额外的调度工作;
2)上层的应用程序比IO调度器更懂底层设备。或者说上层应用程序到达IO调度器的IO请求已经是它经过精心优化的,那么IO调度器就不需要画蛇添足,只需要按序执行上层传达下来的IO请求即可。
3)对于一些非旋转磁头氏的存储设备,使用Noop的效果更好。因为对于旋转磁头式的磁盘来说,IO调度器的请求重组要花费一定的CPU时间,但是对于SSD磁盘来说,这些重组IO请求的CPU时间可以节省下来,因为SSD提供了更智能的请求调度算法,不需要内核去画蛇添足。
1.3 调度器源码分析
首先要了解描述elevator的数据结构。和elevator相关的数据结构有个,一个是elevator_type,一个是elevator_queue,前者对应一个调度器类型,后者对应一个调度器实例,也就说如果内核中只有上述四种类型的调度器,则只有四个elevator_type,但是多个块设备(分区)可拥有多个相应分配器的实例,也就是elevator_queue。两个数据结构中最关键的元素都是struct elevator_ops,该结构定义了一组操作函数,用来描述请求队列的相关算法,实现对请求的处理
struct elevator_type
{
structlist_head list;
structelevator_ops ops;
structelv_fs_entry *elevator_attrs;
charelevator_name[ELV_NAME_MAX];
structmodule *elevator_owner;
};
及:
struct elevator_queue
{
structelevator_ops *ops;
void*elevator_data;
structkobject kobj;
structelevator_type *elevator_type;
structmutex sysfs_lock;
structhlist_head *hash;
};
函数elevator_init()用来为请求队列分配一个I/O调度器的实例
int elevator_init(struct request_queue *q, char*name)
{
structelevator_type *e = NULL;
structelevator_queue *eq;
int ret =0;
void*data;
/*初始化请求队列的相关元素*/
INIT_LIST_HEAD(&q->queue_head);
q->last_merge= NULL;
q->end_sector= 0;
q->boundary_rq= NULL;
/*下面根据情况在elevator全局链表中来寻找适合的调度器分配给请求队列*/
if (name){//如果指定了name,则寻找与name匹配的调度器
e =elevator_get(name);
if(!e)
return-EINVAL;
}
//如果没有指定io调度器,并且chosen_elevator存在,则寻找其指定的调度器
if (!e&& *chosen_elevator) {
e =elevator_get(chosen_elevator);
if(!e)
printk(KERN_ERR"I/O scheduler %s not found\n",
chosen_elevator);
}
//依然没获取到调度器的话则使用默认配置的调度器
if (!e) {
e =elevator_get(CONFIG_DEFAULT_IOSCHED);
if(!e) {//获取失败则使用最简单的noop调度器
printk(KERN_ERR
"DefaultI/O scheduler not found. " \
"Usingnoop.\n");
e= elevator_get("noop");
}
}
//分配并初始化elevator_queue
eq =elevator_alloc(q, e);
if (!eq)
return-ENOMEM;
//调用ops中的elevator_init_fn函数,针对调度器的队列进行初始化
data =elevator_init_queue(q, eq);
if (!data){
kobject_put(&eq->kobj);
return-ENOMEM;
}
//建立数据结构的关系
elevator_attach(q,eq, data);
returnret;
}
所有的I/O调度器类型都会通过链表链接起来(通过struct elevator_type中的list元素),elevator_get()函数便是通过给定的name,在链表中寻找与name匹配的调度器类型。当确定了I/O调度器的类型后,便要通过elevator_alloc()为等待队列分配一个调度器的实例--struct elevator_queue,并进行初始化;其后,由于每个调度器根据自身算法的不同,都会拥有不同的队列结构,在elevator_init_queue()中会调用特定于调度器的初始化函数针对这些队列进行初始化,并且返回特定于调度器的数据结构,最后再elevator_attach()中建立相关结构的关系。
static struct elevator_queue *elevator_alloc(structrequest_queue *q,
struct elevator_type *e)
{
structelevator_queue *eq;
int i;
//为eq分配内存
eq =kmalloc_node(sizeof(*eq), GFP_KERNEL | __GFP_ZERO, q->node);
if(unlikely(!eq))
gotoerr;
//根据之前确定的elevator_type初始化eq
eq->ops= &e->ops;
eq->elevator_type= e;
kobject_init(&eq->kobj,&elv_ktype);
mutex_init(&eq->sysfs_lock);
//分配elevator的哈希表内存
eq->hash= kmalloc_node(sizeof(struct hlist_head) * ELV_HASH_ENTRIES,
GFP_KERNEL,q->node);
if(!eq->hash)
gotoerr;
//初始化哈希表
for (i =0; i < ELV_HASH_ENTRIES; i++)
INIT_HLIST_HEAD(&eq->hash[i]);
return eq;
err:
kfree(eq);
elevator_put(e);
returnNULL;
}
及
static void *elevator_init_queue(structrequest_queue *q,
struct elevator_queue *eq)
{
returneq->ops->elevator_init_fn(q);
}
再:
static void elevator_attach(struct request_queue*q, struct elevator_queue *eq,
void *data)
{
q->elevator= eq;
eq->elevator_data= data;
}
下面就来看一下elevator_ops中定义了哪些操作:
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_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_queue_empty_fn *elevator_queue_empty_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_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;
void(*trim)(struct io_context *);
};
这里只关注几个主要的操作函数,其中前面加了*号的表示这些函数是每个调度器都必须实现的
elevator_merge_fn查询一个request,用于将bio并入
elevator_merge_req_fn将两个合并后的请求中多余的那个给删除
*elevator_dispatch_fn将调度器的队列最前面的元素取出,分派给request_queue中的请求队列以等候响应*
*elevator_add_req_fn将一个新的request添加进调度器的队列
elevator_queue_empty_fn检查调度器的队列是否为空
elevator_set_req_fn和elevator_put_req_fn分别在创建新请求和将请求所占的空间释放到内存时调用
*elevator_init_fn用于初始化调度器实例
一个请求在创建到销毁的过程遵循下面三种流程
set_req_fn->
i. add_req_fn -> (merged_fn ->)* -> dispatch_fn -> activate_req_fn-> (deactivate_req_fn -> activate_req_fn ->)* -> completed_req_fn
ii. add_req_fn-> (merged_fn ->)* -> merge_req_fn iii. [none] ->put_req_fn
在分析调度器的实现时,不妨也以此为依据,选择i或者ii来作为分析的流程。
1.4 NOOP源码分析
Noop调度器的实现非常简单,其主要完成了一个elevator request queue,这个request queue没有进行任何的分类处理,只是对输入的request进行简单的队列操作。但是,需要注意的是,虽然Noop没有做什么事情,但是elevator还是对bio进行了后向合并,从而最大限度的保证相邻的bio得到合并处理。Noop调度器实现了elevator的基本接口函数,并将这些函数注册到linux系统的elevator子系统中。
需要注册到elevator子系统中的基本接口函数声明如下:
static struct elevator_type elevator_noop = {
.ops = {
/* 合并两个request */
.elevator_merge_req_fn = noop_merged_requests,
/* 调度一个合适的request进行发送处理 */
.elevator_dispatch_fn = noop_dispatch,
/* 将request放入调度器的queue中*/
.elevator_add_req_fn = noop_add_request,
/* 获取前一个request */
.elevator_former_req_fn = noop_former_request,
/* 获取后一个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,
};
Noop调度器使用的管理数据:
struct noop_data {
structlist_head queue;
};
noop_init_queue如下:
static void *noop_init_queue(struct request_queue*q)
{
structnoop_data *nd;
//为noop调度器使用的数据结构分配内存
nd = kmalloc_node(sizeof(*nd),GFP_KERNEL, q->node);
if (!nd)
returnNULL;
//初始化noop调度器使用的队列
INIT_LIST_HEAD(&nd->queue);
return nd;
}
由于 Noop 调度器没有对 request 进行任何的分类处理、调度,因此上述这些函数的实现都很简单。例如,当调度器需要发送 request 时,会调用 noop_dispatch 。该函数会直接从调度器所管理的 request queue 中获取一个 request ,然后调用 elv_dispatch_sort 函数将请求加入到设备所在的request queue 中。 Noop dispatch 函数实现如下:
static int noop_dispatch(struct request_queue *q,int force)
{
struct noop_data *nd =q->elevator->elevator_data;
if (!list_empty(&nd->queue)) {
struct request *rq;
/* 从调度器的队列头中获取一个request */
rq = list_entry(nd->queue.next, struct request,queuelist);
list_del_init(&rq->queuelist);
/* 将获取的request放入到设备所属的request queue中 */
elv_dispatch_sort(q, rq);
return 1;
}
return 0;
}
当需要往 noop 调度器中放入 request 时,可以调用 noop_add_request ,该函数的实现及其简单,就是将 request 挂入调度器所维护的 request queue 中。 Noop_add_request 函数实现如下:
static void noop_add_request(struct request_queue*q, struct request *rq)
{
struct noop_data *nd =q->elevator->elevator_data;
/* 将request挂入noop调度器的request queue */
list_add_tail(&rq->queuelist,&nd->queue);
}
由此可见, noop 调度器的实现是很简单的,仅仅实现了一个调度器的框架,用一条链表把所有输入的 request 管理起来。
1.5 简单调度器实现
通过 noop 调度器的例子,我们可以了解到实现一个调度器所需要的基本结构:
/* 包含基本的头文件 */
#include <linux/blkdev.h>
#include <linux/elevator.h>
#include <linux/bio.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/init.h>
/* 定义调度器所需要的数据结构,一条管理request的队列是必须的 */
struct noop_data {
structlist_head queue;
};
/* 实现调度器的接口函数 */
static struct elevator_type elevator_noop = {
.ops = {
/* 调度器的功能函数 */
.elevator_merge_req_fn = noop_merged_requests,
……
/* 初始化/注销调度器,通常在下面这些函数初始化调度器内部的一些数据结构,例如noop_data */
.elevator_init_fn = noop_init_queue,
.elevator_exit_fn = noop_exit_queue,
},
.elevator_name = "noop",
.elevator_owner = THIS_MODULE,
};
/* 注册调度器 */
static int __init noop_init(void)
{
elv_register(&elevator_noop);
return 0;
}
/* 销毁调度器 */
static void __exit noop_exit(void)
{
elv_unregister(&elevator_noop);
}
/* 模块加载时调用noop_init */
module_init(noop_init);
/* 模块退出时调用noop_exit */
module_exit(noop_exit);