您的位置:首页 > 运维架构 > Linux

linux进程调度

2014-11-03 09:12 591 查看

一. 何为进程调度

在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调用前述的函数,提供进程在就绪队列的入队和离队功能。此外,它们还更新内核的统计数据。

[code]
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方法。

[code][code]
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的就绪队列

[code]
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,然后根据进程当前的动态优先级是否处于实时优先级的范围返回不同的结果。

[code]
<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),它就是静态优先级。

[code]
<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 计算负荷权重

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

进程负荷权重保存在数据结构load_weight中:

[code]
struct load_weight{
    unsignedlong weight, inv_weight;
};


其中不仅保存了负荷权重自身,还保存另一个数值inv_weight,用于计算被负荷权重除的结果,它们两者相乘大约等于一个定值。

这个权值是针对普通进程而言的,nice值从-20-19对应有40个元素,这40个优先级对应的权重实现已经计算好了,放在数组prio_to_weight中:

[code]
staticconstint 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,
};


进程权重在函数set_load_weight中设置:

[code]
#define WEIGHT_IDLEPRIO             2
#define WMULT_IDLEPRIO              (1<<32)
<kernel/sched.c>
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];
}


对于实时进程,它的权重是优先级最高的普通进程,即nice值为-20进程的权重的两倍;idle进程的权重最小。

对于实时进程和idle进程,它们的调度不是基于权重的:实时进程基于实时优先级和时间片,而时间片不论优先级大小被赋予了一个定值;idle进程则完全没有时间片的概念,只要没有其它进程,它一直占据CPU运行。这里给它们也赋予权重主要是为了对每个CPU就绪队列struct rq中的负荷进行跟踪,对于负载均衡有重要意义。

每次进程加入到就绪队列rq时,内核会调用相应的函数,将进程权重添加到就绪队列的权重中。如果进程是普通进程,还会增加cfs_rq中的队列权重值。

6.4 调度器的实现

由于我们的讨论中不涉及组调度,所以后面的内容中的进程和调度实体可以看做是一个概念。

6.4.1 与进程调度相关的队列

在进程被创建到消亡之前,它的状态主要有运行状态、就绪状态和睡眠状态。处于运行状态的进程此时被调度程序选择在CPU上运行;就绪状态的进程处于可运行的状态,但是由于多用户、多任务操作系统上同时运行的进程数据不会超过CPU个数,因此需要排队等待其它进程主动放弃CPU的使用权或者调度程序选中它作为下一个投入运行的进程;处于睡眠状态的进程正在等待某些事件的发生,在事件发生以前它们绝不会获得CPU的使用权。

既然进程有不同的状态,那么就需要相应的数据结构来对它们进行合理的组织,以便实现对系统中大量进程进行管理。linux中使用了就绪队列和等待队列。

(1)就绪队列

通过6.1节对进程调度相关数据结构的讲解,对于就绪队列大家应该都有了比较深入的了解。就绪队列每CPU一个,但是它并不直接管理进程,进程的管理是委托给调度器类来管理的。普通进程由完全公平调度器类管理,所有这些非实时进程根据键值组织在一棵红黑树中;实时进程按实时优先级存放在100个双向链表中。

需要注意:

属于运行状态的普通进程,也就是当前正在使用CPU执行其程序的普通进程,不在对应CPU的红黑树中,但是用了一个标志来表示它目前确实是运行状态。换句话说,就绪队列只是保存正在等待使用CPU资源的进程。

上述的就绪状态和运行状态是两个不同的状态,但是linux内核中使用了同一个状态TASK_RUNNING来表示它们,不能单从字面意思来理解它们。

(2)等待队列

在进程执行的过程中,可能需要等待某些事件的发生。例如进程需要从一个磁盘文件中读取数据,而磁盘I/O又是一个相对来说非常耗时的过程。如果在I/O的过程中,进程依旧占用CPU来等待I/O完成,那么对于CPU资源的利用率将会降低。所以目前通用的做法是将等待I/O的进程挂到一个相应的等待队列上,进程进入睡眠状态。当I/O完成后设备会向CPU发起中断,进而唤醒等待进程,重新加入就绪队列中等待调度。具体的过程这里就不详述了,在中断和设备驱动章节有详细介绍。

6.4.2 核心调度器概述

linux进程调度的核心和两个调度函数有关:scheduler_tick()和schedule()。两者分别称为周期性调度器(周期性调度函数)和主调度器(主调度函数)。两者结合在一起称作通用调度器(或核心调度器)。

主调度器——真正完成进程切换的函数

调度器最核心的功能是将CPU的使用权从一个进程切换到另一个进程。这个工作是由被称作主调度器的组件完成的。



注:三个不同的长条分别表示CPU分配给进程A、B、C的时间。这里只是示意,进程切换不一定都是在CPU时间使用完才进行的。

周期性调度器——只更新进程运行相关的统计信息,本身并不完成进程切换

对于周期性调度器,我们需要关心的是单个进程的执行过程。假设进程A正在CPU上运行,那么在A运行的这段时间内,系统会定时调用周期性调度器,即scheduler_tick(),它只是更新进程运行过程中的统计信息,例如进程在CPU上运行的总时间。

周期性调度器的调用频率为时钟中断频率,即HZ。每次硬件时钟中断都会调用该函数。





6.4.3 linux中的调度器类

linux中实现了三个调度器类

完全公平调度器类

实时调度器类

ilde调度器类

它们都是对结构体sched_class的实例化,定义分别如下:

[code]
<kernel/sched_fair.c>
/*
 * All the scheduling class methods:
 */
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,
};
<kernel/sched_rt.c>
const struct sched_class rt_sched_class ={
	.next               =&fair_sched_class,
	.enqueue_task       = enqueue_task_rt,
	.dequeue_task       = dequeue_task_rt,
	.yield_task         = yield_task_rt,
	.check_preempt_curr = check_preempt_curr_rt,
	.pick_next_task     = pick_next_task_rt,
	.put_prev_task      = put_prev_task_rt,
	#ifdef CONFIG_SMP
	.load_balance       = load_balance_rt,
	.move_one_task      = move_one_task_rt,
	#endif
	.set_curr_task      = set_curr_task_rt,
	.task_tick          = task_tick_rt,
};
<kernel/sched_idletask.c>
/*
 * Simple, special scheduling class for the per-CPU idle tasks:
 */
const struct sched_class idle_sched_class ={
/* .next is NULL */
/* no enqueue/yield_task for idle tasks */
/* dequeue is not valid, we print a debug message there: */
	.dequeue_task       = dequeue_task_idle,
	.check_preempt_curr = check_preempt_curr_idle,
	.pick_next_task     = pick_next_task_idle,
	.put_prev_task      = put_prev_task_idle,
	#ifdef CONFIG_SMP
	.load_balance       = load_balance_idle,
	.move_one_task      = move_one_task_idle,
	#endif
	.set_curr_task      = set_curr_task_idle,
	.task_tick          = task_tick_idle,
/* no .task_new for idle tasks */
};


现在我们不急于关注这些类中的各个方法是怎么实现的,需要注意的是它们排列的先后顺序:

[code]
rt_sched_class->fair_sched_class->idle_sched_class


这个顺序对于进程调度具有重要意义:先调度实时进程;没有实时进程就调度普通进程;如果没有可运行进程就选择idle进程来运行。

6.4.4 完全公平调度类

这里先对完全公平调度中相对比较独立的部分进行单独讲解,对于那些与主调度器和周期性调度器关联性比较强的部分,在后面分析主调度器和周期性调度器代码时再作分析。

(1)一个完全理想的多任务处理器

假设我们有一个理想的CPU,可以同时执行任意数量的进程,进程之间共享CPU的处理能力。若某个时刻有N个进程,那么每个进程获得总运算能力的1/N。

举例来说:每个进程单独运行需要10分钟才能完成其工作,现在有5个进程在理想CPU上运行,每个会得到计算能力的20%,这意味着每个进程需要运行50分钟,而不是10分钟。但所有的5进程都会刚好在该时间段之后结束工作,没有哪个进程在此段时间内处于不活动状态。

(2)相关概念与操作

上面的模型在真正的硬件上是无法实现的,一个CPU上只能同时运行一个进程,并通过在进程之间高频率切换来实现多任务。这样的话,相比于正在CPU上运行的进程,那些处于就绪队列的进程显然受到了不公平对待。而完全公平调度算法就是为了解决这个不公平问题,尽量使得每个进程能够根据其优先级获得应有的时间份额,在每个调度周期结束后,没有进程受到了亏待。

调度周期与进程理想运行时间

由于系统中多数进程不是运行一小段时间就结束了,所以不能说只对所有进程调度一次,直到所有进程结束为止。这样就引入了调度周期的概念。完全公平调度根据当前系统中普通进程的数目对调度周期进行动态调整,在一个调度周期内,就绪队列中所有进程都能被调度执行一次。调度周期由函数__sched_period()计算:

[code]
<kernel/sched_fair.c>
/*
 * The idea is to set a period in which each task runs once.
 *
 * When there are too many tasks (sysctl_sched_nr_latency) we have to stretch
 * this period because otherwise the slices get too small.
 *
 * p = (nr <= nl) ? l : l*nr/nl
 */
