Read the fucking source code!
--By 鲁迅A picture is worth a thousand words.
--By 高尔基说明:数组
组调度(task_group
)是使用Linux cgroup(control group)
的cpu子系统来实现的,能够将进程进行分组,按组来分配CPU资源等。
好比,看一个实际的例子:
A和B两个用户使用同一台机器,A用户16个进程,B用户2个进程,若是按照进程的个数来分配CPU资源,显然A用户会占据大量的CPU时间,这对于B用户是不公平的。组调度就能够解决这个问题,分别将A、B用户进程划分红组,并将两组的权重设置成占比50%便可。数据结构
带宽(bandwidth
)控制,是用于控制用户组(task_group
)的CPU带宽,经过设置每一个用户组的限额值,能够调整CPU的调度分配。在给定周期内,当用户组消耗CPU的时间超过了限额值,该用户组内的任务将会受到限制。函数
因为组调度和带宽控制紧密联系,所以本文将探讨这两个主题,本文的讨论都基于CFS调度器,开始吧。工具
struct task_group
来组织的,task_group
自己支持cfs组调度
和rt组调度
,本文主要分析cfs组调度
。sched_entity
调度实体,task_struct(表明进程)
和task_group(表明进程组)
中分别包含sched_entity
,进而来参与调度;关于组调度的相关数据结构,组织以下:3d
task_groups
,建立的task_group
会添加到这个链表中;root_task_group
全局结构,充当task_group
的根节点,以它为根构建树状结构;struct task_group
的子节点,会加入到父节点的siblings
链表中;struct task_group
会分配运行队列数组和调度实体数组(以CFS为例,RT调度相似),其中数组的个数为系统CPU的个数,也就是为每一个CPU都分配了运行队列和调度实体;对应到实际的运行中,以下:指针
struct cfs_rq
包含了红黑树结构,sched_entity
调度实体参与调度时,都会挂入到红黑树中,task_struct
和task_group
都属于被调度对象;task_group
会为每一个CPU再维护一个cfs_rq
,这个cfs_rq
用于组织挂在这个任务组上的任务以及子任务组,参考图中的Group A
;pick_next_task_fair
时,会从遍历队列,选择sched_entity
,若是发现sched_entity
对应的是task_group
,则会继续往下选择;sched_entity
结构中存在parent
指针,指向它的父结构,所以,系统的运行也能从下而上的进行遍历操做,一般使用函数walk_tg_tree_from
进行遍历;/sys
文件系统进行设置,好比操做/sys/fs/cgoup/cpu/A/shares
;调用流程以下图:code
sched_group_set_shares
来完成最终的设置;task_group
为每一个CPU都分配了一个sched_entity
,针对当前sched_entity
设置更新完后,往上对sched_entity->parent
设置更新,直到根节点;shares
的值计算与load
相关,所以也须要调用update_load_avg
进行更新计算;看一下实际的效果图吧:对象
echo XXX > /sys/fs/cgroup/cpu/A/B/cpu.shares
;sched_entity
后,继续按一样的流程处理sched_entity->parent
;先看一下/sys/fs/cgroup/cpu
下的内容吧:blog
cfs_period_us
和cfs_quota_us
,这两个与cfs_bandwidth息息相关;period
表示周期,quota
表示限额,也就是在period
期间内,用户组的CPU限额为quota
值,当超过这个值的时候,用户组将会被限制运行(throttle
),等到下一个周期开始被解除限制(unthrottle
);来一张图直观理解一下:队列
quota
的配额下,超过了就throttle
,下一个周期从新开始;内核中使用struct cfs_bandwidth
来描述带宽,该结构包含在struct task_group
中。
此外,struct cfs_rq
中也有与带宽控制相关的字段。
仍是来看一下代码吧:
struct cfs_bandwidth { #ifdef CONFIG_CFS_BANDWIDTH raw_spinlock_t lock; ktime_t period; u64 quota, runtime; s64 hierarchical_quota; u64 runtime_expires; int idle, period_active; struct hrtimer period_timer, slack_timer; struct list_head throttled_cfs_rq; /* statistics */ int nr_periods, nr_throttled; u64 throttled_time; #endif };
struct cfs_rq
结构中相关字段以下:
struct cfs_rq { ... #ifdef CONFIG_CFS_BANDWIDTH int runtime_enabled; u64 runtime_expires; s64 runtime_remaining; u64 throttled_clock, throttled_clock_task; u64 throttled_clock_task_time; int throttled, throttle_count; struct list_head throttled_list; #endif /* CONFIG_CFS_BANDWIDTH */ ... }
先看一下初始化的操做,初始化函数init_cfs_bandwidth
自己比较简单,完成的工做就是将struct cfs_bandwidth
结构体进程初始化。
period_timer
和slack_timer
;period_timer
定时器,用于在时间到期时从新填充关联的任务组的限额,并在适当的时候unthrottle
cfs运行队列;slack_timer
定时器,slack_period
周期默认为5ms,在该定时器函数中也会调用distribute_cfs_runtime
从全局运行时间中分配runtime;start_cfs_bandwidth
和start_cfs_slack_bandwidth
分别用于启动定时器运行,其中能够看出在dequeue_entity
的时候会去利用slack_timer
,将运行队列的剩余时间返回给tg->cfs_b
这个runtime pool
;unthrottle_cfs_rq
函数,会将throttled_list
中的对应cfs_rq
删除,而且从下往上遍历任务组,针对每一个任务组调用tg_unthrottle_up
处理,最后也会根据cfs_rq
对应的sched_entity
从下往上遍历处理,若是sched_entity
不在运行队列上,那就从新enqueue_entity
以便参与调度运行,这个也就完成了解除限制的操做;do_sched_cfs_period_timer
函数与do_sched_cfs_slack_timer()
函数都调用了distrbute_cfs_runtime()
,该函数用于分发tg->cfs_b
的全局运行时间runtime
,用于在该task_group
中平衡各个CPU上的cfs_rq
的运行时间runtime
,来一张示意图:
task_group
针对每一个cpu都维护了一个cfs_rq
,这些cfs_rq
来共享该task_group
的限额运行时间;用户能够经过操做/sys
中的节点来进行设置:
/sys/fs/cgroup/cpu/
下的cfs_quota_us/cfs_period_us
节点,最终会调用到tg_set_cfs_bandwidth
函数;tg_set_cfs_bandwidth
会从root_task_group
根节点开始,遍历组调度树,并逐个设置限额比率 ;cfs_bandwidth
的runtime
信息;cfs_bandwidth
功能,则启动带宽定时器;task_group
中的每一个cfs_rq
队列,设置runtime_remaining
值,若是cfs_rq
队列限流了,则须要进行解除限流操做;throttle
限流操做cfs_rq
运行队列被限制,是在throttle_cfs_rq
函数中实现的,其中调用关系以下图:
sched_entity
入列时,进行检测是否运行时间已经达到限额,达到则进行限制处理;pick_next_task_fair/put_prev_task_fair
在选择任务调度时,也须要进行检测判断;整体来讲,带宽控制的原理就是经过task_group
中的cfs_bandwidth
来管理一个全局的时间池,分配给属于这个任务组的运行队列,当超过限额的时候则限制队列的调度。同时,cfs_bandwidth
维护两个定时器,一个用于周期性的填充限额并进行时间分发处理,一个用于将未用完的时间再返回到时间池中,大抵如此。
组调度和带宽控制就先分析到此,下篇文章将分析CFS调度器
了,敬请期待。