linux进程调度

一. 何为进程调度 

在linux这样的多用户多任务操作系统上,有大量程序执行的需求。如果每次只运行一个进程,一是其它进程得不到及时的响应,二是CPU时间不能得到充分利用。进程调度就是要解决诸如此类的一些问题,将CPU资源合理地分配给各个进程。同时调度过程又需要对各个进程不可见,在每个进程看来,自己是独占CPU在运行的。这就是虚拟CPU的概念。

调度程序是像linux这样的多任务操作系统的基础。只有通过调度程序的合理调度,系统资源才能最大限度地发挥,多任务才会有并发执行的效果。 



二. 进度调度的目标和基本工作 

进程调度最终要完成的目标就是为了最大限度的利用处理器时间。

只要有可以执行的进程,那么就总会有进程获得CPU资源而运行。当进程数大于处理器个数时,某一时刻总会有一些进程进程不能执行,这些进程处于等待状态。在这些等待运行的进程中选择一个合适的来执行,并且确定该进程运行多长时间,是调度程序所需完成的基本工作。 

 


三. 调度策略 

调度策略决定调度程序在何时让什么进程运行。 

3.1、 根据进程类型:I/O消耗型和处理器消耗型

I/O消耗型指进程的大部分时间用来提交I/O请求或者等待I/O请求,这样的进程经常处于可运行状态,但通常都是只运行短短的一会儿,因为它们等待更多的I/O请求时总会阻塞。举例来说,图形用户界面(GUI)和键盘输入都属于I/O消耗型。

处理器消耗型进程把大多数时间用在执行代码上,除非被抢占,否则它们通常都处于一直不停运行的状态。对于这类进程,调度策略往往是尽量降低它们的调度频率,而延长其运行时间。举例来说:MATLAB

I/O消耗型和处理器消耗型不是绝对划分的,处于动态变化的过程中。

这两种类型的进程是矛盾的,如果调度程序倾向于处理器消耗型进程,则I/O消耗型进程会因为前者长时间占用CPU而得不到好的响应。如果调度程序倾向于I/O消耗型进程,频繁进行调度,则处理器消耗型进程的执行将会不断被打断。

linux为了保证交互式应用和桌面系统性能,对进程的响应做了优化,更倾向于优先调度I/O消耗型进程。

3.2、 根据进程优先级

调度算法中最基本的一类就是基于优先级的调度。调度程序总是选择时间片未用尽而且优先级最高的进程运行。

linux实现了一种基于动态优先级的调度方法。即:一开始,先设置基本的优先级,然后它允许调度程序根据需要加减优先级。例如:如果一个进程在I/O等待上消耗的时间多于运行时间,则明显属于I/O消耗型进程,那么根据上面的考虑,应该动态提高其优先级。 

linux采用了两种不同的优先级范围,分别用于普通进程和实时进程。

  • nice值。范围为-20到+19,默认值为0。越大的nice值意味着越低的优先级,相比高nice值的进程,低nice值的进程可以获得更多的处理器时间。在linux的CFS调度算法中,nice值用于计算时间片的比例。

  • 实时优先级。值可配置,默认变化范围是0到99。与nice值相反,越高的实时优先级意味着越高的进程优先级。

实时优先级和nice优先级处于互不相交的两个范畴,任何实时进程的优先级都高于普通进程。 

3.3、时间片

时间片是一个数值,表明进程在被抢占之前所能持续运行的时间。调度策略必须规定一个默认的时间片,时间片过长会导致系统对交互进程的响应欠佳,时间片太短会增大进程切换带来的处理器消耗。

linux的CFS调度器没有直接分配时间片到进程,而是将处理器的使用比例分给了进程。这样一来,进程所获得的处理器时间其实是和系统负载相关的。这个比例进一步还会受进程nice值的影响,nice值作为权重将调整进程所使用的处理器时间比。



四. 和进程调度相关的知识准备

进程调度涉及到进程切换的问题,也就是使用一个新的进程来替换旧的进程在CPU上运行。进程切换涉及到了CPU寄存器的切换以及进程内核栈以及指令指针的切换。进程切换稍后会详细介绍,这里先介绍相关的预备知识。

4.1、 TSS-任务状态段

