linux的CFQ調度器解析(2)

來源:互聯網
上載者:User

cfq_queue的屬性中,包括workload priority:IDLE, BE, RT,包括workload type:ASYNC, SYNC_NOIDLE, SYNC。同時cfq_queue雖然基於CFQ調度器,但其內部的演算法還是基於dead-line的

cfq_group包含了多個紅/黑樹狀結構service tree,對應不同workload priority, workload type

一些工具性質的函數:

cfq_find_cfqg(struct cfq_data *cfqd, struct blkio_cgroup *blkcg):如果blkcg是全域的blkio_root_cgroup,則返回cfq_data->root_group,否則首先調用blkiocg_lookup_group在全域的blkio_cgroup的hash列表中尋找__key為cfq_data指標的blkio_cgroup結構。通過blkio_cgroup尋找cfq_group的方法很簡單,通過一個container_of宏就可以。說的通俗一點,就是我們已經通過進程找到了對應的blkio_cgroup,可能是一個根cgroup也可能是某個子cgroup,但這個cgroup的設定未必會有相應的device,e.g.
只設定了blkio.weight而沒有設定blkio.dev_weight,這時cfq_data就派上用場了。假設這種情境:一個cgroup中的不同進程讀寫不同的block device,甚至一個進程讀寫不同的device,但proportional weight只是針對單個塊裝置來的,【這裡應該理解為什麼blkio_cgroup會有多個blkio_group的雜湊項了吧】,因此需要通過cfq_data這個key來找到那個blkio_group結構(順便填入cfq_group->blkg.dev,通過cfq_data->queue->backing_dev_info得來)

cfq_get_cfqg(struct cfq_data *cfqd):首先通過current指向的當前的task_struct,尋找其所屬的blkio_cgroup,如果當前task_struct還沒有加入cgroup,則返回全域的blkio_root_cgroup,其對應的cfq_group結構是cfq_data->root_group。再調用cfq_find_cfqg,由於傳入了cfq_data,因此可以找到cfq_data對應的cfq_cgroup結構。如果沒有找到則調cfq_alloc_cfqg初始化一個cfq_group結構,再通過current找到blkio_cgroup,最後調用cfq_init_add_cfqg_lists把cfq_data,
cfq_group, blkio_cgroup三個結構關聯起來。

cfq_get_queue(struct cfq_data *cfqd, bool is_sync, struct io_context *ioc, gfp_t gfp_mask):通過ioc得到cfq_data所關聯的塊裝置上進程的cfq_queue,其關鍵函數為cfq_find_alloc_queue,該函數首先調用cfq_get_cfqg,通過current指向的當前CPU上在跑的進程來找到cfqd所在塊裝置上的cfq_group;其次調用cfq_cic_lookup來得到塊裝置對應的cfq_io_context,
從而得到對應的cfq_queue(根據其是否同步請求);最後如果這時cfq_queue為空白,則調用kmem_cache_alloc_node重新分配一個cfq_queue,並將其初始化完畢

cfq_init_add_cfqg_lists(struct cfq_data *cfqd, struct cfq_group *cfqg, struct blkio_cgroup *blkcg):把cfqg->blkg這個block_group加到blkcg的雜湊表中,這裡雜湊索引值是cfq_data的指標值。同時cfqd這個cfq_data結構也儲存了一個雜湊表,表頭是cfq_data->cfqg_list,該函數會把cfq_group也同時加到這個雜湊表裡【這裡可以看到,blkio_cgroup會儲存一個blkio_group的雜湊表,每個cfq_data對應一個blkio_group】。同時每個cfq_data也會儲存一個雜湊表,記錄這個cfq_data對應的塊裝置下的所有cfq_group

cfq_get_io_context(struct cfq_data *cfqd, gfp_t gfp_mask)

/*
 * Setup general io context and cfq io context. There can be several cfq
 * io contexts per general io context, if this process is doing io to more
 * than one device managed by cfq.
 */

