进程调度的数据结构和优先级

1 进程的优先级


每个普通进程都有它自己的静态优先级,位于task_struct的static_prio字段,调度程序使用静态优先级来估价系统中这个进程与其它普通进程之间调度强度。但是,注意,调度程序不是根据静态优先级来决定调度哪个进程的,而是动态优先级,后面会详细谈到。内核用100(最高优先级)到139(最低优先级)的整数表示普通进程的静态优先级 。注意,值越大静态优先级就越低。

新进程总是继承其父进程的静态优先级。不过,通过系统调用nice()和setprioritry(),用户可以改变自己拥有的进程的静态优先级。

进程静态优先级本质上决定了进程的基本时间片,即进程用完了以前的时间片,系统分配给进程的时间片长度 。静态优先级和基本时间片的关系用下列公式确定:

 

进程的基本时间片实现函数为task_timeslice:

static inline unsigned int task_timeslice(struct task_struct *p)
{
    return static_prio_timeslice(p->static_prio);
}
static unsigned int static_prio_timeslice(int static_prio)
{
    if (static_prio < NICE_TO_PRIO(0)) //静态优先级小于120
        return SCALE_PRIO(DEF_TIMESLICE * 4, static_prio); //(140-static_prio)*20
    else
        return SCALE_PRIO(DEF_TIMESLICE, static_prio);//(140-static_prio)*5
}
#define NICE_TO_PRIO(nice)    (MAX_RT_PRIO + (nice) + 20)
#define MAX_USER_RT_PRIO    100
#define MAX_RT_PRIO        MAX_USER_RT_PRIO

我们看到,静态优先级越高,其基本时间片就越长。最后的结果是,与优先级低的进程相比,通常优先级较高的进程获得更长的CPU时间片。


普通进程除了静态优先级,还有动态优先级,其值的范围也是是100(最高优先级MAX_RT_PRIO,低于100就成了实时进程了 )到139(最低优先级MAX_PRIO)。动态优先级是调度程序选择新进程来运行的时候使用的数。它与静态优先级的关系用下面的所谓经验公式(empirical formula)表示:

动态优先级 = max (100, min (静态优先级 - bonus + 5, 139))   (2)

 

动态优先级的计算主要由 effect_prio() 函数完成,该函数实现相当简单,从中可见非实时进程的优先级仅决定于静态优先级(static_prio)和进程的平均睡眠时间(sleep_avg)两个因素,而实时进程的优先级实际上是在sched_setscheduler() 中设置的(详见"实时进程调度系统"博文,以下仅考虑非实时进程),且一经设定就不再改变。

动态优先级的计算函数是effective_prio,函数effective_prio()读current的static_prio和sleep_avg字段,并根据前面的公式计算出进程的动态优先级:

   

 static int effective_prio(struct task_struct *p)
    {
        p->normal_prio = normal_prio(p);//首先计算出普通进程的优先级,存放在task_struct的normal_prio字段
        if (!rt_prio(p->prio))
            return p->normal_prio;
        return p->prio; //如果是实时进程,优先级不变
    }

    static inline int normal_prio(struct task_struct *p)
    {
        int prio;
        if (has_rt_policy(p))
            prio = MAX_RT_PRIO-1 - p->rt_priority;
        else
            prio = __normal_prio(p);
        return prio;
    }
    #define rt_prio(prio)        unlikely((prio) < MAX_RT_PRIO) //prio小于100就是实时进程
    static inline int __normal_prio(struct task_struct *p)
    {//执行该函数的前提是非实时进程
        int bonus, prio;
        bonus = CURRENT_BONUS(p) - MAX_BONUS / 2;
        prio = p->static_prio - bonus;
        if (prio < MAX_RT_PRIO)  // MAX_RT_PRIO的值为100
            prio = MAX_RT_PRIO;  // 不能让你普通进程的优先级高于实时进程
        if (prio > MAX_PRIO-1)   // MAX_PRIO的值为140
            prio = MAX_PRIO-1;   // 不能超过最大优先级139
        return prio;
    }

动态优先级算法的实现关键在 sleep_avg 变量上,在effective_prio() 中,sleep_avg 的范围是 0~MAX_SLEEP_AVG,

经过以下公式转换后变成-MAX_BONUS/2~MAX_BONUS/2 之间的 bonus:
bonus = (NS_TO_JIFFIES((p)->sleep_avg) * MAX_BONUS / MAX_SLEEP_AVG) - MAX_BONUS/2