static u64 __sched_period(unsignedlong nr_running)
{
    u64 period = sysctl_sched_latency;
    unsignedlong nr_latency = sched_nr_latency;
    if(unlikely(nr_running > nr_latency)){
        period *= nr_running;
        do_div(period, nr_latency);
    }
    return period;
}


sched_nr_latency等于5,sysctl_sched_latency等于20ms。当系统中普通进程数目小于等于5个时,调度周期等于20ms。这样做的目的在于,此时的进程数量很少, 没有必要将调度周期设置得很小,这样可以减少过于频繁的进程切换带来的开销。
当系统中普通进程数目大于5个时,则需要重新计算调度周期的值了,上面if语句完成的就是这个工作。本质上得到的效果就是:

[code]
period = (sysctl_sched_latency / sched_nr_running) * nr_running


即根据进程数据对调度周期进行扩大,这样一来平均每个进程能够得到的运行时间也有4ms。
对于系统中的每个进程,在一个调度周期内CFS调度算法都承诺为它分配一定的CPU时间。对于分配给一个进程的时间,该进程不一定使用完。例如进程执行过程中睡眠或者主动放弃CPU。即使没有使用完,另一个进程也不能使用该时间,否则另一个进程就受到了优待,调度就不公平了。理想的情况是所有进程都用完了一个调度周期内CFS调度算法承若能够分配给它们的CPU时间,但由于进程本身行为的复杂性,该条件不可能时时刻刻都能满足。进程理想运行时间由函数sched_slice()计算:

[code]
<kernel/sched_fair.c>
/*
 * We calculate the wall-time slice from the period by taking a part
 * proportional to the weight.
 *
 * s = p*w/rw
 */
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);/* 根据权重计算调度实体se所占的运行时间 */
    return slice;
}


该函数的完成的工作就是根据进程自身的权重占普通进程所有权重的比例,得到一个调度周期内它能够获得的CPU使用时间的最大值,写成公式就是:

[code]
slice = (se->load.weight / cfs_rq->load.weight) * __sched_period


再强调一下: 前面对调度周期和进程理想运行时间的描述中我们说到,CFS算法根据系统中进程的数目计算一个调度周期,进程根据自己的权重瓜分这个调度周期。理想情况下,进程都会使用完自己的时间份额。但是实际情况并非如此,一个进程被调度执行,可能时间没有使用完就放弃CPU的使用权了。CFS调度算法只是承诺一定能够满足进程的运行时间需求,但是它不会将一个进程前一个调度周期没有用完的时间给它保留到下一个调度周期。到下一个调度周期的时候,又有了新的调度周期和每个进程的运行时间。那么对于那些没有将自己时间份额用完的进程显然是不公平的,那这中不公平性是怎么度量的呢?这就要说进程的虚拟运行时间了。

虚拟运行时间

这个概念对于CFS调度算法极为重要,是该算法得以实现公平性的关键。它的计算公式为:

[code]
vruntime =实际运行时间* NICE_O_LOAD / se->load.weight


可以看出 nice值为0的进程其虚拟运行时间和实际运行时间相等。
如果每个进程都用完了调度算法承诺的能够分配给它的运行时间,那么,当进程运行结束时,实际运行时间等于它的理想运行时间,那么:

[code]
vruntime = se->load.weight / cfs_rq->load.weight * __sched_period
           * NICE_0_LOAD /  se->load.weight
         = __sched_period * NICE_O_LOAD / cfs_rq->load.weight


即进程的虚拟运行时间在本调度周期结束以后是相同的,与单个进程的权重无关。
可以看出,引入虚拟运行时间的作用就在于对进程的运行时间进行了“归一化”。从进程的实际运行时间来看,进程由于权重的不同该值肯定不同,单从时间值来看,这是不公平的;但是从虚拟运行时间来看,这确确实实是公平的!
上面说的是理想情况,对进程调度用轮训的方式就能搞定了,根本用不着按vruntime将进程插入红黑树进行排序。
系统中进程运行的实际情况要复杂得多,进程并非都能用完它在每一个调度周期内调度算法承诺给它的运行时间,因此积累了一定的不公平性。这种不公平性就是通过vruntime的相对大小表现出来的,vruntime越小的进程受到的不公平对待越严重,因此在红黑树中排在越左的位置,下一个调度周期就会越早被调度程序选择来投入运行。

最小虚拟运行时间

每个CFS就绪队列中都有一个最小运行时间min_vruntime,用于跟踪记录该队列上所有进程的最小虚拟运行时间。这个值是单调递增的,但不一定比最左边树节点的vruntime小,如进程唤醒时。
为什么要引入这个参数呢? a、如果进程已经积累了较大的不公平性,用完了本调度周期自己的CPU时间后,它的虚拟运行时间还是最小的,那么它还会被CFS调度算法选择来运行,直到它的虚拟运行时间不是最小的那个为止。这样一来其它进程在这个调度周期就不会得到执行的机会,因为时间都用来补偿该虚拟运行时间太短的进程了。举一个实际的例子,如果进程在运行过程发起I/O请求,那么在它的I/O请求满足以后很可能已经过了很多个调度周期,由于它的vruntime大大小于其它进程,那么接下来的一段时间,它将一直运行,造成其它进程饥饿。对于这个一直执行的进程而言,这似乎是公平的;对于其它进程来说,又是不公平的。我们可以这样理解,进程虽然没有使用CPU资源,但是它用了I/O设备,因此不能过于偏袒它。我们可以调整这个进程,使它的vruntime最小,能够被首先调度执行,比如说将它的虚拟运行时间设置为min_vruntime,至于linux是怎么做的,后面再说。
b、对于新创建的进程而言,它的vruntime等于0。那如果不对它的vruntime进行修正,那么它将会一直运行,直到它的vruntime赶上系统中原来vruntime最小的那个进程为止。对于一个已经运行了1年的服务器,如果不进行进程虚拟运行时间的修正,岂不是接下来的很长一段时间都要用来运行这个新进程,其它进程只有长时间等待的份了?换一个角度想,这个新建的进程过去根本就没有为系统或者用户做出过任何贡献,那么调度算法为什么要为它把过去的时间都弥补上来呢?就像你毕业去了一家公司上班,难不成还要人家把从公司成立到像你入职这段时间的工资给你补上?针对这种情况,也需要结合min_runtime进行时间的修正。

时间记账

对于每个进程,内核需要记录它已经运行的时间,CFS虽然没有使用时间片的概念,但时间概念还是不能少的。时间记账主要是更新进程的实际运行时间和虚拟运行时间,主要是由函数__update_rq_clock()/update_rq_clock()和update_curr()完成的,前者对就绪队列rq进行时间记账,后者对cfs_rq中的当前运行进程进行时间记账。

[code]
<kernel/sched.c>
/*
 * Update the per-runqueue clock, as finegrained as the platform can give
 * us, but without assuming monotonicity, etc.:
 */
staticvoid __update_rq_clock(struct rq *rq)
{
    u64 prev_raw = rq->prev_clock_raw;
    u64 now = sched_clock();
    s64 delta = now - prev_raw;
    u64 clock = rq->clock;
#ifdef CONFIG_SCHED_DEBUG
    WARN_ON_ONCE(cpu_of(rq)!= smp_processor_id());
#endif
    /*
     * Protect against sched_clock() occasionally going backwards:
     */
    if(unlikely(delta <0)){
        clock++;
        rq->clock_warps++;
    }else{
	/*
         * Catch too large forward jumps too:
         */
        if(unlikely(clock + delta > rq->tick_timestamp + TICK_NSEC)){
	if(clock < rq->tick_timestamp + TICK_NSEC)
                clock = rq->tick_timestamp + TICK_NSEC;
	else
                clock++;
        rq->clock_overflows++;
	}else{
	    if(unlikely(delta > rq->clock_max_delta))
                rq->clock_max_delta = delta;
            clock += delta;
        }
    }
    rq->prev_clock_raw = now;
    rq->clock = clock;
}


不考虑其中的unlikely语句,可以看出该函数的主要功能是更新rq队列的时钟。

[code]
<kernel/sched_fair.c>
static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_of(cfs_rq)->clock;
    unsignedlong delta_exec;
    if(unlikely(!curr))
    return;
    /*
     * Get the amount of time the current task was running
     * since the last time we changed load (this cannot
     * overflow on 32 bits):
     */
    delta_exec =(unsignedlong)(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);
    }
}
<kernel/sched_fair.c>
/*
 * Update the current task's runtime statistics. Skip current tasks that
 * are not in our scheduling class.
 */
static inline void
__update_curr(struct cfs_rq *cfs_rq,struct sched_entity *curr,
		unsignedlong delta_exec)
{
    unsignedlong 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;
    /*
     * maintain cfs_rq->min_vruntime to be a monotonic increasing
     * value tracking the leftmost vruntime in the tree.
     */
    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);
}


