块设备层request plug/unplug机制

本文转发自:http://blog.chinaunix.net/uid-14528823-id-4778396.html

经过了删减,删除了一些不必要的细节,让文章更可读。

一、基本原理
Linux块设备层使用了plug/unplug(蓄流/泄流)的机制来提升IO吞吐量。基本原理为:当IO请求提交时,不知直接提交给底层驱动,而是先将其放入一个队列中(相当于水池),待一定时机或周期后再将该队列中的请求统一下发。将请求放入队列的过程即plug(蓄流)过程,统一下发请求的过程即为unplug(泄流)过程。每个请求在队列中等待的时间不会太长,通常在ms级别。
如此设计,可以增加IO合并和排序的机会,便于提升磁盘访问效率。

二、plug
1、基本流程
从mapping层提交到块设备层的io请求为bio,bio会在块设备进行合并,并生成新的request,并经过IO调度(排序和合并)之后下发到底层。下发request时,通过请求队列的make_request_fn接口,其中实质为将请求放入per task的plug队列,当队列满或在进行调度时(schedule函数中)会根据当前进程的状态将该队列中的请求flush到派发队列中,并触发unplug(具体流程后面介绍)。

per task的plug队列:新内核版本中实现的机制。IO请求提交时先链入此队列,当该队列满时(>BLK_MAX_REQUEST_COUNT),会flush到相应设备的请求队列中(request_queue)。
优点:per task维护plug队列,可以避免频繁对设备的请求队列操作导致的锁竞争,能提升效率。

2、plug基本代码流程如下:
submit_bio->
    generic_make_request->
        make_request->
            blk_queue_bio->
                list_add_tail(&req->queuelist, &plug->list);//将请求加入plug队列


三、unplug
unplug分同步unplug和异步unplug两种方式。
同步unplug即当即通过调用blk_run_queue对下发请求队列中的情况。
异步unplug,通过唤醒kblockd工作队列来对请求队列中的请求进行下发。

1、kblockd工作队列的初始化:
1) 分配工作队列
主要代码流程:

blk_dev_init ->
    alloc_workqueue //分配工作队列

2)初始化工作队列
blk_alloc_queue_node():

/*在指定node上分配请求队列*/

struct request_queue *blk_alloc_queue_node(gfp_t gfp_mask, int node_id)

{

struct request_queue *q;

int err;

/*分配请求队列需要的内存,从slab中分配,并初始化为0*/

q = kmem_cache_alloc_node(blk_requestq_cachep,

gfp_mask | __GFP_ZERO, node_id);

if (!q)

return NULL;

...

/*初始化delay_work,用于在kblockd中异步unplug请求队列*/

INIT_DELAYED_WORK(&q->delay_work, blk_delay_work);

...

}

2、kblockd工作队列的工作内容
kblockd工作队列的工作内容有由blk_delay_work()函数实现,主要就是调__blk_run_queue进行unplug请求队列。

/*IO请求队列的delay_work,用于在kblockd中异步unplug请求队列*/

static void blk_delay_work(struct work_struct *work)

{

struct request_queue *q;

/*获取delay_work所在的请求队列*/

q = container_of(work, struct request_queue, delay_work.work);

spin_lock_irq(q->queue_lock);

/*直接run queue,最终调用request_fn对队列中的请求逐一处理*/

__blk_run_queue(q);

spin_unlock_irq(q->queue_lock);

}

2、unplug机制
内核中设计了两种unplug机制:
1)调度时进行unplug(异步方式)
当发生内核调度时,当前进程sleep前,先将当前task的plug列表中的请求flush到派发队列中,并进行unplug。
主要代码流程如下:

queue_unplugged()的代码如下:

/*unplug请求队列,plug相当于蓄水,将请求放入池子(请求队列)中,unplug相当于放水,即开始调用请求队列的request_fn(scsi_request_fn)来处理请求队列中的请求,将请求提交到scsi层(块设备驱动层)*/

static void queue_unplugged(struct request_queue *q, unsigned int depth,

   bool from_schedule)

__releases(q->queue_lock)

