全方位剖析 Linux 操作系统,太全了( 十 )


下面我们更详细的讨论一下 Linux 系统的两个调度算法,它们的内部与调度队列(runqueue) 的设计很相似 。运行队列有一个数据结构用来监视系统中所有可运行的任务并选择下一个可以运行的任务 。每个运行队列和系统中的每个 CPU 有关 。
Linux O(1) 调度器是历史上很流行的一个调度器 。这个名字的由来是因为它能够在常数时间内执行任务调度 。在 O(1) 调度器里,调度队列被组织成两个数组,一个是任务正在活动的数组,一个是任务过期失效的数组 。如下图所示,每个数组都包含了 140 个链表头,每个链表头具有不同的优先级 。

全方位剖析 Linux 操作系统,太全了

文章插图
 
大致流程如下:
调度器从正在活动数组中选择一个优先级最高的任务 。如果这个任务的时间片过期失效了,就把它移动到过期失效数组中 。如果这个任务阻塞了,比如说正在等待 I/O 事件,那么在它的时间片过期失效之前,一旦 I/O 操作完成,那么这个任务将会继续运行,它将被放回到之前正在活动的数组中,因为这个任务之前已经消耗一部分 CPU 时间片,所以它将运行剩下的时间片 。当这个任务运行完它的时间片后,它就会被放到过期失效数组中 。一旦正在活动的任务数组中没有其他任务后,调度器将会交换指针,使得正在活动的数组变为过期失效数组,过期失效数组变为正在活动的数组 。使用这种方式可以保证每个优先级的任务都能够得到执行,不会导致线程饥饿 。
在这种调度方式中,不同优先级的任务所得到 CPU 分配的时间片也是不同的,高优先级进程往往能得到较长的时间片,低优先级的任务得到较少的时间片 。
这种方式为了保证能够更好的提供服务,通常会为 交互式进程 赋予较高的优先级,交互式进程就是用户进程 。
Linux 系统不知道一个任务究竟是 I/O 密集型的还是 CPU 密集型的,它只是依赖于交互式的方式,Linux 系统会区分是静态优先级 还是 动态优先级 。动态优先级是采用一种奖励机制来实现的 。奖励机制有两种方式:奖励交互式线程、惩罚占用 CPU 的线程 。在 Linux O(1) 调度器中,最高的优先级奖励是 -5,注意这个优先级越低越容易被线程调度器接受,所以最高惩罚的优先级是 +5 。具体体现就是操作系统维护一个名为 sleep_avg 的变量,任务唤醒会增加 sleep_avg 变量的值,当任务被抢占或者时间量过期会减少这个变量的值,反映在奖励机制上 。
O(1) 调度算法是 2.6 内核版本的调度器,最初引入这个调度算法的是不稳定的 2.5 版本 。早期的调度算法在多处理器环境中说明了通过访问正在活动数组就可以做出调度的决定 。使调度可以在固定的时间 O(1) 完成 。
O(1) 调度器使用了一种 启发式 的方式,这是什么意思?
在计算机科学中,启发式是一种当传统方式解决问题很慢时用来快速解决问题的方式,或者找到一个在传统方法无法找到任何精确解的情况下找到近似解 。
O(1) 使用启发式的这种方式,会使任务的优先级变得复杂并且不完善,从而导致在处理交互任务时性能很糟糕 。
为了改进这个缺点,O(1) 调度器的开发者又提出了一个新的方案,即 公平调度器(Completely Fair Scheduler, CFS) 。CFS 的主要思想是使用一颗红黑树作为调度队列 。
数据结构太重要了 。
CFS 会根据任务在 CPU 上的运行时间长短而将其有序地排列在树中,时间精确到纳秒级 。下面是 CFS 的构造模型
全方位剖析 Linux 操作系统,太全了

文章插图
 
CFS 的调度过程如下:
CFS 算法总是优先调度哪些使用 CPU 时间最少的任务 。最小的任务一般都是在最左边的位置 。当有一个新的任务需要运行时,CFS 会把这个任务和最左边的数值进行对比,如果此任务具有最小时间值,那么它将进行运行,否则它会进行比较,找到合适的位置进行插入 。然后 CPU 运行红黑树上当前比较的最左边的任务 。
在红黑树中选择一个节点来运行的时间可以是常数时间,但是插入一个任务的时间是 O(loog(N)),其中 N 是系统中的任务数 。考虑到当前系统的负载水平,这是可以接受的 。
调度器只需要考虑可运行的任务即可 。这些任务被放在适当的调度队列中 。不可运行的任务和正在等待的各种 I/O 操作或内核事件的任务被放入一个等待队列中 。等待队列头包含一个指向任务链表的指针和一个自旋锁 。自旋锁对于并发处理场景下用处很大 。


推荐阅读