任务状态段(Task State Segment)是保存一个任务重要信息的特殊段。 TSS在任务切换过程中起着重要作用,通过它实现任务的挂起和恢复。所谓任务切换是指,挂起当前正在执行的任务,恢复或启动另一任务的执行。在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到任务状态段寄存器TR所指定的TSS中,包括通用寄存器状态,段寄存器状态,标志寄存器状态,EIP寄存器状态等等;然后,下一任务的TSS的选择子被装入TR;最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中。

由此可见,通过在TSS中保存任务现场各寄存器状态的完整映象,实现任务的切换。每项任务均有其自己的 TSS,而我们可以通过STR指令来获取指向当前任务中TSS的段选择器。

任务状态段TSS的基本格式如下图所示。


从图中可见,TSS的基本格式由104字节组成。这104字节的基本格式是不可改变的,但在此之外系统软件还可定义若干附加信息。基本的104字节可分为链接字段区域内层堆栈指针区域地址映射寄存器区域寄存器保存区域其它字段等五个区域。

  • 寄存器保存区域   寄存器保存区域位于TSS内偏移20H(32)至5FH(95)处,用于保存通用寄存器、段寄存器、指令指针和标志寄存器。当TSS对应的任务正在执行时,保存区域是未定义的;在当前任务被切换出时,这些寄存器的当前值就保存在该区域。当下次切换回原任务时,再从保存区域恢复出这些寄存器的值,从而,使处理器恢复成该任务换出前的状态,最终使任务能够恢复执行。 从上图可见,各通用寄存器对应一个32位的双字,指令指针和标志寄存器各对应一个32位的双字;各段寄存器也对应一个32位的双字,段寄存器中的选择子只有16位,安排再双字的低16位,高16位未用,一般应填为0。  

  • 内层堆栈指针区域   为了有效地实现保护,同一个任务在不同的特权级下使用不同的堆栈。例如,当从外层特权级3变换到内层特权级0时,任务使用的堆栈也同时从3级变换到0级堆栈;当从内层特权级0变换到外层特权级3时,任务使用的堆栈也同时从0级堆栈变换到3级堆栈。所以,一个任务可能具有四个堆栈,对应四个特权级。四个堆栈需要四个堆栈指针。   TSS的内层堆栈指针区域中有三个堆栈指针,它们都是48位的全指针(16位的选择子和32位的偏移),分别指向0级、1级和2级堆栈的栈顶,依次存放在TSS中偏移为4、12及20开始的位置,3级堆栈的指针就是保存在寄存器域里面的。当发生向内层转移时,把适当的堆栈指针装入SS及ESP寄存器以变换到内层堆栈,外层堆栈的指针保存在内层堆栈中。没有指向3级堆栈的指针,因为3级是最外层,所以任何一个向内层的转移都不可能转移到3级。但是,当特权级由内层向外层变换时,并不把内层堆栈的指针保存到TSS的内层堆栈指针区域。 

  • 地址映射寄存器区域   从虚拟地址空间到线性地址空间的映射由GDT和LDT确定,与特定任务相关的部分由LDT确定,而LDT又由LDTR确定。如果采用分页机制,那么由线性地址空间到物理地址空间的映射由包含页目录表起始物理地址的控制寄存器CR3确定。所以,与特定任务相关的虚拟地址空间到物理地址空间的映射由LDTR和CR3确定。显然,随着任务的切换,地址映射关系也要切换。TSS的地址映射寄存器区域由位于偏移1CH处的双字字段(CR3)和位于偏移60H处的字段 (LDTR)组成。在任务切换时,处理器自动从要执行任务的TSS中取出这两个字段,分别装入到寄存器CR3和LDTR。这样就改变了虚拟地址空间到物理地址空间的映射。 但是,在任务切换时,处理器并不把换出任务的寄存器CR3和LDTR的内容保存到TSS中的地址映射寄存器区域。事实上,处理器也从来不向该区域自动写入。因此,如果程序改变了LDTR或CR3,那么必须把新值人为地保存到TSS中的地址映射寄存器区域相应字段中。可以通过别名技术实现此功能。   

  • 链接字段   链接字段安排在TSS内偏移0开始的双字中,其高16位未用。在起链接作用时,低16位保存前一任务的TSS描述符的选择子。如果当前的任务由段间调用指令CALL或中断/异常而激活,那么链接字段保存被挂起任务的 TSS的选择子,并且标志寄存器EFLAGS中的NT位被置1,使链接字段有效。在返回时,由于NT标志位为1,返回指令RET或中断返回指令IRET将使得控制沿链接字段所指恢复到链上的前一个任务。  

  • 其它字段   为了实现输入/输出保护,要使用I/O许可位图。任务使用的I/O许可位图也存放在TSS中,作为TSS的扩展部分。在TSS内偏移66H处的字用于存放I/O许可位图在TSS内的偏移(从TSS开头开始计算)。   在TSS内偏移64H处的字是为任务提供的特别属性。在80386中,只定义了一种属性,即调试陷阱。该属性是字的最低位,用T表示。该字的其它位置被保留,必须被置为0。在发生任务切换时,如果进入任务的T位为1,那么在任务切换完成之后,新任务的第一条指令执行之前产生调试陷阱。 