update_curr()函数更新当前正在运行进程实际已经运行的总时间和对应的虚拟运行时间。它首先计算rq队列的时钟与当前调度实体(姑且看做进程)的开始执行时间exec_start之间的时间差值,得到进程的实际运行时间的增量。然后调用该函数__update_curr(): a、将该时间增量加到进程的总运行时间sum_exec_runtime上。
b、根据进程权重计算虚拟运行时间的增量delta_exec_weighted。如果进程nice值不为零,则需要调用函数calc_delta_fair()。该函数本质上的工作就是计算delta_exec_weighted * NICE_0_LOAD /curr->load.weight,即根据进程权重对时间增量进行放缩,得到虚拟运行时间的增量。
c、把delta_exec_weighted加到curr->cruntime上更新进程的虚拟运行时间。
最后更新min_runtime,保证该值是递增的。

从红黑树添加和删除进程

该操作分别由函数__enqueue_entity()和__dequeue_entity()完成,将进程插入红黑树前还要调用函数entity_key()计算键值。

[code]
<kernel/sched_fair.c>
staticinline s64 entity_key(struct cfs_rq *cfs_rq,struct sched_entity *se)
{
	return se->vruntime - cfs_rq->min_vruntime;
}
<kernel/sched_fair.c>
/*
 * Enqueue an entity into the rb-tree:
 */
staticvoid __enqueue_entity(struct cfs_rq *cfs_rq,struct sched_entity *se)
{
	struct rb_node **link =&cfs_rq->tasks_timeline.rb_node;
	struct rb_node *parent = NULL;
	struct sched_entity *entry;
	s64 key = entity_key(cfs_rq, se);
	int leftmost =1;
	/*
	 * Find the right place in the rbtree:
	 */
	while(*link){
		parent =*link;
		entry = rb_entry(parent,struct sched_entity, run_node);
		/*
		 * We dont care about collisions. Nodes with
		 * the same key stay together.
		 */
		if(key < entity_key(cfs_rq, entry)){
			link =&parent->rb_left;
			leftmost = 1;
		}else{
			link =&parent->rb_right;
			leftmost =0;
		}
	}
	/*
	 * Maintain a cache of leftmost tree entries (it is frequently
	 * used):
	 */
	if(leftmost)
		cfs_rq->rb_leftmost =&se->run_node;

	rb_link_node(&se->run_node, parent, link);
	rb_insert_color(&se->run_node,&cfs_rq->tasks_timeline);
}


__enqueue_entity()函数的执行流程很简单:从cfs_rq的根节点开始,比较该节点代表的进程的键值和要插入进程的键值之间的大小,进而选择该节点的左子树或者右子树继续搜索。如果最后一次比较时该进程的键值仍然更小,说明它就是vruntime最小的进程,leftmost被置位,相应cfs_rq的rb_leftmost的指向也被更新。

[code]
<kernel/sched_fair.c>
staticvoid __dequeue_entity(struct cfs_rq *cfs_rq,struct sched_entity *se)
{
	if(cfs_rq->rb_leftmost ==&se->run_node)
		cfs_rq->rb_leftmost = rb_next(&se->run_node);
	rb_erase(&se->run_node,&cfs_rq->tasks_timeline);
}


__dequeue_entity()也只是简单地将调度实体从红黑树中删除,如果该节点还是最左边的叶子节点,则将leftmost的指向修改为它右边的那个叶子节点。

6.4.5 实时调度类

实时调度类的调度策略比较简单,使用SCHED_FIFO调度策略的实时进程没有时间片的概念,实时调度类没有调度周期的概念。只要有实时进程,就从优先级最高的开始调度,低优先级的实时进程和普通进程时钟得不到调度的机会,它不像CFS的处理,后者的调度策略中所有优先级的普通进程在一个调度周期内都能得到执行。这里对它只做简单讨论。

可以看出实时调度是一种比较粗野的调度方式,只要系统中有实时进程,那么普通进程的响应肯定会受到影响。所以系统中实时进程的数目一般都很少。使用下面的命令可以查看系统中的所有实时进程,并显示它们的实时优先级:

[code]
ps -eo pid,tid,class,rtprio,ni,pri,pcpu,stat,comm | awk '$4!~/-/{print $0}'


下面是我在ubuntu12.04 LTS 32-bit上的测试结果:



系统中有两个高优先级的实时进程,它们都是内核线程。migration线程完成线程在CPU之间的迁移,以实现负载均衡;watchdog线程则是在系统出现故障的时候重启系统。

时间记账

实时进程的时间记账也是通过调度实体中的sum_exec_runtime来进行的,它没有使用prev_sum_exec_runtime。CFS中使用了这两个成员是为了统计一个调度周期内每个普通进程实际运行的CPU时间(见周期性调度器),实时调度没有调度周期的概念,所以没有使用prev_sum_exec_runtime。
实时进程的时间记账功能由函数update_curr_rt()完成:

[code]
<kernel/sched_rt.c>
/*
 * Update the current task's runtime statistics. Skip current tasks that
 * are not in our scheduling class.
 */
staticvoid update_curr_rt(struct rq *rq)
{
struct task_struct *curr = rq->curr;
	u64 delta_exec;
if(!task_has_rt_policy(curr))
return;
	delta_exec = rq->clock - curr->se.exec_start;
if(unlikely((s64)delta_exec <0))
		delta_exec =0;
	schedstat_set(curr->se.exec_max, max(curr->se.exec_max, delta_exec));
	curr->se.sum_exec_runtime += delta_exec;
	curr->se.exec_start = rq->clock;
	cpuacct_charge(curr, delta_exec);
}


第一个if语句针对的是那些动态优先级临时提高到实时优先级的进程。
然后根据所在rq队列的时钟计算从这次开始执行到现在的时间差值,加到sum_exec_runtime上,得到该实时进程的总运行时间。
最后更新exec_start。

6.4.6 周期性调度器——scheduler_tick()

前面已经提到,周期性调度器并不负责进程的切换,它只是负责更新进程的相关统计信息。周期性调度器是由时钟中断触发的,周期性调度器执行过程中要关闭中断,执行完毕后再打开中断。

下图是中断执行的过程,我们这里不具体讲解,只关注与时钟中断相关的部分。



知道了周期性调度器是在哪儿被调用的,下面我们来分析一下scheduler_tick()的代码。

[code]
<kernel/sched.c>
/*
 * This function gets called by the timer code, with HZ frequency.
 * We call it with interrupts disabled.
 *
 * It also gets called by the fork code, when changing the parent's
 * timeslices.
 */
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);
	/*
	 * Let rq->clock advance by at least TICK_NSEC:
	 */
	if(unlikely(rq->clock < next_tick))
		rq->clock = next_tick;
	rq->tick_timestamp = rq->clock;
	update_cpu_load(rq);
	if(curr != rq->idle)/* FIXME: needed? */
		curr->sched_class->task_tick(rq, curr);
	spin_unlock(&rq->lock);
#ifdef CONFIG_SMP
	rq->idle_at_tick = idle_cpu(cpu);
	trigger_load_balance(rq, cpu);
#endif
}


周期性调度器首先调用函数__update_rq_clock()更新就绪队列rq的时钟。
然后更新就绪队列rq的cpu_load[]数组,它保留了5个历史负载值。
第二个if语句判断当前进程如果不是idle进程的话,则调用该进程的调度类中相应的方法task_tick更新进程的统计信息,包括进程总的运行时间和虚拟运行时间,如果进程用完了这个调度周期内分配给它的CPU时间的话,还要触发主调度器。
最后,在SMP系统上,调用trigger_load_balance()进行负载均衡。有关负载均衡的话题后面讲解。
不用的调度类,提供了不同的task_tick方法。对于普通进程,task_tick指向task_tick_fair();对于实时进程,task_tick指向task_tick_rt();对于ilde进程的调度器类,task_tick指向task_tick_idle()。该函数为空,而且不会被调用到。

task_tick_fair

[code]
<kernel/sched_fair.c>
/*
 * scheduler tick hitting a task of our scheduling class:
 */
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);
	}
}
<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);
}
<kernel/sched_fair.c>
/*
 * Preempt the current task with a newly woken task if needed:
 */
staticvoid
check_preempt_tick(struct cfs_rq *cfs_rq,struct sched_entity *curr)
{
	unsignedlong 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);
}


a、linux实现了组调度,进程是最小的可调度实体,多个进程可以组成大的调度实体,而这些大的调度实体又可以组成更大的调度实体。for_each_sched_entity宏便是用来遍历直到最高层祖先的。我们这里不考虑组调度的情形,所以可以看成直接调用了entity_tick()。
b、entity_tick()的工作是调用update_curr()更新当前运行进程的总运行时间和相应的虚拟运行时间,更新exec_start。如果该cfs_rq中的进程多余一个,还要调用check_preempt_tick()。
c、check_preempt_tick()根据此前已经更新的CPU总运行时间,减去进程本调度周期内被pick_next_task_fair()选中执行时保存到prev_sum_exec_runtime中的运行时间,得到进程在当前调度周期内运行的总时间。如果该值大于通过sched_slice()计算得到的理想运行时间,则说明进程本调度周期内的时间用完了,从而调用resched_task()置位TIF_NEED_RESCHED标志,从而触发主调度器。

task_tick_rt