#define MAX_BONUS        (MAX_USER_PRIO * PRIO_BONUS_RATIO / 100)
#define MAX_USER_PRIO        (USER_PRIO(MAX_PRIO))
#define USER_PRIO(p)        ((p) - MAX_RT_PRIO)
#define MAX_RT_PRIO        MAX_USER_RT_PRIO
#define MAX_USER_RT_PRIO    100
.........弄得那么复杂,其实MAX_BONUS是定值10,MAX_SLEEP_AVG也是定值:
#define MAX_SLEEP_AVG        (DEF_TIMESLICE * MAX_BONUS)
#define DEF_TIMESLICE        (100 * HZ / 1000)

#define CURRENT_BONUS(p) (NS_TO_JIFFIES((p)->sleep_avg) * MAX_BONUS / MAX_SLEEP_AVG)
#define NS_TO_JIFFIES(TIME)    ((TIME) / (1000000000 / HZ))
所以bonus与平均睡眠时间sleep_avg成正比。 不管怎么说,sleep_avg 反映了调度系统的两个策略:交互式进程优先和分时系统的公平共享。

 

bonus(奖赏)是从范围0~10的值,值小于5表示降低动态优先级以惩戒,值大于5表示增加动态优先级以使奖赏。bonus的值依赖于进程的过去情况,与进程的平均睡眠时间有关,也就是说,平均睡眠时间越久,bonus值越大。

那么,什么是平均睡眠时间呢?粗略地讲,平均睡眠时间就是进程在睡眠状态中所消耗的平均纳秒数,其存放在task_struck的sleep_avg字段中。注意,这绝对不是对过去时间的求平均值操作 ,因为TASK_INTERRUPTIBLE 状态和TASK_UNINTERRUPTIBLE状态所计算出的平均睡眠时间是不同的,而且,进程在运行的过程中平均睡眠时间递减。最后,平均睡眠时间永远不会大于1s。

 

根据CURRENT_BONUS宏,我们可以得到bonus和sleep_avg的对应关系:

平均睡眠时间sleep_avg

bonus

粒度

大于或等于 0 小于 100 ms

0

5120

大于或等于100 小于200 ms

1

2560

大于或等于200 小于300 ms

2

1280

大于或等于300 小于 400 ms

3

640

大于或等于400 小于 500 ms

4

320

大于或等于500 小于 600 ms

5

160

大于或等于600 小于 700 ms

6

80

大于或等于700 小于 800 ms

7

40

大于或等于800 小于 900 ms

8

20

大于或等于900 小于 1000 ms

9

10

1 秒

10

10

 

 

平均睡眠时间也被调度程序用来评判一个给定进程是交互式进程还是批处理进程的依据 。如果一个进程满足:

动态优先级 ≤ 3 ×  静态优先级/4 + 28       (3)

那么就看做是交互式进程。 高优先级进程比低优先级进程更容易成为交互式进程。例如,具有最高静态优先级(100)的进程,当他的bonus值超过2,即睡眠超过200ms时,就被看做是交互式进程。判断交互式进程代码的具体实现请参看博文“recalc_task_prio函数 ”。

 

下面再介绍一些内核调用effective_prio给进程计算优先级的时机(计一般在进程状态发生改变,内核就有可能计算并设置进程的动态优先级):

a) 创建进程

在copy_process()中,子进程继承了父进程的动态优先级,平分父进程的时间片,并添加到父进程所在的就绪队列中。如果父进程不在任何就绪队列中(例如它是 IDLE 进程),那么就通过effective_prio() 函数计算出子进程的优先级,而后根据计算结果将子进程放置
到相应的就绪队列中。

b) 唤醒休眠进程

核心调用 recalc_task_prio() 设置从休眠状态中醒来的进程的动态优先级,再根据优先级放置到相应就绪队列中。

c) 调度到从 TASK_INTERRUPTIBLE 状态中被唤醒的进程

实际上此时调度器已经选定了候选进程,但考虑到这一类型的进程很有可能是交互式进程,因此此时仍然调用 recalc_task_prio() 对该进程的优先级进行修正,修正的结果将在下一次调度时体现。

d) 进程因时间片相关的原因被剥夺 cpu

在 schedule_tick() 中(由定时器中断启动),进程可能因两种原因被剥夺 cpu,一是时间片耗尽,一是因时间片过长而分段。这两种情况都会调用effective_prio() 重新计算优先级,重新入队。

e) 其它时机