linux是如何使用TSS的 

intel的建议:为每一个进程准备一个独立的TSS段,进程切换的时候切换TR寄存器使之指向该进程对应的TSS段,然后在任务切换时(比如涉及特权级切换的中断)使用该段保留所有的寄存器。  

Linux的做法:

  •  Linux没有为每一个进程都准备一个TSS段,而是每一个CPU使用一个TSS段,TR寄存器保存该段。进程切换时,只更新TSS段中的esp0字段为新进程的内核栈,esp0字段存放在thread_struct中。

  •  Linux的TSS段中只使用esp0和iomap等字段,不用它来保存寄存器,在一个用户进程被中断进入ring0的时候,TSS中取出esp0,然后切到esp0,其它的寄存器则保存在esp0指示的内核栈上而不保存在TSS中。

  • 结果,Linux中每一个CPU只有一个TSS段,TR寄存器永远指向它。符合x86处理器的使用规范,但不遵循intel的建议,这样的结果是开销更小了,因为不必切换TR寄存器了。 

Linux的TSS实现:

定义tss:

  
  
  
struct tss_struct init_tss[NR_CPUS] __cacheline_aligned ={[0... NR_CPUS-1]= INIT_TSS };(arch/i386/kernel/init_task.c)
INIT_TSS结构定义为:
define INIT_TSS  {                            \
.esp0        =sizeof(init_stack)+(long)&init_stack,    \
.ss0        = __KERNEL_DS,                   \
.esp1        =sizeof(init_tss[0])+(long)&init_tss[0],    \
.ss1        = __KERNEL_CS,                    \
.ldt        = GDT_ENTRY_LDT,                \
.io_bitmap_base    = INVALID_IO_BITMAP_OFFSET,            \
.io_bitmap    ={[0... IO_BITMAP_LONGS]=~0},        \
}

总结

TSS段的作用在于保存每个进程的硬件上下文,intel 80x86CPU为TSS段的使用定义相应的汇编指令,这样在进程切换的时候,只要使用该汇编指令,就可以自动进行硬件上下文的切换了。

这样做的好处在于利用了CPU指令的功能,操作系统设计者不需要专门处理硬件上下文切换的细节,缺点在于

  • 要在TSS段中为每个进程静态定义一个数组元素,这样该数组将会很大;

  • 不能对硬件上下文切换进行优化;

  • linux系统为了运行在各种硬件平台下,需要一个通用模型,而完全遵循intel的建议却受到了太多的约束。

所以linux部分使用了intel80x86硬件平台的TSS段。主要使用了一下功能:

  • 完成从用户态到内核态切换时,内核堆栈地址的获取;

  • I/O端口访问能力检查。

其实windows也没有完全使用TSS段来做任务切换。

4.2、 与程序跳转相关的汇编指令

(1)call

将程序当前执行的位置(ip)压入栈中;

转移到调用的子程序中执行。

(2)ret

用栈中esp指向的内容替换ip,转移到原来的程序继续运行。

(3)jmp

无条件转移指令,需要给出两种信息,转移的目的地址或者转移的距离。

后面我们将会看到linux的进程切换程序是如何使用指令jmp和ret来实现执行流的切换的。



五. 进程切换的实现 switch_to

