# 2.6.4.3 带宽控制 - 挂起与解挂

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

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Me-3_JXLYv-4hrEXyDb%2Fuploads%2FyilQBczj74uxtTDpU8E6%2Fthrottle_cfs_rq.png?alt=media\&token=a801e025-16df-485f-ae8a-831178797c0a)

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

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Me-3_JXLYv-4hrEXyDb%2Fuploads%2FIzrtYTSGNvd3Va361SSA%2Fafter_throttled_cfs_rq.png?alt=media\&token=e343332a-f8c4-4c07-a112-dcc06f630601)

当然挂起 `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` 触发调度，感兴趣的读者可以自行阅读源码，这里不再深究。