这些其它时机包括IDLE 进程初始化(init_idle())、负载平衡以及修改 nice 值(set_user_nice())、修改调度策略等主动要求改变优先级的情况。

 

即使具有较高静态优先级的普通进程获得较大的CPU时间片,也不应该使静态优先级较低的进程无法运行。为了避免饥饿,当一个进程用完它的时间片时,它应该被还没有用完时间片的低优先级的进程取代。为了实现这种机制,调度程序维持两个不相交的可运行进程集合:活动进程和过期进程。太复杂了是不?别着急,我们还是从数据结构入手。

 

2 数据结构


回忆一下前面讲的,系统中有个0号进程的task_struct结构init_task,然后以它打头,系统中每个进程的tasks字段链接在一起形成一个双向循环链表表。另外,每个CPU有个运行进程链表runqueue(2.6.18内核以后叫做rq,存放在位于kernel/Sched.c中),称为运行队列。作为Linux2.6调度程序最重要的数据结构,runqueue数据结构存放在runqueues每个CPU变量中,宏this_rq()产生本地CPU运行队列的地址,而宏cpu_rq(n)产生索引为n的CPU运行队列地址。

 

struct runqueue {
    spinlock_t lock;

    unsigned long nr_running;
#ifdef CONFIG_SMP
    unsigned long cpu_load;
#endif
    unsigned long long nr_switches;

    unsigned long nr_uninterruptible;

    unsigned long expired_timestamp;
    unsigned long long timestamp_last_tick;
    task_t *curr, *idle;
    struct mm_struct *prev_mm;
    prio_array_t *active, *expired, arrays[2];
    int best_expired_prio;
    atomic_t nr_iowait;

#ifdef CONFIG_SMP
    struct sched_domain *sd;

    /* For active balancing */
    int active_balance;
    int push_cpu;

    task_t *migration_thread;
    struct list_head migration_queue;
#endif
};
runqueue数据结构中最重要的字段是与可运行进程的链表相关的字段。系统中的每个可运行进程属于且只属于一个运行队列。只要可运行进程保持在同一个运行队列中,它就只可能在拥有该运行队列的CPU上执行。

运行队列arrays字段是一个包含两个prio_array_t结构的数组。每个数据结构都表示一个可运行进程的集合,并包括140个双向链表头(每个链表对应一个可能的进程优先级)、一个优先级位图和一个集合中所包含的进程数量的计数器:
struct prio_array {
    unsigned int nr_active;
    unsigned long bitmap[BITMAP_SIZE];
    struct list_head queue[MAX_PRIO];
};

下图可以看到,runqueue结构的active字段指向arrays中的两个prio_array_t数据结构之一:对应于包含活动进程的可运行进程的集合。相反,expired字段指向数组中的另一个prio_array_t数据结构:对应于包含过去进程的可运行进程的集合。

 

 

下面简单说一下rq结构中的其他字段的用处:

 

spinlock_t lock:runqueue 的自旋锁,当需要对 runqueue 进行操作时,仍然应该锁定,但这个锁定操作只影响一个 CPU 上的就绪队列,因此,竞争发生的概率要小多了。

task_t *curr:本 CPU 正在运行的进程。

tast_t *idle:指向本 CPU 的 idle 进程,表示本地CPU的swapper进程,相当于 2.4 中 init_tasks[this_cpu()] 的作用。

int best_expired_prio:记录 expired 就绪进程组中的最高优先级(数值最小)。该变量在进程进入expired 队列的时候保存(schedule_tick()),用途见下面expired_timestamp的解释)。

unsigned long expired_timestamp:当新一轮的时间片递减开始后,这一变量记录着最早发生的进程耗完时间片事件的时间(jiffies 的绝对值,在 schedule_tick() 中赋),它用来表征expired 中就绪进程的最长等待时间。它的使用体现在 EXPIRED_STARVING(rq)宏上。

上面已经提到,每个 CPU 上维护了两个就绪队列,active 和 expired。一般情况下,时间片结束的进程应该从 active 队列转移到 expired 队列中(schedule_tick()),但如果该进程是交互式进程(实时进程FIFO或RR),调度器就会让其保持在active 队列上以提高它的响应速度。这种措施不应该让其他就绪进程等待过长时间,也就是说,如果 expired 队列中的进程已经等待了足够长时间了,即使是交互式进程也应该转移到 expired 队列上来,排空 active。这个阀值就体现在EXPIRED_STARVING(rq) 上:在 expired_timestamp 和 STARVATION_LIMIT都不等于 0 的前提下,如果以下两个条件都满足,则 EXPIRED_STARVING() 返回真:
·(当前绝对时间 - expired_timestamp) >= (STARVATION_LIMIT * 队列中所有就绪进程总数 + 1),也就是说 expired 队列中至少有一个进程已经等待了足够长的时间;
·正在运行的进程的静态优先级比 expired 队列中最高优先级要低(best_expired_prio,数值要大),此时当然应该尽快排空 active 切
换到expired 上来。