见附件

下载链接:http://download.csdn.net/detail/da310blog/8115261


六. linux的进程调度

内存中保存了对每个进程的唯一描述,并通过若干结构与其它进程连接起来。调度器面对的情形就是这样,其任务是在程序之间共享CPU时间,创造并行执行的错觉。正如上面的讨论,该任务分为两个不同部分:一个涉及调度策略,另一个涉及上下文切换。上下文切换我们已经详细解释过了,接下来的任务就是研究一下调度策略。

linux进程调度主要包括对以下进程的调度:

  • 普通进程   调度策略CFS

  • 实时进程   调度策略FIFO、RR

  • idle进程

6.1、 数据结构

调度器使用一系列数据结构,来排序和管理系统中的进程。调度器的工作方式与这些结构的设计密切相关。几个组件在许多方面彼此交互,下图表明了这些组件之间的关联。

可以通过两种方法激活调度。一种是直接的,比如进程打算睡眠或者处于其他原因放弃CPU;另一种是通过周期性调度机制,以固定的频率运行,不时检测是否有必要进行进程切换。下文中将这两个组件称为为通用调度器或者核心调度器。

  • 调度器类用于判断接下来运行哪个进程。内核支持不同的调度策略,调度器类使得能够以模块化方法实现这些策略,一个类的代码不需要与其他类的代码交互。而且调度器因此有了很好的层次结构,进程调度的过程就是一个接口的调用过程。在调度器被调用时,它会按照优先级顺序遍历调度器类,选择一个拥有可执行程序的最高优先级的调度器类,再选择具体要投入运行的进程。

  • 在选中要运行的进程以后,必须执行底层任务切换,这需要与CPU的紧密交互。

  • 每个进程都刚好属于某一调度器类,各个调度器类负责管理所属的进程。通用调度器自身完全不涉及进程管理,其工作都委托给调度器类。

(1)task_struct的成员

各进程的task_struct有几个成员与调度相关。

struct task_struct{
...
	int prio, static_prio, normal_prio;
	unsigned int rt_priority;
	struct list_head  run_list;
	const struct sched_class *sched_class;
	struct sched_entity se;
	unsigned int policy;
	cpunask_t cpus_allowd;
	unsigned int time_slice;
...
}

  • task_struct使用了3个成员表示进程的优先级:

    static_prio表示进程的静态优先级。静态优先级是进程启动时分配的,范围为100-139。它可以用nice值和sched_scheduler系统调用修改,否则在进程运行期间会一直保持恒定。计算公式为:static priority = nice + 20 + MAX_RT_PRIO。

    normal_prio表示基于进程的静态优先级和调度策略计算出的优先级。普通进程和实时进程的normal_prio计算方式是不同的,后面会介绍。进程分支时,子进程会继承普通优先级。

    但调度器考虑的优先级则保存在prio。由于某些情况下内核需要暂时提高进程的优先级,因此需要这个成员。这些改变不是持久不变的,静态和普通优先级不受影响。

  • rt_priority表示实时进程的优先级。该值不会代替前面讨论的那些值。实时优先级的范围为0-99,值越大,优先级越高。

  • sched_class表示该进程所属的调度器类。

  • 调度器不限于调度进程,还可以处理更大的实体。这可以用于组调度:可用的CPU时间首先在一般的进程组之间分配,接下来再在组内进程间分配。

    这种一般性要求调度器不直接操作进程,而是处理可调度实体。一个实体用sched_entity的一个实例表示。se在task_struct内部嵌入了一个sched_entity实例,调度器据此可以操作各个task_struct。

  • policy保存了该进程对应的调度策略。linux支持可能的5个值:

    SCHED_NORMAL用于普通进程,它们通过完全公平调度策略来处理。

    SCHED_BATCH和SCHED_IDLE也通过完全公平调度来处理,不过可用于次要的进程。SCHED_BATCH用于非交互、CPU使用密集的批处理进程。调度策略对此类进程给予“冷处理”:它们绝不会抢占CFS调度器处理的另一个进程,因此绝不会干扰交互式进程。SCHED_IDLE的相对权重总是最小的,在没有进程可以调度时被内核选择来运行。

    SCHED_RR和SCHED_FIFO用于实现软实时进程(linux不能保证硬实时工作方式)。SCHED_RR实现了一种循环方法,而SCHED_FIFO则使用先入先出机制,它们由实时调度器类处理。

  • cpus_allowed是一个bitmap,在多处理器上使用,用来限制进程可以再哪些CPU上运行。对于负载均衡有意义。

  • run_list和time_slice标志是循环实时调度器所需要的,但不用于完全公平调度器。runlist是一个表头,用于维护包含各进程的一个运行表,而time_slice则制定进程可用CPU的剩余时间段。

