Kevin.Liu 2012/10/20 ~2012/10/27 内核版本:2.6.24
博客:http://janneo.blog.sohu.com
Linux2.6.24内核,调度器章节的笔记
linux调度原理
linux调度器
本文只是为了方便今后复习整理的读书笔记,仅仅是将现有的知识用我自己的语言(和图画)进行重新表述,没有什么所谓的原创,都是参考他人的或者参考linux内核源码及文档。
博客中图片丢失,排版也不方便,因此建议下载pdf文档,地址:http://download.csdn.net/detail/janneoevans/4699128
主要参考《深入理解Linux内核架构》(Wolfgang Mauerer)一书。
另外还参考了:
对switch_to的理解 |
http://home.ustc.edu.cn/~hchunhui/linux_sched.html#sec9 和 郭海林 同学的ppt |
主调度器执行的时机 |
|
进程的状态切换 |
http://www.ibm.com/developerworks/linux/library/l-task-killable/ |
虚拟运行时间部分 |
|
组调度 |
题外话
此文对linux内核的理解或者表述很有可能出现多处谬误,希望您在读的时候保持怀疑的态度。如果发现错误了希望您能帮我指出,免得此文误导更多人,谢谢!
注意:文中出现的名字“队列”仅仅是一个普通中文名词,表示具有先后顺序的一串事物,不是指抽象数据结构中先进先出的结构体queue.
本文章也是“吃百家饭”而成的,可以任意转载,不要求注明原作者,但是你得保证我在更新了文章中的错误时,你也能够及时更新你转载的副本,否则错误会蔓延下去。
目录
Linux2.6.24内核,调度器章节的笔记
本文只是为了方便今后复习整理的读书笔记,仅仅是将现有的知识用我自己的语言(和图画)进行重新表述,没有什么所谓的原创,都是参考他人的或者参考linux内核源码及文档。
主要参考《深入理解Linux内核架构》(Wolfgang Mauerer)一书。
另外还参考了:
对switch_to的理解 |
http://home.ustc.edu.cn/~hchunhui/linux_sched.html#sec9 和 郭海林 同学的ppt |
主调度器执行的时机 |
|
进程的状态切换 |
http://www.ibm.com/developerworks/linux/library/l-task-killable/ |
虚拟运行时间部分 |
|
组调度 |
题外话
此文对linux内核的理解或者表述很有可能出现多处谬误,希望您在读的时候保持怀疑的态度。如果发现错误了希望您能帮我指出,免得此文误导更多人,谢谢!
注意:文中出现的名字“队列”仅仅是一个普通中文名词,表示具有先后顺序的一串事物,不是指抽象数据结构中先进先出的结构体queue.
本文章也是“吃百家饭”而成的,可以任意转载,不要求注明原作者,但是你得保证我在更新了文章中的错误时,你也能够及时更新你转载的副本,否则错误会蔓延下去。
目录
1. 调度器相关的功能结构概述
在学习调度器前,先从整体上做一个了解,对整体构成有一个清晰的认识,然后在深入细节,才不会像我一样在学习的过程中迷失在细节里。
1.1. 运行(Run Queue)队列
原理概述
linux内核用结构体rq(struct rq)将处于就绪(ready)状态的进程组织在一起。
rq结构体包含cfs和rt成员,分别表示两个就绪队列:cfs就绪队列用于组织就绪的普通进程(这个队列上的进程用完全公平调度器进行调度);rt就绪队列用于用于组织就绪的实时进程(该队列上的进程用实时调度器调度)。
在多核系统中,每个CPU对应一个rq结构体。
Figure.就绪队列简要示意图
细节
cfs队列实际上是用红黑树组织的,rt队列是用链表组织的。
在这里只需要知道如下性质就行了:红黑树是一种二叉搜索树,大小关系是:左孩子<父节点<右边,即最左边的节点的值最小(如果对该内容感兴趣可以参考数据结构和算法分析相关书籍)。
有几个CPU就会有几个rq结构体,所有的结构体保存在 一个数组中(即runqueues)。
1.2. 核心调度器
原理概述
进程调度与两个调度函数有关:scheduler_tick()和schedule(),这两者分别被称作周期性调度器(或周期性调度函数)和主调度器(或主调度函数)。两者合在一起被称作通用调度器(或者核心调度器)(这里一定要分清几个名词分别代表什么意思,不要像我第一次读到书中这个部分时一样,搞定一头雾水,完全不知所云)
1.1.1. 主调度器
在我们通常的概念中:调度器就负责将CPU使用权限从一个进程切换到另一个进程。完成这个工作的这其实就是Linux内核中所谓的主调度器。
上图中,三种不同颜色的长条分别表示CPU分配给进程A、B、C的一小段执行时间,执行顺序是:A,B,C。竖直的虚线表示当前时间,也就是说;A已经在CPU上执行完CPU分配给它的时间,马上轮到B执行了。这时主调度器shedule就负责完成相关处理工作然后将CPU的使用权交给进程B。
总之,主调度器的工作就是完成进程间的切换。
1.1.2. 周期性调度器
再来看看周期性调度器都干些什么吧。同样是刚才的那幅图,不过现在我们关注的不是从进程A切换到进程B这个过程,而是把A在CPU上执行的过程放大后观察细节。
在A享用它得到的CPU时间的过程中,系统会定时调用周期性调度器(即定时执行周期性调度函数scheduler_tick())。
在此版本的内核中,这个周期为10ms(这个10ms是这样得来的:内中定义了一个宏变量:HZ=100,它表示每秒钟周期性调度器执行的次数,那么时间间隔t=1/HZ=1/100s=10ms。10ms是个什么概念呢,我们粗略地计算一下:如果周期性调度程序每次执行100条指令,每秒执行100次,那么一秒钟周期性调度器在CPU上执行的指令就是1万条。如果主频为1GHz的处理器每秒钟执行10亿条指令,就相当于,周期性调度器消耗的CPU只占CPU总处理能力的 1万/10亿=10万分之一,微乎其微)。为了方便理解,上图将A获得的时间段分成长度为10ms的小片(注意:只是为了方便讲解,假想成这样的,内核并没有做这样的划分)。
周期性调度器每10ms执行一次,那它都干了些什么呢?它只是更新了一些统计信息。例如:进程A的结构体的成员sum_exec_runtime记录了A在CPU上运行的总时间,周期性调度器会更新该时间为:sum_exec_runtime+=10ms(这种说法不准确,细节信息后续内容会讲到)。
我第一次在书中看到周期性调度器的时候,就没法儿理解,它又不负责进程切换,怎么能称之为调度器呢,这未免也太误导读者了吧。要记住一点:它不负责进程切换。
细节
周期性调度器是用中断实现的:系统定时产生一个中断,然后在中断过程中执行scheduler_tick()函数,执行完毕后将CPU使用权限还给A(有可能不会还给A了,细节后续在讨论),下一个时间点到了,系统会再次产生中断,然后去执行scheduler_tick()函数。(中断过程对进程A是透明的,所以A是一个傻子,它以为自己连续享用了自己得到的CPU时间段,其实它中途被scheduler_tick()中断过很多次)。
小结:
主调度器负责将CPU的使用权从一个进程切换到另一个进程。周期性调度器只是定时更新调度相关的统计信息。
1.3. 调度器类
原理概述
内核中定义了很多用于处理不同类型进程(普通进程、实时进程、idle进程)的处理函数,例如:将普通进程放入就绪队列的函数:enqueue_task_fair(),将实时进程放入就绪队列的函数enqueue_task_rt()。
调度器需要用到这些函数,如果需要将睡眠的进程重新放入就绪队列,会调用enque_task_XXX()函数。那么,调用哪一个?答:先判断该进程是什么类型的进程,如果是普通进程就调用enqueue_task_fair(),如果是实时进程就调用enqueue_task_rt()。(如下图)
这固然可行,但是内核不是这样做的。Linux内核把这些用于处理普通进程的函数用结构体实例fair_sched_class(该结构体的成员全是指向函数的指针)组织起来,把用于处理实时进程的函数用结构体实例rt_sched_class组织起来,把用于处理idle进程的函数用idle_sched_class组织起来。
然后将普通进程关联到fair_sched_class(task_A->sched_class= fair_sched_class),实时进程关联到rt_sched_class,idle进程关联到idle_sched_class,那么需要用到相关函数的时候就不需要判断进程是什么类型的了,而是直接调用该进程关联的函数就行了(如:task_A->sched_class->enqueue_task(rq,p, wakeup, head);)。
2. 就绪进程在队列中如何排序的
对linux内核调度器有了大概的了解,现在我们接着进入细节的学习。
首先看一看各个进程在就绪队列中究竟是怎样进行先后排序的(即,如何决定进程被调度的先后顺序)。
2.1. 实时进程
原理概述
所有就绪的的实时进程都被组织在rq结构体中的rt(struct rt_rq)就绪队列上,它的排序非常简单。
相同优先等级的实时进程被组织在同一个双向链表中。在本版本的内核中,实时进程的优先等级从[0~99],共100个等级,因此就有100个这样的链表。每个链表的表头记录在rt.active.queue中。
active是rt(struct rt_rq)的一个成员,是struct rt_prio_array类型的:
kernel/sched.c
struct rt_rq {
struct rt_prio_array active;
...
};
struct rt_prio_array的定义如下:
kernel/sched.c
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit fordelimiter */
struct list_head queue[MAX_RT_PRIO];
};
它包含两个成员,一个数组queue,用来存放各个优先级的实时进程的链表头。另一个是一个位图。位图中每一位对应一个实时进程的链表,如果优先级为5的链表上没有进程,那么为图中第5位就为0,反之则为1。如下图:
调度器选择实时进程进程执行时,先在优先级高的链表上查找,如果没有再依次找优先级低的。在同一个优先级中,进程被调度的先后顺序就是它在链表中的先后顺序。
2.2. 普通进程
原理概述
所有就绪的普通进程被组织在rq结构体中的cfs(struct cfs_rq)就绪队列上。这里为了方便把它称作"队列",它实际上是用红黑树进行组织的,红黑树是一种二叉搜索树:左孩子的值小于父节点,右孩子的值大于父节点,即最小的会出现在红黑树最左边。如下图所示:
红黑树的具体细节不做讨论,只需要记住排在最左边进程最先执行就行了。接下来的讨论中我们不关心它如何使使用红黑树进行组织的,只是简单地把它看做是一个具有先后顺序的队列就行了(再次提醒,这里的“队列”仅仅指具有先后顺序的一串事物,不是指抽象数据结构中先进先出的结构体queue)。
这个版本的linux内核中用于决定就绪进程在就绪队列中先后顺序的机理很简单,也很巧妙。直接讲它怎么实现的,对于读者来说要听明白应该也是轻而易举的;但是对于表述能力如此差的我来说,要讲明白则太过于勉强了,所以我就从最简单的"模型"开始一步一步构建"真实模型"。
提示:这一部分内容对调度器的理解不是特别重要,在这里却占用了很多的篇幅,如果不感兴趣最好是直接跳过,直接从进程优先权看起,或者先看完后面的内容,再回头看这部分。
以下内容,为了讲解方便,可能运用了类似于时间片的讲述方法,可能会让读者误以为该版本linux内核采用了时间片的管理方式。因此,需要特别声明一下,早期的内核中有时间片的概念,但是该版本的内核已经没有时间片的概念了。
细节
2.2.1. 模型建立
A. runtime公平模型
原理概述
这个模型的主要目的是要让各个进程尽可能的公平享用CPU时间。其机理如下:
CPU的总时间按就绪的进程数目等分给每个进程,每个进程在就绪队列中的先后顺序由它已享用的CPU时间(runtime)决定,已享用CPU时间短的排在队列的排前面,反之则排在队列后面。
为了好好阐述这个模型是怎么运作的,我们模拟一下进程被调度的过程:调度器每次在所有可运行的进程中挑一个runtime值最小的,让它运行4ms(进程也可以在这4ms还没用完的时候中途释放CPU),刚刚被选中的进程运行完4ms后,调度器再重新挑一个runtime值最小的。规定每次最多执行4ms是为了防止某个进程一直占用CPU不释放而提出的。记住我们这个模型的机理,下面根据一个例子看看具体调度流程什么怎样的:
a)初始的时候,三个进程都没有运行因此,runtime都等于0
进程 |
A |
B |
C |
|
runtime(ms) |
0 |
0 |
0 |
|
按runtime进行排序 |
A B C |
|||
b) 由于runtime值都等于0,假设初始顺序是A B C。那么将A放到CPU上执行,它执行了4ms后:
进程 |
A |
B |
C |
runtime(ms) |
4 |
0 |
0 |
按runtime进行排序 |
B C A |
c) 按进程排列的顺序,接下来该B执行,假设B只执行了2ms
进程 |
A |
B |
C |
runtime(ms) |
4 |
2 |
0 |
按runtime进行排序 |
C B A |
d) 假设B执行了2ms的时候产生特殊事件激活了调度器,调度器选择下一个进程执行。进程C的runtime最小,因此选择C。
C执行了4ms:
进程 |
A |
B |
C |
runtime(ms) |
4 |
2 |
4 |
按runtime进行排序 |
B A C |
假设执行情况是这样的:进程在执行的时候需要从就绪队列中取下来,执行完毕后再放入就绪队列中(真实情况差不多上也是这样的,只是有一点小小的不同)。这就可以解释,为什么C和A有相同的runtime值,而C却排在A后面了。
e) 进程B的runtime值最小,接下来轮到进程B执行,B执行了4ms
进程 |
A |
B |
C |
runtime(ms) |
4 |
6 |
4 |
按runtime进行排序 |
A C B |
这就是最简单的模型:CPU总是挑已经执行时间最短的那个进程到CPU上运行。最简单的模型往往有很大的漏洞,我们接着往下看。
B. min_runtime公平模型。
原理概述
这个模型的主要目的是要让各个进程尽可能的公平享用CPU时间。其机理如下:
CPU的总时间按就绪的进程数目等分给每个进程,每个进程在就绪队列中的先后顺序由它已享用的CPU时间(runtime)决定,已享用CPU时间短的排在队列的排前面,反之则排在队列后面。
为了好好阐述这个模型是怎么运作的,我们模拟一下进程被调度的过程:调度器每次在所有可运行的进程中挑一个runtime值最小的,让它运行4ms(进程也可以在这4ms还没用完的时候中途释放CPU),刚刚被选中的进程运行完4ms后,调度器再重新挑一个runtime值最小的。规定每次最多执行4ms是为了防止某个进程一直占用CPU不释放而提出的。记住我们这个模型的机理,下面根据一个例子看看具体调度流程什么怎样的:
a)初始的时候,三个进程都没有运行因此,runtime都等于0
进程 |
A |
B |
C |
|
runtime(ms) |
0 |
0 |
0 |
|
按runtime进行排序 |
A B C |
|||
b) 由于runtime值都等于0,假设初始顺序是A B C。那么将A放到CPU上执行,它执行了4ms后:
进程 |
A |
B |
C |
runtime(ms) |
4 |
0 |
0 |
按runtime进行排序 |
B C A |
c) 按进程排列的顺序,接下来该B执行,假设B只执行了2ms
进程 |
A |
B |
C |
runtime(ms) |
4 |
2 |
0 |
按runtime进行排序 |
C B A |
d) 假设B执行了2ms的时候产生特殊事件激活了调度器,调度器选择下一个进程执行。进程C的runtime最小,因此选择C。
C执行了4ms:
进程 |
A |
B |
C |
runtime(ms) |
4 |
2 |
4 |
按runtime进行排序 |
B A C |
假设执行情况是这样的:进程在执行的时候需要从就绪队列中取下来,执行完毕后再放入就绪队列中(真实情况差不多上也是这样的,只是有一点小小的不同)。这就可以解释,为什么C和A有相同的runtime值,而C却排在A后面了。
e) 进程B的runtime值最小,接下来轮到进程B执行,B执行了4ms
进程 |
A |
B |
C |
runtime(ms) |
4 |
6 |
4 |
按runtime进行排序 |
A C B |
这就是最简单的模型:CPU总是挑已经执行时间最短的那个进程到CPU上运行。最简单的模型往往有很大的漏洞,我们接着往下看。
B. min_runtime公平模型。
刚刚讨论的runtime有一个致命的缺陷:
假设系统中只有A,B,C三个进程,并且这三个进程都各自运行了1000ms,那么他们的vruntime为:
进程 |
A |
B |
C |
runtime(ms) |
100 |
150 |
200 |
此时,进程D被创建了,它没有享用过CPU时间,因此它的runtime=0。这就产生大问题了,在接下来的100ms内,调度器总是会让进程D在CPU上执行,直到它运行了100ms以上。
如果你觉得这不是问题的话,你考虑一下这个情况:在已经运行了三年的服务器上,进程A,B,C的runtime都等于1年,这时进程D被创建了,那么接下来的一年内,就只有进程D在运行,其他的进程就慢慢等吧!
细节
谈到这里,我们就简单讨论一下什么叫“公平”吧。
假设进程A,B,C是同时创建并加入就绪队列的。
我的理解是:“操作系统应该在‘当前’将时间公平分配给‘当前’系统中的每个进程”。“当前”意味着什么:
a) 进程A,B,C在系统中共同经历了100+150+200=450ms,它们应该公平享用这段时间,即,每个进程应当执行150ms。
b) D进程被创建了,那么从现在起操作系统应该将CPU时间公平分配给这四个进程。
也就是说,从每个进程创建之时起,它就应该受到“不计历史”的公平待遇——无论其他进程之前运行了多长时间,当前的所有进程都应当一视同仁。
在这种情况下就很好办,如果A,B,C的runtime都等于150ms,那么D被创建的时候,也将它的runtime设置成150ms即可,这样在接下来的一段时间内,这四个进程会基本上公平地享用CPU时间。
然而,这只是我们的理想状态,因为第一点 a) 已经被打破了:A,B,C在过去的450ms内已经没有公平享用这段时间了。我们总不能等到A,B也执行了150ms之后才允许进程D被创建吧。
那么,我们要将D的runtime设置成多少才能:1.使得从现在起A,B,C,D进程尽可能地受到公平待遇。2.还要体现出进程A,B在前450ms内受到了不公平待遇。
如果将D的runtime设置为A,B,C中runtime最大的值显然对进程D不公,如果设置为他们中的最小值,那又对进程A不公。我们姑且先将进程D的runtime设置为A,B,C中最小的那个值(即100ms),这个处理方法显然达不到我们的目的,不过,总比将D->runtime设置成0要好多了。(具体怎么处理的,我们留到后面再讲)
这就是我们在runtime公平模型上改进之后得到的min_runtime公平模型。
C. weight优先级模型
原理概述
刚刚讨论了“公平”的问题,然而,在真实系统中,不同的进程具有不同的重要性。重要的进程我们应该尽量多分配CPU时间,不重要的进程应该少分配CPU时间。
为了达到这个目的,我们引入一个权重(weight)参数。即,每个进程有一个权重值,进程得到的CPU时间和这个权重值成正比。
基于刚才的min_runtime公平模型,我们怎样才能引入weight这个参数呢?
刚才的条件不变:
a) 调度器总是选择runtime值最小的进程放到CPU上执行。
b) 每次执行不得超过4ms,执行完4ms后或者因为某些事件中途激活了调度器,再选一个runtime值最小的(如果当前进程的runtime值最小,就还会选中它执行)。
c) 新创建的进程的runtime设置为当前可运行进程中最小的runtime值。
分析
假设进程A,B的权重分别是1,2,怎样才能使得在一段时间内进程B执行的时间是进程A的2倍呢?
在公平模型中我们用runtime来对进程进行排序,runtime小的排在前面:当进程A,B都分别执行了12ms(runtime都等于12)时,如果轮到进程B运行,它最多再运行4ms(B的runtime变成了16),调度器就会将CPU时间切换给进程A。
很容易发现,如果不在runtime上下功夫的话,进程B最多比进程A多执行4ms(除非进程A是个瞌睡虫,经常睡眠;别试图用催眠进程A的方法来解决该问题,这代价太大了。即便代价很小,那进程A醒来之后又总会排在进程B前面)。
换一个角度考虑,我们就很容易发现解决该问题的方法。我们想要达到这样一个效果:A进程执行了N ms后以及B进程执行了2N ms后他们应当具有相同的先后顺序,即他们的runtime值基本相同。
我们用runtime表示进程已经运行的时间,显然和以上表述矛盾,因此我们用另一个参数vruntime(虚拟运行时间)代替它。
我们检验一下是否可行:假设进程A每执行1ms,它的vruntime值就增加1。进程B每2ms,它的vruntime才增加1。(vruntime仅仅是一个数值,用来作为对进程进行排序的参考,不用来反映进程真实执行时间,别像我一样看书看到这一部分时一样陷入理解误区)
我们仍然用之前的过程模拟一下:
a) 初始的时候两个个进程都没有运行,runtime都等于0。
进程 |
A |
B |
|
weight |
1 |