[code]
<kernel/sched_rt.c>
static void task_tick_rt(struct rq *rq, struct task_struct *p)
{
	update_curr_rt(rq);
	/*
	 * RR tasks need a special form of timeslice management.
	 * FIFO tasks have no timeslices.
	 */
	if (p->policy != SCHED_RR)
		return;
	if (--p->time_slice)
		return;
	p->time_slice = DEF_TIMESLICE;
	/*
	 * Requeue to the end of queue if we are not the only element
	 * on the queue:
	 */
	if (p->run_list.prev != p->run_list.next) { /* 不是该链表中的唯一进程 */
		requeue_task_rt(rq, p);
		set_tsk_need_resched(p);
	}
}


实时进程的该方法相对简单:
a、首先更新进程的实际运行时间。
b、实时进程有两种调度策略:SCHED_RR和SCHED_FIFO。前者是内核给进程分配一个时间片,使用完了就调度将CPU使用权给到下一个实时进程,后者则是一直一直占用CPU,直到主动放弃CPU为止。函数中接下来对这两种调度策略有不同的处理方式:如果调度策略不是SCHED_RR,也就是SCHED_FIFO,那么直接返回。否则的话递减它的时间片,如果时间片没有用完也直接返回。时间用完了则需要重新赋值为DEF_TIMESLICE,然后判断该优先级的实时进程是不是只有这一个,如果不是的话则将该实时进程放到该优先级的链表的最后面,然后设置TIF_NEED_RESCHED标志,触发主调度器。
注意: 正在运行的实时进程并没有从链表中删除!
为了加深理解,下面分别给出针对普通进程和实时进程时的执行流程:





6.4.7 主调度器——schedule()

主调度器的工作在于完成进程的切换,将CPU的使用权从一个进程切换到另一个进程。schedule()函数的逻辑还是比较清晰的,下面我们来具体分析一下。

<kernel/sched.c>
/*
 * schedule() is the main scheduler function.
 */
asmlinkage void __sched schedule(void)
{
	struct task_struct *prev, *next;
	long *switch_count;
	struct rq *rq;
	int cpu;
need_resched:
	preempt_disable();
	cpu = smp_processor_id();
	rq = cpu_rq(cpu);
	rcu_qsctr_inc(cpu);
	prev = rq->curr;              
	switch_count = &prev->nivcsw;  /* 用于记录当前进程的切换次数 */
	release_kernel_lock(prev);
need_resched_nonpreemptible:
	schedule_debug(prev);
	/*
	 * Do the rq-clock update outside the rq lock:
	 */
	local_irq_disable();
	__update_rq_clock(rq); 
	spin_lock(&rq->lock);
	clear_tsk_need_resched(prev); /* 清除TIF_NEED_RESCHED标志 */
        /* state: (-1, 不可运行   0,可运行  > 0 stopped) 
         * PREEMPT_ACTIVE标志表明调度是由抢占引起的 */
	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);  
                      		}
		switch_count = &prev->nvcsw;
	}
	if (unlikely(!rq->nr_running))
		idle_balance(cpu, rq);  
	prev->sched_class->put_prev_task(rq, prev);
	next = pick_next_task(rq, prev);
	sched_info_switch(prev, next);
	if (likely(prev != next)) {
		rq->nr_switches++;
		rq->curr = next;
		++*switch_count;  
		context_switch(rq, prev, next); /* unlocks the rq */
	} else
		spin_unlock_irq(&rq->lock);
	if (unlikely(reacquire_kernel_lock(current) < 0)) {
		cpu = smp_processor_id();
		rq = cpu_rq(cpu);
		goto need_resched_nonpreemptible;
	}
	preempt_enable_no_resched();
	if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))   
		goto need_resched;
}


主调度器完成了以下工作:

首先调用__update_rq_clock()更新rq队列的时钟。
清除进程的TIF_NEED_RESCHED标志。
如果进程处于不可运行的状态,那么需要判断进程切换是不是抢占引起的。如果不是,且进程处于TASK_INTERRUPTABLE状态而且收到了信号,则将它的状态修改为TASK_RUNNING,不让进程睡眠。否则的话则需要调用deactivate_task()将进程从就绪队列中删除并清除on_rq标志,然后递减rq队列中的统计数据。
deactivate_task()的代码如下:
<kernel/sched.c>
/*
 * deactivate_task - remove a task from the runqueue.
 */
static void deactivate_task(struct rq *rq, struct task_struct *p, int sleep)
{
	if (p->state == TASK_UNINTERRUPTIBLE)
		rq->nr_uninterruptible++;
	dequeue_task(rq, p, sleep);
	dec_nr_running(p, rq);
}
该函数首先调用dequeue_task()从队列中删除进程,然后调用dec_nr_running()递减rq队列中的进程数目和进程权重之和。

<kernel/sched.c>
static void dequeue_task(struct rq *rq, struct task_struct *p, int sleep)
{
	p->sched_class->dequeue_task(rq, p, sleep);
	p->se.on_rq = 0;
}
<kernel/sched.c>
static void dec_nr_running(struct task_struct *p, struct rq *rq)
{
	rq->nr_running--;
	dec_load(rq, p);
}


dequeue_task()调用进程所属调度类中的方法dequeue_task完成实际的进程出队的操作。对于CFS调度而言,该函数为dequeue_task_fair(),对于实时调度而言,该函数为dequeue_task_rt()。

dequeue_task_fair()

[code]
<kernel/sched_fair.c>
/*
 * The dequeue_task method is called before nr_running is
 * decreased. We remove the task from the rbtree and
 * update the fair scheduling stats:
 */
staticvoid dequeue_task_fair(struct rq *rq,struct task_struct *p,int sleep)
{
<span style="white-space:pre">	</span>struct cfs_rq *cfs_rq;
<span style="white-space:pre">	</span>struct sched_entity *se =&p->se;
	for_each_sched_entity(se){
		cfs_rq = cfs_rq_of(se);
		dequeue_entity(cfs_rq, se, sleep);
<span style="white-space:pre">	</span>    /* Don't dequeue parent if it has other entities besides us */
<span style="white-space:pre">	</span>    if(cfs_rq->load.weight)
<span style="white-space:pre">		</span>break;
	    sleep =1;
<span style="white-space:pre">	</span>}
}


我们同样不考虑组调度,该函数就相当于调用了dequeue_entity()。

[code]
<kernel/sched_fair.c>
staticvoid
dequeue_entity(struct cfs_rq *cfs_rq,struct sched_entity *se,int sleep)
{
<span style="white-space:pre">	</span>/*
	 * Update run-time statistics of the 'current'.
	 */
	update_curr(cfs_rq);
	update_stats_dequeue(cfs_rq, se);
...
<span style="white-space:pre">	</span>if(se != cfs_rq->curr)
		__dequeue_entity(cfs_rq, se);
	account_entity_dequeue(cfs_rq, se);
}


该函数首先调用update_curr()更新它的运行统计信息;然后判断se是否等于cfs_rq->curr,一般它们应该是相等的。因为正在执行的进程不在红黑树中,所以就不会调用到__dequeue_entity()。最后调用account_entity_dequeue()递减该fsc_rq的权重load和进程数目nr_running,并将调度实体的on_rq标志清零。

dequeue_task_rt()

[code]
kernel/sched_rt.c>
/*
 * Adding/removing a task to/from a priority array:
 */
static void dequeue_task_rt(struct rq *rq, struct task_struct *p, int sleep)
{
	struct rt_prio_array *array = &rq->rt.active;
	update_curr_rt(rq);
	list_del(&p->run_list);
	if (list_empty(array->queue + p->prio))
		__clear_bit(p->prio, array->bitmap);
}


该函数的过程类似:首先调用update_curr_rt()更新进程统计信息;然后将该进程从对应优先级的链表中删除;最后判断该链表是否为空,如果是,将位图中对应的位清零。

说完了deactivate_task(),我们接着schedule()的流程往下走。如果rq队列中没有进程可以运行了,则调用idle_balance()尝试从其他CPU的rq队列中迁移进程到本CPU运行。

调用当前进程所在调度器类的put_prev_task方法处理要被撤销CPU的进程,也就是当前进程,CFS调度器对应函数为put_prev_task_fair(),实时进程对应的函数为put_prev_task_rt()。

put_prev_task_fair()

[code]
<pre name="code" class="cpp"><kernel/sched_fair.c>
/*
 * Account for a descheduled task:
 */
static void put_prev_task_fair(struct rq *rq, struct task_struct *prev)
{
	struct sched_entity *se = &prev->se;
	struct cfs_rq *cfs_rq;
	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se);
		put_prev_entity(cfs_rq, se);
	}
}
<kernel/sched_fair.c>
static void put_prev_entity(struct cfs_rq *cfs_rq, struct sched_entity *prev)
{
	/*
	 * If still on the runqueue then deactivate_task()
	 * was not called and update_curr() has to be done:
	 */
	if (prev->on_rq)
		update_curr(cfs_rq);
	check_spread(cfs_rq, prev);
	if (prev->on_rq) {
		update_stats_wait_start(cfs_rq, prev);
		/* Put 'current' back into the tree. */
		__enqueue_entity(cfs_rq, prev);
	}
	cfs_rq->curr = NULL;
}


[code]


不考虑组调度,直接考察put_prev_entity()。该函数先更新进程统计信息;然后判断进程的on_rq标志是否置位,因为前面我们看到如果进程处于不可运行的状态且不是抢占引起的,它的on_rq标志已经清除了,所以这里需要该判断。如果进程仍然处于可运行状态,则调动__enqueue_entity()将进程插入到红黑树中,以等待下次调度。