{

trace_block_unplug(q, depth, !from_schedule);

/*调用块设备驱动层提供的request_fn接口处理请求队列中的请求,分异步和同步两种情况。*/

if (from_schedule)

    /*异步unplug,即通过kblockd工作队列来处理,该工作队列定期唤醒(5s),通过这种方式可以控制流量,提高吞吐量*/

    blk_run_queue_async(q);

else

    /*同步unplug,即直接调用设备驱动层提供的request_fn接口处理请求队列中的请求*/

    __blk_run_queue(q);

spin_unlock(q->queue_lock);

}

blk_run_queue_async()的代码如下:

/*异步unplug,即通过kblockd工作队列来处理,该工作队列定期唤醒(5s),通过这种方式可以控制流量,提高吞吐量*/

void blk_run_queue_async(struct request_queue *q)

{

if (likely(!blk_queue_stopped(q) && !blk_queue_dead(q)))

    /*唤醒kblockd相关的工作队列,进行unplug处理,注意:这里的delay传入0表示立刻唤醒,kblockd对应的处理接口为:blk_delay_work*/

    mod_delayed_work(kblockd_workqueue, &q->delay_work, 0);

}

2)提交IO请求时(make_request)进行unplug
提交IO请求时(make_request),先将请求提交时先链入此队列,当该队列满时(>BLK_MAX_REQUEST_COUNT),会flush到相应设备的请求队列中(request_queue)。
主要代码流程为:

普通块设备的make_request接口在3.10内核版本中被设置为blk_queue_bio,相应代码分析如下:

/*在submit_bio中被调用,用于合并bio,并提交请求(request),请求提交到per task的plug list中*/

void blk_queue_bio(struct request_queue *q, struct bio *bio)

{

    ...

    /*尝试将bio合并到request中*/

    if (blk_attempt_plug_merge(q, bio, &request_count))

        return;


    spin_lock_irq(q->queue_lock);


    el_ret = elv_merge(q, &req, bio);

    /*向后合并*/

    if (el_ret == ELEVATOR_BACK_MERGE) {

        if (bio_attempt_back_merge(q, req, bio)) {

            elv_bio_merged(q, req, bio);

            ...

        }

    /*向前合并*/

    } else if (el_ret == ELEVATOR_FRONT_MERGE) {

        if (bio_attempt_front_merge(q, req, bio)) {

            elv_bio_merged(q, req, bio);

           ...

        }

    }

/*不能合并,需要新建request来处理bio*/

get_rq:

    /*

     * This sync check and mask will be re-done in init_request_from_bio(),

     * but we need to set it earlier to expose the sync flag to the

     * rq allocator and io schedulers.

     */

    rw_flags = bio_data_dir(bio);

    /*判断是否需要sync,即直接将IO请求unplug(提交到块设备驱动层),不用等待kblockd来定期plug*/

    if (sync)

        rw_flags |= REQ_SYNC;


    /*从请求队列中取一个request*/

    req = get_request(q, rw_flags, bio, GFP_NOIO);

    if (unlikely(!req)) {

        bio_endio(bio, -ENODEV);    /* @q is dead */

        goto out_unlock;

    }


    /*将bio加入新的request中*/

    init_request_from_bio(req, bio);


    if (test_bit(QUEUE_FLAG_SAME_COMP, &q->queue_flags))

        req->cpu = raw_smp_processor_id();


    plug = current->plug;

    /*如果有plug,则将请求加入到plug的list中,如果没有则直接调用__blk_run_queue提交请求*/

    if (plug) {

        if (list_empty(&plug->list))

            trace_block_plug(q);

        else {/*如果请求队列中的请求数超过了限值,则先unplug?*/

            if (request_count >= BLK_MAX_REQUEST_COUNT) {

                blk_flush_plug_list(plug, false);

                trace_block_plug(q);

            }

        }

        /*把请求加入到plug的list中,当plug的list满了后(>BLK_MAX_REQUEST_COUNT),会flush到相应设备的请求队列中(request_queue)*/

        list_add_tail(&req->queuelist, &plug->list);

        blk_account_io_start(req, true);

    } else {

        spin_lock_irq(q->queue_lock);

        add_acct_request(q, req, where);

        /*如果没有plug控制,最终调用此接口处理队列中的请求,最终会调用请求队列的request_fn接口处理请求*/

        __blk_run_queue(q);

out_unlock:

        spin_unlock_irq(q->queue_lock);

    }

}


长按识别二维码,了解更多Linux姿势


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值