Linux:03:进程调度

进程调度

什么是调度

      如今的操作系统都是多任务的,为了能让很多其它的任务能同一时候在系统上更好的执行,须要一个管理程序来管理计算机上同一时候执行的各个任务(也就是进程)。

      这个管理程序就是调度程序,它的功能说起来非常easy:

      1.决定哪些进程执行,哪些进程等待

      2.决定每一个进程执行多长时间

      此外,为了获得更好的用户体验,执行中的进程还可以马上被其它更紧急的进程打断。总之,调度是一个平衡的过程。一方面,它要保证各个执行的进程可以最大限度的使用CPU(即尽量少的切换进程,进程切换过多,CPU的时间会浪费在切换上);还有一方面,保证各个进程能公平的使用CPU(即防止一个进程长时间独占CPU的情况)。

策略

I/O消耗型和处理器消耗型的进程

       I/O消耗型进程:大部分时间用来提交I/O请求或是等待I/O请求,常常处于可执行状态,但执行时间短,等待请求过程时处于堵塞状态。如交互式程序。

       处理器消耗型进程:时间大都用在执行代码上,除非被抢占否则一直不停的执行。

       调度策略要在:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量)之间寻找平衡。      

       Linux为了保证交互式应用,所以对进程的对应做了优化,更倾向于优先调度I/O消耗型进程。

进程优先级

        调度算法中最主要的一类就是基于优先级的调度。这是一种依据进程的价值和其对处理器时间的需求来对进程分级的想法。优先级高的进程先执行,低的后执行,同样优先级的进程按轮转方式进行调度。

        Linux依据以上思想实现了一种基于动态优先级的调度方法。一開始,该方法先设置主要的优先级,然而它同意调度程度依据须要来加、减优先级。比如,假设一个进程在I/O等待上耗费的时间多于其执行时间,那么该进程明显属于I/O消耗型,它的优先级会被动态提高。相反,处理器消耗型进程的优先级会被动态减少。

        Linux内核提供两组独立的优先级范围。第一种是nice值,范围从-20到+19,默认值是0。nice值越大优先级越低。另外一种是实时优先级,其值可配置,范围从0到99,不论什么实时进程的优先级都高于普通的进程。

时间片

        时间片是一个数值,它表明进程在被抢占前所能持续执行的时间,I/O消耗型不须要长的时间片,而处理器消耗型的进程则希望越长越好。时间片的大小设置并不简单,设大了,系统响应变慢(调度周期长);设小了,进程频繁切换带来的处理器消耗。  Linux调度程序提高交互程序的优先级,让它们运行得更频繁。于是,调度程序提供了比較长的默认时间片给交互程序。此外,Linux调度程序还能依据进程的优先级动态调整分配给它的时间片。从而保证优先级高的进程,假定也是重要性高的进程,运行的频率高,运行时间长。通过实现这样一种动态调整优先级和时间片长度的机制,Linux调度性性能不但非常稳定并且也非常强健。

       注意,进程并非一定非要一次就用完它全部的时间片,比如一个拥有100毫秒时间片的进程,能够通过反复调度,分5次每次20毫秒用完这些时间片。

       当一个进程的时间耗尽时,就觉得到期了。没有时间片的进程不会再投入执行,除非等到其它全部的进程都耗尽了他们的时间片。那个时候,全部进程的时间片会被又一次计算。

进程抢占

       Linux是抢占式的。当一个进程进入TASK_RUNNING状态,内核会检查它的优先级是否高于当前正在执行的进程。假设是这样,调度程序会被唤醒,抢占当前正在执行的进程并执行新的可执行进程。此外,当一个进程的时间片变为0时,它会被抢占,调度程序被唤醒以选择一个新的进程。

调度算法

可运行队列

       调度程序中最主要的数据结构式运行队列(runqueue)。可运行队列是给定处理器上的可运行进程的链表,每一个处理器一个。每一个可投入运行的进程都唯一的归属于一个可运行队列。此外,可运行队列中还包括每一个处理器的调度信息。所以,可运行队列也是每一个处理器最重要的数据结构。

        为了避免死锁,要锁住多个执行队列的代码必须总是依照相同的顺序获取这些锁:依照可执行队列地址从低向高的顺序。

优先级数组

        每一个执行队列都有两个优先级数组,一个活跃的和一个过期的。优先级数组是一种可以提供O(1)级算法复杂度的数据结构。优先级数组使可执行处理器的每一种优先级都包括一个相应的队列,而这些队列包括相应优先级上的可执行进程链表。优先级数组还拥有一个优先级位图,当须要查找当前系统内拥有最高优先级的可执行进程时,它可以帮助提高效率。