(2)调度器类

调度器类提供了通用调度器和各个调度方法之间的关联。调度器类由特定数据结构中汇集的几个函数指针表示。这使得无需了解不同调度器类的内部工作原理,即可创建通用调度器。

对各个调度器类,都要提供struct sched_class的一个实例。调度器类之间的层次结构是平坦的,实时进程最重要,在完全公平调度之前处理;而完全公平进程则优先于空闲进程;空闲进程只有CPU无事可做时才处于活动状态。

struct sched_class{
	const struct sched_class *next;
	void(*enqueue_task)(struct rq *rq,struct task_struct *p,int wake_up);
	void(*dequeue_task)(struct rq *rq,struct task_struct *p,
	int sleep);
	void(*yield_task)(struct*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);
	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);
};

  • next用于将不同调度器类的sched_class实例按上述处理顺序连接起来。这个层次结构在编译已经建立,没有运行时动态增加新调度器类的机制。

  • enqueue_task向就绪队列添加一个新进程。在进程从睡眠状态变为可运行状态时发生该操作。

  • dequeue_task将一个进程从就绪队列去除。在进程从可运行状态切换到不可运行状态时就会发生该操作;内核也可能因为其它理由将进程从就绪队列去除,例如,进程的优先级可能需要改变。

  • 进程想要自愿放弃对处理器的控制权时,可使用sched_yield系统调用。这导致内核调用yeild_task。

  • 在必要的情况下,会调用check_premmpt_curr,用一个新唤醒的进程来抢占当前进程。比如用wake_up_new_task唤醒新进程时,会调用更该函数。

  • pick_next_task用于选择下一个将要运行的进程,而put_prev_task则在用另一个进程代替当前运行的进程之前调用。

  • set_curr_task 在进程的调度策略发生变化时,需要调用set_curr_task,还有其它一些场合也需要调用该函数。

  • task_tick在每次激活周期性调度器时,由周期性调度器调用。

  • new_task用于建立fork系统调用和调度器之间的关联。每次新进程建立以后,就用new_task通知调度器。

标准函数activate_task和deactivate_task调用前述的函数,提供进程在就绪队列的入队和离队功能。此外,它们还更新内核的统计数据。

   
   
   
static void activate_task(struct rq *rq,struct task_struct *p,int flags);
static void deactivate_task(struct rq *rq,struct task_struct *p,int flags);
内核定义了便捷方法check_preempt_curr,调用与给定进程相关的调度器类的check_preempt_curr方法。
  
  
  
static void check_preempt_curr(struct rq *rq,struct task_struct *p);

(3)调度实体

由于调度器可以操作比进程更一般的实体,因此需要一个适当的数据结构来描述此类实体。

调度实体的作用是用来为普通进程和实时进程进行时间记账。不同之处在于,普通进程由于用到了虚拟时间,所以vruntime字段是有意义的;而实时进程没有虚拟时间,该字段没有意义。

struct sched_entity{
        struct load_weight load;
        struct rb_node run_node;
        unsigned int on_rq;
         
        u64 exec_start;
        u64 sum_exec_runtime;
        u64 vruntime;
        u64 prev_sum_exec_runtime;
...
}

