[IO系统]16 IO调度器-NOOP

        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);

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YoungerChina

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值