数据结构 如果一个用户创建了大量进程,由于CFS的机制,这些进程会和其他用户的进程按照优先级对应比例瓜分CPU时间,最总导致这个用户占用过多的CPU时间。引入组调度后,就可以按照任务组的粒度来平分CPU时间,防止以上的问题出现。struct task_group用于描述一个任务组,裁剪后的数据结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct task_group {#ifdef CONFIG_FAIR_GROUP_SCHED struct sched_entity **se ; struct cfs_rq **cfs_rq ; unsigned long shares;#endif struct list_head list ; struct task_group *parent ; struct list_head siblings ; struct list_head children ;#ifdef CONFIG_SCHED_AUTOGROUP struct autogroup *autogroup ;#endif };
se[i]
表示这个task_group
在第 i 个 CPU 上的 调度实体, 该se
代表的也是一个任务组。
cfs_rq[i]
指向这个任务组在第i个CPU上的rq,组织这个任务组在该CPU上的任务,也等于se[i].my_rq
sched_entity
结构体如下,通过my_q
可以判断一个调度实体是否是任务组,如果该为 NULL, 则表示一个任务,否则就是一个任务组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 struct sched_entity { struct load_weight load ; struct rb_node run_node ; struct list_head group_node ; unsigned int on_rq; u64 exec_start; u64 sum_exec_runtime; u64 vruntime; u64 prev_sum_exec_runtime; u64 nr_migrations; struct sched_statistics statistics ; #ifdef CONFIG_FAIR_GROUP_SCHED int depth; struct sched_entity *parent ; struct cfs_rq *cfs_rq ; struct cfs_rq *my_q ; unsigned long runnable_weight;#endif }
内核有一个全局链表task_groups
,新创建的task_group
会添加到这个链表中;全局根节点struct task_group root_task_group以及
创建任务组/添加任务 cpuset相关函数handler如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct cgroup_subsys cpuset_cgrp_subsys = { .css_alloc = cpuset_css_alloc, .css_online = cpuset_css_online, .css_offline = cpuset_css_offline, .css_free = cpuset_css_free, .can_attach = cpuset_can_attach, .cancel_attach = cpuset_cancel_attach, .attach = cpuset_attach, .post_attach = cpuset_post_attach, .bind = cpuset_bind, .fork = cpuset_fork, .legacy_cftypes = legacy_files, .dfl_cftypes = dfl_files, .early_init = true , .threaded = true , };
通过cgroup的接口可以很方便地创建一个调度组,对应内核代码流程为:
1 cgorup_mkdir ->cgroup_apply_control_enable ->cpuset_css_alloc -> cpuset_css_online
往一个任务组添加任务流程如下:
1 cgroup_file_write ->cgroup_attach_task ->cgroup_migrate ->cgroup_migrate_execute ->cpu_cgroup_attach -> sched_move_task
调度逻辑 组调度并没有改变太多的CFS调度逻辑,下面主要分析下选任务和时间分配相关变更:
摘取核心逻辑如下,如果se
的cfs_rq
是NULL,代表这个se
是一个任务,因此不进入循环;对于任务组,会一直循环cfs_rq
里的se
,直到找到一个se
是一个任务。
1 2 3 4 5 6 7 8 9 10 11 12 struct task_struct *pick_next_task_fair (struct rq *rq, struct task_struct *prev, struct rq_flags *rf) { struct cfs_rq *cfs_rq = &rq->cfs; struct sched_entity *se ; struct task_struct *p ; do { se = pick_next_entity(cfs_rq, NULL ); cfs_rq = group_cfs_rq(se); } while (cfs_rq); p = task_of(se); }
如果se代表的是任务组,那么该se
在当前cfsrq
中根据任务组的时间份额进行二次分配。相关函数是 sched_slice
,采用自下而上的方式遍历整个cfs_rq
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #define for_each_sched_entity(se) \ for (; se; se = se->parent) static u64 sched_slice (struct cfs_rq *cfs_rq, struct sched_entity *se) { u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq); for_each_sched_entity(se) { struct load_weight *load ; struct load_weight lw ; cfs_rq = cfs_rq_of(se); load = &cfs_rq->load; if (unlikely(!se->on_rq)) { lw = cfs_rq->load; update_load_add(&lw, se->load.weight); load = &lw; } slice = __calc_delta(slice, se->load.weight, load); } return slice; }
如下示意图很清楚地解释了计算过程,最终SE2的时间为slice * r2 * r1 * r0
,也很容易理解,因为r0 * r1 * r2
就是SE2在整个cfs_rq里的权重占比。
参考文档
Linux Source V5.10
Linux 内核的 CFS 任务调度
Linux核心概念详解
Linux进程调度-组调度及带宽控制