linux内核里,所有的io都由bio结构来管理,然后bio又合并成request(个别设备会直接处理bio),最后request丢进设备队列里。一个设备能允许最多一次性丢进去多少request,由 /sys/block/sdx/queue/nr_requests 参数决定,默认值是128。
后来,io也有了cgroup,这就意味着我可以设定两个进程A和B到两个group,group1的weight是1000,group2的weight是500,那么如果两个进程都在使劲读文件,那B进程的带宽(或iops)只能是A进程的一半,但是,有例外,如果B进程采用aio,一次发出128个io请求,一下子把设备能处理的128个request缓冲都用完,那A进程就取不到新的request也就无法发出io了,于是,cgroup按比例分配就失效了,高优先级的A进程被低优先级的B进程“抢占”了资源。为了解决这个问题,Tejun Heo提了一个patch系列,办法就是让每个group都可以有nr_requests个请求(针对每个group和一个queue之间,建立一个request_list),这样,如果一个设备由被来自n个group的进程访问,它实际上可以分配(n * nr_requests)个请求(因为有n个request_list),这样group与group之间就没有了竞争,优先级反转的bug就这么fix了。这个patch系列我在做flashcache改造的时候一起打进了阿里kernel(基于redhat-2.6.32)。

故事还没有说完。
最近在rhel 2.6.32-279 kernel上开发io scheduler相关的东西,发现一个诡异想象:我正在使用cfq调度器的cgroup功能,io正哗啦啦的往下发,这时猛然把调度器切换到deadline,会有一部分cfq时发下去的io在调度起切换以后才从设备返回,然后,由于此时调度起已经变了,最后由deadline调度器处理这写返回的io。有点说不通,一个调度起发出的request让另一个调度起来做完成处理,万一request里有cfq特有的指针,deadline岂不悲剧了。于是仔细看了一下调度器切换的代码。

elevator_switch
    --> elv_quiesce_start
        --> blk_drain_queue

函数名很清楚,切换新调度器之前要把老调度器上的request都抽干(drain_queue),看看函数实现:

void blk_drain_queue(struct request_queue *q, bool drain_all)
{   
    while (true) {
        bool drain = false;
        int i;

        spin_lock_irq(q->queue_lock);

        elv_drain_elevator(q);
        if (drain_all)
            blk_throtl_drain(q);

        __blk_run_queue(q);
        
        drain |= q->root_rl.elvpriv;
        
......

        spin_unlock_irq(q->queue_lock);

        if (!drain)
            break;
        msleep(10);
    }
}

__blk_run_queue是把queue里的request都发往设备,难道不等这些request返回就直接切还调度器吗?“等request全部返回”这个功能是这个函数里的哪个语句完成的?又用trace_printk跟踪了一下,哦,是这句

drain |= q->root_rl.elvpriv;

当kernel分配request时调用 rl.elvpriv++,释放request是调用 rl.elvpriv--,elvpriv如果等于0说明所有发出去的request都返回了。这不挺对的吗?哦,request_list现在不只一个了,它是多个!也就是说,如果进程A放在cgroup的group1里,它在group1里申请的request都算在group1的rl.elvpriv里,而blk_drain_queue里却判断root group的root_rl的elvpriv,当然不对了。
再倒回去看看upstream的代码,原来Tejun Heo早就有补丁 https://lkml.org/lkml/2012/6/4/552 ,是我打补丁打漏了......唉,backport也是个高风险高要求的苦活啊。