塊設備驅動之I/O調度層之調度算法


  上一節主要梳理了下調度器的流程,其中對於blk_init_allocated_queue有一個elevator_init的初始化函數,下面從elevator_init為突破口,來看看內核中有哪些的調度算法。對於elevator_init其主要用來為請求隊列分配一個I/O調度器

int elevator_init(struct request_queue *q, char *name)
{
    struct elevator_type *e = NULL;
    int err;

    /*
     * q->sysfs_lock must be held to provide mutual exclusion between
     * elevator_switch() and here.
     */
    lockdep_assert_held(&q->sysfs_lock);        //檢查當前進程是否已經設置了調度標志 if (unlikely(q->elevator))
        return 0;

    INIT_LIST_HEAD(&q->queue_head);           //初始化請求隊列的相關元素
    q->last_merge = NULL;
    q->end_sector = 0;
    q->boundary_rq = NULL;

/*下面根據情況在elevator全局鏈表中來尋找適合的調度器分配給請求隊列*/  
if (name) { e = elevator_get(name, true);      //如果指定了name,則尋找與name匹配的調度器 if (!e) return -EINVAL; }

/*如果沒有指定io調度器,並且chosen_elevator存在,則尋找其指定的調度器*/ if (!e && *chosen_elevator) { e = elevator_get(chosen_elevator, false); if (!e) printk(KERN_ERR "I/O scheduler %s not found\n", chosen_elevator); } /*依然沒獲取到調度器的話則使用默認配置的調度器*/ if (!e) { e = elevator_get(CONFIG_DEFAULT_IOSCHED, false); if (!e) { printk(KERN_ERR "Default I/O scheduler not found. " \ "Using noop.\n"); e = elevator_get("noop", false);        //獲取失敗則使用最簡單的noop調度器 } } err = e->ops.elevator_init_fn(q, e); if (err) elevator_put(e); return err; }

所有的I/O調度器類型都會通過鏈表鏈接起來(通過struct elevator_type中的list元素),elevator_get()函數便是通過給定的name,在鏈表中尋找與name匹配的調度器類型。當確定了I/O調度器的類型后,就會調用對應的elevator_init_fn,由於每個調度器根據自身算法的不同,都會擁有不同的隊列結構,在elevator_init_fn()中會調用特定於調度器的初始化函數針對這些隊列進行初始化,並且返回特定於調度器的數據結構,現在內核提供有三種調度的算法

1). noop-iosched

2). cfq-iosched.c

3). deadline-iosched.c

 那么我們針對這三種調度算法來看看其實現的基本原理

1. noop調度算法

看看其初始化是通過elv_register注冊了一個elevator_noop結構,下面來看看noop的初始化都做了一些什么?

static int noop_init_queue(struct request_queue *q, struct elevator_type *e)
{
    struct noop_data *nd;
    struct elevator_queue *eq;

    eq = elevator_alloc(q, e);                   //為等待隊列分配一個調度器的實例 if (!eq)
        return -ENOMEM;

    nd = kmalloc_node(sizeof(*nd), GFP_KERNEL, q->node);   //從通用緩沖區中分配一個屬於指定 NUMA 節點的對象     
if (!nd) { kobject_put(&eq->kobj); return -ENOMEM; } eq->elevator_data = nd;                     INIT_LIST_HEAD(&nd->queue);                  //加入鏈表 spin_lock_irq(q->queue_lock);           q->elevator = eq;                        //將調度器賦值給等待隊列,以方便后續使用 spin_unlock_irq(q->queue_lock); return 0; }

這個只是主要是將分配的調度器賦予等待隊列,那么看看這個調度方法最核心的數據結構

struct noop_data {
    struct list_head queue;
};

從結構來看只有一個成員queue,其實就noop中維護的一個fifo(先進先出)鏈表的鏈表頭,猜想noop對於調度的處理一個對於基本鏈表的處理方式,就是一個鏈表的當io請求過來了,就會被加入到這個鏈表的后面,在鏈表前面的就會被移到系統的請求隊列(request_queue)中。下面結合代碼看看,整個處理流程。

static void noop_add_request(struct request_queue *q, struct request *rq)
{
   struct noop_data *nd = q->elevator->elevator_data;
   list_add_tail(&rq->queuelist, &nd->queue);
}

當調度器需要發送request時,會調用noop_dispatch。該函數會直接從調度器所管理的request queue中獲取一個request,然后調用elv_dispatch_sort函數將請求加入到設備所在的request queue中,最后

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;
        rq = list_entry(nd->queue.next, struct request, queuelist);    //從調度器的隊列頭中獲取一個request         list_del_init(&rq->queuelist);                     //將獲取到的節點(node)從鏈表中刪掉
elv_dispatch_sort(q, rq);                        //剛取出的rq放入到系統的請求隊列
return 1; } return 0; }

 由此可見,noop調度器的實現是很簡單的,僅僅實現了一個調度器的框架,用一條鏈表把所有輸入的request管理起來,簡單方便,不會陷入極端,並且也不會損失多少性能,還能帶來一定額實時性,但是缺點也非常明顯,沒有對io進行排序,沒有參與調度,對於一些機械式的訪問有明顯的不足之處。

