CFS实现的公平的基本原理是这样的:指定一个周期,根据进程的数量,大家”平分“这个调度周期内的CPU使用权,调度器保证在一个周期内所有进程都能被执行到。CFS和之前O(n)调度器不同,优先级高的进程能获得更多运行时间,但不代表优先级高的进程一定就先运行:
调度器使用vruntime来统计进程运行的累计时间,理想状态下,所有进程的vruntime是相等时代表当前CPU的时间分配是完全公平的。但事实上,即使是多核的系统一般进程数也是大于核心数的,所以一旦有进程占用CPU运行势必会造成不公平,完全公平调度器通过让当前遭受不公最严重(vruntime最小)的进程优先运行来缓解不公平的情况。当然,vruntime所指的运行时间并未非和以往一样每个或每几个cpu tick周期增加1,需要经过优先级加权换算,优先级高的进程可能运行10个tick之后vruntime才加1,反之优先级低的进程可能运行1个tick之后vruntime就被加了10。
附一张图:
1.调度周期如何规定?
CFS引入了一个动态变化的调度周期:period。看两个CFS开放给用户的参数:
{//字面意思调度最小粒度,即进程每次被调度到最少要占用多长时间CPU
.procname = "sched_min_granularity_ns",
.data = &sysctl_sched_min_granularity,
},
{//字面意思调度延迟,即每个进程最长不等待超过调度延迟会被再次调度
.procname = "sched_latency_ns",
.data = &sysctl_sched_latency,
},
设想在最坏的情况下:只有一个CPU,等待运行的所有进程优先级相同所分得时间片相同,当一个进程运行过之后就要等待所有其他进程都运行到之后才能再次被调度。所以用户设置的这两个参数其实只有在rq上面进程数nr_running大于sysctl_sched_latency/sysctl_sched_min_granularity时才能被满足。
//每次更新最小调度粒度和调度延迟两个参数,都会更新sched_nr_latency
sched_nr_latency = DIV_ROUND_UP(sysctl_sched_latency, sysctl_sched_min_granularity);
//获取动态调度周期period需要根据sched_nr_latency计算
static u64 __sched_period(unsigned long nr_running)
{
if (unlikely(nr_running > sched_nr_latency))
return nr_running * sysctl_sched_min_granularity;
else
return sysctl_sched_latency;
}
从上面代码可以看出来,当进程太多时CFS只能先不管调度延迟,只保证最小调度粒度。为什么不保证用户设置调度延迟而保证最小调度粒度?因为最小调度粒度不保证的话,频繁抢占进程只会让更多时间浪费在context switch上,进一步恶化CPU资源紧促的情况。
2.CFS是如何分配时间片的?
CFS引入了vruntime虚拟运行时间的概念,为了让所有进程vruntime趋于相等,每次pick_next_task挑选下个要被运行的进程总会挑vruntime最小的进程出来运行。
但是vruntime和wall-time是不相等的,还要通过优先级加权,也就是说同样运行了10ms,高优先级进程vruntime+1低优先级进程可能要+10。
看看se中和cfs_rq中相关的成员:
tast_struct.se:
struct sched_entity {
struct load_weight load;
unsigned int on_rq;
};
cfs_rq:
struct cfs_rq {
struct load_weight load;
unsigned int nr_running, h_nr_running;
u64 min_vruntime;
}
load_weight:
struct load_weight {
unsigned long weight;
u32 inv_weight;
};
主要就是load_weight这个结构,load_weight字面意思就是权重,调度实体中的load_weight代表的该进程的权重,工作列队里的load_weight代表了整个列队的总权重。
看看CFS如何分配时间片:
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
//获得当前的调度周期
u64 slice = __sc