调度器的一般原理是,根据所能分配的计算能力,向系统中的每个进程提供最大的公平性。调度器分配的资源就是CPU的时间,尽量保证每个进程都获得相同的CPU时间。Linux的CFS调度系统不同于O(1)调度器,不需要时间片概念,至少不需要传统的时间片。CFS调度系统只考虑进程的等待时间,即进程在就绪队列中已经等待了多长时间。但是并非系统上的所有进程都同样重要,调度器也要保证一些重要的进程优先执行,或者要获得更多的CPU时间。对于应用层来说,进程的重要性就是通过优先级来标识的,而CFS调度器在计算进程的虚拟运行时间或者调度延迟时都是使用的权重,下面我们来看一下这两者是如何计算和转换的。
1. 进程优先级
普通进程的优先级由task_struct结构中的prio、static_prio和normal_prio三个成员来描述。static_prio是静态优先级,默认值是在进程创建时从父进程继承过来的,可以使用nice()、sched_setscheduler()或者setpriority()修改。normal_prio是普通优先级,是根据进程的静态优先级和调度策略计算出来的优先级。prio是动态优先级,调度器会根据该成员表示的优先级来给进程分配CPU时间。由于在某些情况下会暂时提高进程的优先级,因此需要3个成员来表示进程的优先级,在提高进程优先级运行的持续时间中,普通和静态优先级的值是不变的。这三个成员的值越低,优先级越高。
实时进程的优先级是由task_struct结构中的rt_priority成员来描述的,该成员保存的值不会代替上面的三个成员。该成员的值越大,优先级越高,实时进程的优先级计算方法和普通进程不同,后面我们会看到。
2. 进程优先级的计算
进程创建时,在copy_process()中调用的dup_task_struct()会将父进程的所有内容都拷贝到子进程的task_struct结构实例中,所以初始时父子进程中所有描述优先级的成员的值都是相同的。在初始化完成后,会调用sched_fork()和调度系统交互,将子进程添加到调度系统中。在sched_fork()中会调整子进程的优先级,相关代码如下所示:
void sched_fork(struct task_struct *p, int clone_flags)
{
......
/*
* Revert to default priority/policy on fork if requested.
*/
if (unlikely(p->sched_reset_on_fork)) {
if (p->policy == SCHED_FIFO || p->policy == SCHED_RR) {
p->policy = SCHED_NORMAL;
p->normal_prio = p->static_prio;
}
if (PRIO_TO_NICE(p->static_prio) < 0) {
p->static_prio = NICE_TO_PRIO(0);
p->normal_prio = p->static_prio;
set_load_weight(p);
}
/*
* We don't need the reset flag anymore after the fork. It has
* fulfilled its duty:
*/
p->sched_reset_on_fork = 0;
}
/*
* Make sure we do not leak PI boosting priority to the child.
*/
p->prio = current->normal_prio;
......
}
如果父进程通过sched_setscheduler()系统调用指定了SCHED_RESET_ON_FORK策略,父进程的sched_reset_on_fork成员的值是1,由于子进程的内容是从父进程拷贝而来的,子进程的sched_reset_on_fork的值也是1,所以这里使用的子进程中的sched_reset_on_fork来判断。同理,这里使用p获取的成员的值在没有修改前,都是和父进程一致的。
如果sched_reset_on_fork成员设置,会重新设置子进程的优先级。如果父进程是实时进程,则会将子进程的调度策略重置为SCHED_NORMAL,即普通进程所属的调度策略。普通优先级normal_prio是根据调度策略和静态优先级static_prio计算出来的,如果父进程是实时进程,其normal_prio是根据实时策略计算的,而此处修改了子进程p的调度策略,所以要重新设置子进程的普通优先级normal_prio,普通进程的normal_prio和static_prio的值是一样的,参见effective_prio()。如果父进程的静态优先级高于DEFAULT_PRIO(值为120,对应的nice值为0),会将子进程的静态优先级重置为DEFAULT_PRIO。普通优先级和权重都是根据静态优先级计算的,所以如果修改了静态优先级,也重新计算普通优先级和权重。SCHED_RESET_ON_FORK策略只作用于主动设置的进程,不会传递到子进程,所以这里会将子进程的sched_reset_on_fork初始化为0。之所以引入这样的策略,主要是出于安全考虑,避免fork炸弹,关于SCHED_RESET_ON_FORK策略的更多信息,参见这里。
进程创建后,普通进程可以通过setpriority()和nice()系统调用来调整进程的优先级(不能用于实时进程),在内核中主要是由set_user_nice()来处理的,代码如下所示:
void set_user_nice(
struct task_struct
*p,
long nice)
{
......
if (TASK_NICE(p)
== nice
|| nice
<
-
20
|| nice
>
19)
return;
......
/*
* The RT priorities are set via sched_setscheduler(), but we still
* allow the 'normal' nice value to be set - but as expected
* it wont have any effect on scheduling until the task is
* SCHED_FIFO/SCHED_RR:
*/
if (task_has_rt_policy(p)) {
p
-
>static_prio
= NICE_TO_PRIO(nice);
goto out_unlock;
}
......
p
-
>static_prio
= NICE_TO_PRIO(nice);
set_load_weight(p);
old_prio
= p
-
>prio;
p
-
>prio
= effective_prio(p);
......
out_unlock
:
task_rq_unlock(rq,
&flags);
}
用户空间设置的时候使用的是nice值,nice值的范围是[-20,19],而内核使用的数值范围是[0,139],用来表示内部优先级。[0,99]的范围专供实时进程使用,nice值[-20,19]映射到[100,139]的范围,用于普通进程,实时进程的优先级总是比普通进程更高。这两种范围都是值越低,优先级越高。
TASK_NICE宏将进程p的静态优先级转换为nice值,如果用户设置的nice值和当前进程的nice值相同,则直接返回。如果设置的nice不是在[-20,19]之间的返回,说明设置的值有误,直接返回。
setpriority()和nice()系统调用不能用于实时进程,如果当前设置的进程是实时进程,会将指定的nice值对应的优先级设置到static_prio成员上,但是不会产生任何效果。如果是普通进程,设置的nice值会通过NICE_TO_PRIO宏转换为对应的内核优先级,然后设置到静态优先级static_prio。动态优先级和普通优先级都是在effective_prio()函数中计算的,动态优先级是effective_prio()的返回值,普通优先级是由effective_prio()中调用的normal_prio()返回的。如果是普通进程,并且没有将优先级提高到实时优先级,普通进程的普通优先级static_prio和动态优先级prio的值都是和静态优先级static_prio的值是一样的。
实时进程的优先级调整要使用sched_setscheduler()系统调用来调整,该系统调用还可以设置进程的优先级。如果要将进程的调度策略设置为SCHED_NORMAL、SCHED_BATCH或者SCHED_IDLE,param参数中的sched_priority成员必须设置为0,否则内核会范围EINVAL错误,具体的描述参见man sched_setscheduler()。内核中是在__sched_setscheduler()函数中使用了一个非常巧妙的判断来做这个检查的,如下所示:
static
int __sched_setscheduler(
struct task_struct
*p,
int policy,
struct sched_param
*param,
bool user)
{
......
if (rt_policy(policy)
!= (param
-
>sched_priority
!=
0))
return
-EINVAL;
__setscheduler(rq, p, policy, param
-
>sched_priority);
......
}
真正的操作是在__setscheduler()中完成的,代码如下所示:
/* Actually do priority change: must hold rq lock. */
static
void
__setscheduler(
struct rq
*rq,
struct task_struct
*p,
int policy,
int prio)
{
BUG_ON(p
-
>se.on_rq);
p
-
>policy
= policy;
switch (p
-
>policy) {
case SCHED_NORMAL
:
case SCHED_BATCH
:
case SCHED_IDLE
:
p
-
>sched_class
=
&fair_sched_class;
break;
case SCHED_FIFO
:
case SCHED_RR
:
p
-
>sched_class
=
&rt_sched_class;
break;
}
p
-
>rt_priority
= prio;
p
-
>normal_prio
= normal_prio(p);
/* we are holding p->pi_lock already */
p
-
>prio
= rt_mutex_getprio(p);
set_load_weight(p);
}
这里的操作也很简单,首先根据指定的调度策略来指定进程所属的调度类,然后将设置的优先级设置到实时优先级rt_priority成员上,并重新计算普通优先级和动态优先级。但是这里并没有设置static_prio,因为在将普通进程修改为实时进程时,并不会修改该成员。
3. 权重的计算
我们前面看到每次在调整进程的优先级时都会调用set_load_weight()来计算进程的权重,因为这个函数是根据静态优先级或者实时优先级和调度策略来计算的,所以优先级的调整必然伴随着权重的计算。计算过程非常简单,如下所示:
static
void set_load_weight(
struct task_struct
*p)
{
if (task_has_rt_policy(p)) {
p
-
>se.load.weight
= prio_to_weight[
0]
*
2;
p
-
>se.load.inv_weight
= prio_to_wmult[
0]
>>
1;
return;
}
/*
* SCHED_IDLE tasks get minimal weight:
*/
if (p
-
>policy
== SCHED_IDLE) {
p
-
>se.load.weight
= WEIGHT_IDLEPRIO;
p
-
>se.load.inv_weight
= WMULT_IDLEPRIO;
return;
}
p
-
>se.load.weight
= prio_to_weight[p
-
>static_prio
- MAX_RT_PRIO];
p
-
>se.load.inv_weight
= prio_to_wmult[p
-
>static_prio
- MAX_RT_PRIO];
}
Linux中调度系统直接操作的对象是调度实体,由sched_entity结构描述,计算出来的权重值分别保存在sched_entity结构load成员中的weight和inv_weight,但是这两个成员是独立的,并不是要合成出一个权重值。这两个成员的值分别来自于prio_to_weight和prio_to_wmult两个数组,关于这两个数组的信息在代码注释和《深入Linux内核架构》中都有描述。prio_to_weight数组是根据根据nice值定义的,一般的概念是进程每降低一个nice值,则多获得10%的CPU时间,每升高一个nice值,则放弃10%的CPU时间。为执行该策略,内核通过这个数组将优先级转换为权重值。而prio_to_wult数组和prio_to_weight数组中相同索引的值的关系是prio_to_wult[i]=2^32 / prio_to_weight[i],之所以引入这个数组是避免在计算虚拟运行时间时执行除法。内核中使用delta_exec * (NICE_0_LOAD / weight)的公式来将实际时钟时间(delta_exec)转换为虚拟运行时间,通过prio_to_wult就可以将这个公式转换为(delta_exec * NICE_0_LOAD) * inv_weight >> 32,将除法很巧妙地转换为乘法和移位操作。