struct mm_struct *prev_mm:保 存进程切换后被调度下来的进程(称之为 prev)的 active_mm 结构指针。因为在 2.6 中 prev 的 active_mm 是在进程切换完成之后释放的(mmdrop()),而此时 prev 的 active_mm 项可能为 NULL,所以有必要在runqueue 中预先保留。

unsigned long nr_running:本 CPU 上的就绪进程数,该数值是 active 和 expired 两个队列中进程数的总和,是说明本 CPU 负载情况的重要参数(详见"调度器相关的负载平衡 ")。

unsigned long nr_switches:记录了本 CPU 上自调度器运行以来发生的进程切换的次数。

unsigned long nr_uninterruptible:记录本 CPU 尚处于 TASK_UNINTERRUPTIBLE 状态的进程数,和负载信息有关。

atomic_t nr_iowait:记录本 CPU 因等待 IO 而处于休眠状态的进程数。

unsigned long timestamp_last_tick:本就绪队列最近一次发生调度事件的时间,在负载平衡的时候会用到(见"调度器相关的负载平衡 ")。

task_t *migration_thread:指向本 CPU 的迁移进程。每个 CPU 都有一个核心线程用于执行进程迁移操作(见"调度器相关的负载平衡 ")。

struct list_head migration_queue:需要进行迁移的进程列表(见"调度器相关的负载平衡 ")。

 

arrays中的两个prio_array_t数据结构的作用会发生周期性的变化:活动进程突然变成过期进程,而过期进程变化为活动进程,调度程序简单地交互运行队列的active和expired字段的内容以完成这种变化。每个进程描述符task_struct都包括几个与调度相关的字段:


1) state

进程的状态仍然用 state 表示,不同的是,2.6 里的状态常量重新定义了,以方便位操作:
/* 节选自[include/linux/sched.h] */

#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4
#define TASK_ZOMBIE 8
#define TASK_DEAD 16
新增加的TASK_DEAD 指的是已经退出且不需要父进程来回收的进程。

2) timestamp

进程发生调度事件的时间点、时间戳(单位是:纳秒 —— nanosecond,见下)。包括以下几类:
· 被唤醒的时间(在 activate_task() 中设置);
· 被切换下来的时间(schedule());
· 被切换上去的时间(schedule());
· 负载平衡相关的赋值(见"调度器相关的负载平衡")。
从这个值与当前时间的差值中可以分别获得"在就绪队列中等待运行的时长"、"运行时长"等与优先级计算相关的信息(见"优化了的优先级计算方法")。

两种时间单位:系统的时间是以 nanosecond(十亿分之一秒)为单位的,但这一数值粒度过细,大部分核心应用仅能取得它的绝对值,感知不到它的精度。时间相关的核心应用通常围绕时钟中断进行,在 Linux 2.6 中,系统时钟每1 毫秒中断一次(时钟频率,用 HZ 宏表示,定义为 1000,即每秒中断 1000次),这个时间单位称为一个 jiffie。很多核心应用都是以 jiffies 作为时间单位,例如进程的运行时间片。

jiffies 与绝对时间之间的转换公式如下:
nanosecond=jiffies*1000000

核心用两个宏来完成两种时间单位的互换:JIFFIES_TO_NS()、NS_TO_JIFFIES(),很多时间宏也有两种形式,例如 NS_MAX_SLEEP_AVG 和
MAX_SLEEP_AVG。

3) prio

优先级,在 0~MAX_PRIO-1 之间取值(MAX_PRIO 定义为 140),其中 0~MAX_RT_PRIO-1 (MAX_RT_PRIO 定义为100)属于实时进程范围,MAX_RT_PRIO~MX_PRIO-1 属于非实时进程。数值越大,表示进程优先级越小。2.6 中,动态优先级不再统一在调度器中计算和比较,而是独立计算,并存储在进程的 task_struct 中,再通过上面描述的 priority_array 结构自动排序。

4) static_prio

