2.6.4.2 带宽控制 - 带宽时间

前文提到实际的带宽控制是在任务组下属的cfs_rq队列中进行的,而cfs_rq 对带宽时间的操作归总起来就两点:更新与申请。

cfsrq申请到的时间保存在字段 runtime_remaining 中,每当更新任务的时间时,系统也会更新该字段。前面讨论vruntime时我们提到系统通过函数 update_curr 来更新与任务相关的时间信息,实际上该函数也会更新与带宽控制相关的时间:

/* file: kernel/sched/fair.c */
static void update_curr(struct cfs_rq *cfs_rq) {
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_clock_task(rq_of(cfs_rq));
    u64 delta_exec;

    /* 本次更新 vruntime 与上次更新 vruntime 之间的差值 */
    delta_exec = now - curr->exec_start;

    /* 省略其他更新时间的代码,这部分在讨论vruntime时已经讨论过 */

    /* 更新与带宽控制相关的字段 */
    account_cfs_rq_runtime(cfs_rq, delta_exec);
}

/* 判断是否启用了带宽控制,如果是的话调用函数__account_cfs_rq_runtime更新对应字段
 */
static __always_inline void account_cfs_rq_runtime(struct cfs_rq *cfs_rq,
                                                   u64 delta_exec) {
    if (!cfs_bandwidth_used() || !cfs_rq->runtime_enabled)
        return;

    __account_cfs_rq_runtime(cfs_rq, delta_exec);
}

static void __account_cfs_rq_runtime(struct cfs_rq *cfs_rq, u64 delta_exec) {
    /* 从剩余时间中减去这次消耗掉的部分,注意这里可能导致cfs_rq->runtime_remaining为负数
     */
    cfs_rq->runtime_remaining -= delta_exec;

    /* 如果还有剩余时间,则函数返回 */
    if (likely(cfs_rq->runtime_remaining > 0))
        return;

    /* 接下来要向所在的任务组申请时间,但如果当前cfs_rq已经被挂起了的话,就不用麻烦了
     */
    if (cfs_rq->throttled)
        return;
    /*
     * 此时我们调用函数assign_cfs_rq_runtime向任务组申请时间。如果申请时间失败,则cfs_rq应该被挂起。
     * 这里调用resched_curr标记cfs_rq->curr的TIF_NEED_RESCHED位,以便随后将其调度出去
     */
    if (!assign_cfs_rq_runtime(cfs_rq) && likely(cfs_rq->curr))
        resched_curr(rq_of(cfs_rq));
}

更新带宽时间的逻辑其实很简单,就是从 cfs->runtime_remaining 减去本次执行的物理时间。如果此时调度器发现时间余额已经耗尽,则会立即尝试从任务组中申请,如果申请失败,说明在本周期内整个任务组的时间都已经耗尽了,而如果当前正在执行的任务在本cfsrq中的话,则需要将其调度出去。从任务组中申请时间的函数是 assign_cfs_rq_runtime:

/* file: kernel/sched/fair.c */
static int assign_cfs_rq_runtime(struct cfs_rq *cfs_rq) {
    /* 返回task_group的cfs_bandwidth字段,该字段封装着整个任务组的带宽数据 */
    struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(cfs_rq->tg);
    int ret;

    raw_spin_lock(&cfs_b->lock);
    /* 实际的申请逻辑,第三个参数是期望申请的时间额度,默认是5ms */
    ret = __assign_cfs_rq_runtime(cfs_b, cfs_rq, sched_cfs_bandwidth_slice());
    raw_spin_unlock(&cfs_b->lock);

    return ret;
}

static int __assign_cfs_rq_runtime(struct cfs_bandwidth *cfs_b,
                                   struct cfs_rq *cfs_rq, u64 target_runtime) {
    u64 min_amount, amount = 0;

    lockdep_assert_held(&cfs_b->lock);

    /* 前文聊到更新带宽时间时,提到 cfs_rq->runtime_remaining
     * 可能为负,这说明上次运行时已经透支了部分时间,这里补回来。
     */
    min_amount = target_runtime - cfs_rq->runtime_remaining;

    /* 没有限制,则要多少分配多少 */
    if (cfs_b->quota == RUNTIME_INF)
        amount = min_amount;
    else {
        /* 保证定时器是打开的,保证周期性地为任务组重置带宽时间 */
        start_cfs_bandwidth(cfs_b);

        /* 如果本周期内还有时间,则可以分配 */
        if (cfs_b->runtime > 0) {
            /* 确保不要透支 */
            amount = min(cfs_b->runtime, min_amount);
            cfs_b->runtime -= amount;
            cfs_b->idle = 0;
        }
    }

    /* 将新申请到的时间补充给cfs_rq */
    cfs_rq->runtime_remaining += amount;

    /* 是否成功地从task_group中申请到了时间 */
    return cfs_rq->runtime_remaining > 0;
}

Last updated