上面這段解釋了為什麼一個io_context裡會有多個cfq_io_context,因為一個進程可能同時讀寫多個裝置,這時需要通過cfq_data來確定塊裝置,從而得到基於這個塊裝置IO的cfq_io_context

該函數首先調用cfq_cic_lookup尋找是否已有cfq_io_context,如果有了就退出,否則調用cfq_alloc_io_context建立一個cfq_io_context,把這個cfq_io_context加入到io_context的radix_tree裡(key值為cfq_data指標),如果有必要則調用cfq_ioc_set_ioprio,cfq_ioc_set_cgroup來設定io priority和cgroup

cfq_ioc_set_cgroup(struct io_context *ioc):對於ioc的雜湊表ioc->cic_list中的每一個hash node(實際上是cfq_io_context),調用changed_cgroup。 其中changed_cgroup的作用是把cfq_io_context的cfq_queue類型的同步隊列設定為NULL,代碼中的解釋如下

/*
 * Drop reference to sync queue. A new sync queue will be
 * assigned in new group upon arrival of a fresh request.
 */

cfq_ioc_set_ioprio(struct io_context *ioc):和cfq_ioc_set_cgroup類似,跳過了

cfq_cic_lookup(struct cfq_data *cfqd, struct io_context *ioc):io_context儲存了一個radix_tree,其樹根為io_context->radix_root。據我猜測,io_context為什麼要包含一個cfq_io_context的radix tree呢?可能是因為進程會同時讀寫多個塊裝置,因此根據cfq_data的成員cic_index,裡面是cfq_data對應的塊裝置在radix
tree裡的索引。最後返回io_context中相應塊裝置對應的cfq_io_context

cfq_cic_link(struct cfq_data *cfqd, struct io_context *ioc, struct cfq_io_context *cic, gfp_t gfp_mask):具體的代碼注釋講的很清楚了,跳過

/*
 * Add cic into ioc, using cfqd as the search key. This enables us to lookup
 * the process specific cfq io context when entered from the block layer.
 * Also adds the cic to a per-cfqd list, used when this queue is removed.
 */

cic_to_cfqd(struct cfq_io_context *cic):cfq_io_context的key就是對應的cfq_data

cfq_set_request(struct request_queue *q, struct request *rq, gfp_t gfp_mask):這裡可以看到,有struct cfq_data *cfqd = q->elevator->elevator_data 說明cfq_data是基於塊裝置的。該函數作用是為一個request來分配相應的cfq_io_context, cfq_queue並存到request->elevator_private中。

cfq_scaled_cfqq_slice(struct cfq_data *cfqd, struct cfq_queue *cfqq):通過一系列公式,計算出一個cfq_queue所佔用的time_slice。首先計算cfq_cgroup中的平均cfq_queue個數,以及每個cfq_queue的time slice,相乘得到expect_latency為這個cgroup希望得到的time slice;同時調用cfq_group_slice按照權重比例計算出cgroup的time
slice;如果這個time slice小於expect_latency,則調整之前根據cfq_queue的優先順序計算出的slice,否則返回之前調用cfq_prio_to_slice得到的time slice

cfq_prio_slice cfq_prio_to_slice cfq_scale_slice:這三個函數都是計算隊列的服務時間slice time的

cfq_group_slice:cfq_data->grp_service_tree為一個cfq_rb_root為一個紅/黑樹狀結構樹根,其成員total_weight為這個塊裝置上所有cgroup的權重值,而cfq_group->weight為該cgroup的權重值,因此該函數返回基於cfq_target_latency,300ms,各個cgroup所佔用的slice時間,基於weight的比例。

cfq_set_prio_slice:設定cfq_queue中對應的slice_start, slice_end, allocated_slice

cfq_choose_req(struct cfq_data *cfqd, struct request *rq1, struct request *rq2, sector_t last):看代碼中的解釋

/*
 * Lifted from AS - choose which of rq1 and rq2 that is best served now.
 * We choose the request that is closest to the head right now. Distance
 * behind the head is penalized and only allowed to a certain extent.
 */