又一次计算时间片

        很多操作系统在全部进程的时间片都用完时,都採用一种显示的方法来计算时间片。典型的实现是循环訪问每一个进程,这样可能会耗费相当长的时间,最坏情况为O(N);重算时必须考锁的形式来保护任务队列和每一个进程描写叙述符,这样做会加剧对锁的争用;又一次计算时间的实际不确定。活跃数组内的可运行队列上的进程都还有时间片剩余,而过期数组内的都耗尽了时间片。当一个进程的时间片耗尽时,它会被移至过期数组,但在此之前,时间片已经给它又一次计算好。又一次计算时间片变得很easy,仅仅要在活跃和过期数组之间来回切换,这是O(1)级调度程序的核心。

schedule()

        选定下一个进程并切换到它去运行是通过schedule()函数实现的。当内核代码想要休眠时,会直接调用该函数,另外,假设有哪个进程将被抢占,那么该函数也会被唤起运行。schedule()函数独立于每一个处理器运行。

        首先要在活动优先级数组中找到第一个被设置的位,该位对于这优先级最高的可运行进程。然后,调度程序选择这个级别链表里的有一个进程。这就是系统中优先级最高的可运行程序。假设被选中的进程不是当前进程,就进行上下文切换。

计算优先级和时间片

        nice值之所以起名为静态优先级,是由于它从一開始由用户指定后,就不能改变。动态优先级通过一个关于静态优先级和进程交互性的函数关系计算而来。effective_prio()函数能够返回一个进程的动态优先级。这个函数以nice值为基数,再加上-5到+5之间的进程交互性的奖励或罚分。

        怎么通过一些判断来获取准确反映进程究竟是I/O消耗型的还是处理器消耗型的。最明显的标准莫过于进程休眠的时间长短了。假设一个进程的大部分时间都在休眠,那么它就是I/O消耗型的。假设一个进程运行的时间比休眠的时间长,那它就是处理器消耗型的。

        还有一方面,又一次计算时间片相对简单了。它仅仅要以静态优先级为基础就能够了。在一个进程创建的时候,新建的子进程和父进程均分父进程剩余的进程时间片。这种分配非常公平而且防止用户通过不断创建新进程来不停地获取时间片。task_timeslice()函数为给定任务返回一个新的时间片。时间片的计算仅仅须要把优先级按比例缩放,使其符合时间片的数值范围要求就能够了。进程的静态优先级越高,它每次运行得到的时间片就越长。调度程序还提供了第二种机制以支持交互进程:假设一个进程的交互性很强,那么当它时间片用完后,它会被放置到活动数组而不是过期数组中。

睡眠与唤醒

       休眠(被堵塞)的进程处于一个特殊的不可运行状态。进程把它自己标记成休眠状态,把自己从可运行队列移出,放入等待队列,然后调用schedule()选择和运行一个其它进程。唤醒的过程刚好相反:进程被设置为可运行状态,然后再从等待队列中移到可运行队列。休眠有两种相关的进程状态:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE。休眠通过等待队列进行处理。等待队列是由等待某些事件发生的进程组成的简单链表。内核用wake_queue_head_t来代表等待队列。等待队列能够通过DECLARE_WAITQUEUE()静态创建,也能够由init_waitqueue_head()动态创建。唤醒操作通过函数wake_up()进行,它会唤醒指定的等待队列上的全部进程。

负载平衡

         Linux的调度程序为堆成多处理系统的每一个处理器准备了单独的可运行队列和锁。为了使各个可运行队列上的负载平衡,提供了负载平衡程序。假设它发现了不平衡,就会把相抵繁忙的队列中的进程抽到当前的可自行队列中来。

        负载平衡程序有kernel/sched.c中的函数load_balance()来实现。它有两种调用方法。在schedule()运行的时候,仅仅要当前的可运行队列为空,它就会被调用。此外,它还会被定时器调用:系统空暇时每隔1毫秒调用一次或者在其它情况下每隔200毫秒调用一次。负载平衡程序调用时须要锁住当前处理器的可运行队列而且屏蔽中断,以避免可运行队列被并发地訪问。

