2.6.4.3 带宽控制 - 挂起与解挂

当需要挂起一个cfs_rq时,系统调用函数 throttle_cfs_rq 来实现,挂起的意思就是不要让CFS再调度到该cfs_rq中的任何任务,也就是说,我们需要将整个cfs_rq从该CPU的rq中移除,实际上就是移除上层cfs_rq中指向当前cfs_rq的那个调度实体。整个流程我们可以参考下图:

这里系统将要挂起CPU0队列里面的 cfs_rq_2, 此时我们需要将 cfs_rq_1 中指向它的调度实体 SE_R1cfs_rq_1 中删除,而将 SE_R1 删除之后, cfs_rq_1 就是一个空队列了,这时又需要将指向它的调度实体 SE_R0cfs_rq_0 中删除。该过程需要一直沿着cfsrq的parent属性往上走,直到遇上不需要删除的cfs_rq为止,在上图中,该cfs_rq就是 cfs_rq_0. 整个挂起操作完成后,CPU0的队列示意图为:

当然挂起 cfs_rq 时,系统还需要更新所有受影响的 cfs_rq 中的对应字段。函数 throttle_cfs_rq 的实现如下:

/* file: kernel/sched/fair.c */
static bool throttle_cfs_rq(struct cfs_rq *cfs_rq) {
    struct rq *rq = rq_of(cfs_rq);
    struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(cfs_rq->tg);
    struct sched_entity *se;
    long task_delta, idle_task_delta, dequeue = 1;

    raw_spin_lock(&cfs_b->lock);
    /* 再最后尝试一把,如果定时器在此刻之前已经更新了任务组的时间了的话,那我们就不用挂起了
     */
    if (__assign_cfs_rq_runtime(cfs_b, cfs_rq, 1)) {
        dequeue = 0;
    } else {
        /* 确实需要挂起,将cfs_rq加入到cfs_bandwidth的挂起列表中,以便后期定时器为其重置时间
         */
        list_add_tail_rcu(&cfs_rq->throttled_list, &cfs_b->throttled_cfs_rq);
    }
    raw_spin_unlock(&cfs_b->lock);

    if (!dequeue)
        return false; /* Throttle no longer required. */

    /* 通过task_group->se拿到对应CPU队列中指向cfs_rq的那个se */
    se = cfs_rq->tg->se[cpu_of(rq_of(cfs_rq))];

    /* freeze hierarchy runnable averages while throttled */
    rcu_read_lock();
    /* 更新以cfs_rq->tg为顶点的所有任务组中cfs_rq的throttle_count字段,将其加一 */
    walk_tg_tree_from(cfs_rq->tg, tg_throttle_down, tg_nop, (void *)rq);
    rcu_read_unlock();

    /* cfs_rq 及其所有子孙 cfs_rq 队列中的可运行任务的总数 */
    task_delta = cfs_rq->h_nr_running;
    idle_task_delta = cfs_rq->idle_h_nr_running;
    /* 删除上层队列中对应的 se, 如上图中所示 */
    for_each_sched_entity(se) {
        struct cfs_rq *qcfs_rq = cfs_rq_of(se);
        /* throttled entity or throttle-on-deactivate */
        if (!se->on_rq)
            goto done;

        /* 从上层 cfs_rq 中删除对应的se */
        dequeue_entity(qcfs_rq, se, DEQUEUE_SLEEP);

        /* 更新上层 cfs_rq 中的对应字段 */
        qcfs_rq->h_nr_running -= task_delta;
        qcfs_rq->idle_h_nr_running -= idle_task_delta;

        /* qcfs_rq->load.weight == 0 的话,说明 qcfs_rq 删除 se
         * 后就是空队列了,否则就可以退出循环了 */
        if (qcfs_rq->load.weight) {
            /* Avoid re-evaluating load for this entity: */
            se = parent_entity(se);
            break;
        }
    }

    /* 经过上面的循环之后,虽然此时se及其父节点都不需要dequeue了,但仍然需要更新对应的字段,因此这里再循环处理一次
     */
    for_each_sched_entity(se) {
        struct cfs_rq *qcfs_rq = cfs_rq_of(se);
        /* throttled entity or throttle-on-deactivate */
        if (!se->on_rq)
            goto done;

        update_load_avg(qcfs_rq, se, 0);
        se_update_runnable(se);

        qcfs_rq->h_nr_running -= task_delta;
        qcfs_rq->idle_h_nr_running -= idle_task_delta;
    }

    /* At this point se is NULL and we are at root level*/
    sub_nr_running(rq, task_delta);

done:
    /*
     * 设置 throttle 的标记位,并记录时间
     */
    cfs_rq->throttled = 1;
    cfs_rq->throttled_clock = rq_clock(rq);
    return true;
}

最后 cfs_rq->throttled 被设置为1后,整个cfs_rq就被挂起了,被挂起的cfs_rq已经不在CPU的运行队列中,因此其中的任务就不会被调度到了。

上面是挂起操作,解挂的逻辑由函数 unthrottle_cfs_rq 完成,解挂是挂起的逆操作,代码结构几乎都可以对应上,主要就是将cfs_rq入队、更新对应se节点的信息、最后有必要的话调用 resched_curr 触发调度,感兴趣的读者可以自行阅读源码,这里不再深究。

Last updated