基本上可以認為,同步請求優先非同步請求,其次根據請求的位置,按照和AS類似的演算法決定優先處理哪個請求

__cfq_group_service_tree_add(struct cfq_rb_root *st, struct cfq_group *cfqg):向service tree插入一個cfq_group,其中紅/黑樹狀結構的key被編程為

static inline s64
cfqg_key(struct cfq_rb_root *st, struct cfq_group *cfqg)
{
return cfqg->vdisktime - st->min_vdisktime;
}

這邊vdisktime和min_vdisktime是幹什麼用的,目前我也不清楚

cfq_link_cfqq_cfqg(struct cfq_queue *cfqq, struct cfq_group *cfqg):這個函數本身沒啥可說的,但是驗證了在CFQ調度器中,所有的非同步請求都屬於cfq_data->root_group這個cgroup,因此不受指定cgroup的任何限制

#1248 - #1267這段代碼,是不需要cgroup調度支援的cfq調度器代碼,可以看出簡單很多,cfq_get_cfqg只是簡單返回cfq_data->root_group

cfq_service_tree_add(struct cfq_data *cfqd, struct cfq_queue *cfqq, bool add_front):該函數目的是把cfq_queue加入到cfq_group對應的service_tree的紅/黑樹狀結構中。首先根據io class, io priority來找到cfq_group對應的service_tree,類型為cfq_rb_root,其中插入的key是計算出來的一個起始時間,應該cfq_group是按照這個起始時間來依次處理掛在上面的所有cfq_queue的請求。最後調用cfq_group_notify_queue_add來通知cfq_data

cfq_prio_tree_lookup,cfq_prio_tree_add:這兩個函數都是把cfq_queue加到cfq_data裡的priority tree的紅/黑樹狀結構中,cfq_data共有8個priority tree,對應不同的優先順序,而紅/黑樹狀結構中的排序基於cfq_queue中第一個請求的sector position

cfq_resort_rr_list,cfq_add_cfqq_rr,cfq_del_cfqq_rr

前者把cfq_queue加到cfq_data中的cgroup對應的service_tree數組,以及cfq_data的priority tree的紅/黑樹狀結構中。

後者除了調用cfq_resort_rr_list以外,還遞增了cfq_data->busy_queues,cfq_data->busy_sync_queues

最後把cfq_queue移除出service tree,和priority tree,並調用cfq_group_notify_queue_del通知cfq_data

cfq_del_rq_rb,cfq_add_rq_rb:這兩個函數操作cfq_queue裡面的request請求,把請求從cfq_queue中添加或者刪除

cfq_add_rq_rb首先調用elv_rb_add把請求插到cfq_queue->sort_list這個紅/黑樹狀結構中,基於請求的起始sector,再調用cfq_dispatch_insert真正把請求下發到底層驅動上,下面再調用cfq_add_cfqq_rr把隊列掛到cfq_data代表的塊裝置上,下面重新選擇cfq_queue->next_rq,如果和之前的cfq_queue->next_rq不同,需要改動cfq_queue對應的優先順序並調整到隊列所在的cfq_data下的priority tree中

cfq_del_rq_rb調用elv_rb_del把請求從cfq_queue->sort_list中刪除,如果此時cfq_queue->sort_list為空白了,而該隊列又在cfq_data的priority tree中,則從紅/黑樹狀結構裡刪除掉

cfq_remove_request(struct request *rq):調用cfq_del_rq_rb從cfq_queue中刪除rq

cfq_find_rq_fmerge(struct cfq_data *cfqd, struct bio *bio):通過當前current的task_struct來找到io_context,從而調用cfq_cic_lookup來找到cfq_io_context,然後根據bio是否同步找到對應的cfq_queue;下面找到bio之後的第一個sector,然後在cfq_queue->sort_list中基於這個sector尋找是否有對應的bio可以被merge

cfq_merge:檢查是否可以merge,如何可以修改對應的request,把bio給merge進request

cfq_merged_request:調用cfq_reposition_rq_rb,把相應的request更新為已經merge了bio的request

__cfq_set_active_queue:似乎是初始化cfq_queue,並將其設定為active_queue

__cfq_slice_expired(struct cfq_data *cfqd, struct cfq_queue *cfqq, bool timed_out)

/*
 * current cfqq expired its slice (or was too idle), select new one
 */

#1744 - #1751 (kernel.org 3.0.23 cfq-iosched.c) 這裡的cfq_queue->slice_resid似乎是還沒有用完的slice_time

nr_sync 計算出cfq_group同步隊列的個數

cfq_group_served

charge表示已經用掉的配額,在不同模式下意義不同,如果是iops模式,用cfq_queue->slice_dispatch,也就是dispatch的請求個數作為不同的cfq_queue的配額,如果是非同步請求則用cfq_queue->allocated_slice,也就是分配給該隊列的時間作為配額;否則是同步非iops模式則等同於used_sl (通過cfq_cfqq_slice_usage計算得出)

#975 - #978 cfq_group->vdisktime似乎是改cgroup至今所有用掉的slice總和

如果當前的cfq_group的expire時間(cfq_data->workload_expires)在jiffies之前,那啥也不用做了,不然儲存相應的cfq_group的saved_workload_slice,saved_workload,saved_serving_prio

最後調用cfq_resort_rr_list,把cfq_queue插到cfq_group,cfq_data對應的service tree, prio tree的紅/黑樹狀結構中

所以代碼看下來,我猜測所有的cfq_queue開始時都在cfq_data和其對應的cfq_group的service tree,priority tree中,當輪到這個cfq_queue被處理時,從這些紅/黑樹狀結構中被摘下來,等到其slice expired之後,更新一系列參數後被放回原來的紅/黑樹狀結構裡;再進一步說,如果cfq_queue裡已經沒有請求了,則會把他們從紅/黑樹狀結構裡移除掉,如果此時cfq_queue的time slice還沒用完,會留著下次再用

這應該就是CFQ大致的工作原理

cfq_dist_from_last,cfq_rq_close

前者計算出request的sector和cfq_data的last_position對應的sector之間相隔的sector數,後者拿這個值和CFQ_CLOSE_THR比較,如果在這個範圍內就認為兩個request是相近的請求(言下之意不會對磁頭轉動造成太多的overhead)

cfqq_close(struct cfq_data *cfqd, struct cfq_queue *cur_cfqq)

/*
* First, if we find a request starting at the end of the last
* request, choose it.
*/

首先調用cfq_prio_tree_lookup尋找同一個priority tree下的cfq_queue中,有沒有起始sector正好在cfq_data->last_position上的,如果有則返回這個離當前磁頭位置最近的cfq_queue

/*
* If the exact sector wasn't found, the parent of the NULL leaf
* will contain the closest sector.
*/

如果cfq_prio_tree_lookup沒有找到,則返回的parent參數包含了sector差別最小的那個cfq_queue,如果這個cfq_queue->next_rq滿足了cfq_rq_close的要求,則返回這個cfq_queue

如果不滿足的,開始遍曆這個紅/黑樹狀結構(只遍曆一次),再次判斷這個cfq_queue是否滿足cfq_rq_close,如果不滿足就返回NULL

cfq_choose_wl,choose_service_tree:選擇最優的workload class,和選擇最優的service tree

choose_service_tree選擇的優先順序是RT > BE > IDLE,這個決定了cfq_data->serving_prio,然後調用cfq_choose_wl來決定cfq_data->serving_type,

/*
* the workload slice is computed as a fraction of target latency
* proportional to the number of queues in that workload, over
* all the queues in the same priority class
*/

group_slice調用cfq_group_slice,根據group的權重計算出來,而slice則是這個workload裡的queues占所有busy_queues_avg的比例而計算得出的

對於同步請求而言,slice在經過一系列的比對之後,會把cfq_data->workload_expires = jiffies + slice,即當前服務的cfq_queue的配額時間被放到cfq_data指定的成員中;而對於非同步請求而言,由於非同步優先順序比同步要低,會再經過一些處理,具體的請參考代碼

/*
* Async queues are currently system wide. Just taking
* proportion of queues with-in same group will lead to higher
* async ratio system wide as generally root group is going
* to have higher weight. A more accurate thing would be to
* calculate system wide asnc/sync ratio.
*/
static struct cfq_queue *cfq_select_queue(struct cfq_data *cfqd)

/*
 * 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.
 */

這裡可以參考之前關於cfq_slice_expired函數的解析,可以看到cfq_data每個時刻只服務一個cfq_queue,就是cfq_data->active_queue

cfq_may_dispatch(struct cfq_data *cfqd, struct cfq_queue *cfqq):判斷是否可以向底層驅動分發請求

#2463 如果cfq_queue允許idle一段時間,同時塊裝置還有非同步請求on-the-fly,暫時不分發

#2469 如果要分發的這個cfq_queue是一個非同步隊列,同時塊裝置上還有同步請求on-the-fly,暫時不分發

#2472 先給max_dispatch設定個初始值,預設是cfq_quantum/2 = 4

#2473 對於idle層級的cfq_queue,max_dispatch設為1

先看#2542,如果當前cfq_queue->dispatched,即已經分發的請求數目沒有超過max_dispatch,如果是同步隊列則允許分發,非同步隊列的話,需要修改下max_dispatch的值並重新和cfq_queue->dispatched比較,具體原因請看代碼注釋

/*
* 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

*/

再回到#2479,如果此時cfq_queue->dispatched已經超過了max_dispatch,如果這是個同步cfq_queue,同時此時塊裝置上只有這個cfq_queue有請求,那麼不限制該隊列的分發請求數,如下面注釋

/*
* 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.
*/

否則如果只有一個cfq_queue,放大一倍max_dispatch的值,到cfq_data->cfq_quantum = 8

不管怎樣,最後還是要比較一次cfq_queue->dispatched和max_dispatch的值,來決定是否給底層驅動分發下一個請求

cfq_exit_io_context:當一個task結束之後,需要對io_context關聯的所有cfq_io_context,調用cfq_exit_single_io_context

cfq_exit_single_io_context,__cfq_exit_single_io_context:對cfq_io_context的非同步,同步隊列,分別調用cfq_exit_cfqq

changed_cgroup(struct io_context *ioc, struct cfq_io_context *cic)

首先無視掉cic對應的非同步隊列,由此也可以看出其實CFQ裡,非同步請求是不分cgroup的,下面直接把cfq_io_context裡的同步隊列設定為NULL,代碼中的注釋告訴了為什麼要這麼做

/*
* Drop reference to sync queue. A new sync queue will be
* assigned in new group upon arrival of a fresh request.
*/

cfq_get_queue(struct cfq_data *cfqd, bool is_sync, struct io_context *ioc, gfp_t gfp_mask):如果是is_sync表示非同步,調用cfq_async_queue_prio返回塊裝置對應的非同步隊列;否則調用cfq_find_alloc_queue來建立一個新隊列

cfq_get_io_context(struct cfq_data *cfqd, gfp_t gfp_mask)

/*
 * Setup general io context and cfq io context. There can be several cfq
 * io contexts per general io context, if this process is doing io to more
 * than one device managed by cfq.
 */

首先調用get_io_context,擷取current指向的task_struct對應的io_context,如果沒有則建立一個io_context;下面調用cfq_cic_lookup擷取cfq_io_context,如果為空白則調用cfq_alloc_io_context建立一個cfq_io_context,並調用cfq_cic_link把cfq_io_context和io_context關聯起來;最後調用cfq_ioc_set_ioprio,cfq_ioc_set_cgroup,實際上是對每個cfq_io_context,調用changed_ioprio,changed_cgroup,這些函數是設定個ioprio_changed,
cgroup_changed之類的標籤

cfq_update_idle_window(struct cfq_data *cfqd, struct cfq_queue *cfqq, struct cfq_io_context *cic)

首先,如果是非同步隊列或者隊列層級為IDLE,不考慮idle(slice_idle, group_idle之類的磁頭停留時間)

如果cfq_queue->next_rq->cmd_flags包含了REQ_NOIDLE,不考慮idle;如果slice_idle為0,也不考慮idle;如果io_context->nr_tasks為0,也不考慮idle

最後根據前後是否idle,調用cfq_mark_cfqq_idle_window/cfq_clear_cfqq_idle_window

cfq_should_preempt(struct cfq_data *cfqd, struct cfq_queue *new_cfqq, struct request *rq):判斷new_cfqq是否可以搶佔

/*
 * Check if new_cfqq should preempt the currently active queue. Return 0 for
 * no or if we aren't sure, a 1 will cause a preempt.
 */

#3317 如果new_cfqq是idle class的,其最低優先順序無法搶佔

#3320 如果cfqq,也就是cfq_data->active_queue是idle class的,必定可以被搶佔

#3326 不允許非RT搶佔RT的cfq_queue

#3333 當前cfqq不是同步隊列,那麼同步請求所在隊列可以搶佔

#3336 如果兩個隊列不屬於同一個cgroup,不可搶佔

#3339 如果當前cfqq時間片已經用完,可以搶佔

#3353 如果請求是基於中繼資料的,可以搶佔

#3359 RT請求可以搶佔非RT的請求

cfq_preempt_queue(struct cfq_data *cfqd, struct cfq_queue *cfqq)

/*
 * cfqq preempts the active queue. if we allowed preempt with no slice left,
 * let it have half of its nominal slice.
 */

cfq_rq_enqueued(struct cfq_data *cfqd, struct cfq_queue *cfqq, struct request *rq)

/*
 * Called when a new fs request (rq) is added (to cfqq). Check if there's
 * something we should do about it
 */

#3419 - #3421 可以看出cfq_data->rq_queued代表了塊裝置調度器等待處理的request,cfq_queue->meta_pending代表了每個隊列的中繼資料請求

#3429 - #3451 如果請求所在的隊列就是當前cfq_data的active_queue

/*
* Remember that we saw a request from this process, but
* don't start queuing just yet. Otherwise we risk seeing lots
* of tiny requests, because we disrupt the normal plugging
* and merging. If the request is already larger than a single
* page, let it rip immediately. For that case we assume that
* merging is already done. Ditto for a busy system that
* has other work pending, don't risk delaying until the
* idle timer unplug to continue working.
*/

#3452 - #3460 這時意味著要搶佔

無論上述哪種情況,都會調用__blk_run_queue,該函數會調用request_queue->request_fn,這個函數由底層驅動初始化,用來從調度隊列裡擷取請求。一般這個函數會調用電梯演算法的__elv_next_request,可能會再調用elevator_dispatch_fn。同時也意味著除此之外的情況不需要把請求立刻交給底層驅動

cfq_kick_queue(struct work_struct *work):該函數是一個工作隊列的順延強制函數,被賦值給cfq_data->unplug_work,該函數最後執行__blk_run_queue

void cfq_idle_slice_timer(unsigned long data)

/*
 * Timer running if the active_queue is currently idling inside its time slice
 */

我猜測是隊列slice_idle時間過去之後,觸發的timer執行的函數

如果cfq_cfqq_must_dispatch(cfqq)為true,無腦dispatch掉;如果time slice到期,調用cfq_slice_expired;如果cfq_data還有其他的busy queue,不作為;如果cfq_queue->sort_list不為空白,dispatch掉

分發的方式是調用cfq_schedule_dispatch,通過一個工作隊列調用cfq_data->unplug_work,這個unplug_work可以看到調用cfq_kick_queue來讓底層驅動得到請求

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.