摘 要:
  本文分析了Linux 2.4和Linux 2.6进程调度程序,并对进程调度程序的执行时间和执行频率进行了分析和研究。
  
  关键词:Linux 2.4 Linux 2.6 调度程序 执行时间 执行频率
  
  1. 引 言
  
  进程调度的研究是整个操作系统理论的核心[1],在多进程的操作系统中,进程调度是一个全局性的、关键性的问题,它对系统的总体设计、系统的实现、功能设置以及各方面的性能都有着决定性的影响[1]。国内在操作系统的研发上仍落后于国外,在开放源代码操作系统中,Linux的发展最为显著,为发展我国的计算机操作系统,有必要对Linux 系统进行分析研究。本文针对linux2.4与2.6内核进程调度程序进行分析,并对进程调度程序执行时间和执行频率进行了分析和研究。
  
  2. Linux 进程调度机制
  
  2.1 Linux 进程调度时机
  
  调度时机是指在什么情况下运行调度程序来选择进程运行。在Linux系统中调度程序是通过函数 schedule()来实现的,这个函数被调用的频率很高,由它来决定要运行的进程。Linux调度时机主要分两种情况:主动调度和被动调度。
  
  主动调度是指当进程状态发生变化时直接调用schedule()来实现调度,比如进程进入睡眠状态或运行终止时会调用相应的系统调用,在这些系统调用完成状态转变后即将结束时会主动调用schedule()进行进程调度。
  
  被动调度是指当一个进程运行时间片到或就绪队列中增加了一个进程,此时系统并不立即进行调度,而仅仅是将当前进程的调度标志位置1,当系统由核心态往用户态转变之前检查当前进程的调度标志是否为1,若为1,则调用schedule()进行调度。
  
  在Linux2.6中,增加了内核抢占调度机制,当系统由中断返回时也要检查当前进程的调度标志位是否为1,若为1,也要进行调度。所以,在没有锁保护的任何代码段都有可能被中断,有中断就有机会进行调度。抢占机制提高了系统对实时任务或需要紧急处理的任务的响应时间。
  
  2.2 Linux 进程调度策略
  
  在Linux 中,进程的调度策略有三种:SCHED_FIFO、SCHED_RR和SCHED_NORMAL(Linux 2.4中为SCHED_OTHER),可通过函数setscheduler()改变进程的调度策略。前两种主要用于实时进程的调度,其中SCHED_FIFO 指先进先出调度算法,适合于时间性要求比较强,但每次运行所需时间比较短的进程,采用该策略的进程获得CPU 后,除非有更高优先级进程申请运行外,否则该进程将保持运行至退出或自愿放弃CPU;SCHED_RR 指优先级轮转调度算法,按实时进程的优先级大小进行调度。SCHED_NORMAL 适合于普通分时进程的调度,调度原则是由优先级的大小决定。
  
  2.3 Linux 进程调度方式文献
  
  1 提出Linux 2.4 调度方式是有条件可剥夺方式,根据Linux 2.4 调度时机可知,当就绪队列中有进程加入时,就会将当前进程的调度标志置1,当系统由核心态往用户态转变时才会引起进程调度。所以从调度时机来看,当有更高优先级的进程进入就绪队列后,系统并没有立即运行调度程序,而仅仅是设置调度标志,而从设置调度标志到系统由核心态往用户态转变之前的时间并不是一个确切的值。
  
  Linux 2.6 采用的调度方式仍然是有条件可剥夺方式,Linux 2.6 虽然实现了内核抢占运行,但并不是系统中来了更高优先级的进程,立即进行调度,而是在中断结束或系统调用完成后才进行判断是否需要调度,如果需要,则进行进程调度。比如系统在核心态时,来了外部中断,系统响应中断转去执行相应的中断处理程序,中断处理程序执行完后回到断点继续执行,若在回到用户态之前来了若干中断,这样系统就会花更多的时间去处理中断。由此可见,更高优先级的进程有时并不能立即得到运行。
  
  3. Linux 进程调度依据及调度算法
  
  在Linux 2.4 中,每个进程控制块中都有5 个参数,分别是policy、priority、counter、rt_priority和nice。这5 个参数是调度程序选择进程的依据。policy决定调度策略,另外在policy中还包含了一个SCHED_YIELD位,置位时表示主动放弃CPU;priority决定进程的优先级;counter是进程的剩余时间片,它的初值由priority和nice决定;rt_priority是实时优先级,这是实时进程所特有的,用于实时进程间的选择;nice是负向优先级,其值标志“谦让”的程度[1],其值越大,表示其优先级越低,nice取值范围为-20~+19,可由系统调用nice()改变。
  
  进程调度算法的关键在于选择某一就绪进程来运行,在 Linux 2.4 中进程调度程序是通过计算就绪队列中的每一个进程的权值,选择权值最大的进程来运行。函数goodness()就是用来计算进程的权值,其算法描述如下:
  
  (1) 设置权值 weight 为 -1(2) 如果该进程的 SCHED_YIELD 位置位,则转(9)(3) 如果该进程的调度策略不是 SCHED_OTHER,则转(8)(4) 设置权值 weight 为该进程的counter 值(5) 如果权值 weight 为0,则转(9)(6) 如果该进程是当前进程或内核线程时,则权值 weight 加1(7) 权值 weight 加(20-该进程的nice 值),转(9)(8) 权值 weight 加(1000+该进程的 rt_priorty)(9) 返回权值 weight由上面的算法可知,goodness()根据policy区分实时进程和普通进程,若为实时进程返回权值1000 + p->rt_priority,该值远大于普通进程的权值;若为普通进程,将进程剩余时间片加上20-进程的nice作为权值;如果进程是当前进程或内核线程, 那么就不必进行上下文切换,则给予权值加一的"优惠"。goodness()还可能返回-1,表示该进程设置了SCHED_YIELD位,将主动放弃运行的机会。
  
  当所有就绪进程的权值weight都为0时,表示当前就绪队列中的所有进程的时间片都用完了,此时将重新计算所有进程(包括处于其它状态的进程)的counter值,然后再计算就绪队列中所有进程的权值 ,从中选择权值最大的进程。
  
  通过上面的分析可知,Linux 2.4进程调度算法的时间复杂度为Ο(n),表明进程越多,进程调度所用时间成线性增长。
  
  Linux 2.6的进程调度算法比较巧妙,在多CPU的系统中,每一个CPU都将维护一个与自己相关的就绪队列runqueue,本文仅分析在单CPU情况下的进程调度。Linux 2.6调度程序的算法复杂度为O(1),其关键技术与数据结构runqueue 有关。在runqueue 数据结构中,有两个就绪队列,分别通过类型为prio_array的active指针和expired指针访问,我们把由active指针访问的队列,称作active队列,由expired指针访问的队列,称为expired队列。在active队列中的进程是由时间片没有用完的进程组成,它是当前可被调度的就绪进程;在expired队列中的进程是由时间片已用完的就绪进程组成。队列的结构用struct prio_array 表示如下:
  
  struct prio_array {unsigned int nr_active;unsigned long bitmap[BITMAP_SIZE];struct list_head queue[MAX_PRIO];};其中nr_active表示该队列中存在的进程总数。MAX_PRIO定义为140,即优先级分为140级,分别为0至139,queue[MAX_PRIO]是表示共有140个队列,每个队列的优先级为queue的下标,即进程按优先级的大小放在不同的队列中,相同优先级的进程在同一队列中。bitmap[BITMAP_SIZE]表示队列的位图,它对应140级队列,若某一队列中有进程,则该队列对应位为1,否则为0。通过bitmap可快速确定在140级队列中,最高优先级的进程所在的队列。当active队列中的进程将时间片用完后,就被放到expired队列中,并设置好新的初始时间片。当active队列中无就绪进程时,即所有进程用完时间片,或进程的状态发生了改变离开了该队列,这时,active和expired两队列进行对换,重新开始下一轮的调度过程。
  
  Linux2.6根据优先级大小进行调度,实时进程的优先级在运行过程中不会动态地改变,普通进程的优先级随进程的运行时间、等待时间、是否为交互式进程而动态地改变。
  
  4. Linux 调度程序性能分析
  
  本文对Linux 2.4与2.6调度程序执行时间和执行频率进行了测定,所用硬件环境:CPU是Intel P4,主频1.7GHz,内存384MB。
  
  在Intel Pentium以上级别的CPU中,有一个称为“时间戳(Time Stamp)”的部件,它以64位无符号整型数的格式,记录了自CPU上电以来所经过的时钟周期数。机器指令RDTSC用来读取这个时间戳的数字,并将其保存在EDX:EAX寄存器对中。利用“时间戳”即可实现对调度程序执行时间的测定。
  
  4.1 Linux 调度程序执行时间的测定
  
  本文所指调度程序执行时间是指调度程序开始运行到选中某一进程这一段时间,不包括进程的上下文切换时间。采用的测试方法是在调用schedule()时记录“时间戳”的值t1,在schedule()选中某一进程后再记录“时间戳”的值t2,这样调度程序从开始运行到选中某一进程所用时间是(t2-t1)/1700μs。
  
  本文分别选用10、20、30、40、50个用户进程进行测试,因调度程序执行时间与就绪队列、当前系统环境有关,因此每次调度程序所用时间不一定一样,所以取平均调度时间,即当用户进程数为n时,计算出调度程序执行m次所用的总的时间t,取平均时间为t/m,该值为当用户进程数为n时,调度程序平均执行时间为t/m。另外,内核线程同样需要调度,它也同样在就绪队列中等待调度,而本文仅考虑用户进程数,而未将内核线程算进来。
  
  图1表示用户进程数与调度程序平均执行时间的关系,当用户进程数分别为10、20、30、40和50时,Linux2.4调度程序平均执行时间分别是1.4719μs、3.3148μs、6.4617μs、7.9023μs和9.3065μs;Linux 2.6平均执行时间分别是0.38669μs、0.36736μs、0.39911μs、0.43905μs和0.42583μs。由此可见,Linux 2.4进程调度算法的时间复杂度为Ο(n),Linux 2.6进程调度算法的时间复杂度为Ο(1)。
  
  4.2 Linux调度程序调用频率的测定
  
  调度程序调用频率是指每秒执行schedule()的次数,在父进程创建子进程的过程中,调度程序的调用频率会显著地提高,本文所采集的数据是系统中有稳定的n个用户进程时,即避开创建子进程过程所采集的数据。当用户进程数分别为10、20、30、40、50时,Linux 2.4调度程序的调用频率分别为48.83、35.2、25.2、21.9、20.3次;Linux 2.6调度程序的调用频率分别为14.25、13.5、13.5、14.0、14.0次。在Linux 2.4中随着进程数的增多,调度程序的执行频率下降,而在Linux 2.6中,调用频率基本恒定,如图2所示。
  
  由调度程序执行时间和调用频率可推算出每秒调度程序总的执行时间,即单位时间内进程调度程序的系统开销,如图3所示,当用户进程数分别为10、20、30、40、50时,1秒内Linux 2.4进程调度程序总的执行时间分别为71.873μs、116.681μs、162.835μs、173.060μs、188.922μs;Linux 2.6进程调度程序总的执行时间分别为5.51μs、4.96μs、5.39μs、6.15μs、5.96μs。由此得出结论:在Linux 2.4中用户进程数越多,系统开销越大;而Linux 2.6系统开销并不随用户进程数增加而增加。
  
  5. 结论
  
  Linux 2.6调度程序比Linux 2.4有了很大的改进,做到了调度程序算法复杂度与系统负载无关,但对于实时进程的调度性能的改变还是不大,仅仅实现了内核抢占调度,离真正的实时性还有一定的距离,但Linux本身是一个通用的操作系统,要将其应用在实时系统或嵌入式系统中,还需要软件开发人员的共同努力。