这一篇讲解 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,这里不多说了,详细的内容请去看源码。