预计阅读本页时间:-
10.3.4 Linux中的调度
现在我们来关注Linux系统的调度算法。首先要认识到,Linux系统的线程是内核线程,所以Linux系统的调度是基于线程的,而不是基于进程的。
为了进行调度,Linux系统将线程区分为三类:
1)实时先入先出。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
2)实时轮转。
3)分时。
实时先入先出线程具有最高优先级,它不会被其他线程抢占,除非那是一个刚刚准备好的、拥有更高优先级的实时先入先出线程。实时轮转线程与实时先入先出线程基本相同,只是每个实时轮转线程都有一个时间量,时间到了之后就可以被抢占。如果多个实时轮转线程都准备好了,每一个线程运行它的时间量所规定的时间,然后插入到实时轮转线程列表的末尾。事实上,这两类线程都不是真正的实时线程。执行的最后期限无法确定,更无法保证最后期限前线程可以执行完毕。这两类线程比起分时线程来说只是具有更高的优先级而已。Linux系统之所以称它们为“实时”是因为Linux系统遵循的P1003.4标准(UNIX系统对“实时”含义的扩展)使用了这个名称。在系统内部,实时线程的优先级从0到99,0是实时线程的最高优先级,99是实时线程的最低优先级。
传统的非实时线程按照如下的算法进行调度。在系统内部,非实时线程的优先级从100到139,也就是说,在系统内部,Linux系统区分140级的优先级(包括实时和非实时任务)。就像实时轮转线程一样,Linux系统根据非实时线程的优先级分配时间量。这个时间量是线程可以连续运行的时钟周期数。在当前的Linux版本中,时钟频率为1000赫兹,每个时钟周期为1ms,也叫做一个最小时间间隔(jiffy)。
像大多数UNIX系统一样,Linux系统给每个线程分配一个nice值(即优先级调节值)。默认值是0,但是可以通过调用系统调用nice(value)来修改,修改值的范围从-20到+19。这个值决定了线程的静态优先级。一个在后台大量计算π值的用户可以在他的程序里调用这个系统调用为其他用户让出更多计算资源。只有系统管理员可以要求比普通服务更好的服务(意味着nice函数参数值的范围从-20到-1)。推断这条规则的理由作为练习留给读者。
Linux调度算法使用一个重要的数据结构——调度队列。在系统中,一个CPU有一个调度队列,除了其他信息,调度队列中有两个数组,一个是正在活动的,一个是过期失效的。如图10-10所示,这两个分量都是指向数组的指针,每个数组都包含了140个链表头,每个链表具有不同的优先级。链表头指向给定优先级的双向进程链表。调度的基本操作如下所述。

调度器从正在活动数组中选择一个优先级最高的任务。如果这个任务的时间片(时间量)过期失效了,就把它移动到过期失效数组中(可能会插入到优先级不同的列表中)。如果这个任务阻塞了,比如说正在等待I/O事件,那么在它的时间片过期失效之前,一旦所等待的事件发生,任务就可以继续运行,它将被放回到之前正在活动的数组中,时间片根据它所消耗的CPU时间相应的减少。一旦它的时间片消耗殆尽,它也会被放到过期失效数组中。当正在活动数组中没有其他的任务了,调度器交换指针,使得正在活动数组变为过期失效数组,过期失效数组变为正在活动数组。这种方法可以保证低优先级的任务不会被饿死(除非实时先入先出线程完全占用CPU,但是这种情况是不会发生的)。
不同的优先级被赋予不同的时间片长度。Linux系统会赋予高优先级的进程较长的时间片。例如,优先级为100的任务可以得到800ms的时间片,而优先级为139的任务只能得到5ms的时间片。
这种调度模式的思想是为了使进程更快地出入内核。如果一个进程试图读取一个磁盘文件,在调用read函数之间等待一秒钟的时间显然会极大地降低进程的效率。每个请求完成之后让进程立即运行的做法会好得多,同时这样做也可以使下一个请求更快的完成。相似地,如果一个进程因为等待键盘输入而阻塞,那么它明显是一个交互进程,这样的进程只要准备好运行后就应当被赋予较高的优先级,从而保证交互进程可以提供较好的服务。在这种情况下,当I/O密集进程和交互进程被阻塞之后,CPU密集进程基本上可以得到所有被留下的服务。
由于Linux系统(或其他任何操作系统)事先不知道一个任务究竟是I/O密集的,还是CPU密集的,它只是依赖于连续保持的互动启发模式。通过这种方式,Linux系统区分静态优先级和动态优先级。线程的动态优先级不断地被重新计算,其目的在于:(1)奖励互动进程,(2)惩罚占用CPU的进程。最高的优先级奖励是-5,是从调度器接收的与更高优先级相对应的较低优先级的值。最高的优先级惩罚是+5。
说得更详细些,调度器给每一个任务维护一个名为sleep_avg的变量。每当任务被唤醒时,这个变量会增加;当任务被抢占或时间量过期时,这个变量会相应地减少。减少的值用来动态生成优先级奖励,奖励的范围从-5到+5。当一个线程从正在活动数组移动到过期失效数组中时,Linux系统的调度器会重新计算它的优先级。
这里讲述的调度算法指的是2.6版本内核,最初引入这个调度算法的是不稳定的2.5版本内核。早期的调度算法在多处理器环境中所表现的性能十分低下,并且当任务的数量大量增长时,不能很好地进行调度。由于上面描述的内容说明了通过访问正在活动数组就可以做出调度决定,那么调度可以在一个固定的时间O(1)内完成,而与系统中进程的数量无关。
另外,调度器包含了对于多处理器和多核平台而言非常有益的特性。首先,在多处理器平台上,运行队列数据结构与某一个处理器相对应,调度器尽量进行亲和调度,即将之前在某个处理器上运行过的任务再次调入该处理器。第二,为了更好地描述或修改一个选定的线程对亲和性的要求,有一组系统调用可供调用。最后,在满足特定性能和亲和要求的前提下,调度器实现在不同处理器上阶段性地加载平衡,从而保证整个系统的加载是平衡的。
调度器只考虑可以运行的任务,这些任务被放在适当的调度队列当中。不可运行的任务和正在等待各种I/O操作或内核事件的任务被放入另一个数据结构当中,即等待队列。每一种任务可能需要等待的事件对应了一个等待队列。等待队列的头包含一个指向任务链表的指针及一枚自旋锁。为了保证等待队列可以在主内核代码、中断处理函数或其他异步处理请求代码中进行并发操作,自旋锁是非常必要的。