put_prev_task_rt()

<kernel/sched_rt.c>
static void put_prev_task_rt(struct rq *rq,struct task_struct *p)
{
	update_curr_rt(rq);
	p->se.exec_start =0;
}


该函数首先调用update_curr_rt()更新它的统计信息,然后将exec_start清零。由于正在运行的实时进程是没有从队列中删除的,它不像 put_prev_task_fair() 一样,还要进程重新入队。

上述对原来进程的处理完成后,调用pick_next_task()选择下一个要投入运行的进程

[code]
<kernel/sched.c>
/*
 * Pick up the highest-prio task:
 */
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev)
{
	const struct sched_class *class;
	struct task_struct *p;
	/*
	 * Optimization: we know that if all tasks are in
	 * the fair class we can call that function directly:
	 */
	if (likely(rq->nr_running == rq->cfs.nr_running)) {
		p = fair_sched_class.pick_next_task(rq);
		if (likely(p))
			return p;
	}
	class = sched_class_highest; //&rt_sched_class
	for ( ; ; ) {
		p = class->pick_next_task(rq);
		if (p)
			return p;
		/*
		 * Will never be NULL as the idle class always
		 * returns a non-NULL p:
		 */
		class = class->next;
	}
}


如果rq->nr_running等于cfs_rq->nr_running,说明该CPU的就绪队列中除了idle进程外全部都是普通进程,从而直接调用CFS调度器类 fair_sched_class 的 pick_next_task方法选择一个可以投入运行的进程。否则的话就要按照rt_sched_class --->fair_sched_class --->idle_sched_class的顺序选择一个进程投入运行。该函数一定能返回一个进程描述符,因为有idle进程做备胎……

对于上述的三个调度器类,pick_next_task方法分别对应函数 pick_next_task_rt(), pick_next_task_fair()和 pick_next_task_idle()。

pick_next_task_fair()

[code]
<kernel/sched_fair.c>
static struct task_struct *pick_next_task_fair(struct rq *rq)
{
	struct cfs_rq *cfs_rq = &rq->cfs;
	struct sched_entity *se;
	if (unlikely(!cfs_rq->nr_running))
		return NULL;
	do {
		se = pick_next_entity(cfs_rq);
		cfs_rq = group_cfs_rq(se);
	} while (cfs_rq);
	return task_of(se);
}


不考虑组调度,相当于调用函数pick_next_entity()。

<kernel/sched_fair.c>
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
	struct sched_entity *se = NULL;
	if (first_fair(cfs_rq)) {
		se = __pick_next_entity(cfs_rq);
		set_next_entity(cfs_rq, se);
	}
	return se;
}


该函数通过first_fair()判断cfs_rq的left_most是否指向一个调度实体。如果是,则说明可以选择一个进程投入运行,从而调用__pick_next_entity()返回该最左边的调度实体:

[code]
<kernel/sched_fair.c>
static inline struct rb_node *first_fair(struct cfs_rq *cfs_rq)
{
	return cfs_rq->rb_leftmost;
}
static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
	return rb_entry(first_fair(cfs_rq), struct sched_entity, run_node);
}


然后调用set_next_entity()对选中的下一个调度实体完成相应的一些设置:

[code]
<kernel/sched_fair.c>
static void
set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	/* 'current' is not kept within the tree. */
	if (se->on_rq) {
		/*
		 * Any task has to be enqueued before it get to execute on
		 * a CPU. So account for the time it spent waiting on the
		 * runqueue.
		 */
		update_stats_wait_end(cfs_rq, se);
		__dequeue_entity(cfs_rq, se); 
	}
	update_stats_curr_start(cfs_rq, se); 
	cfs_rq->curr = se;
...
	se->prev_sum_exec_runtime = se->sum_exec_runtime; 
}


这里首先先判断选择的进程的on_rq标志是不是置位了的,既然进程能够被选择执行,那怎么会不在就绪队列上呢?当然该条件一般应该也是满足的,这时需要调用__dequeue_entity()将进程从红黑树中删除。

接下来的一步很重要:调用update_stats_curr_start()将调度实体的exec_start更新为rq队列的时钟。对于一个被调度程序选中要投入运行的进程来说,它的exec_start是它被撤销CPU之前更新的,这个值与当前的rq->clock肯定是有一定的差值的,要准确计算进程重新被投入运行以后的运行总时间,则需要将它的exec_start更新到最新的值。

更新cfs_rq->curr为新选择的进程。

最后将sum_exec_runtime的值保存到prev_sum_exec_runtime中。我们前面已经看到,在周期性调度器中的时间记账完成以后,会判断该调度周期内当前进程的时间是否已经用完了,判断的标准之一就是此时保存到prev_sum_exec_runtime中的时间值。

pick_next_task_rt()

[code]
<kernel/sched_rt.c>
static struct task_struct *pick_next_task_rt(struct rq *rq)
{
	struct rt_prio_array *array = &rq->rt.active;
	struct task_struct *next;
	struct list_head *queue;
	int idx;
	idx = sched_find_first_bit(array->bitmap);
	if (idx >= MAX_RT_PRIO)
		return NULL;
	queue = array->queue + idx;
	next = list_entry(queue->next, struct task_struct, run_list);
	next->se.exec_start = rq->clock;
	return next;
}


该函数首先调用sched_find_first_bit()找到拥有进程的最高优先级链表,将其地址存放在queue中。

然后将链表中第一个进程的进程描述符地址存放到next中。这里可以看出:对于实时进程,调度器的策略是优先调度排在链表前面的进程。

和CFS一样,最后也要将exec_start更新为rq队列的时钟。

pick_next_task_idle

<kernel/sched_idletask.c>
static struct task_struct *pick_next_task_idle(struct rq *rq)
{
	schedstat_inc(rq, sched_goidle);
	return rq->idle;
}


该函数直接返回本CPU的idle进程的进程描述符。

选择了下一个要投入运行的进程以后,如果它是一个新的进程(可以说几乎都是这样的情况,因为系统中同时运行着大量的进程),则首先将rq->curr更新为新的进程描述符,然后调用contex_switch()完成上下文切换,该函数中会调用到switch_to(),此后CPU就到另一个进程的控制路径上去执行程序了。

如果调度程序再次调度到本次本撤销CPU的进程,则该进程继续执行context_switch()之后的程序,如果此时该进程又被设置了TIF_NEED_RESCHED标志(可能有可运行的实时进程),那么又要重新选择新的进程来投入运行。
对于不考虑进程因为I/O或者其它原因处于不可运行状态的情况,针对普通进程和实时进程,它们的主调度器schedule()的执行流程分别如下:





下面是pick_next_task()的执行流程,在查找的过程中,如果找到了可以投入运行的下一个进程,则函数会提前结束,不过图中没有表示出来。



6.4.8 新建进程的处理和睡眠进程的唤醒

前面介绍最小虚拟运行时间已经为这部分作好了铺垫,那么我们直接来看linux进程调度时怎么处理这两种情形的。
(1)首先来看函数place_entity()

<kernel/sched_fair.c>
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
	u64 vruntime;
	vruntime = cfs_rq->min_vruntime;
    ...
	/*
	 * The 'current' period is already promised to the current tasks,
	 * however the extra weight of the new task will slow them down a
	 * little, place the new task so that it fits in the slot that
	 * stays open at the end.
	 */
	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;
}


该函数会根据sched_feature()查询的结果来执行部分代码,这里保留该函数中我们关心的部分来研究。
place_entity()函数是用来调整进程的虚拟运行时间的,当新进程被创建或者进程被唤醒时都需要调整它的vruntime值。参数initial表明该进程是不是新建进程。如果是,执行前面if语句的那部分代码;否则就是一个呗唤醒的进程,执行后面的if语句那部分代码。
对于新建进程来说,它的vruntime被设置为min_vruntime
+ sched_vslice_add(cfs_rq, se),即在min_runtime的基础上加上一个调度周期的时间。这样做的目的有两个:
a、如果将新进程的vruntime设置为min_vruntime,那么新进程将会第一个被调度,这样的系统容易受到攻击。只需要不断地fork子进程就可以让CPU忙于处理新进程而不去响应系统中已经存在的其它进程。
b、本调度周期内的CPU使用时间已经承诺分配给原本已经存在的进程了,新建进程需要等到下一个调度周期才能运行,不能再当前调度周期内去抢夺其它进程的CPU时间。
c、单从这个函数来看,似乎子进程会在父进程之后执行。但是根据前面对fork系统调用的讨论,我们知道linux倾向于先调度子进程,以避免不必要的内存拷贝。那子进程是如何先被调度的呢?见6.4.8节处理新建进程部分。
对于被唤醒的睡眠进程而言,它的vruntime被设置为min_runtime - syscrl_sched_latency。但设置的结果不能使得它的vruntime比原来的还小,否则就成了对该进程的奖励了。所以它的vruntime应该取二者中的最大值。

(2)check_preempt_curr()函数