如果编译内核时启用了调度器统计,那么该结构会包含很多用于统计的成员。如果启用了组调度,还会增加一些成员。但目前我们只对上面列出的几项感兴趣。
  • load指定了权重,决定了各个实体占队列总负荷的比例。

  • run_node是标准的树节点,使得实体可以在红黑树上排序。

  • on_rq表示该实体当前是否在就绪队列上接受调度。

  • 在进程运行时,需要记录消耗的CPU时间,以用于完全公平调度。sum_exec_runtime即用于该目的。该时间的更新是通过计算当前时间和exec_start之间的差值, 累加到sum_exec_runtime上。而exec_start则会更新到当前时间。

    在进程执行期间虚拟时钟上流逝的时间数量由vruntime统计。

  • 在进程被撤销CPU时,其当前sum_exec_runtime值保存到prev_sum_exec_runtime。此后,在进程抢占时又需要该数据。需要注意的是:在prev_sum_exec_runtime中保存sum_exec_runtime的值并不意味着重置sum_exec_runtime。原值保存下来,而sum_exec_runtime则持续保持单调增长。

(4)就绪队列

核心调度器用于管理活动进程的主要数据结构称为就绪队列。各个CPU都有自身的就绪队列,每个活动进程只出现在一个就绪队列中,在多个CPU上同时运行一个进程是不可能的。注意:进程并不是由就绪队列的成员直接管理的,而是由各个调度器类来完成的,就绪队列只是定义了进程管理的数据结构。

下面列出就绪队列的主要成员

struct rq {
...
        unsigned long nr_running;
        #define CPU_LOAD_IDX_MAX 5
        unsigned long cpu_load[CPU_LOAD_IDX_MAX];
...
        struct load_weight load;
  
        struct cfs_rq cfs;
        struct rt_rq rt;      
         
        struct task_struct *curr, *idle;
        u64 clock;
...
};

  • nr_running指定了队列上可运行进程数,不考虑其优先级和调度类。

  • cpu_load用于跟踪此前的负荷状态。

  • load提供了就绪队列当前负荷的度量。队列的负荷本质上与队列上当前活动进程的数目成正比,其中的各个进程又有优先级作为权重。每个就绪队列的虚拟时钟的速度即基于该信息。

  • cfs和rt是嵌入的子就绪队列,分别用于完全公平调度器和实时调度器。

  • curr指向当前运行进程的task_struct实例。

  • idle指向idle进程的task_struct实例。

  • clock用于实现就绪队列自身的时钟。每次调用周期性调度器时,都会更新clock的值。另外,内核还提供了标准函数update_rq_clock,可在操作就绪队列的调度器中多处调用。

系统的所有就绪队列都在runqueues数组中,该数组的每个元素分别对应于系统中的一个CPU。

static DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);

内核还定义了一些便利的宏
   
   
   
#define cpu_rq(cpu)(&per_cpu(runqueues,(cpu)))
#define this_rq()(&__get_cpu_var(runqueues))
#define task_rq(p)            cpu_rq(task_cpu(p))
#define cpu_curr(cpu)(cpu_rq(cpu)->curr)
  • cpu_rq用于获取编号为cpu的就绪队列

  • this_rq用于获取当前cpu的就绪队列

  • task_rq用于获取当前进程所在cpu的就绪队列

  • cpu_curr用于获取编号为cpu的处理器上正在执行的进程

(5)CFS的就绪队列

  
  
  
struct cfs_rq{
	struct load_weight load;
	unsignedlong nr_running;
      		  u64 min_runtime;
	struct rb_root tasks_timeline;
	struct rb_node *rb_leftmost;
	struct sched_entity *curr;
}
  • nr_running表示该就绪队列上运行进程的数目。

  • load维护了所有这些进程的累积负荷值。负荷值的计算方法稍后介绍。

  • min_vruntime跟踪记录队列上所有进程的最小虚拟运行时间,这个值是实现与就绪队列相关的虚拟时钟的基础。

  • task_timeline是一个基本成员,用于在按时间排序的红黑树中管理所有进程。

  • rb_leftmost总是设置为指向树最左边的节点,即需要被调度的进程。加入该字段的根本原因在于进程加入红黑树和调度程序选择下一进程之间是异步的关系。使用该变量可以缓存虚拟运行时间最小的进程,省去了遍历红黑树来寻找的过程,减少了搜索树所花的平均时间。

  • curr指向当前执行进程的可调度实体。

(6)实时进程的就绪队列

实时进程的就绪队列非常简单,使用链表就够了。对与组调度和SMP系统,还有更多的成员,我们这里不予考虑。

struct rt_prio_array {
         DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
         struct list_head queue[MAX_RT_PRIO];
  };

