Linux内核学习笔记(一)CFS完全公平调度类
我的邮箱:zhw.smile@gmail.com 总结可能存在很多错误,欢迎交流
前导:
最近在学习Linux内核的相关知识,参考的资料是《Professional Linux Kernel Architecture》和linux2.6.24的内核源码。对Linux2.6.24中的核心调度器做一下总结。
Linux2.6.24内核采用分层的思想管理调度。可以看作两层,第一层被称为核心调度器,在核心调度器下面为调度器类。在调度器被调用时,它会查询调度器类,得知接下来运行哪个进程。内核支持不同的调度策略(完全公平调度和实时调度等),调度器类使得能够以模块化的方法实现这些策略,这样的好处就是代码维护更加简单,如果你喜欢,可以实现自己的调度器类,而不用关心核心调度器到底是怎么在进程间进行切换的。总结主要涉及完全公平调度器类。在介绍以下内容前需要说明Linux内核是软实时的,进程类型主要有普通进程和实时进程两种,而完全公平调度器类主要用于普通进程的调度策略。对于普通进程是放在就绪队列中等待被调度上处理器的,就绪队列采用的数据结构是红黑树。
Linux内核通过task_struct结构体管理进程,该结构体定义在include/linux/sched.h文件中。首先看看该结构体中涉及到的与调度有关的变量:
struct task_struct {
int prio, static_prio, normal_prio;
struct list_head run_list; /*实时调度器所需,不是总结重点*/
const struct sched_class *sched_class;
struct sched_entity se;
unsigned int policy; /*调度策略选择,SCHED_NORMAL用于普通进程,还有其他4种*/
cpumask_t cpus_allowed; /*限制此进程在哪个处理器上运行*/
unsigned int time_slice; /*实时调度器所需,不是总结重点*/
};
一.进程优先级:
prio和normal_prio为动态优先级,static_prio为静态优先级。static_prio是进程创建时分配的优先级,如果不人为的更改,那么在这个进程运行期间不会发生变化。 normal_prio是基于static_prio和调度策略计算出的优先级。prio是调度器类考虑的优先级,某些情况下需要暂时提高进程的优先级(实时互斥量),因此有此变量,对于优先级未动态提高的进程来说这三个值是相等的。以上三个优先级值越小,代表进程的优先级有高。一般情况下子进程的静态优先级继承自父进程,子进程的prio继承自父进程的normal_prio。
rt_policy表示实时进程的优先级,范围为0~99,该值与prio,normal_prio和static_prio不同,值越大代表实时进程的优先级越高。
那么内核如何处理这些优先级之间的关系呢?其实,内核使用0~139表示内部优先级,值越低优先级越高。其中0~99为实时进程,100~139为非实时进程。
当static_prio分配好后,prio和normal_prio计算方法实现如下:
首先,大家都知道进程创建过程中do_fork会调用wake_up_new_task,在该函数中会调用static int effective_prio(struct task_struct *p)函数。
void fastcall wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
{
unsigned long flags;
struct rq *rq;
...
p->prio = effective_prio(p);
...
}
static int effective_prio(struct task_struct *p)函数的实现如下:
static int effective_prio(struct task_struct *p)
{
p->normal_prio = normal_prio(p);
/*
* If we are RT tasks or we were boosted to RT priority,
* keep the priority unchanged. Otherwise, update priority
* to the normal priority:
*/
if (!rt_prio(p->prio))
return p->normal_prio;
return p->prio;
}
在函数中设置了normal_prio的值,返回值有设置了prio,真是一箭双雕,对于实时进程需要特殊处理,总结主要涉及非实时进进程,就对实时进程的处理方法不解释了。
static inline int normal_prio(struct task_struct *p)的实现如下:
static inline int normal_prio(struct task_struct *p)
{
int prio;
if (task_has_rt_policy(p))
prio = MAX_RT_PRIO-1 - p->rt_priority;
else
prio = __normal_prio(p);
return prio;
}
对于普通进程会调用static inline int __normal_prio(struct task_struct *p)函数。
static inline int __normal_prio(struct task_struct *p)函数的实现如下:
static inline int __normal_prio(struct task_struct *p)
{
return p->static_prio;
}
这样大家应该很清楚了,对于非实时进程prio,normal_prio和static_prio是一样的,但是也有特殊情况,当使用实时互斥量时prio会暂时发生变化。
二.调度实体:
se是struct sched_entity的一个实例,为什么要有调度实体,主要是因为linux的调度不只局限于进程,还可以用于组调度。这种思想在Linux内核里很普遍了,主要可以通过container_of机制找到包含此实例的task_struct。struct sched_entity中含有struct rb_node的实例,struct rb_node是红黑树的节点类型,这样在红黑树中也是通过container_of机制找到struct sched_entity实体的。
struct sched_entity { /*这个非常重要*/
struct load_weight load; /* for load-balancing负荷权重,这个决定了进程在CPU上的运行时间和被调度次数 */
struct rb_node run_node;
unsigned int on_rq; /*是否在就绪队列上*/
u64 exec_start; /*以下4个变量是有关进程在CPU上的消耗时间的,对于它们的解释我觉得书中原文解释的很好*/
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
...
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq;
};
When a process is running, the consumed CPU time needs to be recorded for the completely fair scheduler. sum_exec_runtime is used for this purpose. Tracking the run time is done cumulatively, in update_curr. The function is called from numerous places in the scheduler, for instance, when a new task is enqueued, or from the periodic tick. At each invocation, the difference between the current time and exec_start is computed, and exec_start is updated to the current time. The difference interval is added to sum_exec_runtime.
The amount of time that has elapsed on the virtual clock during process execution is accounted in vruntime.
When a process is taken off the CPU, its current sum_exec_runtime value is preserved in prev_exec_runtime. The data will later be required in the context of process preemption. Notice, however, that preserving the value of sum_exec_runtime in prev_exec_runtime
does not mean that sum_exec_runtime is reset! The old value is kept, and sum_exec_runtime continues to grow monotonically.
对于负荷权重的计算方法是与静态优先级有关的,以下函数中只保留了与普通进程相关的内容,至于这个权重到底是干什么的,后面会说到:
static void set_load_weight(struct task_struct *p)
{
...
p->se.load.weight = prio_to_weight[p->static_prio - MAX_RT_PRIO]; /*MAX_RT_PRIO为100
p->se.load.inv_weight = prio_to_wmult[p->static_prio - MAX_RT_PRIO];
...
}
prio_to_weight是张表:
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
prio_to_wmult也是一张表,这张表主要是为了计算方便,大家可以看一下注释:
* Inverse (2^32/x) values of the prio_to_weight[] array, precalculated.
*
* In cases where the weight does not change often, we can use the
* precalculated inverse to speed up arithmetics by turning divisions
* into multiplications:
*/
static const u32 prio_to_wmult[40] = {
/* -20 */ 48388, 59856, 76040, 92818, 118348,
/* -15 */ 147320, 184698, 229616, 287308, 360437,
/* -10 */ 449829, 563644, 704093, 875809, 1099582,
/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326,
/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587,
/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126,
/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717,
/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};
三.调度器类:
sched_class是一个const struct sched_class形的指针变量,给变量就是指向调度器类的指针。struct sched_class(include/linux/sched.h)的定义如下:
struct sched_class {
const struct sched_class *next;
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int wakeup); /*向就绪队列插入进程。*/
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int sleep); /*将进程移除就绪队列。*/
void (*yield_task) (struct rq *rq); /*进程主动放弃处理器*/
void (*check_preempt_curr) (struct rq *rq, struct task_struct *p); /*用一个新唤醒的进程抢占当前进程*/
struct task_struct * (*pick_next_task) (struct rq *rq); /*选择下一个将要运行的进程*/
void (*put_prev_task) (struct rq *rq, struct task_struct *p);
#ifdef CONFIG_SMP /*对于多处理器,支持负载平衡*/
unsigned long (*load_balance) (struct rq *this_rq, int this_cpu,
struct rq *busiest, unsigned long max_load_move,
struct sched_domain *sd, enum cpu_idle_type idle,
int *all_pinned, int *this_best_prio);
int (*move_one_task) (struct rq *this_rq, int this_cpu,
struct rq *busiest, struct sched_domain *sd,
enum cpu_idle_type idle);
#endif
void (*set_curr_task) (struct rq *rq);
void (*task_tick) (struct rq *rq, struct task_struct *p); /*由周期调度器调用*/
void (*task_new) (struct rq *rq, struct task_struct *p); /*每次建立新进程调用此函数通知调度器*/
};
四.就绪队列:
Linux2.6.24内核的就绪队列是使用struct rq结构体进行组织的,该结构体定义如下:
struct rq {
spinlock_t lock; /*锁*/
unsigned long nr_running; /*当前就绪对列进程的数目*/
struct load_weight load; /*当前就绪队列负荷*/
struct cfs_rq cfs; /*完全公平调度队列*/
struct rt_rq rt; /*实时调度队列*/
struct task_struct *curr, *idle; /*curr指向当前运行的task_struct,idle指向空闲进程的task_struct*/
...
u64 clock; /*就绪队列的时钟,这个是周期更新的,真实的系统晶振时钟*/
};
每个CPU都会有一个就绪队列,调度器类管理就绪队列。
五.核心调度器
接下来看一下核心调度器是如何从调度器类实例中得到下一个应该执行的进程。首先看一下周期调度器:
kernel/sched.c
void scheduler_tick(void)
{
...
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
struct task_struct *curr = rq->curr;
u64 next_tick = rq->tick_timestamp + TICK_NSEC;
spin_lock(&rq->lock);
__update_rq_clock(rq);
update_cpu_load(rq);
if (curr != rq->idle) /* FIXME: needed? */
curr->sched_class->task_tick(rq, curr);
spin_unlock(&rq->lock);
...
}
从函数名就可以看出这个scheduler_tick是系统时钟中断中会调用的,学过操作系统的都知道时钟脉搏。在这个函数中会更新就绪队列的时钟。从
curr->sched_class->task_tick(rq, curr)可以看出会掉用调度器类的task_tick方法,这样核心调度器就与调度器类联系起来。核心调度器将工作委托给了调度器类。从这句我们也可以看出内核数据的组织方式。这个可以慢慢体会。那么 当前进程如何选择自己的调度器类的,我们知道do_fork中调用copy_process,而copy_process调用了sched_fork函数。看一下sched_fork的实现:
void sched_fork(struct task_struct *p, int clone_flags)
{
...
p->prio = current->normal_prio;
if (!rt_prio(p->prio))
p->sched_class = &fair_sched_class;
...
}
在该函数中将fair_sched_class调度器类实例的地址赋给新进程中的sched_class指针,rt_prio(p->prio)检查新进程是否是实时进程。fair_sched_class是调度器类的实例,专门用于处理普通进程,也是总结的重点。
我们再看一下主调度器,省略了很多内容:
asmlinkage void __sched schedule(void)
{
...
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
unlikely(signal_pending(prev)))) {
prev->state = TASK_RUNNING;
} else {
deactivate_task(rq, prev, 1); /*当需要发生调度时,调用调度器类的方法deactivate_task停止当前进程*/
}
switch_count = &prev->nvcsw;
}
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);
prev->sched_class->put_prev_task(rq, prev); /*put_prev_task通知调度器类当前运行的进程将要被另一个进程代替*/
next = pick_next_task(rq, prev); /*选择一个应该执行的进程*/
...
}
上面注释的三个函数都是调度器类实例的相应函数,以后会说明。
六.CFS完全公平调度器类:
接下来就是总结的重点了,前面那些都是准备工作。首先看一下完全公平调度器类的实现:
kernel/sched_fair.c
static const struct sched_class fair_sched_class = {
.next = &idle_sched_class,
.enqueue_task = enqueue_task_fair,
.dequeue_task = dequeue_task_fair,
.yield_task = yield_task_fair,
.check_preempt_curr = check_preempt_wakeup,
.pick_next_task = pick_next_task_fair,
.put_prev_task = put_prev_task_fair,
#ifdef CONFIG_SMP
.load_balance = load_balance_fair,
.move_one_task = move_one_task_fair,
#endif
.set_curr_task = set_curr_task_fair,
.task_tick = task_tick_fair,
.task_new = task_new_fair,
};
等号后面的都是具体的函数实现,一会会介绍。该完全公平调度器类主要负责管理普通进程。我们还记得struct cfs_rq结构体吧,在就绪队列中有一个该类型的变量cfs,cfs就是普通进程的就绪队列组织结构,struct cfs_rq定义如下:
struct cfs_rq {
struct load_weight load; /*所有进程的累计负荷值*/
unsigned long nr_running; /*当前就绪队列的进程数*/
u64 min_vruntime;
struct rb_root tasks_timeline; /*红黑树的头结点*/
struct rb_node *rb_leftmost; /*红黑树的最左面节点*/
struct sched_entity *curr; /*当前执行进程的可调度实体*/
...
};
为了实现完全公平调度,内核引入了虚拟时钟(virtual clock)的概念,实际上我觉得这个虚拟时钟为什叫虚拟的,是因为这个时钟与具体的时钟晶振没有关系,他只不过是为了公平分配CPU时间而提出的一种时间量度,它与进程的权重有关,这里就知道权重的作用了,权重越高,说明进程的优先级比较高,进而该进程虚拟时钟增长的就慢。当然这是我个人理解。
我们再回忆一下调度实体struct sched_entity结构体中的变量:
struct sched_entity { /*这个非常重要*/
struct load_weight load; /* for load-balancing负荷权重,这个决定了进程在CPU上的运行时间和被调度次数 */
struct rb_node run_node;
unsigned int on_rq; /*是否在就绪队列上*/
u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq;
...
};
sum_exec_runtime是用于记录该进程的CPU消耗时间,这个是真实的CPU消耗时间。在进程撤销时会将sum_exec_runtime保存到 prev_sum_exec_runtime中。vruntime是本进程生命周期中在CPU上运行的虚拟时钟。那么何时应该更新这些时间呢?这是通过调用update_curr实现的,该函数在多处调用。我们还记得周期调度器函数scheduler_tick会掉用调度器类的task_tick吧,而完全公平调度器类的实例fair_sched_class中task_tick指向的是task_tick_fair,我们看一下task_tick_fair的实现:
kernel/sched_fair.c
static void task_tick_fair(struct rq *rq, struct task_struct *curr)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &curr->se;
for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
entity_tick(cfs_rq, se);
}
}
主要看entity_tick函数,实现如下:
kernel/sched_fair.c
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
/*
* Update run-time statistics of the 'current'.
*/
update_curr(cfs_rq);
if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))
check_preempt_tick(cfs_rq, curr);
}
这回大家知道哪调用update_curr了吧。还有很多地方调用此函数。entity_tick函数一会还会说明。接下来看一下update_curr的实现:
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_of(cfs_rq)->clock;
unsigned long delta_exec;
if (unlikely(!curr))
return;
delta_exec = (unsigned long)(now - curr->exec_start);
__update_curr(cfs_rq, curr, delta_exec);
curr->exec_start = now;
if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);
cpuacct_charge(curtask, delta_exec);
}
}
__update_curr的实现如下:
static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
unsigned long delta_exec)
{
unsigned long delta_exec_weighted;
u64 vruntime;
schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq, exec_clock, delta_exec);
delta_exec_weighted = delta_exec;
if (unlikely(curr->load.weight != NICE_0_LOAD)) {
delta_exec_weighted = calc_delta_fair(delta_exec_weighted,
&curr->load);
}
curr->vruntime += delta_exec_weighted;
if (first_fair(cfs_rq)) {
vruntime = min_vruntime(curr->vruntime,
__pick_next_entity(cfs_rq)->vruntime);
} else
vruntime = curr->vruntime;
cfs_rq->min_vruntime =
max_vruntime(cfs_rq->min_vruntime, vruntime);
}
sum_exec_runtime和prev_sum_exec_runtime的更新很简单,这里最主要的是vruntime的更新和min_vruntime的更新。vruntime与进程的权重有关,这里就知道权重的作用了,权重越高,说明进程的优先级比较高,进而该进程虚拟时钟增长的就慢。delta_exec_weighted就是vruntime增加值,calc_delta_fair实现如下:
static inline unsigned long
calc_delta_fair(unsigned long delta_exec, struct load_weight *lw)
{
return calc_delta_mine(delta_exec, NICE_0_LOAD, lw);
}
calc_delta_mine返回值是delta_exec*(NICE_0_LOAD/curr->load.weight),可见该进程的权重越大,delta_exec*(NICE_0_LOAD/curr->load.weight)就越小,这样vruntime增长的就越小,越小有什么用的,那么我们需要看一下红黑树是如何组织的。红黑树的键值是通过entity_key获得的,该函数的实现如下:
static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
return se->vruntime - cfs_rq->min_vruntime;
}
可见vruntime越小,该进程就会越靠红黑树的左边,就会更有机会被调度。cfs_rq->min_vruntime是为了跟踪红黑树中最左面节点的时钟,但它必须是单调递增的,防止时钟倒流。为什么要有cfs_rq->min_vruntime,后面会作更详细的说明。cfs_rq->min_vruntime会使就绪队列中的所有进程的虚拟时间围绕在min_vruntime附近,它的时钟推进与就绪队列中时钟的推进很微妙,update_curr中的代码如下:
if (first_fair(cfs_rq)) {
vruntime = min_vruntime(curr->vruntime,
__pick_next_entity(cfs_rq)->vruntime);
} else
vruntime = curr->vruntime;
cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime);
首先它是在判断当前就绪队列是否为空,如果不为空,它会选取当前运行进程的vruntime和就绪队列中最左面节点的vruntime的最小值,与cfs_rq->min_vruntime比较,将较大值赋给cfs_rq->min_vruntime,这样可以保证cfs_rq->min_vruntime推进的同时不会倒流。那么进程是如何确定最初的vruntime呢?我们看一下新进程是如何加入到就绪队列的:
static void task_new_fair(struct rq *rq, struct task_struct *p)
{
struct cfs_rq *cfs_rq = task_cfs_rq(p);
struct sched_entity *se = &p->se, *curr = cfs_rq->curr;
int this_cpu = smp_processor_id();
sched_info_queued(p);
update_curr(cfs_rq);
place_entity(cfs_rq, se, 1);
...
}
很明显该函数是通过调用place_entity将进程的调度实体加入队列的,该函数实现:
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime;
vruntime = cfs_rq->min_vruntime;
...
if (initial && sched_feat(START_DEBIT))
vruntime += sched_vslice_add(cfs_rq, se);
if (!initial) {
/* sleeps upto a single latency don't count. */
if (sched_feat(NEW_FAIR_SLEEPERS) && entity_is_task(se))
vruntime -= sysctl_sched_latency;
/* ensure we never gain time by being placed backwards. */
vruntime = max_vruntime(se->vruntime, vruntime);
}
...
se->vruntime = vruntime;
}
新进程创建时initial为1,所以它会执行vruntime += sched_vslice_add(cfs_rq, se);这句,而这里的vruntime就是当前CFS就绪队列的min_vruntime,新加进程应该在最近很快被调度,这样减少系统的响应时间,我们已经知道当前进程的vruntime越小,它在红黑树中就会越靠左,就会被很快调度到处理器上执行。但是,Linux内核需要根据新加入的进程的权重决策一下应该何时调度该进程,而不能任意进程都来抢占当前队列中靠左的进程,因为必须保证就绪队列中的所有进程尽量得到他们应得的时间响应,这是很平滑的。那看一下sched_vslice_add的实现:
static u64 sched_vslice_add(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
return __sched_vslice(cfs_rq->load.weight + se->load.weight,
cfs_rq->nr_running + 1);
}
再看一下__sched_vslice的实现:
static u64 __sched_vslice(unsigned long rq_weight, unsigned long nr_running)
{
u64 vslice = __sched_period(nr_running);
vslice *= NICE_0_LOAD;
do_div(vslice, rq_weight);
return vslice;
}
这个函数在干什么呢?在解释它之前,必须说明内核如何决定进程应该在CPU上运行多长时间?选择哪个进程我们已经知道了。我们看一下周期调度器中调用的task_tick_fair都作了什么:
static void task_tick_fair(struct rq *rq, struct task_struct *curr)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &curr->se;
for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
entity_tick(cfs_rq, se);
}
}
不用考虑组调度,我们看一下entity_tick的实现:
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
/*
* Update run-time statistics of the 'current'.
*/
update_curr(cfs_rq);
if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))
check_preempt_tick(cfs_rq, curr);
}
该函数会检测当前队列可以运行的进程是否会多于一个,如果多于一个,会调用check_preempt_tick,该函数实现:
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
unsigned long ideal_runtime, delta_exec;
ideal_runtime = sched_slice(cfs_rq, curr);
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
if (delta_exec > ideal_runtime)
resched_task(rq_of(cfs_rq)->curr);
}
可见进程的运行时间就是ideal_runtime,那我们看一下sched_slice的实现:
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
u64 slice = __sched_period(cfs_rq->nr_running);
slice *= se->load.weight;
do_div(slice, cfs_rq->load.weight);
return slice;
}
在解释这个函数前我们需要了解Linux是支持调度延迟的,这样可以保证可运行的进程都应该至少运行一次。它与运行队列上的进程数目有关,进程数越多,该时间延迟越长,所有的进程根据权重平分该时间,权重越大,分的的时间越长。那我们看一下时间延迟周期是如何增长的:
static u64 __sched_period(unsigned long nr_running)
{
u64 period = sysctl_sched_latency;
unsigned long nr_latency = sched_nr_latency;
if (unlikely(nr_running > nr_latency)) {
period *= nr_running;
do_div(period, nr_latency);
}
return period;
}
从这个函数我们可以看出这个时间延迟周期就是(sysctl_sched_latency*(nr_running/sched_nr_latency)),sched_nr_latency是sysctl_sched_latency这么长时间里应该处理的进程数目,这两值是可配置的,以改变内核的响应速度和调度频率。可见nr_running越大,即当前队列的进程数越多,进程的调度延迟越长。那么一个进程如何获得它的执行时间呢?从sched_slice可以看出,就是(sysctl_sched_latency*(se->load.weight/cfs_rq->load.weight)),很清晰吧。那么当从check_preempt_tick就可以看出,当进程运行时间超过分配时间,他就会置位当前执行进程的需要调度的标记,然后调度器就会进行调度。那我们再回到新建立进程的vruntime的确定:
static u64 sched_vslice_add(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
return __sched_vslice(cfs_rq->load.weight + se->load.weight,
cfs_rq->nr_running + 1);
}
再看一下__sched_vslice的实现:
static u64 __sched_vslice(unsigned long rq_weight, unsigned long nr_running)
{
u64 vslice = __sched_period(nr_running);
vslice *= NICE_0_LOAD;
do_div(vslice, rq_weight);
return vslice;
}
可以看出sched_vslice_add的返回值就是(sysctl_sched_latency*(NICE_0_LOAD/(cfs_rq->load.weight + se->load.weight))),为什么用NICE_0_LOAD,我们只需知道它是一个权重的分界点。优先级在它之前的权重更重一些,这样vruntime += sched_vslice_add(cfs_rq, se)更小,新进程被调度的延迟就会更小。所有进程都是按照这个规矩排队的,所以很公平。用数学解释就是一个分式(k/(m+x)),k和m都是常数,x越大,分式的值越小;x越小,分式的值越大。
还差一项没有介绍,那就是对于睡眠的进程醒来后应该加在就绪队列的什么地方呢?我们回忆以下调度实体中的enqueue_task_fair函数,也调用了place_entity,函数如下:
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime;
vruntime = cfs_rq->min_vruntime;
...
if (initial && sched_feat(START_DEBIT))
vruntime += sched_vslice_add(cfs_rq, se);
if (!initial) {
/* sleeps upto a single latency don't count. */
if (sched_feat(NEW_FAIR_SLEEPERS) && entity_is_task(se))
vruntime -= sysctl_sched_latency;
/* ensure we never gain time by being placed backwards. */
vruntime = max_vruntime(se->vruntime, vruntime);
}
...
se->vruntime = vruntime;
}
enqueue_task_fair调用place_entity传递的initial参数为0,所以会执行if (!initial)后的语句。因为进程睡眠后,vruntime就不会增加了,当它醒来后不知道过了多长时间,可能vruntime已经比min_vruntime小了很多,如果只是简单的将其插入到就绪队列中,它将拼命追赶min_vruntime,因为它总是在红黑树的最左面。如果这样,它将会占用大量的CPU时间,导致红黑树右边的进程被饿死。但是我们又必须及时响应醒来的进程,因为它们可能有一些工作需要立刻处理,所以系统采取了一种折衷的办法,将当前cfs_rq->min_vruntime时间减去sysctl_sched_latency赋给vruntime,这时它会被插入到就绪队列的最左边。这样刚唤醒的进程在当前执行进程时间耗尽时就会被调度上处理器执行。当然如果进程没有睡眠那么多时间,我们只需保留原来的时间vruntime = max_vruntime(se->vruntime, vruntime)。这有什么好处的,我觉得它可以将所有唤醒的进程排个队,睡眠越久的越快得到响应。
结束语:
对于CFS调度器类的其它函数实现很简单,不用多说。那么此方法可以避免进程被饿死,因为优先级高的进程在执行是虚拟时钟一定向前推进,超过优先级低的进程的虚拟时钟时,优先级低的进程就会被调度执行。
前导:
最近在学习Linux内核的相关知识,参考的资料是《Professional Linux Kernel Architecture》和linux2.6.24的内核源码。对Linux2.6.24中的核心调度器做一下总结。
Linux2.6.24内核采用分层的思想管理调度。可以看作两层,第一层被称为核心调度器,在核心调度器下面为调度器类。在调度器被调用时,它会查询调度器类,得知接下来运行哪个进程。内核支持不同的调度策略(完全公平调度和实时调度等),调度器类使得能够以模块化的方法实现这些策略,这样的好处就是代码维护更加简单,如果你喜欢,可以实现自己的调度器类,而不用关心核心调度器到底是怎么在进程间进行切换的。总结主要涉及完全公平调度器类。在介绍以下内容前需要说明Linux内核是软实时的,进程类型主要有普通进程和实时进程两种,而完全公平调度器类主要用于普通进程的调度策略。对于普通进程是放在就绪队列中等待被调度上处理器的,就绪队列采用的数据结构是红黑树。
Linux内核通过task_struct结构体管理进程,该结构体定义在include/linux/sched.h文件中。首先看看该结构体中涉及到的与调度有关的变量:
struct task_struct {
int prio, static_prio, normal_prio;
struct list_head run_list; /*实时调度器所需,不是总结重点*/
const struct sched_class *sched_class;
struct sched_entity se;
unsigned int policy; /*调度策略选择,SCHED_NORMAL用于普通进程,还有其他4种*/
cpumask_t cpus_allowed; /*限制此进程在哪个处理器上运行*/
unsigned int time_slice; /*实时调度器所需,不是总结重点*/
};
一.进程优先级:
prio和normal_prio为动态优先级,static_prio为静态优先级。static_prio是进程创建时分配的优先级,如果不人为的更改,那么在这个进程运行期间不会发生变化。 normal_prio是基于static_prio和调度策略计算出的优先级。prio是调度器类考虑的优先级,某些情况下需要暂时提高进程的优先级(实时互斥量),因此有此变量,对于优先级未动态提高的进程来说这三个值是相等的。以上三个优先级值越小,代表进程的优先级有高。一般情况下子进程的静态优先级继承自父进程,子进程的prio继承自父进程的normal_prio。
rt_policy表示实时进程的优先级,范围为0~99,该值与prio,normal_prio和static_prio不同,值越大代表实时进程的优先级越高。
那么内核如何处理这些优先级之间的关系呢?其实,内核使用0~139表示内部优先级,值越低优先级越高。其中0~99为实时进程,100~139为非实时进程。
当static_prio分配好后,prio和normal_prio计算方法实现如下:
首先,大家都知道进程创建过程中do_fork会调用wake_up_new_task,在该函数中会调用static int effective_prio(struct task_struct *p)函数。
void fastcall wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
{
unsigned long flags;
struct rq *rq;
...
p->prio = effective_prio(p);
...
}
static int effective_prio(struct task_struct *p)函数的实现如下:
static int effective_prio(struct task_struct *p)
{
p->normal_prio = normal_prio(p);
/*
* If we are RT tasks or we were boosted to RT priority,
* keep the priority unchanged. Otherwise, update priority
* to the normal priority:
*/
if (!rt_prio(p->prio))
return p->normal_prio;
return p->prio;
}
在函数中设置了normal_prio的值,返回值有设置了prio,真是一箭双雕,对于实时进程需要特殊处理,总结主要涉及非实时进进程,就对实时进程的处理方法不解释了。
static inline int normal_prio(struct task_struct *p)的实现如下:
static inline int normal_prio(struct task_struct *p)
{
int prio;
if (task_has_rt_policy(p))
prio = MAX_RT_PRIO-1 - p->rt_priority;
else
prio = __normal_prio(p);
return prio;
}
对于普通进程会调用static inline int __normal_prio(struct task_struct *p)函数。
static inline int __normal_prio(struct task_struct *p)函数的实现如下:
static inline int __normal_prio(struct task_struct *p)
{
return p->static_prio;
}
这样大家应该很清楚了,对于非实时进程prio,normal_prio和static_prio是一样的,但是也有特殊情况,当使用实时互斥量时prio会暂时发生变化。
二.调度实体:
se是struct sched_entity的一个实例,为什么要有调度实体,主要是因为linux的调度不只局限于进程,还可以用于组调度。这种思想在Linux内核里很普遍了,主要可以通过container_of机制找到包含此实例的task_struct。struct sched_entity中含有struct rb_node的实例,struct rb_node是红黑树的节点类型,这样在红黑树中也是通过container_of机制找到struct sched_entity实体的。
struct sched_entity { /*这个非常重要*/
struct load_weight load; /* for load-balancing负荷权重,这个决定了进程在CPU上的运行时间和被调度次数 */
struct rb_node run_node;
unsigned int on_rq; /*是否在就绪队列上*/
u64 exec_start; /*以下4个变量是有关进程在CPU上的消耗时间的,对于它们的解释我觉得书中原文解释的很好*/
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
...
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq;
};
When a process is running, the consumed CPU time needs to be recorded for the completely fair scheduler. sum_exec_runtime is used for this purpose. Tracking the run time is done cumulatively, in update_curr. The function is called from numerous places in the scheduler, for instance, when a new task is enqueued, or from the periodic tick. At each invocation, the difference between the current time and exec_start is computed, and exec_start is updated to the current time. The difference interval is added to sum_exec_runtime.
The amount of time that has elapsed on the virtual clock during process execution is accounted in vruntime.
When a process is taken off the CPU, its current sum_exec_runtime value is preserved in prev_exec_runtime. The data will later be required in the context of process preemption. Notice, however, that preserving the value of sum_exec_runtime in prev_exec_runtime
does not mean that sum_exec_runtime is reset! The old value is kept, and sum_exec_runtime continues to grow monotonically.
对于负荷权重的计算方法是与静态优先级有关的,以下函数中只保留了与普通进程相关的内容,至于这个权重到底是干什么的,后面会说到:
static void set_load_weight(struct task_struct *p)
{
...
p->se.load.weight = prio_to_weight[p->static_prio - MAX_RT_PRIO]; /*MAX_RT_PRIO为100
p->se.load.inv_weight = prio_to_wmult[p->static_prio - MAX_RT_PRIO];
...
}
prio_to_weight是张表:
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
prio_to_wmult也是一张表,这张表主要是为了计算方便,大家可以看一下注释:
* Inverse (2^32/x) values of the prio_to_weight[] array, precalculated.
*
* In cases where the weight does not change often, we can use the
* precalculated inverse to speed up arithmetics by turning divisions
* into multiplications:
*/
static const u32 prio_to_wmult[40] = {
/* -20 */ 48388, 59856, 76040, 92818, 118348,
/* -15 */ 147320, 184698, 229616, 287308, 360437,
/* -10 */ 449829, 563644, 704093, 875809, 1099582,
/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326,
/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587,
/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126,
/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717,
/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};
三.调度器类:
sched_class是一个const struct sched_class形的指针变量,给变量就是指向调度器类的指针。struct sched_class(include/linux/sched.h)的定义如下:
struct sched_class {
const struct sched_class *next;
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int wakeup); /*向就绪队列插入进程。*/
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int sleep); /*将进程移除就绪队列。*/
void (*yield_task) (struct rq *rq); /*进程主动放弃处理器*/
void (*check_preempt_curr) (struct rq *rq, struct task_struct *p); /*用一个新唤醒的进程抢占当前进程*/
struct task_struct * (*pick_next_task) (struct rq *rq); /*选择下一个将要运行的进程*/
void (*put_prev_task) (struct rq *rq, struct task_struct *p);
#ifdef CONFIG_SMP /*对于多处理器,支持负载平衡*/
unsigned long (*load_balance) (struct rq *this_rq, int this_cpu,
struct rq *busiest, unsigned long max_load_move,
struct sched_domain *sd, enum cpu_idle_type idle,
int *all_pinned, int *this_best_prio);
int (*move_one_task) (struct rq *this_rq, int this_cpu,
struct rq *busiest, struct sched_domain *sd,
enum cpu_idle_type idle);
#endif
void (*set_curr_task) (struct rq *rq);
void (*task_tick) (struct rq *rq, struct task_struct *p); /*由周期调度器调用*/
void (*task_new) (struct rq *rq, struct task_struct *p); /*每次建立新进程调用此函数通知调度器*/
};
四.就绪队列:
Linux2.6.24内核的就绪队列是使用struct rq结构体进行组织的,该结构体定义如下:
struct rq {
spinlock_t lock; /*锁*/
unsigned long nr_running; /*当前就绪对列进程的数目*/
struct load_weight load; /*当前就绪队列负荷*/
struct cfs_rq cfs; /*完全公平调度队列*/
struct rt_rq rt; /*实时调度队列*/
struct task_struct *curr, *idle; /*curr指向当前运行的task_struct,idle指向空闲进程的task_struct*/
...
u64 clock; /*就绪队列的时钟,这个是周期更新的,真实的系统晶振时钟*/
};
每个CPU都会有一个就绪队列,调度器类管理就绪队列。
五.核心调度器
接下来看一下核心调度器是如何从调度器类实例中得到下一个应该执行的进程。首先看一下周期调度器:
kernel/sched.c
void scheduler_tick(void)
{
...
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
struct task_struct *curr = rq->curr;
u64 next_tick = rq->tick_timestamp + TICK_NSEC;
spin_lock(&rq->lock);
__update_rq_clock(rq);
update_cpu_load(rq);
if (curr != rq->idle) /* FIXME: needed? */
curr->sched_class->task_tick(rq, curr);
spin_unlock(&rq->lock);
...
}
从函数名就可以看出这个scheduler_tick是系统时钟中断中会调用的,学过操作系统的都知道时钟脉搏。在这个函数中会更新就绪队列的时钟。从
curr->sched_class->task_tick(rq, curr)可以看出会掉用调度器类的task_tick方法,这样核心调度器就与调度器类联系起来。核心调度器将工作委托给了调度器类。从这句我们也可以看出内核数据的组织方式。这个可以慢慢体会。那么 当前进程如何选择自己的调度器类的,我们知道do_fork中调用copy_process,而copy_process调用了sched_fork函数。看一下sched_fork的实现:
void sched_fork(struct task_struct *p, int clone_flags)
{
...
p->prio = current->normal_prio;
if (!rt_prio(p->prio))
p->sched_class = &fair_sched_class;
...
}
在该函数中将fair_sched_class调度器类实例的地址赋给新进程中的sched_class指针,rt_prio(p->prio)检查新进程是否是实时进程。fair_sched_class是调度器类的实例,专门用于处理普通进程,也是总结的重点。
我们再看一下主调度器,省略了很多内容:
asmlinkage void __sched schedule(void)
{
...
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
unlikely(signal_pending(prev)))) {
prev->state = TASK_RUNNING;
} else {
deactivate_task(rq, prev, 1); /*当需要发生调度时,调用调度器类的方法deactivate_task停止当前进程*/
}
switch_count = &prev->nvcsw;
}
if (unlikely(!rq->nr_running))
idle_balance(cpu, rq);
prev->sched_class->put_prev_task(rq, prev); /*put_prev_task通知调度器类当前运行的进程将要被另一个进程代替*/
next = pick_next_task(rq, prev); /*选择一个应该执行的进程*/
...
}
上面注释的三个函数都是调度器类实例的相应函数,以后会说明。
六.CFS完全公平调度器类:
接下来就是总结的重点了,前面那些都是准备工作。首先看一下完全公平调度器类的实现:
kernel/sched_fair.c
static const struct sched_class fair_sched_class = {
.next = &idle_sched_class,
.enqueue_task = enqueue_task_fair,
.dequeue_task = dequeue_task_fair,
.yield_task = yield_task_fair,
.check_preempt_curr = check_preempt_wakeup,
.pick_next_task = pick_next_task_fair,
.put_prev_task = put_prev_task_fair,
#ifdef CONFIG_SMP
.load_balance = load_balance_fair,
.move_one_task = move_one_task_fair,
#endif
.set_curr_task = set_curr_task_fair,
.task_tick = task_tick_fair,
.task_new = task_new_fair,
};
等号后面的都是具体的函数实现,一会会介绍。该完全公平调度器类主要负责管理普通进程。我们还记得struct cfs_rq结构体吧,在就绪队列中有一个该类型的变量cfs,cfs就是普通进程的就绪队列组织结构,struct cfs_rq定义如下:
struct cfs_rq {
struct load_weight load; /*所有进程的累计负荷值*/
unsigned long nr_running; /*当前就绪队列的进程数*/
u64 min_vruntime;
struct rb_root tasks_timeline; /*红黑树的头结点*/
struct rb_node *rb_leftmost; /*红黑树的最左面节点*/
struct sched_entity *curr; /*当前执行进程的可调度实体*/
...
};
为了实现完全公平调度,内核引入了虚拟时钟(virtual clock)的概念,实际上我觉得这个虚拟时钟为什叫虚拟的,是因为这个时钟与具体的时钟晶振没有关系,他只不过是为了公平分配CPU时间而提出的一种时间量度,它与进程的权重有关,这里就知道权重的作用了,权重越高,说明进程的优先级比较高,进而该进程虚拟时钟增长的就慢。当然这是我个人理解。
我们再回忆一下调度实体struct sched_entity结构体中的变量:
struct sched_entity { /*这个非常重要*/
struct load_weight load; /* for load-balancing负荷权重,这个决定了进程在CPU上的运行时间和被调度次数 */
struct rb_node run_node;
unsigned int on_rq; /*是否在就绪队列上*/
u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq;
...
};
sum_exec_runtime是用于记录该进程的CPU消耗时间,这个是真实的CPU消耗时间。在进程撤销时会将sum_exec_runtime保存到 prev_sum_exec_runtime中。vruntime是本进程生命周期中在CPU上运行的虚拟时钟。那么何时应该更新这些时间呢?这是通过调用update_curr实现的,该函数在多处调用。我们还记得周期调度器函数scheduler_tick会掉用调度器类的task_tick吧,而完全公平调度器类的实例fair_sched_class中task_tick指向的是task_tick_fair,我们看一下task_tick_fair的实现:
kernel/sched_fair.c
static void task_tick_fair(struct rq *rq, struct task_struct *curr)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &curr->se;
for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
entity_tick(cfs_rq, se);
}
}
主要看entity_tick函数,实现如下:
kernel/sched_fair.c
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
/*
* Update run-time statistics of the 'current'.
*/
update_curr(cfs_rq);
if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))
check_preempt_tick(cfs_rq, curr);
}
这回大家知道哪调用update_curr了吧。还有很多地方调用此函数。entity_tick函数一会还会说明。接下来看一下update_curr的实现:
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_of(cfs_rq)->clock;
unsigned long delta_exec;
if (unlikely(!curr))
return;
delta_exec = (unsigned long)(now - curr->exec_start);
__update_curr(cfs_rq, curr, delta_exec);
curr->exec_start = now;
if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);
cpuacct_charge(curtask, delta_exec);
}
}
__update_curr的实现如下:
static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
unsigned long delta_exec)
{
unsigned long delta_exec_weighted;
u64 vruntime;
schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq, exec_clock, delta_exec);
delta_exec_weighted = delta_exec;
if (unlikely(curr->load.weight != NICE_0_LOAD)) {
delta_exec_weighted = calc_delta_fair(delta_exec_weighted,
&curr->load);
}
curr->vruntime += delta_exec_weighted;
if (first_fair(cfs_rq)) {
vruntime = min_vruntime(curr->vruntime,
__pick_next_entity(cfs_rq)->vruntime);
} else
vruntime = curr->vruntime;
cfs_rq->min_vruntime =
max_vruntime(cfs_rq->min_vruntime, vruntime);
}
sum_exec_runtime和prev_sum_exec_runtime的更新很简单,这里最主要的是vruntime的更新和min_vruntime的更新。vruntime与进程的权重有关,这里就知道权重的作用了,权重越高,说明进程的优先级比较高,进而该进程虚拟时钟增长的就慢。delta_exec_weighted就是vruntime增加值,calc_delta_fair实现如下:
static inline unsigned long
calc_delta_fair(unsigned long delta_exec, struct load_weight *lw)
{
return calc_delta_mine(delta_exec, NICE_0_LOAD, lw);
}
calc_delta_mine返回值是delta_exec*(NICE_0_LOAD/curr->load.weight),可见该进程的权重越大,delta_exec*(NICE_0_LOAD/curr->load.weight)就越小,这样vruntime增长的就越小,越小有什么用的,那么我们需要看一下红黑树是如何组织的。红黑树的键值是通过entity_key获得的,该函数的实现如下:
static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
return se->vruntime - cfs_rq->min_vruntime;
}
可见vruntime越小,该进程就会越靠红黑树的左边,就会更有机会被调度。cfs_rq->min_vruntime是为了跟踪红黑树中最左面节点的时钟,但它必须是单调递增的,防止时钟倒流。为什么要有cfs_rq->min_vruntime,后面会作更详细的说明。cfs_rq->min_vruntime会使就绪队列中的所有进程的虚拟时间围绕在min_vruntime附近,它的时钟推进与就绪队列中时钟的推进很微妙,update_curr中的代码如下:
if (first_fair(cfs_rq)) {
vruntime = min_vruntime(curr->vruntime,
__pick_next_entity(cfs_rq)->vruntime);
} else
vruntime = curr->vruntime;
cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime);
首先它是在判断当前就绪队列是否为空,如果不为空,它会选取当前运行进程的vruntime和就绪队列中最左面节点的vruntime的最小值,与cfs_rq->min_vruntime比较,将较大值赋给cfs_rq->min_vruntime,这样可以保证cfs_rq->min_vruntime推进的同时不会倒流。那么进程是如何确定最初的vruntime呢?我们看一下新进程是如何加入到就绪队列的:
static void task_new_fair(struct rq *rq, struct task_struct *p)
{
struct cfs_rq *cfs_rq = task_cfs_rq(p);
struct sched_entity *se = &p->se, *curr = cfs_rq->curr;
int this_cpu = smp_processor_id();
sched_info_queued(p);
update_curr(cfs_rq);
place_entity(cfs_rq, se, 1);
...
}
很明显该函数是通过调用place_entity将进程的调度实体加入队列的,该函数实现:
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime;
vruntime = cfs_rq->min_vruntime;
...
if (initial && sched_feat(START_DEBIT))
vruntime += sched_vslice_add(cfs_rq, se);
if (!initial) {
/* sleeps upto a single latency don't count. */
if (sched_feat(NEW_FAIR_SLEEPERS) && entity_is_task(se))
vruntime -= sysctl_sched_latency;
/* ensure we never gain time by being placed backwards. */
vruntime = max_vruntime(se->vruntime, vruntime);
}
...
se->vruntime = vruntime;
}
新进程创建时initial为1,所以它会执行vruntime += sched_vslice_add(cfs_rq, se);这句,而这里的vruntime就是当前CFS就绪队列的min_vruntime,新加进程应该在最近很快被调度,这样减少系统的响应时间,我们已经知道当前进程的vruntime越小,它在红黑树中就会越靠左,就会被很快调度到处理器上执行。但是,Linux内核需要根据新加入的进程的权重决策一下应该何时调度该进程,而不能任意进程都来抢占当前队列中靠左的进程,因为必须保证就绪队列中的所有进程尽量得到他们应得的时间响应,这是很平滑的。那看一下sched_vslice_add的实现:
static u64 sched_vslice_add(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
return __sched_vslice(cfs_rq->load.weight + se->load.weight,
cfs_rq->nr_running + 1);
}
再看一下__sched_vslice的实现:
static u64 __sched_vslice(unsigned long rq_weight, unsigned long nr_running)
{
u64 vslice = __sched_period(nr_running);
vslice *= NICE_0_LOAD;
do_div(vslice, rq_weight);
return vslice;
}
这个函数在干什么呢?在解释它之前,必须说明内核如何决定进程应该在CPU上运行多长时间?选择哪个进程我们已经知道了。我们看一下周期调度器中调用的task_tick_fair都作了什么:
static void task_tick_fair(struct rq *rq, struct task_struct *curr)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &curr->se;
for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
entity_tick(cfs_rq, se);
}
}
不用考虑组调度,我们看一下entity_tick的实现:
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
/*
* Update run-time statistics of the 'current'.
*/
update_curr(cfs_rq);
if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))
check_preempt_tick(cfs_rq, curr);
}
该函数会检测当前队列可以运行的进程是否会多于一个,如果多于一个,会调用check_preempt_tick,该函数实现:
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
unsigned long ideal_runtime, delta_exec;
ideal_runtime = sched_slice(cfs_rq, curr);
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
if (delta_exec > ideal_runtime)
resched_task(rq_of(cfs_rq)->curr);
}
可见进程的运行时间就是ideal_runtime,那我们看一下sched_slice的实现:
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
u64 slice = __sched_period(cfs_rq->nr_running);
slice *= se->load.weight;
do_div(slice, cfs_rq->load.weight);
return slice;
}
在解释这个函数前我们需要了解Linux是支持调度延迟的,这样可以保证可运行的进程都应该至少运行一次。它与运行队列上的进程数目有关,进程数越多,该时间延迟越长,所有的进程根据权重平分该时间,权重越大,分的的时间越长。那我们看一下时间延迟周期是如何增长的:
static u64 __sched_period(unsigned long nr_running)
{
u64 period = sysctl_sched_latency;
unsigned long nr_latency = sched_nr_latency;
if (unlikely(nr_running > nr_latency)) {
period *= nr_running;
do_div(period, nr_latency);
}
return period;
}
从这个函数我们可以看出这个时间延迟周期就是(sysctl_sched_latency*(nr_running/sched_nr_latency)),sched_nr_latency是sysctl_sched_latency这么长时间里应该处理的进程数目,这两值是可配置的,以改变内核的响应速度和调度频率。可见nr_running越大,即当前队列的进程数越多,进程的调度延迟越长。那么一个进程如何获得它的执行时间呢?从sched_slice可以看出,就是(sysctl_sched_latency*(se->load.weight/cfs_rq->load.weight)),很清晰吧。那么当从check_preempt_tick就可以看出,当进程运行时间超过分配时间,他就会置位当前执行进程的需要调度的标记,然后调度器就会进行调度。那我们再回到新建立进程的vruntime的确定:
static u64 sched_vslice_add(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
return __sched_vslice(cfs_rq->load.weight + se->load.weight,
cfs_rq->nr_running + 1);
}
再看一下__sched_vslice的实现:
static u64 __sched_vslice(unsigned long rq_weight, unsigned long nr_running)
{
u64 vslice = __sched_period(nr_running);
vslice *= NICE_0_LOAD;
do_div(vslice, rq_weight);
return vslice;
}
可以看出sched_vslice_add的返回值就是(sysctl_sched_latency*(NICE_0_LOAD/(cfs_rq->load.weight + se->load.weight))),为什么用NICE_0_LOAD,我们只需知道它是一个权重的分界点。优先级在它之前的权重更重一些,这样vruntime += sched_vslice_add(cfs_rq, se)更小,新进程被调度的延迟就会更小。所有进程都是按照这个规矩排队的,所以很公平。用数学解释就是一个分式(k/(m+x)),k和m都是常数,x越大,分式的值越小;x越小,分式的值越大。
还差一项没有介绍,那就是对于睡眠的进程醒来后应该加在就绪队列的什么地方呢?我们回忆以下调度实体中的enqueue_task_fair函数,也调用了place_entity,函数如下:
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime;
vruntime = cfs_rq->min_vruntime;
...
if (initial && sched_feat(START_DEBIT))
vruntime += sched_vslice_add(cfs_rq, se);
if (!initial) {
/* sleeps upto a single latency don't count. */
if (sched_feat(NEW_FAIR_SLEEPERS) && entity_is_task(se))
vruntime -= sysctl_sched_latency;
/* ensure we never gain time by being placed backwards. */
vruntime = max_vruntime(se->vruntime, vruntime);
}
...
se->vruntime = vruntime;
}
enqueue_task_fair调用place_entity传递的initial参数为0,所以会执行if (!initial)后的语句。因为进程睡眠后,vruntime就不会增加了,当它醒来后不知道过了多长时间,可能vruntime已经比min_vruntime小了很多,如果只是简单的将其插入到就绪队列中,它将拼命追赶min_vruntime,因为它总是在红黑树的最左面。如果这样,它将会占用大量的CPU时间,导致红黑树右边的进程被饿死。但是我们又必须及时响应醒来的进程,因为它们可能有一些工作需要立刻处理,所以系统采取了一种折衷的办法,将当前cfs_rq->min_vruntime时间减去sysctl_sched_latency赋给vruntime,这时它会被插入到就绪队列的最左边。这样刚唤醒的进程在当前执行进程时间耗尽时就会被调度上处理器执行。当然如果进程没有睡眠那么多时间,我们只需保留原来的时间vruntime = max_vruntime(se->vruntime, vruntime)。这有什么好处的,我觉得它可以将所有唤醒的进程排个队,睡眠越久的越快得到响应。
结束语:
对于CFS调度器类的其它函数实现很简单,不用多说。那么此方法可以避免进程被饿死,因为优先级高的进程在执行是虚拟时钟一定向前推进,超过优先级低的进程的虚拟时钟时,优先级低的进程就会被调度执行。