linux io的cfq代碼理解一


內核版本:

3.10內核。

CFQ,即Completely Fair Queueing絕對公平調度器,原理是基於時間片的角度去保證公平,其實如果一台設備既有單隊列,又有多隊列,既有快速的NVME,又有慢速的sas,各個磁盤都配置為CFQ的話,那么這個Completely Fair 明顯無法保證,可能會演變為Completely unFair 。所以nvme的盤,一般使用的是noop策略,因為一定時間之內的io,可能會下發很多給快速設備,也可能下發很少給慢速設備,這樣就無公平可言了,吞吐量也不行。

IO調度的原理:

看到io調度的時候,一定要想為什么需要io調度,調度其實就是封裝一層,管理下發的io request,由於存儲介質不同,需要的調度算法肯定也不一樣,IO調度就是在實時性和io吞吐上的一個折中,不管采用什么的調度算法,調度的目的是明確的,就是通過io的合並或者重排,提高硬件設備的性能,不管是虛擬的硬件設備還是實際的硬件設備。

單隊列和多隊列

和網卡的發展一樣,硬盤設備也有單隊列和多隊列的區別,單隊列有一個明顯的弱點,就是所有的io都最終匯聚到一個隊列,這個面對目前多核的設備,性能很難發揮。

優先級:

      每個進程都會有IO優先級,這個和進程的調度優先級類似。CFQ調度器將會將其作為考慮的因素之一,來確定該進程(或者說cggroup進程組)的請求隊列何時可以獲取塊設備的使用權。IO優先級從高到低可以分為三大類:RT(real time),BE(best effort),IDLE(idle),其中RT和BE又可以再划分為8個子優先級。實際上,我們已經知道CFQ調度器的公平是針對於進程而言的,而只有同步請求(read或syn write)才是針對進程而存在的,他們會放入進程自身的請求隊列,而所有同優先級的異步請求,無論來自於哪個進程,都會被放入公共的隊列,異步請求的隊列總共有8(RT)+8(BE)+1(IDLE)=17個。

調度器:

cfq是各種io調度器中的一種,其實就是實例化了調度器的類。可以和qdisc類比,就比較清晰了。

static struct elevator_type iosched_cfq = {
    .ops = {
        .elevator_merge_fn =         cfq_merge,---------------request和bio合並的時候調用
        .elevator_merged_fn =        cfq_merged_request,------看到這個成員的ed沒?這個merged是指已經合並了的request
        .elevator_merge_req_fn =    cfq_merged_requests,------這個是兩個request合並的時候調用
        .elevator_allow_merge_fn =    cfq_allow_merge,--------request和bio能否合並的時候調用
        .elevator_bio_merged_fn =    cfq_bio_merged,
        .elevator_dispatch_fn =        cfq_dispatch_requests,---用當前的request來填充dispatch queue,一旦dispatch,調度器就不能控制這些io請求了,
        .elevator_add_req_fn =        cfq_insert_request,-------增加一個新請求到調度器
        .elevator_activate_req_fn =    cfq_activate_request,----這個相當於讓block層看到這個request,
        .elevator_deactivate_req_fn =    cfq_deactivate_request,---這個就是block驅動再推遲這個請求
        .elevator_completed_req_fn =    cfq_completed_request,----req結束之后調用
        .elevator_former_req_fn =    elv_rb_former_request,-------查找給定請求的前一個請求
        .elevator_latter_req_fn =    elv_rb_latter_request,-------查找給定請求的后一個請求,在進行request合並時很有用
        .elevator_init_icq_fn =        cfq_init_icq,--------icq的構造函數
        .elevator_exit_icq_fn =        cfq_exit_icq,--------icq的析構函數
        .elevator_set_req_fn =        cfq_set_request,------創建請求時填充req的一些必要字段
        .elevator_put_req_fn =        cfq_put_request,------釋放請求時填充req的一些必要字段和處理
        .elevator_may_queue_fn =    cfq_may_queue,----如果調度器允許在超過queue的limit情況下依然插入請求,return true
        .elevator_init_fn =        cfq_init_queue,----隊列初始化時調用,相當於構造函數
        .elevator_exit_fn =        cfq_exit_queue,----隊列釋放時調用,相當於析構函數
    },
    .icq_size    =    sizeof(struct cfq_io_cq),
    .icq_align    =    __alignof__(struct cfq_io_cq),
    .elevator_attrs =    cfq_attrs,
    .elevator_name    =    "cfq",
    .elevator_owner =    THIS_MODULE,
};

這里只展示了elevator_type的部分字段,比如還有些字段是將所有的elevator_type串接起來的,elevator_attrs里面存放某個elevator的屬性,設計這個
指針的原因是各種elevator的參數配置都不一樣,這樣各種elevator都可以統一了。

iosched_cfq = $1 = {
icq_cache = 0xffff880135ad1000,
ops = {
elevator_merge_fn = 0xffffffff81323480 <cfq_merge>,
elevator_merged_fn = 0xffffffff81324280 <cfq_merged_request>,
elevator_merge_req_fn = 0xffffffff81324a60 <cfq_merged_requests>,
elevator_allow_merge_fn = 0xffffffff81323830 <cfq_allow_merge>,
elevator_bio_merged_fn = 0xffffffff81321b60 <cfq_bio_merged>,
elevator_dispatch_fn = 0xffffffff81326960 <cfq_dispatch_requests>,
elevator_add_req_fn = 0xffffffff81329250 <cfq_insert_request>,
elevator_activate_req_fn = 0xffffffff81323b20 <cfq_activate_request>,
elevator_deactivate_req_fn = 0xffffffff81323950 <cfq_deactivate_request>,
elevator_completed_req_fn = 0xffffffff81328840 <cfq_completed_request>,
elevator_former_req_fn = 0xffffffff812fbcd0 <elv_rb_former_request>,
elevator_latter_req_fn = 0xffffffff812fbd00 <elv_rb_latter_request>,
elevator_init_icq_fn = 0xffffffff81321bb0 <cfq_init_icq>,
elevator_exit_icq_fn = 0xffffffff81326770 <cfq_exit_icq>,
elevator_set_req_fn = 0xffffffff813283e0 <cfq_set_request>,
elevator_put_req_fn = 0xffffffff813264a0 <cfq_put_request>,
elevator_may_queue_fn = 0xffffffff81322fd0 <cfq_may_queue>,
elevator_init_fn = 0xffffffff813276c0 <cfq_init_queue>,
elevator_exit_fn = 0xffffffff81327a20 <cfq_exit_queue>
},
icq_size = 120,
icq_align = 8,
elevator_attrs = 0xffffffff81ac2280 <cfq_attrs>,
elevator_name = "cfq\000\000\000\000\000\000\000\000\000\000\000\000",
elevator_owner = 0x0,
icq_cache_name = "cfq_io_cq\000\000\000\000\000\000\000\000\000\000\000",
list = {
next = 0xffffffff81abf9b0 <elv_list>,
prev = 0xffffffff81ac20a8 <iosched_deadline+232>
}
}

關鍵數據結構:

兩大關鍵結構,一個是cfq_data,一個是cfq_queue,cfq_data是管理和調度cfq_queue的結構,而cfq_queue是管理io request的結構,cfq_data通過選擇cfq_queue來下發對應的io request 到設備的request_queue中,達到調度的目的,在引入了io的cggroup之后,cfq_data通過選擇cfq_group來先確定服務的對象,而cfq_group中的用紅黑樹管理的cfq_queue為二級對象,cfq_queue中紅黑樹管理的request為三級對象。