2.  deadline調度算法

從noop的調度來看,缺少優化和調度,那么deadline是如何來處理調度算法呢?從noop的分析過程來看,其數據結構決定了其方法,那么首先來看看deadline數據結構

struct deadline_data {
    struct rb_root sort_list[2];            //采用紅黑樹管理所有的request,請求地址作為索引值 struct list_head fifo_list[2];         //采用FIFO隊列管理所有的request,所有請求按照時間先后次序排列 struct request *next_rq[2];           //批量處理請求過程中,需要處理的下一個request
    unsigned int batching;                 //統計當前已經批量處理完成的request
    sector_t last_sector;                  /* head position */
    unsigned int starved;                  /* times reads have starved writes */

    int fifo_expire[2];               //讀寫請求的超時時間值  int fifo_batch;                  //批量處理的request數量  int writes_starved;               //寫飢餓值 int front_merges;
};

從其內容來看,這個比noop的調度復雜好幾倍,還引入了紅黑樹縮短查找時間,通過noop的elevator_init_fn我們大致可以看出會做些什么操作?基本都類似,對於deadline會多一些數據結構初始化的操作,所以沒有分析的必要,我們主要關注其差異,那么deadline是如何處理請求,加入到隊列中呢?

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);                        //請求加入到deadline調度器的sort_list紅黑樹中 /*
     * 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]);      //將請求加入deadline調度器的list_fifo鏈表中 }

一種是采用紅黑樹(RB tree)的方式將所有request組織起來,通過request的訪問地址作為索引;另一種方式是采用隊列的方式將request管理起來,所有的request采用先來后到的方式進行排序,即FIFO隊列。各個請求被放入到隊列后,那么該輪到合並出場了。

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);                  //取bio的最后一個扇區 

        __rq = elv_rb_find(&dd->sort_list[bio_data_dir(bio)], sector);   //從紅黑樹中查找起始扇區號與sector相同的request  if (__rq) {
            BUG_ON(sector != blk_rq_pos(__rq));

            if (elv_rq_merge_ok(__rq, bio)) {                  //各項屬性的檢查,確定bio可以插入                 ret = ELEVATOR_FRONT_MERGE;
                goto out;
            }
        }
    }

    return ELEVATOR_NO_MERGE;
out:
    *req = __rq;
    return ret;
}

那么通過上面的函數可能改變紅黑樹的結構,deadline_merged_request進行bio插入的善后工作,所以要將節點刪除再重新進行插入

static void deadline_merged_request(struct request_queue *q,
                    struct request *req, int type)
{
    struct deadline_data *dd = q->elevator->elevator_data;

    /*
     * if the merge was a front merge, we need to reposition request
     */
    if (type == ELEVATOR_FRONT_MERGE) {
        elv_rb_del(deadline_rb_root(dd, req), req);            //將request從紅黑樹中刪除 
        deadline_add_rq_rb(dd, req);                     //重新添加至紅黑樹
    }
}

上面完成隊列的合並后,該輪到調度登場了,deadline_dispatch_requests完成這份工作。

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]);        //確定讀fifo的狀態   const int writes = !list_empty(&dd->fifo_list[WRITE]);       //確定讀fifo的狀態 struct request *rq;
    int data_dir;

    /*
     * batches are currently reads XOR writes
     */
/* 如果批量請求處理存在,並且還沒有達到批量請求處理的上限值,那么繼續請求的批量處理 */
  if (dd->next_rq[WRITE]) rq = dd->next_rq[WRITE]; else rq = dd->next_rq[READ]; if (rq && dd->batching < dd->fifo_batch) /* we have a next request are still entitled to batch */ goto dispatch_request; /* * at this point we are not running a batch. select the appropriate * data direction (read / write) */ /* 優先處理讀請求隊列 */ if (reads) {                            //讀請求fifo不為空 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; } /* * there are either no reads or writes have been starved */ 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: /* * we are not running a batch, find best request for selected data_dir */ if (deadline_check_fifo(dd, data_dir) || !dd->next_rq[data_dir]) { /* 如果請求隊列中存在即將餓死的request,或者不存在需要批量處理的請求,那么從FIFO隊列頭獲取一個request */
/* * A deadline has expired, the last request was in the other * direction, or we have run out of higher-sectored requests. * Start again from the request with the earliest expiry time. */ rq = rq_entry_fifo(dd->fifo_list[data_dir].next); } else { /* 繼續批量處理,獲取需要批量處理的下一個request */
/* * The last req was the same dir and we have a next request in * sort order. No expired requests so continue on from here. */ rq = dd->next_rq[data_dir]; } dd->batching = 0; /* 將request從調度器中移出,發送至設備 */ dispatch_request: /* * rq is the selected appropriate request. */ dd->batching++; deadline_move_request(dd, rq); return 1; }

 deadline調度算法相對noop要復雜一點,其設計目標是,在保證請求按照設備扇區的順序進行訪問的同時,兼顧其它請求不被餓死,要在一個最終期限前被調度到,同時增加了讀操作的具有有限度。

3. CFQ調度算法

暫不分析,待續....


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM