這一篇講解 ioband 的機制
ioband的原理很簡單:ioband裝置設定了許多group,每個group有自己的權重和閥值,ioband驅動對於這些group的IO請求進行QoS控制。ioband裝置基於token來進行控制,根據group的權重分配不同的token。而策略也包括基於request的和基於sector的token控制
dm-ioband涉及到幾個重要的資料結構:
struct ioband_device:代表了/dev/mapper/下的一個ioband塊裝置,其上有數個ioband_group,至少有一個default group
struct ioband_group:代表了ioband裝置上attached的一個group,每個group有不同的權重,策略等。 ioband_group上兩個bio列表:c_prio_bios和c_blocked_bios,前者代表優先順序較高的struct bio。
ioband_device->g_issued[BLK_RW_ASYNC], ioband_device->g_issued[BLK_RW_SYNC] 代表了device 所有blocked, issued的bio個數
ioband_group->c_issued[BLK_RW_SYNC]代表了group所有blocked的bio個數
static void suspend_ioband_device(struct ioband_device*, unsigned long, int) :首先set_device_suspended設定DEV_SUSPENDED標籤,同時set_group_down和set_group_need_up設定IOG_GOING_DOWN和IOG_NEED_UP標籤。然後wake_up_all喚醒所有等待在ioband_device->g_waitq和ioband_group->c_waitq上的進程。對於已經mapped好的bio,調用queue_delayed_work
+ flush_workqueue來通過工作隊列處理這些bio,最後調用wait_event_lock_irq,等待ioband_device上的所有bio請求都被flush成功。 BTW,這裡的wait_event_lock_irq的實現和pthread裡的condition非常類似。
static void resume_ioband_device(struct ioband_device* dp):該函數清除所有的DEV_SUSPENDED, IOG_GOING_DOWN, IOG_NEED_UP標籤,同時喚醒所有等待在ioband_device->g_waitq_suspend上的函數。這個ioband_device->g_waitq_suspend可以看到只在ioband_map中有用到,因為一旦ioband_device被suspend,所有的bio都會在這裡被hang住。
static void ioband_group_stop_all(struct ioband_group* head, int suspend):對所有group設定IOG_SUSPENDED,IOG_GOING_DOWN標誌,通過g_ioband_wq去flush所有工作隊列的bio
static void ioband_group_resume_all(struct ioband_group* head):恢複上述的標誌位
ioband在device mapper的架構中,是和linear, stripped, snapshot等類似的struct target_type,定義如下
static struct target_type ioband_target = {
.name = "ioband",
.module = THIS_MODULE,
.version = {1, 14, 0},
.ctr = ioband_ctr,
.dtr = ioband_dtr,
.map = ioband_map,
.end_io
= ioband_end_io,
.presuspend = ioband_presuspend,
.resume
= ioband_resume,
.status
= ioband_status,
.message = ioband_message,
.merge = ioband_merge,
.iterate_devices = ioband_iterate_devices,
};
static int ioband_ctr(struct dm_target *ti, unsigned argc, char **argv)
ioband_ctr首先調用alloc_ioband_device產生一個ioband_device的ioband裝置。alloc_ioband_device首先調用 create_workqueue("kioband") 建立一個 workqueue_struct 成員 g_ioband_wq 。之後初始化一系列的 ioband_device 的成員變數,最後返回新建立並初始化好的 ioband_device 結構指標
static void ioband_dtr(struct dm_target* ti)
調用ioband_group_stop_all停止ioband上所有group的請求(設IOG_GOING_DOWN 和 IOG_SUSPENDED兩個標誌位),調用cancel_delayed_work_sync去取消之前的delayed work_struct,調用ioband_group_destroy_all去destroy ioband裝置上的所有group。這裡可以看出ioband裝置上的group是以紅/黑樹狀結構的資料結構儲存的,而不是device mapper用到的btree
static int ioband_map(struct dm_target* ti, struct bio* bio, union map_info* map_context)
注意,ioband_map是把同步和非同步請求區分對待的,比如ioband_device結構的g_issued[2], g_blocked[2], g_waitq[2], ioband_group結構的 c_waitq[2], 都是用來區分sync, async請求控制的
ioband_group通過dm_target->private擷取,而ioband_device又可以通過ioband_group->c_banddev來擷取。之後步驟如下:
- 如果ioband_device是suspend狀態,調用wait_event_lock_irq 等待其恢複
- 調用ioband_group_get,通過bio找到對應的ioband_group
- prevent_burst_bios這個函數很有意思,我的理解是,如果當前是核心線程在執行(is_urgent_bio貌似只是簡單實現了下,作者認為今後bio結構需要有個控製為來判斷是否是urgent bio),就調用device_should_block來判斷是否當前device阻塞了,判斷的依據是根據io_limit這個參數:對於同步請求如果超過了io_limit,則所有這個device上的同步請求都阻塞,對於非同步請求處理和同步一致;如果不是核心線程,就調用group_should_block來判斷是否當前group阻塞了,對於group是否應該阻塞,不同的policy有不同的判斷方法:對於基於weight的判斷,會最終調用is_queue_full,而基於頻寬的判斷則調用range_bw_queue_full。這兩個函數後續再深入研究。
- 如果should_pushback_bio返回true,這個bio將會被重新放入隊列,此時返回 DM_MAPIO_REQUEUE
- 下面查看ioband_group->c_blocked[2]判斷該請求是否被阻塞,調用room_for_bio_sync判斷io_limit是否已經滿了,如果都是false,則此時bio可以被提交,否則調用hold_bio暫停該bio。hold_bio的核心函數是調用 ioband_device->g_hold_bio。對於ioband_device->g_hold_bio而言其函數指標指向ioband_hold_bio,該函數只是把bio放入 ioband_group->c_blocked_bios隊列中。(作者認為c_blocked_bios應該有兩個,區分同步或者非同步請求)
- 如果bio可以被提交,會調用ioband_device->g_can_submit,這裡 g_can_submit 會根據policy的不同採用不同的判斷方法,如果是基於weight的policy,則g_can_submit會調用is_token_left,如果是基於頻寬的policy,則g_can_submit會調用has_right_to_issue,這兩個函數後續再深入研究
- 如果g_can_submit返回false,說明bio還是不能提交,此時還是會走到hold_bio。這裡有個queue_delayed_work的核心調用,會延遲1個jiffies之後啟動工作隊列ioband_device->g_ioband_wq,這個工作隊列會調用ioband_device->g_conductor->work.func(work_struct *)
- 如果bio確定可以提交,會調用prepare_to_issue(struct ioband_group*, struct bio*),該函數首先對ioband_device->g_issued計數器加1,接著調用ioband_device->g_prepare_bio,該函數同樣是個policy相關的函數,在基於weight的policy下調用prepare_token,在基於頻寬的policy下調用range_bw_prepare_token,在基於weight-iosize的policy下調用iosize_prepare_token
static int ioband_end_io(struct dm_target* ti, struct bio* bio, int error, union map_info* map_context)
調用should_pushback_bio判斷ioband_group是否已經suspend,如果已經suspend就直接返回DM_ENDIO_REQUEUE重新放入隊列,否則如果有blocked bio,則啟動工作隊列ioband_device->g_ioband_wq;如果ioband_device已經suspend了,喚醒所有wait在 g_waitq_flush上的程式
static void ioband_conduct(struct work_struct* work)
ioband_conduct 函數是核心工作隊列延遲處理所調用的方法,傳入的參數指標指向ioband_device->g_conductor.work結構,可以通過這個struct work_struct* 得到struct ioband_device。步驟如下:
- 首先調用release_urgent_bios,把ioband_device->g_urgent_bios全部放入issue_list列表
- 如果ioband_device中有阻塞的bio請求,根據一定策略選取一個ioband_group,該ioband_goup需要有阻塞的bio,同時io_limit沒有滿。基於該ioband_group調用release_bios,release_bios分別調用release_prio_bios和release_norm_bios,其目的是把阻塞的bio放入issue_list列表。
- release_prio_bios操作ioband_group->c_prio_bios裡的bio(如果當前group無法submit bio,比如token用完,此時直接返回R_BLOCK),對每一個bio調用make_issue_list,放到issue_list或者pushback_list列表中,此時如果group的c_blocked為0,可以清楚group的block標誌:IOG_BIO_BLOCKED_SYNC/IOG_BIO_BLOCKED_ASYNC,同時喚醒等待在ioband_group->c_waitq[2]上的程式。最後調用prepare_to_issue。
- release_norm_bios操作ioband_group->c_blocked_bios裡的bio,此類bio的個數為 nr_blocked_group(ioband_group*) - ioband_group->c_prio_blocked,剩下的代碼和 release_prio_bios完全一致,不多說了
- 如果release_bios返回了R_YIELD,此時說明這個group已經用了所有的token,需要把submit bio的優先順序讓出來,此時會再調一次 queue_delayed_work,等待下次處理
- 對device上之前所有block的bio請求,開始重新提交過程,首先清除掉ioband_device上的阻塞標誌 DEV_BIO_BLOCKED_SYNC/DEV_BIO_BLOCKED_ASYNC,並喚醒所有等待在wait_queue_head_t ioband_device->g_waitq[2]上的代碼
- 如果此時ioband_device還有block bio,同時經過上述代碼之後issue_list還是為空白,此時基本上是所有group都把token耗盡了,重新加入工作隊列等待下一次執行
- 最後對於issue_list裡所有的bio,調用常規方法generic_make_request 交給底層塊裝置執行,對於pushback_list的所有bio,調用ioband_end_io結束該bio請求(大部分情況下返回一個EIO錯誤)
----------------------------------------------------華麗的分割線---------------------------------------------------
下面研究dm-ioband中的policy,在ioband_ctr中,會調用policy_init來初始化指定策略。目前ioband有如下幾種policy:default,weight,weight-iosize,range-bw。
weight策略:基於權重來分配bio的策略,下面對相應方法逐一分析
dp->g_group_ctr/db->g_group_dtr = policy_weight_ctr/policy_weight_dtr:建立/銷毀一個基於weight的group。
dp->g_set_param = policy_weight_param:調用set_weight, init_token_bucket等來設定權重值。從set_weight的實現我們可以看出,ioband_group是按照rbtree的方式來組織的,如果ioband_group->c_parent == NULL,那麼說明這是default group或者是新的group類型的root group,因此用 ioband_device的參數:g_root_groups,g_token_bucket,g_io_limit來初始化,否則這個ioband_group是另一個ioband_group的child(這種配置很少見),因此用ioband_group的參數來配置。
dp->g_should_block = is_queue_full:是否超過了ioband_group->c_limit來判斷隊列是否滿
dp->g_restart_bios = make_global_epoch:這個ioband_device裝置上的ioband_group都已經耗盡了token,調用這個函數重新分配新一輪token。
dp->g_can_submit = is_token_left:查看是否還有token剩餘(通過計算iopriority),首先可以查看ioband_group->c_token,其次可以查看這個iobnad_group的epoch是否落後於整個ioband_device的epoch(epoch加一表示又進行了一次token重新整理,是一個自增量),如果是,可以把這些epoch以來的所有新增token都補上(nr_epoch*ioband_group->c_token_initial),再次重新計算iopriority並返回。
PS,這裡我們也可以看出,一個IO請求的優先順序和group所剩餘的token數和group的初始token數都有關係,這樣的好處是不會餓死那些低token數的group上的IO請求。
dp->g_prepare_bio = prepare_token:對於weight策略而言,每一次IO請求消耗一個token。prepare_token會調用consume_token,consume_token會更新ioband_group->g_dominant 和 ioband_group->g_expired,同時減去ioband_group->c_token,加上ioband_group->c_consumed,值都為1。這裡有個g_yield_mark,用來gracefully的讓出IO,這裡不多說了,詳細的內容請去看源碼。