塊設備層request plug/unplug機制

轉載至博客:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=14528823&id=4778396

一、基本原理

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;
 
 
if (percpu_counter_init(&q->mq_usage_counter, 0))
goto fail_q;
 
 
q->id = ida_simple_get(&blk_queue_ida, 0, 0, gfp_mask);
if (q->id < 0)
goto fail_c;
 
 
q->backing_dev_info.ra_pages =
(VM_MAX_READAHEAD * 1024) / PAGE_CACHE_SIZE;
q->backing_dev_info.state = 0;
q->backing_dev_info.capabilities = BDI_CAP_MAP_COPY;
q->backing_dev_info.name = "block";
q->node = node_id;
 
 
err = bdi_init(&q->backing_dev_info);
if (err)
goto fail_id;
/*設置laptop模式下的定時器*/
setup_timer(&q->backing_dev_info.laptop_mode_wb_timer,
   laptop_mode_timer_fn, (unsigned long) q);
/*
 * 關鍵點:設置請求隊列的超時定時器,默認超時時間爲30s,當30s內IO請求未完成時,定時器到期,
 * 進行重試或錯誤處理。這是IO 錯誤處理架構中的關鍵點之一,在內核老版本中(2.6.38?),該定時器
 * 是在scsi中間層定義的,新版本中將其上移至塊設備層。Fixme:爲何要這樣?*/
setup_timer(&q->timeout, blk_rq_timed_out_timer, (unsigned long) q);
/*初始化各個隊列*/
INIT_LIST_HEAD(&q->queue_head);
INIT_LIST_HEAD(&q->timeout_list);
INIT_LIST_HEAD(&q->icq_list);
#ifdef CONFIG_BLK_CGROUP
INIT_LIST_HEAD(&q->blkg_list);
#endif
INIT_LIST_HEAD(&q->flush_queue[0]);
INIT_LIST_HEAD(&q->flush_queue[1]);
INIT_LIST_HEAD(&q->flush_data_in_flight);
/*初始化delay_work,用於在kblockd中異步unplug請求隊列*/
INIT_DELAYED_WORK(&q->delay_work, blk_delay_work);
 
 
kobject_init(&q->kobj, &blk_queue_ktype);
 
 
mutex_init(&q->sysfs_lock);
spin_lock_init(&q->__queue_lock);
 
 
/*
* By default initialize queue_lock to internal lock and driver can
* override it later if need be.
*/
q->queue_lock = &q->__queue_lock;
 
 
/*
* A queue starts its life with bypass turned on to avoid
* unnecessary bypass on/off overhead and nasty surprises during
* init. The initial bypass will be finished when the queue is
* registered by blk_register_queue().
*/
q->bypass_depth = 1;
__set_bit(QUEUE_FLAG_BYPASS, &q->queue_flags);
 
 
init_waitqueue_head(&q->mq_freeze_wq);
 
 
if (blkcg_init_queue(q))
goto fail_id;
 
 
return q;
 
 
fail_id:
ida_simple_remove(&blk_queue_ida, q->id);
fail_c:
percpu_counter_destroy(&q->mq_usage_counter);
fail_q:
kmem_cache_free(blk_requestq_cachep, q);
return NULL;
}


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。
主要代碼流程如下:

schedule->
    sched_submit_work ->
        blk_schedule_flush_plug()->
            blk_flush_plug_list(plug, true) ->注意:這裏傳入的from_schedule參數爲true,表示將觸發異步unplug,即喚醒kblockd工作隊列來進行unplug操作。後續的kblockd的喚醒週期在塊設備驅動中設置,比如scsi中設置爲3ms。
                queue_unplugged->
                    blk_run_queue_async

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


scsi_request_fn()://scsi塊設備驅動的request_fn()接口,其中當scsi命令下發失敗時,會重設kblockd,延遲unplug請求隊列。
/*異步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)。
主要代碼流程爲:

submit_bio->
    generic_make_request->
        make_request->
            blk_queue_bio->
                blk_flush_plug_list(plug, false) ->注意:這裏傳入的from_schedule參數爲false,表示將觸發同步unplug,即當即下發請求。
                    queue_unplugged->
                        blk_run_queue_async ->
                            __blk_run_queue

普通塊設備的make_request接口在3.10內核版本中被設置爲blk_queue_bio,相應代碼分析如下:
/*異步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);
}

參考博客:
https://blog.csdn.net/g382112762/article/details/53221272

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章