该函数的作用是判断进程p是否可以抢占当前正在运行的进程,具体操作由调度器类的check_preempt_curr方法完成。CFS调度器类对应的函数为check_preempt_wakeup(),实时调度类对应函数check_preempt_curr_rt(),idle调度类对应方法为check_preempt_curr_idle()。

[code]
<kernel/sched.c>
staticinlinevoid check_preempt_curr(struct rq *rq,struct task_struct *p)
{
	rq->curr->sched_class->check_preempt_curr(rq, p);
}


check_preempt_wakeup()

[code]


<pre name="code" class="cpp"><kernel/sched_fair.c>
/*
 * Preempt the current task with a newly woken task if needed:
 */
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p)
{
	struct task_struct *curr = rq->curr;
	struct cfs_rq *cfs_rq = task_cfs_rq(curr);
	struct sched_entity *se = &curr->se, *pse = &p->se;
	unsigned long gran;
	if (unlikely(rt_prio(p->prio))) {
		update_rq_clock(rq);
		update_curr(cfs_rq);  
		resched_task(curr);  
		return;
	}
	/*
	 * Batch tasks do not preempt (their preemption is driven by
	 * the tick):
	 */
	if (unlikely(p->policy == SCHED_BATCH))
		return;
	if (!sched_feat(WAKEUP_PREEMPT))
		return;
	... //省略和组调度相关的代码
	gran = sysctl_sched_wakeup_granularity;
	if (unlikely(se->load.weight != NICE_0_LOAD))
		gran = calc_delta_fair(gran, &se->load);
	if (pse->vruntime + gran < se->vruntime) 
		resched_task(curr);
}


a、如果进程p是一个实时进程,那么立即请求重新调度,因为实时进程总会抢占普通进程。
b、如果进程p的调度策略是SCHED_BATCH,那么直接返回,因为该类型的进程从来不抢占其它进程。
c、接下来的工作很有意思。变量gran被赋值为sysctl_sched_wakeup_granularity,默认值为10ms,然后将gran根据进程的权重换算为虚拟时间得到一个最小时间限额。如果新唤醒进程的虚拟运行时间加上该最小时间限额仍然小于当前进程的虚拟运行时间,那么才请求调度。这样做的目的在于引入了一个“缓冲”,使得进程切换不至于太过频繁,将过多的时间用于上下文切换。

check_preempt_curr_rt()

[code]
<kernel/sched_rt.c>
/*
 * Preempt the current task with a newly woken task if needed:
 */
static void check_preempt_curr_rt(struct rq *rq, struct task_struct *p)
{
	if (p->prio < rq->curr->prio)
		resched_task(rq->curr);
}


该函数比较简单,如果进程p的优先级大于当前进程的优先级,则调用resched_task()设置当前进程的TIF_NEED_RESCHED标志,触发主调度器。

check_preempt_curr_idle()

[code]
<kernel/sched_idletask.c>
/*
 * Idle tasks are unconditionally rescheduled:
 */
static void check_preempt_curr_idle(struct rq *rq, struct task_struct *p)
{
	resched_task(rq->idle);
}


对于idle进程而言,只要系统中有其它可运行进程,则无条件放弃对CPU的使用权,所以该函数直接调用resched_task()。

(3)处理新进程

CFS调度器类使用函数task_new_fair()处理新创建的进程,该函数的行为还受到sysctl_sched_child_runs_first的控制,用于判断新建子进程是否应该在父进程之前运行。

[code]
<kernel/sched_fair.c>
/*
 * Share the fairness runtime between parent and child, thus the
 * total amount of pressure for CPU stays equal - new tasks
 * get a chance to run but frequent forkers are not allowed to
 * monopolize the CPU. Note: the parent runqueue is locked,
 * the child is not running yet.
 */
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);
	/* 'curr' will be NULL if the child belongs to a different group */
	if (sysctl_sched_child_runs_first && this_cpu == task_cpu(p) &&
			curr && curr->vruntime < se->vruntime) {
		/*
		 * Upon rescheduling, sched_class::put_prev_task() will place
		 * 'current' within the tree based on its new key value.
		 */
		swap(curr->vruntime, se->vruntime);   
	}
	enqueue_task_fair(rq, p, 0);
	resched_task(rq->curr);  
}


该函数首先调用update_curr()更新当前进程,也就是父进程的时间统计信息。
然后调用place_entity(),初始化新进程的虚拟运行时间 。
如果系统参数sysctl_sched_child_runs_first置位,说明需要使子进程先返回。如果父子进程处于同一个cpu,且父进程的虚拟运行时间更小,那么需要交换父子进程的vruntime,使得在红黑树中子进程排在前面,优先得到调度的机会。
新建进程还要调用enqueue_task_fair()插入到cfs的就绪队列中,该函数最终会调用__enqueue_entity()。
为了使得fork系统调用返回的时候子进程先运行,父进程需要放弃当前调度周期内它剩余的CPU使用时间,所以调用resched_task()设置了它的TIF_NEED_RESCHED标志,系统调用返回的时候主调度器schedule()将会将父进程从CPU上切换下来。
父子进程交换了vruntime,子进程通过place_entity()插入到了红黑树中相应的位置。那么父进程是什么时候调整它的位置的呢?——在schedule()函数中调用put_prev_task方法完成的。

实时调度器类没有实现task_new方法。

linux的进程创建使用fork、clone或者vfork系统调用,它们最终都会调用do_fork()。下面列出do_fork()中我们比较关心的部分:

[code]
<kernel/fork.c>
/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      struct pt_regs *regs,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{
	struct task_struct *p;
	int trace = 0;
	long nr;
	...
	p = copy_process(clone_flags, stack_start, regs, stack_size,
			child_tidptr, NULL);
	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	if (!IS_ERR(p)) {
		struct completion vfork;
		...
		if (!(clone_flags & CLONE_STOPPED))
			wake_up_new_task(p, clone_flags);
		else
			p->state = TASK_STOPPED;
		...
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}


copy_process()完成进程创建的大部分工作,分配一个新的进程描述符,并完成一些初始化操作。该函数中又会调用sched_fork()来初始化与进程调度相关的信息:

[code]
<kennel/sched.c>
/*
 * fork()/clone()-time setup:
 */
void sched_fork(struct task_struct *p, int clone_flags)
{
	int cpu = get_cpu();
	__sched_fork(p);
#ifdef CONFIG_SMP
	cpu = sched_balance_self(cpu, SD_BALANCE_FORK);
#endif
	set_task_cpu(p, cpu);
	/*
	 * Make sure we do not leak PI boosting priority to the child:
	 */
	p->prio = current->normal_prio;   /* 设置p->prio为父进程的normal->prio */
	if (!rt_prio(p->prio))
		p->sched_class = &fair_sched_class;
...
}


a、调用__sched_fork()将进程统计信息清零,设置进程状态为TASK_RUNNING。
b、将子进程的动态优先级p->prio设置为父进程的普通优先级。
c、根据动态优先级设置调度类。

如果进程创建时设置了标志CLONE_STOPPED,则将新建进程的状态设置为TASK_STOPPED;否则调用wake_up_new_task()唤醒新创建的进程:

[code]
<kernel/sched.c>
/*
 * wake_up_new_task - wake up a newly created task for the first time.
 *
 * This function will do some initial scheduler statistics housekeeping
 * that must be done for every newly created context, then puts the task
 * on the runqueue and wakes it.
 */
void fastcall wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
{
	unsigned long flags;
	struct rq *rq;
	rq = task_rq_lock(p, &flags);
	BUG_ON(p->state != TASK_RUNNING);
	update_rq_clock(rq);
	p->prio = effective_prio(p);
        
	if (!p->sched_class->task_new || !current->se.on_rq) {
		activate_task(rq, p, 0);  /* 参数0表示进程不是被唤醒的 */
	} else {
		/*
		 * Let the scheduling class do new task startup
		 * management (if any):
		 */
		p->sched_class->task_new(rq, p);
		inc_nr_running(p, rq);
	}
	check_preempt_curr(rq, p);
	task_rq_unlock(rq, &flags);
}


a、首先调用update_rq_clock()更新rq队列的时钟。
b、调用effective_prio()计算进程的动态优先级。
c、如果进程对应调度器类没有task_new方法,或者当前进程不在就绪队列上(后一个条件什么时候成立?)。那么调用activate_task()将进程加入队列中,对于实时进程就是这种情形。
d、如果调度器类实现了task_new方法调用该方法将新建进程加入队列中,普通进程就是这种情形,它调用上面提到的task_new_fair()完成该操作。新进程插入红黑树以后,调用inc_nr_running()更新rq队列的进程数目和权重。

上述操作完成以后,wake_up_new_task()调用check_preempt_curr(),后者调用相应调度器类的check_preempt_curr方法来检查并触发对当前进程的抢占。

下面是新建进程的处理流程:

普通进程



实时进程



current为idle进程的情形比较简单,这里就不画图了。

(4)唤醒抢占
当使用try_to_wake_up()函数来唤醒一个睡眠进程时,内核使用check_preempt_curr()来判断唤醒进程是否可以抢占当前进程。

[code]
<kernel/sched.c>
static int try_to_wake_up(struct task_struct *p, unsigned int state, int sync)
{
	int cpu, orig_cpu, this_cpu, success = 0;
	unsigned long flags;
	long old_state;
	struct rq *rq;
...
	update_rq_clock(rq);
	activate_task(rq, p, 1);
	check_preempt_curr(rq, p);
	success = 1;
out_running:
	p->state = TASK_RUNNING;
out:
	task_rq_unlock(rq, &flags);
	return success;
}


我们只保留了try_to_wakeup()中与进程调度相关的一小段代码。唤醒睡眠进程的主要步骤为:

调用update_rq_clock()更新rq队列的时钟。
调用activate_task()将待唤醒进程加入到就绪队列中。
调用check_preempt_curr()来处理可能发生的进程抢占。

这对实时进程的activate_task()处理流程上面已经提到过了,这里对普通进程的activate_task()执行流程做简要分析:



6.4.9 进程的睡眠、退出与进程调度的关系

我们知道进程从创建到退出可能会经历一系列的状态,前面的讨论中覆盖了进程状态转换中的一部分,如从新建进程从READY状态到RUNNING状态,从RUNNING状态到SLEEP状态,从SLEEP状态唤醒进入READY状态。
这一节我们讨论另外两个重要的进程状态转换过程——进程从运行状态到睡眠状态和进程退出,以及它们是怎么调用核心调度器的。
(1)进程的睡眠
进程在访问临界区中的共享数据或者进行I/O时会进入睡眠状态,这里我们拿sleep_on()和inerruptable_sleep_on()来简单说明一下状态转换过程。这两个函数的不同之处在于。前者使进程进入UNINTERRUPTABLE状态,后者使进程进入INTERRUPTABLE状态。这两个函数实际都是调用sleep_on_common()完成的,只是给的参数不同。



(2)进程退出



以exit系统调用为例

6.5 内核抢占和低延迟

6.5.1 内核抢占

内核抢占用来为用户提供更平滑的体验,特别是多媒体环境中。在不支持内核抢占的内核中,内核代码可以一直执行,到它完成为止。调度程序没有办法在一个内核级的任务正在执行的时候重新调度,内核代码一直要执行到完成(返回到用户空间)或明显的阻塞为止。在2.6版本的内核中引入了内核抢占。现在,只要内核是安全的,内核就可以在任何时间抢占正在执行的任务。
那么,什么时候重新调度才是安全的呢?只要没有持有锁,内核就可以进行抢占。锁是非抢占区域的标志
(1)抢占计数器
为了支持内核抢占,每个进程的thread_info中引入了抢占计数器preempt_count。该计数器初始值为0,每当使用锁的时候值加1,释放锁的时候值减1。当其数值为0的时候,内核就可以执行抢占。
preempt_count是一个32位的字段,这32位又做了如下划分:

0 ~ 7preempt_counter,内核抢占计数器(0-255)
8 ~ 15softirq counter,软中断计数器(0-255)
16~ 27hardirq counter,硬中断计数器(0-4096)
28PREEMPT_ACTIVE标志
第一个计数表示内核进入临界区的次数。每次内核进入临界区都会增加该计数,退出临界区的时候又会递减该计数。
第二个计数表示下半部被关闭的次数,也就是正在执行的软中断处理程序的个数,0标志没有软中软正在执行。
第三个计数表示本地CPU中断嵌套的层数,irq_enter()增加该值,irq_exit()减小该值。
PREEMPT_ACTIVE标志置位的话,会使得抢占计数器有一个很大的值,这样就区别于普通的抢占计数器加1的影响了。它用于向主调度器schedule()表明,调度不是普通方式触发的,而是由于内核抢占。

那什么时候会发生内核抢占呢?

从中断返回内核空间的时候,内核会检查need_resched和preempt_count的值。如果TIF_NEED_RESCHED被设置,且preempt_count等于0的话,就会调用主调度器完成进程的切换,实现内核抢占。
释放锁的代码在preempt_count为0的时候,检查need_resched是否被设置。如果设置的话,就会调用调度程序,实现内核抢占。
内核中的进程被阻塞了,或者它显式地调用了schedule(),也会发生内核抢占。这种形式的内核抢占从来都是受支持的,但进程必须清楚自己是可以安全地被抢占的。

(2)从中断返回内核空间时的内核抢占
在开启中断的情况下,中断可以打断内核控制路径的运行。在执行完中断处理程序后,由ret_from_except()完成从从中断到内核的返回。流程如下:



抢占是由preempt_schedule_ieq()完成的,在抢占内核之前,需要先设置进程的PREEMPT_ACTIVE标志,这样在调用schedule()时才能做相应的判断。

(3)释放锁时的内核抢占
释放锁的函数都会调用preempt_enable()来开启内核抢占:



该函数的流程如下:

将抢占计数器减1.
判断进程的TIF_NEED_RESCHED是否置位,如果是,继续。
调用preempt_schedule()完成进程抢占。
可以抢占的条件是抢占计数器为0且没有关闭中断。如不满足上面的条件,前者如进程还在临界区中;后者是因为中断执行的任务不能被打断,而且此时在执行中断处理函数,没有上下文。如果满足条件的话,设置PREEMPT_ACTIVE标志,并调用schedule()完成进程切换。
在切换回该进程的时候,要需要将其PREEMPT_ACTIVE标志清除。

(4)为什么要引入PREEMPT_ACTIVE

在主调度器schedule()中有这么一个判断:

[code]
<kernel/sched.c>
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);  
                        		}
		switch_count = &prev->nvcsw;
	}
...
}


不考虑进程在此时收到信号的情况,如果进程不是由于内核抢占引起的,才调用deactive_task()将进程从就绪队列中删除(对于CFS调度而器的curr进程就是清除on_rq标志而已,因为进程已经从红黑树中删除了)。那为什么有这个判断呢?
还是考虑sleep_on_common:

[code]
static long __sched
sleep_on_common(wait_queue_head_t *q, int state, long timeout)
{
	unsigned long flags;
	wait_queue_t wait;
	init_waitqueue_entry(&wait, current);
	__set_current_state(state);
	spin_lock_irqsave(&q->lock, flags);
	__add_wait_queue(q, &wait);
	spin_unlock(&q->lock);
	timeout = schedule_timeout(timeout);
	spin_lock_irq(&q->lock);
	__remove_wait_queue(q, &wait);
	spin_unlock_irqrestore(&q->lock, flags);
	return timeout;
}


我们考虑一种比较特殊的情况,在__set_current_state()将进程设置为不可运行状态以后,在没有将自己加入相应的等待队列之前,发生了一个中断。中断执行完成后会执行上面提到的preempt_schedule_irq(),并调用到schedule()。如果schedule()中不判断PREEMPT_ACTIVE是否置位,如果此时没有收到信号,则调用deactivate_task()将进程从运行队列删除。这样的话,进程既不存在于等待队列中也不存在于就绪队列中,此后就不可能被唤醒也不会被调度执行了。
在schedule()函数中加入该判断后即可避免这种情况的发生,保证进程至少会存在于一个队列中,不论是等待队列还是就绪队列。
总的来说,因为设置进程状态和进程调度之间是有时间间隙的,在这个过程中,内核可能发生很多事件从而发生内核抢占。为了避免已经处于非运行态的进程还没有加入睡眠队列的时候就被抢占然后剔除出运行队列,需要设置PREEMPT_ACTIVE标志,在主调度器中对它们做特殊处理。

6.5.2 低延迟

即使没有启用内核抢占,内核也关注提供良好的延迟时间。基本上,内核中耗时长的操作不应该完全占据操作系统。相反,它应该不时地检测是否有另一个进程变为可运行,并在必要的情况调用调度器选择一个相应的进程运行。该机制不依赖于内核抢占,即使内核编译时不支持抢占,也能够降低延迟。

发起有条件重调度的函数是cond_resched(),其实现如下:

[code]
int __sched cond_resched(void)
{
	if (need_resched() && !(preempt_count() & PREEMPT_ACTIVE) &&
					system_state == SYSTEM_RUNNING) {
		__cond_resched();
		return 1;
	}
	return 0;
}


static void __cond_resched(void)
{
#ifdef CONFIG_DEBUG_SPINLOCK_SLEEP
	__might_sleep(__FILE__, __LINE__);
#endif
	/*
	 * The BKS might be reacquired before we have dropped
	 * PREEMPT_ACTIVE, which could trigger a second
	 * cond_resched() call.
	 */
	do {
		add_preempt_count(PREEMPT_ACTIVE);
		schedule();
		sub_preempt_count(PREEMPT_ACTIVE);
	} while (need_resched());
}


cond_resched()首先检查TIF_NEED_RESCHED标志是否设置,还要判断PREEMPT_ACTIVE标志是否设置。如果PREEMPT_ACTIVE置位,说明是支持内核抢占的,且进程即将被抢占,已经能保证低延迟了。

__cond_resched()设置PREEMPT_ACTIVE标志并调用主调度器完成进程切换。
那如何使用cond_resched()?考虑内核读取与给定内存映射相关的内存页的情况。这可以通过无限循环来完成,直至所有的数据读取完毕:

[code]
for(;;)
{
     /* 读入数据 */
     if(exit_condition)
            continue;
}


如果需要大量的读取操作,可能耗时会很长。由于进程在内核空间中,调度器器无法像在用户空间那样撤销其CPU,假定也没有启用内核抢占。通过在每个循环中调用cond_resched(),即可改进此种情况。

