进程—进程调度(1)
上下文切换
进程可以调度,但必须保证每个进程都可以顺序的执行,而一个进程执行所需的全部信息可由进程的PCB(task_struct)维护,所以在进程发生切换的时候可以将当前进程的运行状态信息(快照)保存到它的PCB中(这样就能在下一次调度程序选择到它时接着上一状态继续执行),将马上要执行的进程的运行状态信息(在PCB中)恢复,这样就可以合理的完成调度,这个过程就叫上下文切换。
中断
上下文切换是在内核中完成的,对用户透明,所以在上下文切换的时候必须先陷入内核(一般是通过时钟中断和系统调用)。上下文的切换需要硬件的支持。当前进程正在运行,当中断发生时,中断硬件将程序计数器、程序状态字、有时还有一个或多个寄存器(进程的运行状态信息)压入当前进程的内核堆栈中,PC随即跳转到中断服务程序入口(根据硬件向量法或软件查询法得到中断服务程序入口地址)去执行中断服务程序。注意,这些工作都是硬件完成的,在这些工作完成的同时完成了一次堆栈切换(从进程的用户堆栈切换到进程的内核堆栈,由中断硬件完成)。然后,PC的控制权转移到了软件(中断服务程序),一般地,中断服务程序有一个自己的中断堆栈(就像进程有自己的内核堆栈一样),为了不破坏内核堆栈,在执行中断服务程序的过程中又会发生一次堆栈切换(从内核堆栈切换到中断堆栈,这次的切换是软件完成的),进入到中断上下文之中,随后中断服务程序会调用处理特定中断请求的中断处理例程,让中断处理例程运行在中断上下文之中。中断处理例程完成中断处理之后返回到中断服务程序,返回的过程又会涉及到一次堆栈切换(由中断堆栈切换到内核堆栈,这次的切换也是软件完成的),最后,中断服务程序执行中断返回指令,由中断硬件将内核堆栈中的状态信息恢复到相应的寄存器中,在恢复寄存器的同时清空内核堆栈中保存的状态信息,系统从内核态返回到用户态,这里又发生了一次堆栈切换(从内核堆栈切换到用户堆栈,由中断硬件完成)。
发生上下文切换的中断过程
中断服务程序调用中断处理例程,中断处理例程在执行的过程中调用了调度程序进行调度,这时,调度程序会检查是否需要发生上下文切换(不是每次中断都会发生上下文的切换,例如某个时钟中断就可能不会导致上下文切换),发现需要发生上下文切换,接下来,首先要完成的就是保存当前进程的运行状态到它的PCB中(这个工作是调度程序做的,由软件过程完成)。
保存当前进程的状态的工作会由一段短小的汇编语言例程来完成,它将中断硬件保存到内核堆栈中的状态数据全部保存(pop)到进程的PCB中,与此同时,pop操作会将内核堆栈中进程的状态信息全部清空,为加载下一个被调度器选择的进程的状态信息做好准备。
在当前进程的运行状态被保存之后,进程内核堆栈中被中断的进程的状态信息已被清空,为装载下一个进程的运行状态信息做好了准备。接下来,调度程序会选择一个进程(位于当前最高优先级队列中的一个进程),将它上一次保存的运行状态信息(在PCB中)压入到内核堆栈中,更新内核堆栈中的thread_info结构体,然后返回到中断服务程序。随后,中断服务程序执行从中断返回指令(return-from-trap),剩下的恢复工作就交由中断硬件完成,之后,系统从内核态切换到用户态,整个上下文切换过程完成。
Linux进程类别
1.实时进程:高优先级,响应快,优先级范围[0,99]。
2.普通进程:分为交互式进程(I/O消耗型)和批处理进程(CPU消耗型),优先级低于实时进程,范围为[100,139]。
在Linux中,调度算法可以明确地确认所有实时进程的身份,但却不能区分交互式进程和批处理进程。Linux2.6调度程序实现了基于进程过去行为的启发式算法,以确定进程此刻应该被当作交互式进程还是批处理进程。与批处理进程相比,交互式进程的调度优先级要更高一些。
Linux中与调度相关的系统调用
系统调用 | 说明 |
---|---|
nice() | 改变一个普通进程的静态优先级 |
getpriority() | 获得一组普通进程的最大静态优先级 |
setpriority() | 设置一组普通进程的静态优先级 |
sched_getscheduler() | 获得一个进程的调度策略 |
sched_setscheduler() | 设定一个进程的调度策略和实时优先级 |
sched_getparam() | 获得一个进程的实时优先级 |
sched_setparam() | 设置一个进程的实时优先级 |
sched_yield() | 自愿放弃处理器而不阻塞 |
sched_get_priority_min() | 获得一种策略的最小实时优先级 |
sched_rr_get_interval() | 获得时间片轮转策略的时间片值 |
sched_setaffinity() | 设置进程的CPU亲和力掩码 |
sched_getaffinity() | 获得进程的CPU亲和力掩码 |
几个优先级字段
nice
static_prio
nice和static_prio是普通进程会用到的两个优先级字段,用轮转策略的实时进程也会用到它们,只是用来重新计算轮转时间片长度。
rt_priority
rt_priority是实时进程用到的优先级字段。
prio
这才是决定进程调度优先级的字段,也就是说,进程在哪个就绪队列中由这个字段决定。
Linux进程调度策略
用户可以调用系统调用sched_setscheduler()设置调度策略。
unsigned int policy;
Linux的进程调度是抢占式的,允许高优先级进程抢占低优先级进程,这是下面几个调度策略对应的算法都必须确保的基本机制。另外,调度只会发生在位于就绪队列中的进程之间(ranqueue)。
实时进程的调度优先级只由实时优先级(rt_priority,范围为[0,MAX_RT_PRIO-1])静态的决定,从一开始由用户指定后,就不再动态地改变,不受nice值的影响(nice值只影响调度优先级在[100,139]范围内的普通进程的调度优先级)。实时优先级的调度优先级(prio)通过其实时优先级(rt_priority)计算(prio = MAX_RT_PRIO-1 - rt_priority)得到,范围在0~MAX_RT_PRIO-1间。默认MAX_RT_PRIO为100,所以默认的实时进程的调度优先级(prio)范围是[0,99],实时进程的调度优先级虽然不会自己动态的改变,但是可以由用户使用系统调用sched_setparam()和sched_setscheduler()来重新设置它的实时优先级,进而改变调度优先级。实时进程位于高优先级队列(明确地说,优先权在[0~MAX_RT_PRIO-1]区间内的优先级队列)中,而且总是位于活动运行队列中,所以只要有实时进程存在,普通进程永远也别想运行。
普通进程的调度优先级根据静态优先级static_prio和平均睡眠时间sleep_avg动态的来计算,static_prio是根据nice值得到的,两者可以相互转换。详细的说明请见task_struct代码注释。默认的普通进程的调度优先级(prio)范围是[100,139]。
1.SCHED_FIFO
值为1-实时进程-用基于优先权的先进先出算法。基于SCHED_FIFO调度机制的实时进程在运行时会一直占用CPU,除非就绪队列中有优先级更高的实时进程,或自愿调用阻塞原语(如
sleep_on_interruptible()
),或停止,或被杀死,或通过调用sched_yield()自动放弃CPU。
2.SCHED_RR
值为2-实时进程-用基于优先权的轮转法。一旦当前进程自愿调用阻塞原语(如
sleep_on_interruptible()
),或停止,或被杀死,或通过调用sched_yield()自动放弃CPU或一个时间片消耗完毕,或就绪队列中有优先级更高的实时进程,则会在中断返回时发生调度。基于SCHED_RR调度机制的调度程序将该进程置于同优先级队列的末尾,然后运行该优先级队列中的下一个实时进程。这是分时系统实现良好交互性的基础算法,又名时间片轮转调度算法。
3.SCHED_NORMAL
值为0-非实时进程-用基于优先权的多级反馈队列轮转法,根据进程过去的执行情况动态的调整调度优先级,一般还会将执行完轮转时间片的进程放入过期可运行队列中。兼顾了作业的周转时间和交互性,并且防止了饿死。
普通进程的调度策略:SCHED_NORMAL
普通进程的调度优先级(prio)由2个因素决定,一个是进程的静态优先级(static_prio,又叫基本优先级,默认值是120),另一个是进程的平均睡眠时间(sleep_avg)。static_prio的范围为[100,139],新进程总是继承其父进程的静态优先级。
static_prio
static_prio通过nice(范围为[-20,19])计算得到(static_prio = 120 + nice),所以用户可以通过系统调用nice()和setpriority()来设置nice值进而改变自己拥有的进程的静态优先级(也只能通过这两个系统调用改变静态优先级,否则静态优先级不会发生变化,尽管动态优先级会发生变化)。
普通进程每次执行的时候都有一个有限的轮转时间片,这是进程在被抢占前能够连续占用CPU的时间片长度,如果没有高优先级进程抢断,当前进程会可以一直占用CPU直到用完它的轮转时间片。静态优先级用来计算普通进程的轮转时间片,基本规则是静态优先级越高,static_prio越小,轮转时间片越长。所以对于普通进程,静态优先级越高的进程获得的连续执行CPU的时间片越长,也可以说nice值唯一决定了普通进程能获得的轮转时间片长度。具体的计算规则可以参考../kernel/sched.c 中的task_timeslice(),这个函数计算并返回一个进程轮转时间片长度,或者参考《深入理解Linux内核》第七章中普通进程的调度章节下的基本时间片一节。
prio
task_struct中的这个字段是进程的调度优先级,用来决定进程在哪个优先级队列中,进而实现O(1)调度。对于普通进程,这个字段被称为它的动态优先级,通过静态优先级和平均睡眠时间计算得到,范围是[100,139]。
普通进程的调度策略(SCHED_NORMAL)的主要思想是通过进程过去的执行情况来衡量这个进程是属于I/O消耗型还是CPU消耗型,具体的衡量指标是一个进程的平均睡眠时间sleep_avg。如果一个进程的sleep_avg大,则该进程更加趋向于I/O消耗型,优先级就越高,prio就越小。
动态优先级:
prio = max (100, min (static_prio - bonus,139) )
bonus = 10*sleep_avg/HZ - 5,范围在[-5,5]之间。(sleep_avg 范围为[0,HZ]),所以sleep_avg = HZ/2时,进程调度优先级保持不变;当HZ > sleep_avg > HZ/2时,进程调度优先级会提高;当0 < sleep_avg < HZ/2时,进程调度优先级会降低。
对于交互性强的进程,它不会一次性的就将轮转时间片给用光(这一般是CPU消耗型进程干的事),而是用一小会儿之后,进行一次I/O操作,放弃掉CPU,这使得它可以长时间的位于活期可执行队列中,不至于用完时间片了就被重新计算时间片,然后放到过期可执行队列中,久久得不到执行机会。而且经常性的执行I/O操作正是交互型进程的一大特点,这也是交互型进程之所以为交换型进程的根本原因,系统给予这种进程以较高的bonus,使其调度优先级更高,有更多的被调度的机会。
实时进程的调度策略
参考task_struct代码注释和Linux进程调度策略的前言部分。
进程调度时机
调度时机来临时,内核或驱动将调用schedule(),在Linux中调度的时机主要有:
一、current的状态从running转换为其它状态时,如:
1)进程终止。exit()在最后调用schedule()。
2)进程因某种原因进入等待状态(随后会从就绪队列中删除,被插入到等待队列中)。
比较常见的就是进程调用nanosleep()或者wait系列的系统调用。此外,在设备驱动程序中,最常见的原因就是驱动程序引发一次I/O操作后,为等待I/O操作的结束而进入等待状态。多数情况下,驱动程序会直接调用schedule()。
二、当前进程的时间片用完时。
时间片是否用完,由时钟中断处理程序进行判断,若用完,就将current进程的need_resched位置1。在中断将要返回用户态时,如果current的need_resched位为1,则调用schedule()。
三、进程从中断、异常、系统调用状态(即内核态)返回时。
每次从内核返回到用户态时都会检查need_resched标记,若在中断、异常、系统调用中,current的need_resched被置1,就会导致进程调度,时钟中断属于此类。
计算调度优先级:effective_prio(p)
计算进程p的调度优先级,在更新进程调度优先级时会被
recalc_task_prio()
调用。
//../kernel/sched.c
/*
* return the priority that is based on the static
* priority but is modified by bonuses/penalties.
*
* We scale the actual sleep average [0 .... MAX_SLEEP_AVG]
* into the -5 ... 0 ... +5 bonus/penalty range.
*
*/
static int effective_prio(task_t *p)
{
int bonus, prio;
if (rt_task(p))//如果是实时进程
return p->prio; //返回实时进程的调度优先级
//如果是普通进程
bonus = CURRENT_BONUS(p) - MAX_BONUS / 2; // 10*(sleep_avg/HZ) - 5,值在[-5,5]之间。
prio = p->static_prio - bonus;
if (prio < MAX_RT_PRIO)
prio = MAX_RT_PRIO;
if (prio > MAX_PRIO-1)
prio = MAX_PRIO-1;
return prio; //普通进程的调度优先级
//prio = max (100, min (static_prio - bonus, 139))
}
//下面的代码段是一些宏定义,用来参考。
//../include/linux/sched.h
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO //100
#define MAX_PRIO (MAX_RT_PRIO + 40) //140
#define rt_task(p) (unlikely((p)->prio < MAX_RT_PRIO)) //prio<100?
//../kernel/sched.c
#define USER_PRIO(p) ((p)-MAX_RT_PRIO) //p-100
#define MAX_USER_PRIO (USER_PRIO(MAX_PRIO)) //40
#define PRIO_BONUS_RATIO 25
#define MAX_BONUS (MAX_USER_PRIO * PRIO_BONUS_RATIO / 100) //10
#define DEF_TIMESLICE (100 * HZ / 1000)
#define MAX_SLEEP_AVG (DEF_TIMESLICE * MAX_BONUS) //HZ
#define NS_TO_JIFFIES(TIME) ((TIME) / (1000000000 / HZ))
#define MAX_SLEEP_AVG (DEF_TIMESLICE * MAX_BONUS) //HZ
#define CURRENT_BONUS(p) \
(NS_TO_JIFFIES((p)->sleep_avg) * MAX_BONUS / MAX_SLEEP_AVG) // 10*sleep_avg/HZ 值在[0,10]之间
参考
task_struct
//--------------------------------------- Linux 2.6 进程调度相关信息 -----------------------------------------
long nice; //进程的初始优先级,范围[-20,+19],默认0,nice值越大优先级越低,分配的
//时间片可能更少。能通过系统调用nice()可以修改nice值。
int static_prio;//静态优先级。范围为[MAX_RT_PRIO, MAX_RT_PRIO+39],默认情况
//[100,139]。Normal进程使用静态优先级static_prio和平均睡眠时间sleep_avg动态的计算进程的调度优先级prio。
/*
static_prio= MAX_RT_PRIO + nice + 20
在../kernel/sched.c中有两个宏实现nice值和static_prio值之间的转换
#defineNICE_TO_PRIO(nice) (MAX_RT_PRIO + (nice)+ 20)
#definePRIO_TO_NICE(prio) ((prio) - MAX_RT_PRIO- 20)
*/
unsigned int rt_priority; //实时优先级,[0,MAX_RT_PRIO-1],默认情况下范围[0,99],在
//setscheduler()中设置,且一经设定就不再改变。Real_time进程使用实时优先级rt_priority静态的计算进程的调度优先级prio。
/*
0 -> normal
1-99 -> realtime
*/
int prio; //存放"调度程序"要用到的优先级,对应优先级位图中的相应优先级位。数值越
//大,表示进程优先级越小。
/*
0-99 -> Realtime process
100-140 -> Normal process
for Realtime process: prio = MAX_RT_PRIO-1 – rt_priority
for Normal process: prio = max (100, min (static_prio - bonus, 139))
其中bonus在[-5,5]之间。bonus越大,prio越小,优先级越高。
*/
unsigned long sleep_avg;//这个字段的值用来支持调度程序对进程的类型(I/O消耗型 or CPU消耗型)
//进行判断,值越大表示睡眠的时候越多,更趋向于I/O消耗型,系统调度时,会给该进程更多奖励以便该进程有更多的机会能够执行,反
//之,更趋向于CPU消耗型,会给该进程更多惩罚。sleep_avg 的范围是 0~MAX_SLEEP_AVG。
unsigned long long timestamp;//进程最近插入运行队列的时间,或涉及本进程的最近一次进程切换发生的时
//间。
unsigned long long last_ran;//最近一次替换本进程的进程切换发生的时间。
cputime_t utime;//该进程在用户态的cpu的使用时间。
cputime_t stime;//该进程在内核态的cpu的使用时间。
unsigned long sleep_time; //进程的睡眠时间
unsigned int time_slice;//进程剩余时间片,当一个普通进程(或者基于时间片轮转策略的实时进程)
//的时间片用完之后,要根据任务的静态优先级static_prio重新计算时间片。task_timeslice()为给定的任务返回一个新的时间
//片。对于交互性强的进程,时间片用完之后,它会被再放到活动数组而不是过期数组,该逻辑在scheduler_tick()中实现。
unsigned int first_time_slice;//如果进程肯定不会用完其时间片,就把该标志设置为1
const struct sched_class *sched_class; //与调度相关的函数
struct sched_entity se; //调度实体
struct sched_rt_entity rt; //实时任务调度实体
#ifdef CONFIG_PREEMPT_NOTIFIERS
/*list of struct preempt_notifier:*/
struct hlist_head preempt_notifiers; //与抢占有关
#endif
#if defined(CONFIG_SCHEDSTATS)||define(CONFIG_TASK_DELAY_ACCT)
unsigned int policy; //表示该进程的进程调度策略。调度策略有:
//SCHED_NORMAL 0, 非实时进程, 用基于优先权的轮转法。
//SCHED_FIFO 1, 实时进程, 用先进先出算法。
//SCHED_RR 2, 实时进程, 用基于优先权的轮转法
struct sched_info sched_info; //调度相关的信息,如进程在cpu上运行的时间/在队列中等待的时间...
#endif
struct list_head tasks; //任务队列,通过这个寄宿于PCB(task_struct)中的字段构成的双向循环链表
//将宿主PCB链接起来。
volatile long need_resched; //调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到
//用户态或从中断返回时,会发生调度。当进程的时间片耗尽时,scheduler_tick()会设置这个标识,当一个优先级高的进程进入可执
//行状态的时候,try_to_wake_up()会设置这个标识。
struct list_head run_list; //该进程所在的运行队列。这个队列有一个与之对应的优先级k,所有位于这个
//队列中的进程的优先级都是k,这些k优先级进程之间使用轮转法进行调度。k的取值是0~139。这个位于宿主PCB中的struct
//list_head类型的run_list字段将构成一个优先级为k的双向循环链表,像一条细细的绳子一样,将所有优先级为k的处于可运行状态
//的进程的PCB(task_struct)链接起来。
prio_array_t *array; //指向当前进程所在CPU的就绪进程链表。
cpumask_t cpus_allowed//能执行进程的CPU的位掩码
struct thread_info *thread_info;
/*
thread_info中与调度相关字段:
__u32 flags;//存放TIF_NEED_RESCHED标志,如果必须调用调度程序,则设置该标志
__u32 cpu;//运行进程所在运行队列的CPU逻辑号
*/