cfq_data是一個per block 結構,也就是每個使用cfq調度策略的塊設備有一個這個結構對應,用來管理下發給這個block設備的請求。cfq_data的指針是作為一個blkio_cgroup的哈希表的key,而對應的value則是cfq_group;同時也是io_context的radix_tree的一個key,對應的value是cfq_io_context,這樣cfq_data,blkio_group和cfq_io_context就一一對應起來了。通過

static void cfq_insert_request(struct request_queue *q, struct request *rq)
{
struct cfq_data *cfqd = q->elevator->elevator_data;

這個函數可以看出,cfqd是在request_queue->elevator->elevator_data中保存。

而request和對應的cfq_queue以及cfqg關聯是通過如下:

#define RQ_CFQQ(rq)        (struct cfq_queue *) ((rq)->elv.priv[0])
#define RQ_CFQG(rq)        (struct cfq_group *) ((rq)->elv.priv[1])

下面詳細描述cfq_dtata:

/*
 * Per block device queue structure
 */
struct cfq_data {
    struct request_queue *queue;--------------指向塊設備對應的request_queue
    /* Root service tree for cfq_groups */
    struct cfq_rb_root grp_service_tree;------所有待調度的cfq_group都被加入到該紅黑樹,管理cfq_group的rb_node成員。
    struct cfq_group *root_group;---所有cggroup的根cgroup

    /*
     * The priority currently being served
     */
    enum wl_class_t serving_wl_class;---------------當前服務的BE_WORKLOAD = 0,RT_WORKLOAD = 1,IDLE_WORKLOAD = 2,CFQ_PRIO_NR=3,
    enum wl_type_t serving_wl_type;-----------------當前服務的workload_type,可能的值為async_workload,sync_NOidle_workload,sync_workload
    unsigned long workload_expires;
    struct cfq_group *serving_group;----------------當前服務的cfq_group

    /*
     * Each priority tree is sorted by next_request position.  These
     * trees are used when determining if two or more queues are
     * interleaving requests (see cfq_close_cooperator).
     */
    struct rb_root prio_trees[CFQ_PRIO_LISTS];----CFQ_PRIO_LISTS為8,8個優先級的紅黑樹,所有優先級為rt或be的進程的同步請求隊列,都會根據優先級添加到對應的紅黑樹

    unsigned int busy_queues;------------該cfqd中有多少個隊列在等待調度
    unsigned int busy_sync_queues;-------該cfqd中有多少個同步隊列在等待調度

    int rq_in_driver;---------在驅動中,也就是調度層已經下發,但還沒有收到結果的request數
    int rq_in_flight[2];------已經下發還未收到結果的同步和異步的io個數

    /*
     * queue-depth detection
     */
    int rq_queued;-----------------queue深度的一些成員,這個是積壓的request的個數
    int hw_tag;
    /*
     * hw_tag can be
     * -1 => indeterminate, (cfq will behave as if NCQ is present, to allow better detection)
     *  1 => NCQ is present (hw_tag_est_depth is the estimated max depth)
     *  0 => no NCQ
     */
    int hw_tag_est_depth;
    unsigned int hw_tag_samples;

    /*
     * idle window management
     */
    struct timer_list idle_slice_timer;
    struct work_struct unplug_work;

    struct cfq_queue *active_queue;--------------指向當前服務的隊列,因為cfq_data是每個使用cfq調度算法的設備的數據結構,對每個時刻而言,每個cfq_data只服務一個cfq_queue
    struct cfq_io_cq *active_cic;----------------當前活動的io上下文,和active_queue一樣,都算是緩存

    /*
     * async queue for each priority case
     */
    struct cfq_queue *async_cfqq[2][IOPRIO_BE_NR];---對應RT和BE優先級類的各8個異步請求隊列,加起來16個
    struct cfq_queue *async_idle_cfqq;---------------對應優先級類別為IDLE的異步請求隊列----加上前面16個就是17個

    sector_t last_position;

    /*
     * tunables, see top of file
     */
    unsigned int cfq_quantum;-------------用於計算在一個隊列的時間片內,最多發放多少個請求到底層的塊設備, cfq_quantum = 8;
    unsigned int cfq_fifo_expire[2];------同步、異步請求的響應期限時間,cfq_fifo_expire[2] = { HZ / 4, HZ / 8 };
    unsigned int cfq_back_penalty;------cfq_back_penalty = 2;
    unsigned int cfq_back_max;-------cfq_back_max = 16 * 1024;
    unsigned int cfq_slice[2];------------同步、異步請求隊列的時間片長度,cfq_slice_async = HZ / 25; cfq_slice_sync = HZ / 10;
    unsigned int cfq_slice_async_rq;----cfq_slice_async_rq = 2;
    unsigned int cfq_slice_idle;----------idle的時間片,cfq_slice_idle = HZ / 125;
    unsigned int cfq_group_idle;----------cfq_group_idle = HZ / 125;
    unsigned int cfq_latency;-------cfqd->cfq_latency = 1,可以設置為0
    unsigned int cfq_target_latency;----cfq_target_latency = HZ * 3/10; /* 300 ms */

    /*
     * Fallback dummy cfqq for extreme OOM conditions
     */
    struct cfq_queue oom_cfqq;------------在極端的oom的情況下,因為alloc cfq_queue失敗,則使用這個queue,相當於備用的

    unsigned long last_delayed_sync;
};

 另外一個結構是cfq_queue,管理cfq調度器的的request請求的,每個process-group有一個這個結構,也就是跟進程組相關,會和一個cfq_io_context關聯,cfq_io_context在代碼中一般

簡寫為cic,這個cfq_queue要注意和request_queue區分。request_queue是管理某個block設備下所有下發的io request的一個隊列,而cfq_queue只是cfq調度類的一個被管理對象,而它也是

管理各個io request的,主要使用了sort_list和fifo隊列來管理io的request,一個io request在具體某個時刻要么在cfq_queue中,要么在request queue中。

/*
 * Per process-grouping structure
 */
struct cfq_queue {
    /* reference count */
    int ref;
    /* various state flags, see below */
    unsigned int flags;
    /* parent cfq_data */
    struct cfq_data *cfqd;-------指向隊列所屬的cfq_data
    /* service_tree member */
    struct rb_node rb_node;------用於將該隊列插入service_tree,就是紅黑樹的node,注意和sort_list相區別,sort_list是管理的對象是request,
    /* service_tree key */
    unsigned long rb_key;-------紅黑樹節點關鍵值,用於確定隊列在service_tree中的位置,該值要綜合jiffies,進程的io優先級等考慮
    /* prio tree member */
    struct rb_node p_node;-------用於將隊列插入對應優先級的prio_tree
    /* prio tree root we belong to, if any */
    struct rb_root *p_root;------對應的prio_tree樹根,就是當前cfq_q歸屬於cfqd中的prio_trees的節點的地址
    /* sorted list of pending requests */
    struct rb_root sort_list;----組織隊列內的請求用的紅黑樹,按請求的起始扇區進行排序,便於查找某個扇區以及合並請求,
    /* if fifo isn't expired, next request to serve */
    struct request *next_rq;-----紅黑樹中下一個要處理的請求,
    /* requests queued in sort_list */
    int queued[2];---------------目前管理的同步和異步的request的數量,數組1是同步io請求的數量
    /* currently allocated requests */
    int allocated[2];------------讀寫兩個方向的分配的request的數量,allocated包含dispatch+queued
    /* fifo list of requests in sort_list */
    struct list_head fifo;---------fifo的list,pending requests形成的fifo鏈表,fifo主要按時間來管理,主要防止隊列內某些request餓死。
//為什么既用fifo來管理request,又用rb_tree來管理呢?其實內核中很多這樣的管理方式,比如說我們的線性區也有多個管理結構,deadline io scheduler也有類似的,主要是不同維度來管理
    /* time when queue got scheduled in to dispatch first request. */
    unsigned long dispatch_start;---當被選中來調度時的時間
    unsigned int allocated_slice;---被選中時分配的時間片
    unsigned int slice_dispatch;----時間片內給對應的request queue下發的請求數,這個在一個調度周期內有效,下次選擇到這個cfq_q會清零
    /* time when first request from queue completed and slice started. */
    unsigned long slice_start;---指定時間片什么時候開始
    unsigned long slice_end;-----指明時間片何時消耗完
    long slice_resid;

    /* pending priority requests */
    int prio_pending;
    /* number of requests that are on the dispatch list or inside driver */
    int dispatched;---------------該cfq_q總共下發的request總數,這些是驅動待處理的請求

    /* io prio of this group */
    unsigned short ioprio, org_ioprio;----這組的io優先級
    unsigned short ioprio_class;

    pid_t pid;-----該cfq_queue歸屬的進程的pid

    u32 seek_history;
    sector_t last_request_pos;

    struct cfq_rb_root *service_tree;----指向cfq_data對應的cfq_rb_tree
    struct cfq_queue *new_cfqq;
    struct cfq_group *cfqg;--------------cfq_queue對應的cgroup,主要是進程信息,看組內的大家怎么分時間片,異步隊列的話,這個就指向cfq_data->root_group
    /* Number of sectors dispatched from queue in single dispatch round */
    unsigned long nr_sectors;------------調度周期內下發的扇區總數
};

 

cfq代碼中常見的簡寫:

cic是 cfq_io_context 的簡寫

cfqq 是cfq_queue  的簡寫

cfqd是cfq_data 的簡寫

be是  best effort 的簡寫

rt是 real time 的簡寫

其中有三種class: idle(3), best effort(2), real time(1),具體用法man ionice

real time 和 best effort 內部都有 0-7 一共8個優先級,對於real time而言,由於優先級高,有可能會餓死其他進程,對於 best effort 而言,2.6.26之后的內核如不指定io priority,那就有io priority = cpu nice

如何生成一個request:

  io生成request的方法,常見的是進程上下文的時候,使用 bio_alloc 生成一個bio,bio描述的是這個io是需要訪問那些扇區序列,然后調用  submit_bio 提交這個請求,blockdump的情況下,是能夠打印這個進程提交的bio的具體信息的,submit_bio使用 generic_make_request 函數進入通用塊層,如果 當前進程的 current->bio_list 不為NULL,那么新的bio僅僅是掛在這個bio_list末尾就返回了,為NULL的話,則根據bio所要讀寫的硬盤,找到對應的request_queue,然后使用 q->make_request_fn 來生成一個request,在之前關於NVME的博客描述中,曾經對這個 make_request_fn 

有過解釋,對於多隊列的硬盤,一般是blk_mq_make_request,而對於普通單隊列硬盤,則以 blk_queue_bio 居多,最終根據能否merge,調用 elv_merge 或者生成一個new request,

生成new request 之后,調用init_request_from_bio 使用bio 來初始化這個request,網上有圖,這個基本都寫爛了,關於扇區,bio,request這三級關系的,struct request_queue中有幾個重要的成員,一個就是創建request的 make_request_fn,一個就是准備命令的成員prep_rq_fn,對於scsi協議的設備來說,就是 scsi_prep_fn,這個函數會為request_queue設置io cmd執行出錯之后的回調等;一個就是執行request的 request_fn 成員,對由於scsi設備來說,就是 scsi_request_fn 來執行request 了。

對請求進行merge:

唐突地描述合並請求,其實有多個階段,比如在submit_bio階段,如果 current->bio_list 不為NULL,則bio請求掛在這個隊列尾就返回了,算不算一種合並,從時間角度說,其實算一種合並,

因為這些bio下發的時候,是一起下發的,但是從空間的角度,每個bio所要讀寫的扇區是不相關的(如果是某個進程順序讀,則可能連續),傳統意義上的合並request,可能在於電梯算法的

合並,而bio合入到其他的request是一種級別的合並,request與request之間又是一種級別的合並,cfq是如何合並的呢,主要就是調用 cfq_merge,一般的調用鏈如下:

blk_queue_bio---》elv_merge---》elevator_merge_fn--》cfq_merge----》cfq_find_rq_fmerge-----找到相鄰的request

                                                                                                            ----》elv_rq_merge_ok--------bio與對應的request看能否合並,會回調 elevator_allow_merge_fn,因為不同的硬件和調度算法,合並的條件都不一樣。
cfq_find_rq_fmerge()進行實際的搜索工作,要確定bio的歸屬request,必須先確定進程的通信對象是誰(因為一個進程有可能和多個塊設備通信),也就是要找到進程對應的cfq_io_context結構,其中包含了進程的同步請求隊列和異步請求隊列的地址,只要找到了相應的cfq_io_context,就可以通過bio的同異步性確定對應的cfq_queue了,最后再判斷對應的cfq_queue中是否存在可以容納bio的request。推導 cfq_io_context 的關鍵在於以塊設備CFQ調度器的描述結構cfq_data的地址為關鍵值,在進程的io_context的基數樹中進行搜索。
int elv_merge(struct request_queue *q, struct request **req, struct bio *bio)
{
    struct elevator_queue *e = q->elevator; struct request *__rq; int ret; /* * Levels of merges: * nomerges: No merges at all attempted * noxmerges: Only simple one-hit cache try * merges: All merge tries attempted */ if (blk_queue_nomerges(q)) return ELEVATOR_NO_MERGE; /* * First try one-hit cache. */ if (q->last_merge && elv_rq_merge_ok(q->last_merge, bio)) {------嘗試看一下這個request_queue的最后一次合並的request能否合並,適合順序讀寫的情況 ret = blk_try_merge(q->last_merge, bio);----嘗試前后合並,也就是這個request的起始扇區是該bio的最后一個扇區,或者request的最后扇區是bio的第一個扇區 if (ret != ELEVATOR_NO_MERGE) {-------------可以合並,則依然記錄最后合並的request *req = q->last_merge; return ret; } } if (blk_queue_noxmerges(q))--------本身這個queue就有標志確定不能合並的,則直接返回不合並 return ELEVATOR_NO_MERGE; /* * See if our hash lookup can find a potential backmerge. */ __rq = elv_rqhash_find(q, bio->bi_sector);-----根據bio起始的sector,去request_queue中hash查找能合並的request, if (__rq && elv_rq_merge_ok(__rq, bio)) {------找到了並且能合並ok,則返回這個request, *req = __rq; return ELEVATOR_BACK_MERGE; } if (e->type->ops.elevator_merge_fn) return e->type->ops.elevator_merge_fn(q, req, bio);--------這個對於cfq來說,就是調用cfq_merge return ELEVATOR_NO_MERGE; }

上面使用的 elv_rq_merge_ok 函數,判斷能否合並,比如對於cfq的實現來說,bio中必須是同步io和req中也必須是同步io,具體可見:cfq_allow_merge 實現,在此不展開。

 
static int cfq_merge(struct request_queue *q, struct request **req,
             struct bio *bio) { struct cfq_data *cfqd = q->elevator->elevator_data; struct request *__rq; __rq = cfq_find_rq_fmerge(cfqd, bio);-------根據當前bio,查找可以merge的request if (__rq && elv_rq_merge_ok(__rq, bio)) {---找到了對應的request,只是第一步,elv_rq_merge_ok才能決定是否可以merge *req = __rq; return ELEVATOR_FRONT_MERGE; } return ELEVATOR_NO_MERGE; }
查找rq的過程其實就是根據bio的最后扇區號來訪問紅黑樹的過程:
static struct request *
cfq_find_rq_fmerge(struct cfq_data *cfqd, struct bio *bio) { struct task_struct *tsk = current; struct cfq_io_cq *cic; struct cfq_queue *cfqq; cic = cfq_cic_lookup(cfqd, tsk->io_context);---cic是cfq_io_context的簡寫,在進程的io_context中,找到進程特定於塊設備的cfq_io_context,因為一個進程可能和多個設備通信 if (!cic) return NULL; cfqq = cic_to_cfqq(cic, cfq_bio_sync(bio));----根據同步還是異步,確定cfq_queue if (cfqq) return elv_rb_find(&cfqq->sort_list, bio_end_sector(bio));---找到cfqq之后,按照本次bio的最后一個扇區,從cfq_queue的紅黑樹中查找對應的節點,也就是對應的請求 要注意的一點是,bio傳入的是該bio的最后一個扇區,比較的時候,適合紅黑樹中的request中的第一個扇區進行比較,如果小於則找左子樹,大於則找右子樹,相同則返回對應的request。 return NULL;----無法merge,因為沒有找到對應的cfqq }

對cfq_cic_lookup---》ioc_lookup_icq 的流程再分析一下:

struct io_cq *ioc_lookup_icq(struct io_context *ioc, struct request_queue *q)
{
    struct io_cq *icq; lockdep_assert_held(q->queue_lock); /* * icq's are indexed from @ioc using radix tree and hint pointer, * both of which are protected with RCU. All removals are done * holding both q and ioc locks, and we're holding q lock - if we * find a icq which points to us, it's guaranteed to be valid. */ rcu_read_lock(); icq = rcu_dereference(ioc->icq_hint); if (icq && icq->q == q) goto out; icq = radix_tree_lookup(&ioc->icq_tree, q->id);----從ioc中的radix樹中找對應的icq,這個類似於在pogecache的address_space中找對應的page if (icq && icq->q == q) rcu_assign_pointer(ioc->icq_hint, icq); /* allowed to race */ else icq = NULL; out: rcu_read_unlock(); return icq; }

對無法merge的BIO,則新生成請求:

merge的結果可能返回ELEVATOR_BACK_MERGE,也有可能是ELEVATOR_FRONT_MERGE,這兩種結果都是可以merge的,要注意的是,有些請求是無法merge的,比如包含barrier i/o。然后會調用

elv_merged_request 來實現merge,elevator_merged_fn---》cfq_merged_request,但是如果merge不了的請求,則會生成新的request,從bio到新的request,也是有一段路要走的,

這個要類比的話,可以參照net里面實現的skb申請的對比,當要發包的時候,如果當前的報文可以和之前的skb合並,則合並,否則申請新的skb來管理。

get_request---》cfq_may_queue,如果它的返回值是ELV_MQUEUE_NO,則報沒有空間,否則根據它的返回值是  ELV_MQUEUE_MUST 還是 ELV_MQUEUE_MAY 來 判斷是否擁塞

之類的,這個也可以和tcp發包時候來對比,比如我們的緩存不夠了,不能填下當前的網絡請求,則也會報沒有空間之類的。如果存在空間,則 

mempool_alloc 會返回正常的request來承載這個bio請求。申請request成功之后,elv_set_request就要被呼喚出場了,分配一個request必須進行初始化,需要調用cfq_set_request()函數。調用鏈如下:

blk_queue_bio-->get_request --->__get_request-->elv_set_request--->elevator_set_req_fn----->cfq_set_request:來到了構造request的方法:

static int
cfq_set_request(struct request_queue *q, struct request *rq, struct bio *bio, gfp_t gfp_mask) { struct cfq_data *cfqd = q->elevator->elevator_data; struct cfq_io_cq *cic = icq_to_cic(rq->elv.icq); const int rw = rq_data_dir(rq); const bool is_sync = rq_is_sync(rq); struct cfq_queue *cfqq; might_sleep_if(gfp_mask & __GFP_WAIT); spin_lock_irq(q->queue_lock); check_ioprio_changed(cic, bio); check_blkcg_changed(cic, bio); new_queue: cfqq = cic_to_cfqq(cic, is_sync);---根據同異步情況,獲取進程中對應的cfq_queue if (!cfqq || cfqq == &cfqd->oom_cfqq) {----如果還沒有相應的cfqq則進行分配 cfqq = cfq_get_queue(cfqd, is_sync, cic, bio, gfp_mask);---分配cfq_queue cic_set_cfqq(cic, cfqq, is_sync);----設置cic->cfqq[is_sync] = cfqq } else { /* * If the queue was seeky for too long, break it apart.-------------queue太長了,把它掰開 */ if (cfq_cfqq_coop(cfqq) && cfq_cfqq_split_coop(cfqq)) { cfq_log_cfqq(cfqd, cfqq, "breaking apart cfqq"); cfqq = split_cfqq(cic, cfqq); if (!cfqq) goto new_queue; } /* * Check to see if this queue is scheduled to merge with * another, closely cooperating queue. The merging of * queues happens here as it must be done in process context. * The reference on new_cfqq was taken in merge_cfqqs. */ if (cfqq->new_cfqq) cfqq = cfq_merge_cfqqs(cfqd, cic, cfqq); } cfqq->allocated[rw]++; cfqq->ref++; cfqg_get(cfqq->cfqg); rq->elv.priv[0] = cfqq;------保留相應的cfqq信息到rq中 rq->elv.priv[1] = cfqq->cfqg; spin_unlock_irq(q->queue_lock); return 0; }

 

如何配對request和對應的cfq_queue:

 生成的新請求,如何設置其對應的cfqq呢?在 cfq_get_queue 函數中有實現:

static struct cfq_queue *
cfq_get_queue(struct cfq_data *cfqd, bool is_sync, struct cfq_io_cq *cic,
          struct bio *bio, gfp_t gfp_mask)
{
    const int ioprio_class = IOPRIO_PRIO_CLASS(cic->ioprio);
    const int ioprio = IOPRIO_PRIO_DATA(cic->ioprio);
    struct cfq_queue **async_cfqq = NULL;
    struct cfq_queue *cfqq = NULL;

    if (!is_sync) {
        async_cfqq = cfq_async_queue_prio(cfqd, ioprio_class, ioprio);-------異步的,根據io優先級使用公共隊列就行
        cfqq = *async_cfqq;
    }

    if (!cfqq)
        cfqq = cfq_find_alloc_queue(cfqd, is_sync, cic, bio, gfp_mask);----同步的需要費一番周折

    /*
     * pin the queue now that it's allocated, scheduler exit will prune it
     */
    if (!is_sync && !(*async_cfqq)) {
        cfqq->ref++;
        *async_cfqq = cfqq;
    }

    cfqq->ref++;
    return cfqq;
}

異步的就是根據io的優先級使用cfqd中的公共隊列,同步的話,task中的ioc就發揮作用了,task可能會同時和多個磁盤進行通信,為了記錄這種情況,task_struct中的io_context 成員記錄了這些通信的信息。

io_context 中的 icq_tree 成員其實是一顆radix 樹,和我們pagecache中的radix樹類似,只不過我們這顆radix樹的index是request_queue的id,如果是第一次訪問到這個磁盤,

則會 調用  ioc_create_icq 來創建一個cfq,並加入到對應request_queue 中的id 的slot中去。

crash> task_struct.io_context ffff881f52764500
  io_context = 0xffff881fc5cb1548
crash> struct -xo io_context.icq_tree 0xffff881fc5cb1548
struct io_context {
  [ffff881fc5cb1570] struct radix_tree_root icq_tree;
}
crash> tree -t radix  ffff881fc5cb1570----------下面這些都是該進程通信的cfqq
ffff881fc9e4fca8
ffff8801977764b0
ffff88065fd311e0
ffff881450699a50
ffff8814489712d0
ffff8814512e80f0
ffff880d83418ca8
ffff880d83418f00
ffff8804729f9078
ffff8815769b6e10
ffff88065fd310f0
ffff88144e209d20
ffff8804a2055bb8
ffff881fc9e4f708
ffff8804a2055168
ffff880eb1f53d98
ffff880d2ec6d4b0
ffff881450699d98
ffff8804729f9618
ffff8804a2055528
ffff88065fd31168
ffff881446b20348
ffff8804a20554b0
ffff880cb47a3bb8
ffff880d2ec6d618
ffff881450699168
ffff8804a2055078
ffff880d83418078
ffff880dab6c55a0
ffff8804729f9438
ffff881448971b40
ffff880dab6c5a50

新的request 找到對應的cfq_queue之后,則建立起來了對應的request 和cfq_q之間的聯系了,具體可以見本文最后的那張圖。

新請求如何插入到cfqq

generic_make_request----(q->make_request_fn)對於單隊列,就是 blk_queue_bio,當然對於md設備之類的,指針不一樣,在此不展開。

blk_queue_bio-----blk_flush_plug_list---__elv_add_request-->cfq_insert_request----

一個request生成之后,會插入到對應的管理隊列中,目前內核設計了三層的管理,一層是進程級,也就是屬於這個進程的io,用plug來管理,一層是elv級,是用調度器來管理,一層是硬件的

request_queue,這個是下發給device驅動的。最后調用 scsi_dispatch_cmd 方法將請求發送到HBA。如果 scsi 層需要重新調度一個 request ,可以通過 blk_requeue_request 接口來完成。通過該接口,可以把 request 重新放回到 device request queue 中進行調度(具體可以看scsilib.c).在blk_queue_bio 函數中,如果已經創建了一個新的request的話,則需要將該請求插入到隊列,如果當前進程的plug為NULL,說明不需要在進程級緩存該進程的io,則調用__blk_run_queue 來刷掉當前queue的request,進入了elv層,否則如果進程有PLUG但當前plug請求數是0,則直接緩存在該plug中,如果plug已經滿了,則調用blk_flush_plug_list 來刷掉該plug緩存的io請求,刷取的方式就是遍歷這個list,調用 __elv_add_request:

void __elv_add_request(struct request_queue *q, struct request *rq, int where)
{
    trace_block_rq_insert(q, rq);

    blk_pm_add_request(q, rq);

    rq->q = q;

    if (rq->cmd_flags & REQ_SOFTBARRIER) {
        /* barriers are scheduling boundary, update end_sector */
        if (rq->cmd_type == REQ_TYPE_FS) {
            q->end_sector = rq_end_sector(rq);
            q->boundary_rq = rq;
        }
    } else if (!(rq->cmd_flags & REQ_ELVPRIV) &&
            (where == ELEVATOR_INSERT_SORT ||
             where == ELEVATOR_INSERT_SORT_MERGE))
        where = ELEVATOR_INSERT_BACK;

    switch (where) {
    case ELEVATOR_INSERT_REQUEUE:
    case ELEVATOR_INSERT_FRONT:---將request加入到device request queue的隊列前
        rq->cmd_flags |= REQ_SOFTBARRIER;
        list_add(&rq->queuelist, &q->queue_head);
        break;

    case ELEVATOR_INSERT_BACK:--- 將request加入到device request queue的隊列尾,注意此時操作的對象是request queue
        rq->cmd_flags |= REQ_SOFTBARRIER;
        elv_drain_elevator(q);
        list_add_tail(&rq->queuelist, &q->queue_head);
        /*
         * We kick the queue here for the following reasons.
         * - The elevator might have returned NULL previously
         *   to delay requests and returned them now.  As the
         *   queue wasn't empty before this request, ll_rw_blk
         *   won't run the queue on return, resulting in hang.
         * - Usually, back inserted requests won't be merged
         *   with anything.  There's no point in delaying queue
         *   processing.
         */
        __blk_run_queue(q);----如注釋,添加隊列尾的,已經無法合並,刷掉該隊列中的請求
        break;

    case ELEVATOR_INSERT_SORT_MERGE:
        /*
         * If we succeed in merging this request with one in the
         * queue already, we are done - rq has now been freed,
         * so no need to do anything further.
         */
        if (elv_attempt_insert_merge(q, rq))---嘗試對request進行合並操作,如果無法合並將request加入到elevator request queue中
            break;
    case ELEVATOR_INSERT_SORT:---需要重排,則將request加入到elevator request queue中,進入elv層
        BUG_ON(rq->cmd_type != REQ_TYPE_FS);
        rq->cmd_flags |= REQ_SORTED;
        q->nr_sorted++;
        if (rq_mergeable(rq)) {
            elv_rqhash_add(q, rq);
            if (!q->last_merge)
                q->last_merge = rq;
        }

        /*
         * Some ioscheds (cfq) run q->request_fn directly, so
         * rq cannot be accessed after calling
         * elevator_add_req_fn.
         */
        q->elevator->type->ops.elevator_add_req_fn(q, rq);----對於cfq來說,就是 cfq_insert_request,對於deadline來說,就是deadline_add_request
        break;

    case ELEVATOR_INSERT_FLUSH:
        rq->cmd_flags |= REQ_SOFTBARRIER;
        blk_insert_flush(rq);
        break;
    default:
        printk(KERN_ERR "%s: bad insertion point %d\n",
               __func__, where);
        BUG();
    }
}

其實插入到cfqq還是比較簡單的,記住兩個主要的結構,一個是紅黑樹,按扇區大小管理,一個是fifo隊列,按照request到來時間先后來管理:

static void cfq_insert_request(struct request_queue *q, struct request *rq)
{
    struct cfq_data *cfqd = q->elevator->elevator_data;
    struct cfq_queue *cfqq = RQ_CFQQ(rq);

    cfq_log_cfqq(cfqd, cfqq, "insert_request");
    cfq_init_prio_data(cfqq, RQ_CIC(rq));

    rq->fifo_time = jiffies + cfqd->cfq_fifo_expire[rq_is_sync(rq)];
    list_add_tail(&rq->queuelist, &cfqq->fifo);----兩個主要數據結構,一個是將request加入到cfqq的fifo隊列,一個是將request根據扇區加到對應的紅黑樹
    cfq_add_rq_rb(rq);
    cfqg_stats_update_io_add(RQ_CFQG(rq), cfqd->serving_group,
                 rq->cmd_flags);
    cfq_rq_enqueued(cfqd, cfqq, rq);
}

如何從調度隊列cfqq中下發一個request到設備的request queue:

CFQ調度器在發送request到底層塊設備時的流程大致如下:

1.選擇一個cfq_queue

2.從cfq_queue中選擇一個request進行發送

選擇cfq_queue的思路如下:

1.如果當前的cfq_queue的時間片還沒用完,則繼續當前的cfq_queue

2.如果當前的cfq_queue的時間片消耗完了,則優先在對應的prio_tree中選擇一個cfq_queue,該cfq_queue的第一個訪問扇區與整個調度器最后處理的扇區之間的差值必須小於一個閾值,如果OK的話就選擇這個cfq_queue

3.如果找不到這樣的cfq_queue,再從service_tree中調度其他的cfq_queue

調用鏈:cfq_dispatch_requests----cfq_select_queue

                                                  -----cfq_dispatch_request

調用鏈可以簡單分為兩部分,一部分是選擇queue,另一部分是根據選擇的queue取其中request下發給request queue,就是從cfqq中取請求到request_queue

/*
 * Find the cfqq that we need to service and move a request from that to the
 * dispatch list
 */
static int cfq_dispatch_requests(struct request_queue *q, int force)
{
    struct cfq_data *cfqd = q->elevator->elevator_data;
    struct cfq_queue *cfqq;

    if (!cfqd->busy_queues)
        return 0;

    if (unlikely(force))
        return cfq_forced_dispatch(cfqd);

    cfqq = cfq_select_queue(cfqd);-------------選擇一個隊列,它會優先選擇active_queue,如果沒有則選擇新的queue,並將新的queue設置為active
    if (!cfqq)
        return 0;

    /*
     * Dispatch a request from this cfqq, if it is allowed
     */
    if (!cfq_dispatch_request(cfqd, cfqq))-------從選擇的隊列中選擇request進行派發,這個放在后面描述,因為cfq_select_queue就已經較復雜了,選擇好queue之后來輪到它。
        return 0;

    cfqq->slice_dispatch++;
    cfq_clear_cfqq_must_dispatch(cfqq);

    /*
     * expire an async queue immediately if it has used up its slice. idle
     * queue always expire after 1 dispatch round.
     */
    if (cfqd->busy_queues > 1 && ((!cfq_cfqq_sync(cfqq) &&
        cfqq->slice_dispatch >= cfq_prio_to_maxrq(cfqd, cfqq)) ||
        cfq_class_idle(cfqq))) {
        cfqq->slice_end = jiffies + 1;
        cfq_slice_expired(cfqd, 0);
    }
//異步隊列發送的請求數超過了時間片內的最大請求數,且有其他隊列等待調度,則將隊列掛起,如果是idle隊列,則直接掛起
    cfq_log_cfqq(cfqd, cfqq, "dispatched a request");
    return 1;
}

如何選擇一個cfq調度隊列:

那么具體怎么選擇隊列來進行request的派發呢?選擇隊列其實也是有講究的,

cfq_select_queue的調用鏈:cfq_select_queue----cfq_close_cooperator

cfq_select_queue的實現如下:

/*
 * Select a queue for service. If we have a current active queue,
 * check whether to continue servicing it, or retrieve and set a new one.
 */
static struct cfq_queue *cfq_select_queue(struct cfq_data *cfqd)
{
    struct cfq_queue *cfqq, *new_cfqq = NULL;

    cfqq = cfqd->active_queue;
    if (!cfqq)-----------------沒有指定active_queue則跳轉到new_queue去選擇新的隊列
        goto new_queue;

    if (!cfqd->rq_queued)--------當前cfqd沒有積壓的request了,則返回,因為queue里面肯定也沒有request了
        return NULL;

    /*
     * We were waiting for group to get backlogged. Expire the queue
     */
    if (cfq_cfqq_wait_busy(cfqq) && !RB_EMPTY_ROOT(&cfqq->sort_list))
        goto expire;

    /*
     * The active queue has run out of time, expire it and select new.
     */
    if (cfq_slice_used(cfqq) && !cfq_cfqq_must_dispatch(cfqq)) {----active_queue,這里檢查該隊列的時間片是否已經過去
        /*
         * If slice had not expired at the completion of last request
         * we might not have turned on wait_busy flag. Don't expire
         * the queue yet. Allow the group to get backlogged.
         *
         * The very fact that we have used the slice, that means we
         * have been idling all along on this queue and it should be
         * ok to wait for this request to complete.
         */
        if (cfqq->cfqg->nr_cfqq == 1 && RB_EMPTY_ROOT(&cfqq->sort_list)
            && cfqq->dispatched && cfq_should_idle(cfqd, cfqq)) {
            cfqq = NULL;
            goto keep_queue;
        } else
            goto check_group_idle;
    }

    /*
     * The active queue has requests and isn't expired, allow it to
     * dispatch.
     */
    if (!RB_EMPTY_ROOT(&cfqq->sort_list))-------表示時間片尚在,這里檢查cfq_queue的sort_list是否為空
        goto keep_queue;

    /*
     * If another queue has a request waiting within our mean seek
     * distance, let it run.  The expire code will check for close
     * cooperators and put the close queue at the front of the service
     * tree.  If possible, merge the expiring queue with the new cfqq.
     */
    new_cfqq = cfq_close_cooperator(cfqd, cfqq);-----走到這里說明active_queue內已經沒有請求了,因此要找一個最適合的cfq_queue,怎么查找下面描述
    if (new_cfqq) {-----------找到了對應的比較靠近當前請求的cfqq
        if (!cfqq->new_cfqq)
            cfq_setup_merge(cfqq, new_cfqq);---------嘗試merge兩個queue
        goto expire;
    }

    /*
     * No requests pending. If the active queue still has requests in
     * flight or is idling for a new request, allow either of these
     * conditions to happen (or time out) before selecting a new queue.
     */
    if (timer_pending(&cfqd->idle_slice_timer)) {
        cfqq = NULL;
        goto keep_queue;
    }

    /*
     * This is a deep seek queue, but the device is much faster than
     * the queue can deliver, don't idle
     **/
    if (CFQQ_SEEKY(cfqq) && cfq_cfqq_idle_window(cfqq) &&
        (cfq_cfqq_slice_new(cfqq) ||
        (cfqq->slice_end - jiffies > jiffies - cfqq->slice_start))) {
        cfq_clear_cfqq_deep(cfqq);
        cfq_clear_cfqq_idle_window(cfqq);
    }

    if (cfqq->dispatched && cfq_should_idle(cfqd, cfqq)) {
        cfqq = NULL;
        goto keep_queue;
    }

    /*
     * If group idle is enabled and there are requests dispatched from
     * this group, wait for requests to complete.
     */
check_group_idle:
    if (cfqd->cfq_group_idle && cfqq->cfqg->nr_cfqq == 1 &&
        cfqq->cfqg->dispatched &&
        !cfq_io_thinktime_big(cfqd, &cfqq->cfqg->ttime, true)) {
        cfqq = NULL;
        goto keep_queue;
    }

expire:
    cfq_slice_expired(cfqd, 0);--------將時間片消耗完的active_queue重新插入service_tree,體現公平的含義
new_queue:
    /*
     * Current queue expired. Check if we have to switch to a new
     * service tree
     */
    if (!new_cfqq)
        cfq_choose_cfqg(cfqd);-------------選擇新的service tree

    cfqq = cfq_set_active_queue(cfqd, new_cfqq);----------設置new_cfqq為新的active_tree,開始接收時間片
keep_queue:
    return cfqq;
}

 如果當前queue因為沒有request,cfq_close_cooperator會選擇一個新的queue來調度,,其實選擇好了之后光派發request還是比較簡單的。這個函數實現如下:

static struct cfq_queue *cfq_close_cooperator(struct cfq_data *cfqd,
                          struct cfq_queue *cur_cfqq)
{
    struct cfq_queue *cfqq;

    if (cfq_class_idle(cur_cfqq))
        return NULL;
    if (!cfq_cfqq_sync(cur_cfqq))----------------只考慮sync的類型,其他類型返回NULL
        return NULL;
    if (CFQQ_SEEKY(cur_cfqq))
        return NULL;

    /*
     * Don't search priority tree if it's the only queue in the group.
     */
    if (cur_cfqq->cfqg->nr_cfqq == 1)
        return NULL;

    /*
     * We should notice if some of the queues are cooperating, eg
     * working closely on the same area of the disk. In that case,
     * we can group them together and don't waste time idling.
     */
    cfqq = cfqq_close(cfqd, cur_cfqq);----找一個比較近的扇區,靠近cfqd->last_position 的,優先找緊靠的,然后兩者相差一定閾值的,下面詳細描述
if (!cfqq)
        return NULL;----沒找到,則返回

    /* If new queue belongs to different cfq_group, don't choose it */
    if (cur_cfqq->cfqg != cfqq->cfqg)----就算找到了對應的cfqq,但如果它的group不一樣,還是不行
        return NULL;

    /*
     * It only makes sense to merge sync queues.
     */
    if (!cfq_cfqq_sync(cfqq))--------非同步請求的隊列,不能合並
        return NULL;
    if (CFQQ_SEEKY(cfqq))
        return NULL;

    /*
     * Do not merge queues of different priority classes
     */
    if (cfq_class_rt(cfqq) != cfq_class_rt(cur_cfqq))-------不同優先級class也不能合並
        return NULL;

    return cfqq;
}
cfqq_close的選擇很講究:
static struct cfq_queue *cfqq_close(struct cfq_data *cfqd,
                    struct cfq_queue *cur_cfqq)
{
    struct rb_root *root = &cfqd->prio_trees[cur_cfqq->org_ioprio];
    struct rb_node *parent, *node;
    struct cfq_queue *__cfqq;
    sector_t sector = cfqd->last_position;

    if (RB_EMPTY_ROOT(root))
        return NULL;

    /*
     * First, if we find a request starting at the end of the last
     * request, choose it.
     */
    __cfqq = cfq_prio_tree_lookup(cfqd, root, sector, &parent, NULL);-------如果有個請求他的start的扇區是上一個請求的最后位置,當然選它了,說明連續啊
    if (__cfqq)
        return __cfqq;

    /*
     * If the exact sector wasn't found, the parent of the NULL leaf
     * will contain the closest sector.
     */
    __cfqq = rb_entry(parent, struct cfq_queue, p_node);----parent保存了最接近的節點的父節點,如果沒找到相鄰的cfq_queue,獲取父節點對應的cfq_queue
    if (cfq_rq_close(cfqd, cur_cfqq, __cfqq->next_rq))--間隙滿足要求
        return __cfqq;

    if (blk_rq_pos(__cfqq->next_rq) < sector)---下一個請求的位置小於之前的扇區,則往next找
        node = rb_next(&__cfqq->p_node);
    else
        node = rb_prev(&__cfqq->p_node);------否則往前找,
    if (!node)
        return NULL;

    __cfqq = rb_entry(node, struct cfq_queue, p_node);---找到node后還是取對應的cfqq
    if (cfq_rq_close(cfqd, cur_cfqq, __cfqq->next_rq))---再次判斷這個cfqq能不能用,還是按照間隙來
        return __cfqq;

    return NULL;
}

如何根據選擇的調度隊列下發request到 request queue:

如果找到了這樣的隊列,就要把這個隊列設置為active隊列,即調用 cfq_set_active_queue 來設置這個queue為active,然后即調用 cfq_dispatch_request 進行request的派發了。

cfq_dispatch_request 的實現如下,如果你到這個地方已經迷糊了,請回到調用鏈部分就行。

調用鏈:cfq_dispatch_request-----cfq_may_dispatch

                                                -----cfq_check_fifo----------先看有沒有快餓死的request,也就是等待過長時間的request

                                                -----cfq_dispatch_insert

下面分別描述這幾個函數實現:

/*
 * Dispatch a request from cfqq, moving them to the request queue
 * dispatch list.
 */
static bool cfq_dispatch_request(struct cfq_data *cfqd, struct cfq_queue *cfqq)
{
    struct request *rq;

    BUG_ON(RB_EMPTY_ROOT(&cfqq->sort_list));

    if (!cfq_may_dispatch(cfqd, cfqq))------下面描述,主要是判斷是否繼續發放cfqq中的請求
        return false;

    /*
     * follow expired path, else get first next available
     */
    rq = cfq_check_fifo(cfqq);-------------如果fifo為空,或者fifo中第一個請求的期限還沒到,則返回NULL,
    if (!rq)
        rq = cfqq->next_rq;----------------既然時間上不緊急,那么就從磁盤連續性考慮下一個請求 /*
     * insert request into driver dispatch list
     */
    cfq_dispatch_insert(cfqd->queue, rq);----取到請求了,放到設備的request queue

    if (!cfqd->active_cic) {
        struct cfq_io_cq *cic = RQ_CIC(rq);

        atomic_long_inc(&cic->icq.ioc->refcount);
        cfqd->active_cic = cic;
    }

    return true;
}

 cfq_may_dispatch:

static bool cfq_may_dispatch(struct cfq_data *cfqd, struct cfq_queue *cfqq)
{
    unsigned int max_dispatch;

    /*
     * Drain async requests before we start sync IO
     */
    if (cfq_should_idle(cfqd, cfqq) && cfqd->rq_in_flight[BLK_RW_ASYNC])---如果cfqq可以被idle並且設備有異步請求處理,則不進行新的同步請求的發放
        return false;

    /*
     * If this is an async queue and we have sync IO in flight, let it wait
     */
    if (cfqd->rq_in_flight[BLK_RW_SYNC] && !cfq_cfqq_sync(cfqq))---如果發放的是異步請求並且request_queue中還有同步請求在等待提交,則不下發
        return false;

    max_dispatch = max_t(unsigned int, cfqd->cfq_quantum / 2, 1);-----------cfq_quantum是可以配置的,詳見:/sys/block/sdb/queue/iosched/quantum
    if (cfq_class_idle(cfqq))
        max_dispatch = 1;------cfqq的優先級類為idle類的話,最多一個請求,idle類好可憐,

    /*
     * Does this cfqq already have too much IO in flight?
     */
    if (cfqq->dispatched >= max_dispatch) {----超閾值
        bool promote_sync = false;
        /*
         * idle queue must always only have a single IO in flight
         */
        if (cfq_class_idle(cfqq))------idle類,歇着吧你,你就是天朝的屁民,優先級最低
            return false;

        /*
         * If there is only one sync queue
         * we can ignore async queue here and give the sync
         * queue no dispatch limit. The reason is a sync queue can
         * preempt async queue, limiting the sync queue doesn't make
         * sense. This is useful for aiostress test.
         */
        if (cfq_cfqq_sync(cfqq) && cfqd->busy_sync_queues == 1)
            promote_sync = true;

        /*
         * We have other queues, don't allow more IO from this one
         */
        if (cfqd->busy_queues > 1 && cfq_slice_used_soon(cfqd, cfqq) &&
                !promote_sync)
            return false;----cfqd的其他隊列在等待發放請求

        /*
         * Sole queue user, no limit
         */
        if (cfqd->busy_queues == 1 || promote_sync)----只有自己在發放請求
            max_dispatch = -1;
        else
            /*
             * Normally we start throttling cfqq when cfq_quantum/2
             * requests have been dispatched. But we can drive
             * deeper queue depths at the beginning of slice
             * subjected to upper limit of cfq_quantum.
             * */
            max_dispatch = cfqd->cfq_quantum;
    }

    /*
     * Async queues must wait a bit before being allowed dispatch.
     * We also ramp up the dispatch depth gradually for async IO,
     * based on the last sync IO we serviced
     */
    if (!cfq_cfqq_sync(cfqq) && cfqd->cfq_latency) {
        unsigned long last_sync = jiffies - cfqd->last_delayed_sync;
        unsigned int depth;

        depth = last_sync / cfqd->cfq_slice[1];
        if (!depth && !cfqq->dispatched)
            depth = 1;
        if (depth < max_dispatch)
            max_dispatch = depth;
    }---根據最后一次發送的同步請求和現在的時間間隔以及同步請求時間片的值,計算出depth,根據depth重置max_dispatch
/*
     * If we're below the current max, allow a dispatch
     */
    return cfqq->dispatched < max_dispatch;
}

cfq_check_fifo 只是比較一下時間,比較簡單,略過不描述。通過選擇之后,選擇到期的request來下發。

cfq_dispatch_insert的實現如下:

static void cfq_dispatch_insert(struct request_queue *q, struct request *rq)
{
    struct cfq_data *cfqd = q->elevator->elevator_data;
    struct cfq_queue *cfqq = RQ_CFQQ(rq);

    cfq_log_cfqq(cfqd, cfqq, "dispatch_insert");---記錄log

    cfqq->next_rq = cfq_find_next_rq(cfqd, cfqq, rq);---取下一個請求,保存好
    cfq_remove_request(rq);---從cfqq中sortlist和fifo隊列中移除,要知道,此前的request的queuelist成員還是指向fifo中的請求,之后就指向request queue了
    cfqq->dispatched++;
    (RQ_CFQG(rq))->dispatched++;
    elv_dispatch_sort(q, rq);----此時rq是自由的了,進入block device的request_queue,離開電梯調度層

    cfqd->rq_in_flight[cfq_cfqq_sync(cfqq)]++;---這個request就像tcp發包一樣,已經屬於inflight了,不屬於具體的cfq_queue了,不過還要在設備的request_queue呆一會排隊
    cfqq->nr_sectors += blk_rq_sectors(rq);
    cfqg_stats_update_dispatch(cfqq->cfqg, blk_rq_bytes(rq), rq->cmd_flags);
}

進入request queue,需要進行排隊, elv_dispatch_sort就負責干這個

/*
 * Insert rq into dispatch queue of q.  Queue lock must be held on
 * entry.  rq is sort instead into the dispatch queue. To be used by
 * specific elevators.
 */
void elv_dispatch_sort(struct request_queue *q, struct request *rq)
{
    sector_t boundary;
    struct list_head *entry;
    int stop_flags;

    if (q->last_merge == rq)----------這個變量之前也看到過,就是記錄最后merge的request
        q->last_merge = NULL;

    elv_rqhash_del(q, rq);

    q->nr_sorted--;

    boundary = q->end_sector;
    stop_flags = REQ_SOFTBARRIER | REQ_STARTED;
    list_for_each_prev(entry, &q->queue_head) {
        struct request *pos = list_entry_rq(entry);

        if ((rq->cmd_flags & REQ_DISCARD) !=
            (pos->cmd_flags & REQ_DISCARD))
            break;
        if (rq_data_dir(rq) != rq_data_dir(pos))
            break;
        if (pos->cmd_flags & stop_flags)
            break;
        if (blk_rq_pos(rq) >= boundary) {
            if (blk_rq_pos(pos) < boundary)
                continue;
        } else {
            if (blk_rq_pos(pos) >= boundary)
                break;
        }
        if (blk_rq_pos(rq) >= blk_rq_pos(pos))
            break;
    }

    list_add(&rq->queuelist, entry);--------rq的queuelist成員嵌入到request_queue的queue_head管理的雙向鏈表
}

 如何執行一個request:

已經下發到request_queue中的request是如何被執行的呢?

主要的調用入口為:__blk_run_queue,

IO命令的下發最終都是調用__blk_run_queue函數實現。__blk_run_queue調用塊設備注冊的request_fn方法下發塊IO請求。

inline void __blk_run_queue_uncond(struct request_queue *q)
{
    if (unlikely(blk_queue_dead(q)))
        return;

    /*
     * Some request_fn implementations, e.g. scsi_request_fn(), unlock
     * the queue lock internally. As a result multiple threads may be
     * running such a request function concurrently. Keep track of the
     * number of active request_fn invocations such that blk_drain_queue()
     * can wait until all these request_fn calls have finished.
     */
    q->request_fn_active++;
    q->request_fn(q);--------------還記得這個么?比如對於scsi設備來說,執行request的就是scsi_request_fn 了
    q->request_fn_active--;
}

以 scsi_request_fn 為例,這個函數是調度層進入scsi層的入口,不過所謂層次概念,在目前的linux內核實現中,blk都是通過過程調用方式,就像linux net代碼中的層次概念一樣,不是windows的那種消息驅動方式。比如說我們fs層通過bio的方式申請request 來觸發io,它的cmd_type 是REQ_TYPE_FS,在人們使用SG_IO發送命令的時候,scsi層也可以從request_list 中申請request,只不過它的類型是 REQ_TYPE_BLOCK_PC 而已,這個request中的bio指針則為NULL,sg內核驅動的存在使我們可以不使用文件系統,直接在用戶空間調用scsi命令。

如果能一步步看到這里,說明你定力不錯,給自己加個雞腿吧,cfq里面還有timer那部分沒有描述,留着下次補充。

附一張網上看到的圖,挺佩服那些畫圖的兄弟,一圖勝千言。

由於已經不知道原圖的出處,無法給出鏈接,再次對圖作者表示感謝。由於本文過長,計划下一篇學習記錄《linux io 的cfq代碼理解二》,描述時間片的計算,對cfqg的選擇,組內的選擇等。

參考資料:

https://www.kernel.org/doc/Documentation/block/cfq-iosched.txt

 


免責聲明!

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



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