CFS-完全公平调度器

CFS(Completely Fair Scheduler)完全公平调度器是Linux内核的一种调度策略,它通过vruntime来衡量进程运行时间,确保每个进程在调度周期内都有机会运行。CFS使用动态调度周期,并根据进程优先级加权分配时间片,使得高优先级进程获得更多时间但不会立即抢占。当vruntime最小的进程运行时,可以缓解不公平情况。调度器在进程数量过多时优先保证最小调度粒度,以减少上下文切换的开销。CFS通过负载权重分配时间片,并在特定条件下进行抢占,如任务唤醒、优先级调整等。

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 = __sched_period(cfs_rq->nr_running + !se->on_rq);

	for_each_sched_entity(se) {
		struct load_weight 
CFS完全公平调度器)的工作原理基于一个核心理念:确保所有进程能够公平地获得 CPU 时间。为此,CFS 引入了虚拟时钟的概念来跟踪每个进程使用 CPU 的时间[^2]。当系统中存在多个可运行的进程时,CFS 会尝试让它们的虚拟时钟值尽可能接近相等,以此保证每个进程都能得到大致相同的 CPU 时间。 ### 虚拟运行时间 在 CFS 中,每个进程都有一个称为“虚拟运行时间”(vruntime)的属性。这个值反映了该进程已经消耗了多少 CPU 时间。理想情况下,如果所有进程都得到了完全公平的处理,那么它们的 vruntime 值应该是相同的。然而,在实际操作中,由于不同进程可能具有不同的优先级和权重,因此需要对这些因素进行调整以计算出准确的 vruntime。 ### 红黑树结构 为了高效管理就绪队列中的进程并快速找到下一个要执行的进程,CFS 使用了一种特殊的红黑树数据结构。这棵红黑树按照进程的 vruntime 排序,最小的 vruntime 进程位于树的最左边。每当发生上下文切换或者有新进程加入就绪队列时,都会更新相应的节点位置以保持树的有序性。 ### 调度决策 CFS调度决策主要依赖于当前运行进程与其他等待进程之间的 vruntime 比较结果。如果发现有其他进程的 vruntime 明显小于当前运行进程,则触发一次抢占事件,将当前进程从 CPU 上移除,并选择那个具有最低 vruntime 的进程来执行。这样可以确保没有哪个进程长时间独占 CPU 资源而不给其他进程机会。 ### 实现机制示例代码 下面是一个简化的伪代码片段,用来展示如何维护 CFS 的红黑树以及更新进程的 vruntime: ```c struct cfs_rq { struct rb_root tasks_timeline; // 红黑树根节点 struct rb_node *rb_leftmost; // 指向红黑树中最左侧节点(即下一个要被调度的进程) }; struct sched_entity { struct rb_node run_node; // 用于链接到红黑树中的节点 unsigned long vruntime; // 虚拟运行时间 }; // 更新进程A的vruntime void update_vruntime(struct sched_entity *se, unsigned long delta_exec) { se->vruntime += calc_delta_fair(delta_exec, se); } // 将进程插入到合适的红黑树位置 void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { struct rb_node **link = &cfs_rq->tasks_timeline.rb_node; struct rb_node *parent = NULL; while (*link) { parent = *link; if (se->vruntime < rb_entry(parent, struct sched_entity, run_node)->vruntime) link = &(*link)->rb_left; else link = &(*link)->rb_right; } rb_link_node(&se->run_node, parent, link); rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline); if (!cfs_rq->rb_leftmost || se->vruntime < rb_entry(cfs_rq->rb_leftmost, struct sched_entity, run_node)->vruntime) cfs_rq->rb_leftmost = &se->run_node; } // 从红黑树中删除指定进程 void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { if (cfs_rq->rb_leftmost == &se->run_node) cfs_rq->rb_leftmost = rb_next(&se->run_node); rb_erase(&se->run_node, &cfs_rq->tasks_timeline); } ``` 上述代码仅展示了部分功能,包括更新 vruntime、将进程添加至红黑树及从中移除的过程。完整的 CFS 实现更为复杂,包含了更多细节如负载均衡、组调度支持等功能。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值