nice 值沿用 Linux 的传统,在 -20 到 19 之间变动,数值越大,进程的优先级越小。nice 是用户可维护的,但仅影响非实时进程的优先级。2.6 内核中不再存储 nice 值,而代之以 static_prio:static_prio = MAX_RT_PRIO + nice + 20(MAX_RT_PRIO=100)。 进程初始时间片的大小仅取决于进程的静态优先级,  这一点不论是实时进程还是非实时进程都一样  ,不过实时进程的 static_prio 不参与优先级计算。 

5) activated

表示进程因什么原因进入就绪态,这一原因会影响到调度优先级的计算。activated 有四个值:
· -1,进程从 TASK_UNINTERRUPTIBLE 状态被唤醒;
· 0,缺省值,进程原本就处于就绪态;
· 1,进程从 TASK_INTERRUPTIBLE 状态被唤醒,且不在中断上下文中;
· 2,进程从 TASK_INTERRUPTIBLE 状态被唤醒,且在中断上下文中。
activated 初值为 0,在两个地方修改,一是在 schedule() 中,被恢复为 0,另一个就是 activate_task(),这个函数由 try_to_wake_up()
函数调用,用于激活休眠进程:
· 如果是中断服务程序调用的 activate_task(),也就是说进程由中断激活,则该进程最有可能是交互式的,因此,置 activated=2;否则置
activated=1。
· 如果进程是从 TASK_UNINTERRUPTIBLE 状态中被唤醒的,则activated=-1( 在try_to_wake_up()函数中 )。

6) sleep_avg

进程的平均等待时间(以 nanosecond 为单位),在 0 到 NS_MAX_SLEEP_AVG之间取值,初值为 0,相当于进程等待时间与运行时间的差值。sleep_avg 所代表的含义比较丰富,既可用于评价该进程的"交互程度",又可用于表示该进程需要运行的紧迫性。这个值是动态优先级计算的关键因子,sleep_avg 越大,计算出来的进程优先级也越高(数值越小)。在博文" recalc_task_prio函数 " 中会详细分析 sleep_avg 的变化过程。

7) policy: 进程的调度类型(SCHED_NORMAL, SCHED_RR, 或 SCHED_FIFO)

8) thread_info->flags:存放TIF_NEED_RESCHED 标志,如果必须调用调度程序,则设置该标志

9) thread_info->cpu:可运行进程所在运行队列的CPU逻辑号

10) run_list:指向进程所属的运行队列链表中的下一个和前一个元素

12) array:指向包含进程运行队列的集合prio_array_t

13) last_ran:最近一次替换本进程的进程切换时间

14) cpus_allowed:能执行进程的CPU的位掩码

15) time_slice:在进程的时间片中还剩余的时钟节拍数

16) first_time_slice:如果进程肯定不会用完其时间片,就把该标志设置为1

17) rt_priority:进程的实时优先级

所有state处于TASK_RUNNING状态的进程,则在运行队列链表中以run_list组成以prio_array[prio]打头的一个进程循环链表。

当新进程被创建的时候,由copy_process()调用的函数sched_fork()用下述方法设置current进程(父进程)和p进程(子进程)的time_slice字段:
p->time_slice = (current->time_slice + 1) >> 1;
current->time_slice >>= 1;

由此可以看出,父进程剩余的节拍数被划分成两等分,一份给父进程,另一份给子进程。如果父进程的时间片只剩下一个时钟节拍,则划分操作强行把current->time_slice重新置为1,然后调用scheduler_tick()递减该字段,从而使current->time_slice变为0,耗尽父进程的时间片,把父进程移入expired中。

函数copy_process()也初始化子进程描述符中与进程调度相关的几个字段:
p->first_time_slice = 1;
p->timestamp = sched_clock( );

因为子进程没有用完它的时间片(如果一个进程在它的第一个时间片内终止或执行新的程序,就把子进程的剩余时间奖励给父进程),所以first_time_slice标志置为1.用函数sched_clock()所产生的时间戳的值初始化timestamp字段:函数sched_clock返回被转化成纳秒的64位寄存器TSC的内容。

 

3 调度程序所使用的函数


调度程序基本依靠下面几个函数来完成调度工作:
scheduler_tick( ):维持当前最新的time_slice计数器。
try_to_wake_up( ):唤醒睡眠进程。
recalc_task_prio( ):更新进程的动态优先级。
schedule( ):选择要被执行的新进程。
load_balance():维持多处理器系统中运行队列的平衡

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值