抢占和上下文切换

       上下文切换,也就是从一个可运行进程切换到还有一个可运行进程。进程切换schedule函数调用context_switch()函数完毕下面工作:

       1.调用定义在<asm/mmu_context.h>中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中。

       2.调用定义在<asm/system.h>中的switch_to(),该函数负责从上一个进程的处理器状态切换到新进程的处理器状态。这包含保存、恢复栈信息和寄存器信息。

      前面看到schedule函数调用有非常多种情况,全然依靠用户来调用不能达到非常好的效果。内核须要推断什么时候调用schedule,内核提供了一个need_resched标志来表明是否须要又一次运行一次调度:

       1当某个进程耗尽它的时间片时,scheduler_tick()就会设置这个标志;

       2当一个优先级高的进程进入可运行状态的时候,try_to_wake_up()也会设置这个标志。

       每一个进程都包括一个need_resched标志,这是由于訪问进程描写叙述符内的数值要比訪问一个全局变量快

用户抢占

       内核即将返回用户空间时候,假设need_resched标志被设置,会导致schedule函数被调用,此时发生用户抢占。

       用户抢占在下面情况时产生:

       1.从系统调返回用户空间。

       2.从中断处理程序返回用户空间。

内核抢占

       仅仅要又一次调度是安全的,那么内核就能够在不论什么时间抢占正在运行的任务。

       什么时候又一次调度才是安全的呢?仅仅要没有持有锁,内核就能够进行抢占。

      锁是非抢占区域的标志。因为内核是支持SMP的,所以,假设没有持有锁,那么正在运行的代码就是可又一次导入的,也就是能够抢占的。

      内核抢占会发生在:

      1.当从中断处理程序正在运行,且返回内核空间之前。

      2.当内核代码再一次具有可抢占性的时候。

      3.假设内核中的任务显式的调用schedule()。

      4.假设内核中的任务堵塞(这相同也会导致调用schedule())。

SMP调度背景

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

  • CPU负荷必须尽可能公平地在所有处理器上共享。
  • 进程与系统中某些处理器的亲合性(affinity)必须是可设置的。
  • 内核必须是能够将进程从一个CPU迁移到另一个上。

linux SMP调度就是将进程安排/迁移到合适的CPU中去,保持各CPU负载均衡的过程。如下图所示。

smp

SMP调度时机

  • scheduler_tick
  • try_to_wake_up(优先选择在唤醒的CPU上运行)
  • exec系统调用启动一个新进程时

SMP调度分析

CPU拓扑关系构建

系统启动时开始构建CPU拓扑关系。

这里写图片描述

在ARM中,4核处理器示意图如下所示:

这里写图片描述

上述4核处理器最后生成的调度域与调度组的拓扑关系图如下图如示:

这里写图片描述

load_balance

  1. 首先,load_balance()调用find_busiest_queue()来选出最忙的运行队列,在这个队列中具有最多的进程数。这个最忙的运行队列应该至少比当前的队列多出25%的进程数量。如果不存在具备这样条件的队列,find_busiest_queue()函数NULL,同时load_balance()也返回.如果存在,那么将返回这个最忙的运行时队列.

  2. 然后,load_balance()函数从这个最忙的运行时队列中选出将要进行负载平衡的优先级数组(priority array).选取优先级数组原则是,首先考虑过期数组(expired array),因为这个数组中的进程相对来说已经很长时间没有运行了,所以它们极有可能不在处理器缓冲中.如果过期数组(expired priority array)为空,那就只能选择活跃数组(active array).

  3. 下一步,load_balance()找出具有最高优先级(最小的数字)链表,因为把高优先级的进程分发出去比分发低优先级的更重要。

  4. 为了能够找出一个没有运行,可以迁移并且没有被缓冲的进程,函数将分析每一个该队列中的进程.如果有一个进程符合标准,pull_task()函数将把这个进程从最忙的运行时队列迁移到目前正在运行的队列。

  5. 只要这个运行时队列还处于不平衡的状态,函数将重复执行3和4,直到将多余进程从最忙的队列中迁移至目前正在运行的队列.最后,系统又处于平衡状态,当前运行队列解锁,load_balance()返回。

try_to_wake_up

唤醒进程涉及到应该由哪个CPU来运行唤醒进程。当找到一个亲和性的调度域且唤醒的CPU与之前该进程运行的CPU不是同一个CPU,考虑使用当前CPU来唤醒进程。也就是说优先选择wakeup CPU,否则选择pre CPU.

猜你喜欢

转载自blog.csdn.net/xuanying_china/article/details/81193633