转自:http://www.cppblog.com/mysileng/archive/2012/10/16/193380.html
1.概述
CFS(completely fair schedule)是最终被内核采纳的调度器。它从RSDL/SD中吸取了完全公平的思想,不再跟踪进程的睡眠时间,也不再企图区分交互式进程。它将所有 的进程都统一对待,这就是公平的含义。CFS的算法和实现都相当简单,众多的测试表明其性能也非常优越。
CFS 背后的主要想法是维护为任务提供处理器时间方面的平衡(公平性)。这意味着应给进程分配相当数量的处理器。分给某个任务的时间失去平衡时(意味着一个或多个任务相对于其他任务而言未被给予相当数量的时间),应给失去平衡的任务分配时间,让其执行。
CFS抛弃了时间片,抛弃了复杂的算法,从一个新的起点开始了调度器的新时代,最开始的2.6.23版本,CFS提供一个虚拟的时钟,所有进程复用这个虚拟时钟的时间,CFS将时钟的概念从底层体系相关的硬件中抽象出来,进程调度模块直接和这个虚拟的时钟接口 而不必再为硬件时钟操作而操心,如此一来,整个进程调度模块就完整了,从时钟到调度算法,到不同进程的不同策略,全部都由虚拟系统提供,也正是在这个新的内核,引入了调度类。因此新的调度器就是不同特性的进程在统一的虚拟时钟下按照不同的策略被调度。
按照作者Ingo Molnar的说法:"CFS百分之八十的工作可以用一句话概括:CFS在真实的硬件上模拟了完全理想的多任务处理器"。在“完全理想的多任务处理器 “下,每个进程都能同时获得CPU的执行时间。当系统中有两个进程时,CPU的计算时间被分成两份,每个进程获得50%。然而在实际的硬件上,当一个进程占用CPU时,其它进程就必须等待。这就产生了不公平。
2.相关概念
调度实体(sched entiy):就是调度的对象,可以理解为进程。
虚拟运行时间(vruntime):即每个调度实体的运行时间。任务的虚拟运行时间越小, 意味着任务被允许访问服务器的时间越短 — 其对处理器的需求越高。
公平调度队列(cfs_rq):采取公平调度的调度实体的运行队列。
3.CFS的核心思想
全公平调度器(CFS)的设计思想是:在一个真实的硬件上模型化一个理想的、精确的多任务CPU。该理想CPU模型运行在100%的负荷、在精确 平等速度下并行运行每个任务,每个任务运行在1/n速度下,即理想CPU有n个任务运行,每个任务的速度为CPU整个负荷的1/n。
由于真实硬件上,每次只能运行一个任务,这就得引入"虚拟运行时间"(virtual runtime)的概念,虚拟运行时间为一个任务在理想CPU模型上执行的下一个时间片(timeslice)。实际上,一个任务的虚拟运行时间为考虑到 运行任务总数的实际运行时间。
CFS 背后的主要想法是维护为任务提供处理器时间方面的平衡(公平性)。CFS为了体现的公平表现在2个方面
(1)进程的运行时间相等
CFS 在叫做虚拟运行时 的地方维持提供给某个任务的时间量。任务的虚拟运行时越小, 意味着任务被允许访问服务器的时间越短 — 其对处理器的需求越高。
假设runqueue中有n个进程,当前进程运行了 10ms。在“完全理想的多任务处理器”中,10ms应该平分给n个进程(不考虑各个进程的nice值),因此当前进程应得的时间是(10/n)ms,但 是它却运行了10ms。所以CFS将惩罚当前进程,使其它进程能够在下次调度时尽可能取代当前进程。最终实现所有进程的公平调度。
(2)睡眠的进程进行补偿
CFS 还包含睡眠公平概念以便确保那些目前没有运行的任务(例如,等待 I/O)在其最终需要时获得相当份额的处理器。
CFS调度器的运行时间是O(logN),而以前的调度器的运行时间是O(1),这是不是就是说CFS的效率比O(1)的更差呢?
答案并不是那样,我们知道 CFS调度器下的运行队列是基于红黑树组织的,找出下一个进程就是截下左下角的节点,固定时间完成,所谓的O(logN)指的是插入时间,可是红黑树的统 计性能是不错的,没有多大概率真的用得了那么多时间,因为红节点和黑节点的特殊排列方式既保证了树的一定程度的平衡,又不至于花太多的时间来维持这种平 衡,插入操作大多数情况下都可以很快的完成,特别是对于组织得相当好的数据。
4.CFS的实现
4.1 2.6.23 VS 2.6.25
在2.6.23内核中,刚刚实现的CFS调度器显得很淳朴,每次的时钟滴答中都会将当前进程先出队,推进其虚拟时钟和系统虚拟时钟后再入队,然后判断红黑 树的左下角的进程是否还是当前进程而抉择是否要调度,这种调度器的key的计算是用当前的虚拟时钟减去待计算进程的等待时间,如果该计算进程在运行,那么 其等待时间就是负值,这样,等待越长的进程key越小,从而越容易被选中投入运行;
在2.6.25内核以后实现了一种更为简单的方式,就是设置一个运行队列的虚拟时钟,它单调增长并且跟踪该队列的最小虚拟时钟的进程,key值由进程的 vruntime和队列的虚拟时钟的差值计算,这种方式就是真正的追赶, 比2.6.23实现的简单,但是很巧妙,不必在每次时钟滴答中都将当前进程出队,入队,而是根据当前进程实际运行的时间和理想应该运行的时间判断是否应该 调度。
4.2红黑树
与之前的 Linux 调度器不同,它没有将任务维护在运行队列中,CFS 维护了一个以时间为顺序的红黑树(参见下图)。 红黑树 是一个树,具有很多有趣、有用的属性。首先,它是自平衡的,这意味着树上没有路径比任何其他路径长两倍以上。 第二,树上的运行按 O(log n) 时间发生(其中 n 是树中节点的数量)。这意味着您可以快速高效地插入或删除任务。
任务存储在以时间为顺序的红黑树中(由 sched_entity 对象表示),对处理器需求最多的任务 (最低虚拟运行时)存储在树的左侧,处理器需求最少的任务(最高虚拟运行时)存储在树的右侧。 为了公平,调度器先选取红黑树最左端的节点调度为下一个以便保持公平性。任务通过将其运行时间添加到虚拟运行时, 说明其占用 CPU 的时间,然后如果可运行,再插回到树中。这样,树左侧的任务就被给予时间运行了,树的内容从右侧迁移到左侧以保持公平。 因此,每个可运行的任务都会追赶其他任务以维持整个可运行任务集合的执行平衡。
4.3 CFS内部原理
Linux 内的所有任务都由称为 task_struct 的任务结构表示。该结构完整地描述了任务并包括了任务的当前状态、其堆栈、进程标识、优先级(静态和动态)等等。您可以在 ./linux/include/linux/sched.h 中找到这些内容以及相关结构。 但是因为不是所有任务都是可运行的,您在 task_struct 中不会发现任何与 CFS 相关的字段。 相反,会创建一个名为 sched_entity 的新结构来跟踪调度信息(参见下图)。
树的根通过 rb_root 元素通过 cfs_rq 结构(在 ./kernel/sched.c 中)引用。红黑树的叶子不包含信息,但是内部节点代表一个或多个可运行的任务。红黑树的每个节点都由 rb_node 表示,它只包含子引用和父对象的颜色。 rb_node 包含在 sched_entity 结构中,该结构包含 rb_node 引用、负载权重以及各种统计数据。最重要的是, sched_entity 包含 vruntime(64 位字段),它表示任务运行的时间量,并作为红黑树的索引。 最后,task_struct 位于顶端,它完整地描述任务并包含 sched_entity 结构。
CFS 调度函数非常简单。 在 ./kernel/sched.c 中的 schedule() 函数中,它会先抢占当前运行任务(除非它通过 yield() 代码先抢占自己)。注意 CFS 没有真正的时间切片概念用于抢占,因为抢占时间是可变的。 当前运行任务(现在被抢占的任务)通过对 put_prev_task 调用(通过调度类)返回到红黑树。 当 schedule 函数开始确定下一个要调度的任务时,它会调用 pick_next_task 函数。此函数也是通用的(在 ./kernel/sched.c 中),但它会通过调度器类调用 CFS 调度器。 CFS 中的 pick_next_task 函数可以在 ./kernel/sched_fair.c(称为 pick_next_task_fair())中找到。 此函数只是从红黑树中获取最左端的任务并返回相关 sched_entity。通过此引用,一个简单的 task_of() 调用确定返回的 task_struct 引用。通用调度器最后为此任务提供处理器。
4.4 CFS的优先级
CFS 不直接使用优先级而是将其用作允许任务执行的时间的衰减系数。 低优先级任务具有更高的衰减系数,而高优先级任务具有较低的衰减系数。 这意味着与高优先级任务相比,低优先级任务允许任务执行的时间消耗得更快。 这是一个绝妙的解决方案,可以避免维护按优先级调度的运行队列。
CFS(completely fair schedule)是最终被内核采纳的调度器。它从RSDL/SD中吸取了完全公平的思想,不再跟踪进程的睡眠时间,也不再企图区分交互式进程。它将所有 的进程都统一对待,这就是公平的含义。CFS的算法和实现都相当简单,众多的测试表明其性能也非常优越。
CFS 背后的主要想法是维护为任务提供处理器时间方面的平衡(公平性)。这意味着应给进程分配相当数量的处理器。分给某个任务的时间失去平衡时(意味着一个或多个任务相对于其他任务而言未被给予相当数量的时间),应给失去平衡的任务分配时间,让其执行。
CFS抛弃了时间片,抛弃了复杂的算法,从一个新的起点开始了调度器的新时代,最开始的2.6.23版本,CFS提供一个虚拟的时钟,所有进程复用这个虚拟时钟的时间,CFS将时钟的概念从底层体系相关的硬件中抽象出来,进程调度模块直接和这个虚拟的时钟接口 而不必再为硬件时钟操作而操心,如此一来,整个进程调度模块就完整了,从时钟到调度算法,到不同进程的不同策略,全部都由虚拟系统提供,也正是在这个新的内核,引入了调度类。因此新的调度器就是不同特性的进程在统一的虚拟时钟下按照不同的策略被调度。
按照作者Ingo Molnar的说法:"CFS百分之八十的工作可以用一句话概括:CFS在真实的硬件上模拟了完全理想的多任务处理器"。在“完全理想的多任务处理器 “下,每个进程都能同时获得CPU的执行时间。当系统中有两个进程时,CPU的计算时间被分成两份,每个进程获得50%。然而在实际的硬件上,当一个进程占用CPU时,其它进程就必须等待。这就产生了不公平。
2.相关概念
调度实体(sched entiy):就是调度的对象,可以理解为进程。
虚拟运行时间(vruntime):即每个调度实体的运行时间。任务的虚拟运行时间越小, 意味着任务被允许访问服务器的时间越短 — 其对处理器的需求越高。
公平调度队列(cfs_rq):采取公平调度的调度实体的运行队列。
3.CFS的核心思想
全公平调度器(CFS)的设计思想是:在一个真实的硬件上模型化一个理想的、精确的多任务CPU。该理想CPU模型运行在100%的负荷、在精确 平等速度下并行运行每个任务,每个任务运行在1/n速度下,即理想CPU有n个任务运行,每个任务的速度为CPU整个负荷的1/n。
由于真实硬件上,每次只能运行一个任务,这就得引入"虚拟运行时间"(virtual runtime)的概念,虚拟运行时间为一个任务在理想CPU模型上执行的下一个时间片(timeslice)。实际上,一个任务的虚拟运行时间为考虑到 运行任务总数的实际运行时间。
CFS 背后的主要想法是维护为任务提供处理器时间方面的平衡(公平性)。CFS为了体现的公平表现在2个方面
(1)进程的运行时间相等
CFS 在叫做虚拟运行时 的地方维持提供给某个任务的时间量。任务的虚拟运行时越小, 意味着任务被允许访问服务器的时间越短 — 其对处理器的需求越高。
假设runqueue中有n个进程,当前进程运行了 10ms。在“完全理想的多任务处理器”中,10ms应该平分给n个进程(不考虑各个进程的nice值),因此当前进程应得的时间是(10/n)ms,但 是它却运行了10ms。所以CFS将惩罚当前进程,使其它进程能够在下次调度时尽可能取代当前进程。最终实现所有进程的公平调度。
(2)睡眠的进程进行补偿
CFS 还包含睡眠公平概念以便确保那些目前没有运行的任务(例如,等待 I/O)在其最终需要时获得相当份额的处理器。
CFS调度器的运行时间是O(logN),而以前的调度器的运行时间是O(1),这是不是就是说CFS的效率比O(1)的更差呢?
答案并不是那样,我们知道 CFS调度器下的运行队列是基于红黑树组织的,找出下一个进程就是截下左下角的节点,固定时间完成,所谓的O(logN)指的是插入时间,可是红黑树的统 计性能是不错的,没有多大概率真的用得了那么多时间,因为红节点和黑节点的特殊排列方式既保证了树的一定程度的平衡,又不至于花太多的时间来维持这种平 衡,插入操作大多数情况下都可以很快的完成,特别是对于组织得相当好的数据。
4.CFS的实现
4.1 2.6.23 VS 2.6.25
在2.6.23内核中,刚刚实现的CFS调度器显得很淳朴,每次的时钟滴答中都会将当前进程先出队,推进其虚拟时钟和系统虚拟时钟后再入队,然后判断红黑 树的左下角的进程是否还是当前进程而抉择是否要调度,这种调度器的key的计算是用当前的虚拟时钟减去待计算进程的等待时间,如果该计算进程在运行,那么 其等待时间就是负值,这样,等待越长的进程key越小,从而越容易被选中投入运行;
在2.6.25内核以后实现了一种更为简单的方式,就是设置一个运行队列的虚拟时钟,它单调增长并且跟踪该队列的最小虚拟时钟的进程,key值由进程的 vruntime和队列的虚拟时钟的差值计算,这种方式就是真正的追赶, 比2.6.23实现的简单,但是很巧妙,不必在每次时钟滴答中都将当前进程出队,入队,而是根据当前进程实际运行的时间和理想应该运行的时间判断是否应该 调度。
4.2红黑树
与之前的 Linux 调度器不同,它没有将任务维护在运行队列中,CFS 维护了一个以时间为顺序的红黑树(参见下图)。 红黑树 是一个树,具有很多有趣、有用的属性。首先,它是自平衡的,这意味着树上没有路径比任何其他路径长两倍以上。 第二,树上的运行按 O(log n) 时间发生(其中 n 是树中节点的数量)。这意味着您可以快速高效地插入或删除任务。
任务存储在以时间为顺序的红黑树中(由 sched_entity 对象表示),对处理器需求最多的任务 (最低虚拟运行时)存储在树的左侧,处理器需求最少的任务(最高虚拟运行时)存储在树的右侧。 为了公平,调度器先选取红黑树最左端的节点调度为下一个以便保持公平性。任务通过将其运行时间添加到虚拟运行时, 说明其占用 CPU 的时间,然后如果可运行,再插回到树中。这样,树左侧的任务就被给予时间运行了,树的内容从右侧迁移到左侧以保持公平。 因此,每个可运行的任务都会追赶其他任务以维持整个可运行任务集合的执行平衡。
4.3 CFS内部原理
Linux 内的所有任务都由称为 task_struct 的任务结构表示。该结构完整地描述了任务并包括了任务的当前状态、其堆栈、进程标识、优先级(静态和动态)等等。您可以在 ./linux/include/linux/sched.h 中找到这些内容以及相关结构。 但是因为不是所有任务都是可运行的,您在 task_struct 中不会发现任何与 CFS 相关的字段。 相反,会创建一个名为 sched_entity 的新结构来跟踪调度信息(参见下图)。
树的根通过 rb_root 元素通过 cfs_rq 结构(在 ./kernel/sched.c 中)引用。红黑树的叶子不包含信息,但是内部节点代表一个或多个可运行的任务。红黑树的每个节点都由 rb_node 表示,它只包含子引用和父对象的颜色。 rb_node 包含在 sched_entity 结构中,该结构包含 rb_node 引用、负载权重以及各种统计数据。最重要的是, sched_entity 包含 vruntime(64 位字段),它表示任务运行的时间量,并作为红黑树的索引。 最后,task_struct 位于顶端,它完整地描述任务并包含 sched_entity 结构。
CFS 调度函数非常简单。 在 ./kernel/sched.c 中的 schedule() 函数中,它会先抢占当前运行任务(除非它通过 yield() 代码先抢占自己)。注意 CFS 没有真正的时间切片概念用于抢占,因为抢占时间是可变的。 当前运行任务(现在被抢占的任务)通过对 put_prev_task 调用(通过调度类)返回到红黑树。 当 schedule 函数开始确定下一个要调度的任务时,它会调用 pick_next_task 函数。此函数也是通用的(在 ./kernel/sched.c 中),但它会通过调度器类调用 CFS 调度器。 CFS 中的 pick_next_task 函数可以在 ./kernel/sched_fair.c(称为 pick_next_task_fair())中找到。 此函数只是从红黑树中获取最左端的任务并返回相关 sched_entity。通过此引用,一个简单的 task_of() 调用确定返回的 task_struct 引用。通用调度器最后为此任务提供处理器。
4.4 CFS的优先级
CFS 不直接使用优先级而是将其用作允许任务执行的时间的衰减系数。 低优先级任务具有更高的衰减系数,而高优先级任务具有较低的衰减系数。 这意味着与高优先级任务相比,低优先级任务允许任务执行的时间消耗得更快。 这是一个绝妙的解决方案,可以避免维护按优先级调度的运行队列。