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_R1
从 cfs_rq_1
中删除,而将 SE_R1
删除之后, cfs_rq_1
就是一个空队列了,这时又需要将指向它的调度实体 SE_R0
从 cfs_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
Was this helpful?