具有相同优先级的所有实时进程都保存在一个链表中,表头为active.queue[prio],而active.bitmap位图中的每个比特对应于一个链表,如果链表不为空,则相应的比特置位。其结构如下:


6.2 上述数据结构之间的关系 




6.3 处理优先级

6.3.1 优先级的内核表示

linux进程描述符中与进程优先级有关的域有4个:

  • static_prio  静态优先级的计算基于nice值,不论是普通进程还是实时进程,它们的静态优先级的表示都是相同的,结果为NICE_TO_PRIO(nice),即120 + nice。可以通过系统调用nice()对普通进程和实时进程指定nice值,该系统调用会如前所属计算进程的静态优先级。对于实时进程来说,指定了nice值并不会将其降级为普通进程,因为实时进程对静态优先级并不感冒,它更感兴趣的是实时优先级。详情参考set_user_nice()函数。

    静态优先级是基于nice值的,值越小说明进程的优先级越高。

  • rt_priority  实时进程的优先级,范围从0-99。值越小优先级越低。

  • normal_prio 由于静态优先级和实时优先级的表示正好相反,引入一个normal_prio对它们进行统一。实时进程的普通优先级计算方式为MAX_RT_PRIO - 1- rt_priority,范围从99 - 0。普通进程的普通优先级等于静态优先级。统一以后进程优先级的表示如下。



    :只要没有通过系统调用nice()或者sched_setscheduler()系统调用修改,上述的三个优先级在进程运行过程中都不会改变。

  • prio 是进程的动态优先级,该优先级在进程创建的时候继承自父进程的normal_prio(见copy_process()->sched_fork())。在进程运行的过程中可能会临时提高进程的优先级,如磁盘I/O时,所以引入了动态优先级。

下列宏用于在各种不同形式之间转换(MAX_RT_PRIO 等于实时进程的最大优先级加1,而MAX_PRIO则等于普通进程的最大优先级加1,他们的值分别为100和140)。

<sched.h>
#define MAX_USER_RT_PRIO        100
#define MAX_RT_PRIO             MAX_USER_RT_PRIO
#define MAX_PRIO                (MAX_RT_PRIO +40)
#define DEFAULT_PRIO            (MAX_RT_PRIO +20)//普通进程默认静态优先级为120
<kernel/sched.c>
#define NICE_TO_PRIO(nice)((nice)+ DEFAULT_PRIO)
#define PRIO_TO_NICE(prio)((prio)- DEFAULT_PRIO)
#define TASK_NICE(P)            PRIO_TO_NICE((P)->static_prio)
6.3.2 计算优先级

static_prio是计算的起点。假设它已经设置好,内核现在想要计算其它优先级,只需要一行代码即可:

p->prio = effective_prio(p);
<kernel/sched.c>
staticint effective_prio(struct task_struct *p)
{
        p->normal_prio = normal_prio(p);
	if(!rt_prio(p->prio))
		return p->normal_prio;
	return p->prio;
}

该函数首先计算了normal_prio,然后根据进程当前的动态优先级是否处于实时优先级的范围返回不同的结果。
   
   
   
<kernel/sched.c>
staticinlineint 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;
}
对于实时进程,它的普通优先级是将实时优先级按6.3.1中提到的方法进行反转,折算成低数值高优先级的情况。注意这里判断是否是实时优先级是看进程的调度策略,而不是动态优先级的数值。为什么呢?因为普通优先级是一个确定不变的数值,而动态优先级是一个变化的量。

对于普通进程而言,返回值__normal_prio(p),它就是静态优先级。

  
  
  
<kernel/sched.c>
staticinlineint __normal_prio(struct task_struct *p)
{
	return p->static_prio;
}
综上所述:

 进程类型/优先级                  static_prio                normal_prio                                          prio

 非实时进程                          static_prio                static_prio                                             static_prio

 优先级提高的非实时进程    static_prio                static_prio                                             effective_prio/不变

 实时进程                              static_prio                MAX_RT_PRIO - 1 - p->rt_priority     normal_prio/不变


6.3.3 计算负荷权重

进程重要性和两个因素有关:进程类型和进程优先级。实时进程重要性高于非实时进程,而实时进程又根据优先级而重要性不同。

进程负荷权重保存在数据结构l

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值