[code]
for(;;)
{
      cond_resched();
      /* 读入数据 */
      if(exit_condition)
           continue;
}


内核代码已经仔细检查过,以找出长时间运行的函数,并在适当之处插入cond_resched()的调用,即使没有显式内额和抢占,也能够保证较高的响应速度。
总结成一句话就是,在长时间运行的函数中加入主动调度的功能,自己检查是否需要抢占,并在需要的时候完成进程切换。

6.6 SMP调度

多处理器系统上,内核必须考虑几个额外的问题,以确保良好的调度。

CPU负荷必须尽可能公平地在所有的处理器上共享。
进程与系统中某些处理器的亲和性(affinity)必须是可设置的——绑定CPU。
内核必须能够将进程从一个CPU迁移到另一个。但该选项必须谨慎使用,因为它会严重危害性能。

进程对特定CPU的亲和性,定义在task_struct的cpus_allowed成员中。linux提供sched_setaffinity系统调用,可修改进程与CPU的现有分配关系。

6.6.1 数据结构的扩展

在SMP系统上,每个调度器类的调度方法必须增加两个额外的函数:
<span style="font-size:10px;"><sched.h>
#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</span>


虽然其名字称之为load_balance,但这些函数并不直接负责处理负载均衡。每当内核认为有必要重新均衡时,核心调度器代码都会调用这些函数。特定于调度器类的函数接下来建立一个迭代器,使得核心调度器能够遍历所有可能迁移到另一个队列的备选进程,但各个调度器类的内部结构不能因为迭代器而暴露给核心调度器。load_balance函数指针采用了一般性的函数load_balance,而move_one_task则使用了iter_move_one_task。这些函数用于不同的目的。

iter_move_one_task从最忙碌的就绪队列移出一个进程,迁移到当前CPU的就绪队列。
load_balance则允许从最忙的就绪队列分配多个进程到当前CPU,但移动的负荷不能比max_load_move更多。

负载均衡处理过程是如何发起的?在SMP系统上,周期性调度器函数scheduler_tick按上文所述完成所有系统都需要的任务之后,会调用trigger_load_balance函数。这会引发SCHEDULE_
SOFTIRQ软中断
softIRQ,该中断确保会在适当的时机执行run_rebalance_domains。该函数最终对当前CPU调用rebalance_domains,实现负载均衡。

所有的就绪队列组织为调度域(scheduling
domain)。这可以将物理上邻近或共享高速缓存的CPU群集起来,应优先选择在这些CPU之间迁移进程。但在"普通"的SMP系统上,所有的处理器都包含在一个调度域中。要提的一点是该结构包含了大量参数,可以通过/proc/sys/kernel/cpuX/domainY设置。其中包括了在多长时间之后发起负载均衡(包括最大/最小时间间隔),导致队列需要重新均衡的最小不平衡值,等等。此外该结构还管理一些字段,可以在运行时设置,使得内核能够跟踪记录上一次均衡操作在何时执行,下一次将在何时执行。

rebalance_domains()从基本调度域开始从下往上遍历所有调度域,判断是否满足该调度域进行load_balance的一些限制条件。如果满足,就调用load_balance()进行进程迁移。



为执行重新均衡的操作,内核需要更多信息。因此在SMP系统上,就绪队列增加了额外的字段:

<span style="font-size:10px;"><kernel/sched.c>
#ifdef CONFIG_SMP
	struct sched_domain *sd;
	/* For active balancing */
	int active_balance;
	int push_cpu;
	/* cpu of this runqueue: */
	int cpu;
	struct task_struct *migration_thread;
	struct list_head migration_queue;
#endif</span>


就绪队列是特定于CPU的,因此cpu表示了该就绪队列所属的处理器。内核为每个就绪队列提供了一个迁移线程,可以接收迁移请求,这些请求保存在链表migration_queue中。这样的请求通常发源于调度器自身,但如果进程被限制在某一特定的CPU集合上,而不能在当前执行的CPU上继续运行时,也可能出现这样的请求。内核试图周期性地均衡就绪队列,但如果对某个就绪队列效果不佳,则必须使用主动均衡(active
balancing)。如果需要主动均衡,则将active_balance设置为非零值,而push_cpu则记录了从哪个处理器发起的主动均衡请求。

那么load_balance做什么呢?该函数会检测在上一次重新均衡操作之后是否已经过去了足够的时间,在必要的情况下通过调用load_balance发起一轮新的重新均衡操作。该函数的代码流程图如下图所示。该图中描述的是一个简化的版本,因为SMP调度器必须处理大量边边角角的情况。如果都画出来,相关的细节会扰乱图中真正的实质性操作。



首先该函数必须标识出哪个队列工作量最大。该任务委托给find_busiest_queue,后者对一个特定的就绪队列rq调用。函数迭代所有处理器的队列(或确切地说,当前调度组中的所有处理器),比较其负荷权重。最忙的队列就是最后找到的负荷值最大的队列。

在find_busiest_queue标识出一个非常繁忙的队列之后,如果至少有一个进程在该队列上执行(否则负载均衡就没多大意义),则使用 move_tasks将该队列中适当数目的进程迁移到当前队列。move_tasks函数接下来会调用特定于调度器类的load_balance方法。

在选择被迁移的进程时,内核必须确保所述的进程:

目前没有运行或刚结束运行,因为对运行进程而言,CPU高速缓存充满了进程的数据,迁移该进程则完全抵消了高速缓存带来的好处;
根据其CPU亲合性,可以在与当前队列关联的处理器上执行。

如果均衡操作失败(例如,远程队列上所有进程都有较高的内核内部优先级值,即较低的nice值),那么将唤醒负责最忙的就绪队列的迁移线程。为确保主动负载均衡执行得比上述方法更积极一点,load_balance会设置最忙的就绪队列的active_balance标志,并将发起请求的CPU记录到rq->cpu。

6.6.2 迁移线程

迁移线程用于两个目的。一个是用于完成发自调度器的迁移请求(如迁移新建进程),另外一个是用于实现主动均衡。迁移线程是一个执行migration_thread的内核线程。该函数的代码流程图如下图所示。



migration_thread内部是一个无限循环,在无事可做时进入睡眠状态。首先,该函数检测是否需要主动均衡。如果需要,则调用 active_load_balance满足该请求。该函数试图从当前就绪队列移出一个进程,且移至发起主动均衡请求CPU的就绪队列。它使用 move_one_task完成该工作,后者又对所有的调度器类,分别调用特定于调度器类的move_one_task函数,直至其中一个成功。注意,这些函数移动进程时会尝试比load_balance更激烈的方法。例如,它们不进行此前提到的优先级比较,因此它们更有可能成功。
完成主动负载均衡之后,迁移线程会检测migrate_req链表中是否有来自调度器的待决迁移请求。如果没有,则线程发出重调度请求。否则,用__migrate_task完成相关请求,该函数会直接移出所要求的进程,而不再与调度器类进一步交互。

6.6.3 核心调度器的改变

除了上述增加的特性之外,在SMP系统上还需要对核心调度器的现存方法作一些修改。虽然到处都是一些小的细节变化,与单处理器系统相比最重要的差别如下所示。

在用exec系统调用启动一个新进程时,是调度器跨越CPU移动该进程的一个良好的时机。事实上,该进程尚未执行,因此将其移动到另一个CPU不会带来对CPU高速缓存的负面效应。exec系统调用会调用挂钩函数sched_exec,其代码流程图如下图所示。
sched_balance_self挑选当前负荷最少的CPU(而且进程得允许在该CPU上运行)。如果不是当前CPU,那么会使用sched_migrate_task,向迁移线程发送一个迁移请求。

完全公平调度器的调度粒度与CPU的数目是成比例的。系统中处理器越多,可以采用的调度粒度就越大。sysctl_sched_min_granularity和sysctl_sched_latency都乘以校正因子1 +log2(nr_cpus),其中nr_cpus表示现有的CPU的数目。但它们不能超出200毫秒。sysctl_sched_wakeup_granularity也需要乘以该因子,但没有上界。



6.7 关于CFS讨论的一个更正

前面对CFS的讨论中提到处于TASK_RUNNING状态的进程在每个调度周期都会得到一次执行机会,这个说法是不对的,应该是最少一次。

按照内核2.6.24的CFS调度算法,如果一个进程受到了很大的不公平对待,也就是它的vruntime相对来说很小,在这个进程被调度一次以后,它可能还是vruntime最小的那个进程,那么下一次调度器选择投入运行的进程还是它自己。这样,一个调度周期因此而延长了。

所以存在调度器schedule()中调用pick_next_task()选中的进程还是当前进程的情况,但这种情况的出现概率比较低,其中就包括了上面提到的情况。内核代码也用likely(prev != next)做了优化。

参考资料:

TSS任务状态段 http://www.cnblogs.com/guanlaiy/archive/2012/10/25/2738355.html

Linux进程管理之CFS调度器分析 http://blog.chinaunix.net/uid-20543183-id-1930843.html

linux2.6.24内核,调度器章节的笔记 http://blog.csdn.net/janneoevans/article/details/8125106

《深入linux内核架构》
《linux内核设计与实现》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: