linux内核调度器 调度原理(2.6.24笔记整理)

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.linuxdiyf.com/linux/201107/648.html

进程的状态切换

http://www.ibm.com/developerworks/linux/library/l-task-killable/

虚拟运行时间部分

http://blog.csdn.net/ustc_dylan/article/details/4140245

组调度

http://book.51cto.com/art/201005/200918.htm

 

题外话

此文对linux内核的理解或者表述很有可能出现多处谬误,希望您在读的时候保持怀疑的态度。如果发现错误了希望您能帮我指出,免得此文误导更多人,谢谢!

 注意:文中出现的名字“队列”仅仅是一个普通中文名词,表示具有先后顺序的一串事物,不是指抽象数据结构中先进先出的结构体queue.

本文章也是“吃百家饭”而成的,可以任意转载,不要求注明原作者,但是你得保证我在更新了文章中的错误时,你也能够及时更新你转载的副本,否则错误会蔓延下去。 

目录

Linux2.6.24内核,调度器章节的笔记...1

1.     调度器相关的功能结构概述...2

1.1.        运行(Run Queue)队列...2

1.2.        核心调度器...3

1.1.1.       主调度器...3

1.1.2.       周期性调度器...4

1.3.        调度器类...5

2.     就绪进程在队列中如何排序的...7

2.1.        实时进程...7

2.2.        普通进程...8

2.2.1.       模型建立...9

2.2.2.       抽象模型的总结...18

2.2.3.       真实模型概述...18

2.2.4.       对应关系...21

2.2.5.       真实模型详述...22

3.     进程优先权...30

4.     核心调度器...35

4.1.        周期性调度器...36

4.2.        主调度器...42

4.2.1.       代码块1.43

4.2.2.       代码块2.43

4.2.3.       代码块3.44

4.2.4.       代码块4.45

4.2.5.       总结...46

4.2.6.       switch_to.47

5.     进程的状态切换...54

 

Linux2.6.24内核,调度器章节的笔记

 本文只是为了方便今后复习整理的读书笔记,仅仅是将现有的知识用我自己的语言(和图画)进行重新表述,没有什么所谓的原创,都是参考他人的或者参考linux内核源码及文档。

主要参考《深入理解Linux内核架构》(Wolfgang Mauerer)一书。

另外还参考了:

switch_to的理解

http://home.ustc.edu.cn/~hchunhui/linux_sched.html#sec9  郭海林 同学的ppt

主调度器执行的时机

http://www.linuxdiyf.com/linux/201107/648.html 

进程的状态切换

http://www.ibm.com/developerworks/linux/library/l-task-killable/ 

虚拟运行时间部分

http://blog.csdn.net/ustc_dylan/article/details/4140245 

组调度

http://book.51cto.com/art/201005/200918.htm 

 

题外话

此文对linux内核的理解或者表述很有可能出现多处谬误,希望您在读的时候保持怀疑的态度。如果发现错误了希望您能帮我指出,免得此文误导更多人,谢谢!

 注意:文中出现的名字“队列”仅仅是一个普通中文名词,表示具有先后顺序的一串事物,不是指抽象数据结构中先进先出的结构体queue.

本文章也是“吃百家饭”而成的,可以任意转载,不要求注明原作者,但是你得保证我在更新了文章中的错误时,你也能够及时更新你转载的副本,否则错误会蔓延下去。 

目录

Linux2.6.24内核,调度器章节的笔记... 1

1.      调度器相关的功能结构概述... 2

1.1.         运行(Run Queue)队列... 2

1.2.         核心调度器... 3

1.1.1.        主调度器... 3

1.1.2.        周期性调度器... 4

1.3.         调度器类... 5

2.      就绪进程在队列中如何排序的... 7

2.1.         实时进程... 7

2.2.         普通进程... 8

2.2.1.        模型建立... 8

2.2.2.        抽象模型的总结... 17

2.2.3.        真实模型概述... 18

2.2.4.        对应关系... 21

2.2.5.        真实模型详述... 22

3.      进程优先权... 30

4.      核心调度器... 34

4.1.         周期性调度器... 36

4.2.         主调度器... 42

4.2.1.        代码块143

4.2.2.        代码块243

4.2.3.        代码块343

4.2.4.        代码块445

4.2.5.        总结... 45

4.2.6.        switch_to47

5.      进程的状态切换... 54

 

1.      调度器相关的功能结构概述

在学习调度器前,先从整体上做一个了解,对整体构成有一个清晰的认识,然后在深入细节,才不会像我一样在学习的过程中迷失在细节里。

1.1. 运行(Run Queue)队列

原理概述

linux内核用结构体rqstruct rq)将处于就绪(ready)状态的进程组织在一起。

rq结构体包含cfsrt成员,分别表示两个就绪队列:cfs就绪队列用于组织就绪的普通进程(这个队列上的进程用完全公平调度器进行调度)rt就绪队列用于用于组织就绪的实时进程(该队列上的进程用实时调度器调度)

在多核系统中,每个CPU对应一个rq结构体。

Figure.就绪队列简要示意图

linux内核调度器 调度原理(2.6.24笔记整理)_第1张图片

细节

cfs队列实际上是用红黑树组织的,rt队列是用链表组织的。

在这里只需要知道如下性质就行了:红黑树是一种二叉搜索树,大小关系是:左孩子<父节点<右边,即最左边的节点的值最小(如果对该内容感兴趣可以参考数据结构和算法分析相关书籍)。

有几个CPU就会有几个rq结构体,所有的结构体保存在 一个数组中(即runqueues)

1.2. 核心调度器

原理概述

进程调度与两个调度函数有关:scheduler_tick()schedule(),这两者分别被称作周期性调度器(或周期性调度函数)主调度器(或主调度函数)。两者合在一起被称作通用调度器(或者核心调度器)(这里一定要分清几个名词分别代表什么意思,不要像我第一次读到书中这个部分时一样,搞定一头雾水,完全不知所云)

1.1.1.     主调度器

在我们通常的概念中:调度器就负责将CPU使用权限从一个进程切换到另一个进程。完成这个工作的这其实就是Linux内核中所谓的主调度器

linux内核调度器 调度原理(2.6.24笔记整理)_第2张图片

上图中,三种不同颜色的长条分别表示CPU分配给进程ABC的一小段执行时间,执行顺序是:A,B,C。竖直的虚线表示当前时间,也就是说;A已经在CPU上执行完CPU分配给它的时间,马上轮到B执行了。这时主调度器shedule就负责完成相关处理工作然后将CPU的使用权交给进程B

总之,主调度器的工作就是完成进程间的切换。

1.1.2.     周期性调度器

再来看看周期性调度器都干些什么吧。同样是刚才的那幅图,不过现在我们关注的不是从进程A切换到进程B这个过程,而是把ACPU上执行的过程放大后观察细节。

A享用它得到的CPU时间的过程中,系统会定时调用周期性调度器(即定时执行周期性调度函数scheduler_tick())

linux内核调度器 调度原理(2.6.24笔记整理)_第3张图片

在此版本的内核中,这个周期为10ms(这个10ms是这样得来的:内中定义了一个宏变量:HZ=100,它表示每秒钟周期性调度器执行的次数,那么时间间隔t=1/HZ=1/100s=10ms10ms是个什么概念呢,我们粗略地计算一下:如果周期性调度程序每次执行100条指令,每秒执行100次,那么一秒钟周期性调度器在CPU上执行的指令就是1万条。如果主频为1GHz的处理器每秒钟执行10亿条指令,就相当于,周期性调度器消耗的CPU只占CPU总处理能力的 1/10亿=10万分之一,微乎其微)。为了方便理解,上图将A获得的时间段分成长度为10ms的小片(注意:只是为了方便讲解,假想成这样的,内核并没有做这样的划分)。

周期性调度器每10ms执行一次,那它都干了些什么呢?它只是更新了一些统计信息。例如:进程A的结构体的成员sum_exec_runtime记录了ACPU上运行的总时间,周期性调度器会更新该时间为: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内核调度器 调度原理(2.6.24笔记整理)_第4张图片

这固然可行,但是内核不是这样做的。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);)。

linux内核调度器 调度原理(2.6.24笔记整理)_第5张图片

 

2.      就绪进程在队列中如何排序的

linux内核调度器有了大概的了解,现在我们接着进入细节的学习。

首先看一看各个进程在就绪队列中究竟是怎样进行先后排序的(即,如何决定进程被调度的先后顺序)。

2.1. 实时进程

原理概述

所有就绪的的实时进程都被组织在rq结构体中的rt(struct rt_rq)就绪队列上,它的排序非常简单。

相同优先等级的实时进程被组织在同一个双向链表中。在本版本的内核中,实时进程的优先等级从[0~99],共100个等级,因此就有100个这样的链表。每个链表的表头记录在rt.active.queue中。

activert(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。如下图:

linux内核调度器 调度原理(2.6.24笔记整理)_第6张图片

调度器选择实时进程进程执行时,先在优先级高的链表上查找,如果没有再依次找优先级低的。在同一个优先级中,进程被调度的先后顺序就是它在链表中的先后顺序。

2.2. 普通进程

原理概述

所有就绪的普通进程被组织在rq结构体中的cfs(struct cfs_rq)就绪队列上。这里为了方便把它称作"队列",它实际上是用红黑树进行组织的,红黑树是一种二叉搜索树:左孩子的值小于父节点,右孩子的值大于父节点,即最小的会出现在红黑树最左边。如下图所示:

linux内核调度器 调度原理(2.6.24笔记整理)_第7张图片

红黑树的具体细节不做讨论,只需要记住排在最左边进程最先执行就行了。接下来的讨论中我们不关心它如何使使用红黑树进行组织的,只是简单地把它看做是一个具有先后顺序的队列就行了(再次提醒,这里的“队列”仅仅指具有先后顺序的一串事物,不是指抽象数据结构中先进先出的结构体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的时候产生特殊事件激活了调度器,调度器选择下一个进程执行。进程Cruntime最小,因此选择C

C执行了4ms

进程

A

B

C

runtime(ms)

4

2

4

按runtime进行排序

B  A  C

假设执行情况是这样的:进程在执行的时候需要从就绪队列中取下来,执行完毕后再放入就绪队列中(真实情况差不多上也是这样的,只是有一点小小的不同)。这就可以解释,为什么CA有相同的runtime值,而C却排在A后面了。

e)       进程Bruntime值最小,接下来轮到进程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不释放而提出的。记住我们这个模型的机理,下面根据一个例子看看具体调度流程什么怎样的:

linux内核调度器 调度原理(2.6.24笔记整理)_第8张图片

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的时候产生特殊事件激活了调度器,调度器选择下一个进程执行。进程Cruntime最小,因此选择C

C执行了4ms

进程

A

B

C

runtime(ms)

4

2

4

按runtime进行排序

B  A  C

假设执行情况是这样的:进程在执行的时候需要从就绪队列中取下来,执行完毕后再放入就绪队列中(真实情况差不多上也是这样的,只是有一点小小的不同)。这就可以解释,为什么CA有相同的runtime值,而C却排在A后面了。

e)       进程Bruntime值最小,接下来轮到进程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内,调度器总是会让进程DCPU上执行,直到它运行了100ms以上。

如果你觉得这不是问题的话,你考虑一下这个情况:在已经运行了三年的服务器上,进程A,B,Cruntime都等于1年,这时进程D被创建了,那么接下来的一年内,就只有进程D在运行,其他的进程就慢慢等吧!

细节

谈到这里,我们就简单讨论一下什么叫“公平”吧。

假设进程A,B,C是同时创建并加入就绪队列的。

我的理解是:“操作系统应该在‘当前’将时间公平分配给‘当前’系统中的每个进程”。“当前”意味着什么:

a)    进程A,B,C在系统中共同经历了100+150+200=450ms,它们应该公平享用这段时间,即,每个进程应当执行150ms

b)   D进程被创建了,那么从现在起操作系统应该将CPU时间公平分配给这四个进程。

也就是说,从每个进程创建之时起,它就应该受到“不计历史”的公平待遇——无论其他进程之前运行了多长时间,当前的所有进程都应当一视同仁。

在这种情况下就很好办,如果A,B,Cruntime都等于150ms,那么D被创建的时候,也将它的runtime设置成150ms即可,这样在接下来的一段时间内,这四个进程会基本上公平地享用CPU时间。

然而,这只是我们的理想状态,因为第一点 a) 已经被打破了:A,B,C在过去的450ms内已经没有公平享用这段时间了。我们总不能等到A,B也执行了150ms之后才允许进程D被创建吧。

那么,我们要将Druntime设置成多少才能:1.使得从现在起A,B,C,D进程尽可能地受到公平待遇。2.还要体现出进程A,B在前450ms内受到了不公平待遇。

如果将Druntime设置为A,B,Cruntime最大的值显然对进程D不公,如果设置为他们中的最小值,那又对进程A不公。我们姑且先将进程Druntime设置为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的权重分别是12,怎样才能使得在一段时间内进程B执行的时间是进程A2倍呢?

在公平模型中我们用runtime来对进程进行排序,runtime小的排在前面:当进程A,B都分别执行了12msruntime都等于12)时,如果轮到进程B运行,它最多再运行4msBruntime变成了16),调度器就会将CPU时间切换给进程A

很容易发现,如果不在runtime上下功夫的话,进程B最多比进程A多执行4ms(除非进程A是个瞌睡虫,经常睡眠;别试图用催眠进程A的方法来解决该问题,这代价太大了。即便代价很小,那进程A醒来之后又总会排在进程B前面)。

换一个角度考虑,我们就很容易发现解决该问题的方法。我们想要达到这样一个效果:A进程执行了N ms后以及B进程执行了2N ms后他们应当具有相同的先后顺序,即他们的runtime值基本相同。

我们用runtime表示进程已经运行的时间,显然和以上表述矛盾,因此我们用另一个参数vruntime(虚拟运行时间)代替它。

我们检验一下是否可行:假设进程A每执行1ms,它的vruntime值就增加1。进程B2ms,它的vruntime才增加1(vruntime仅仅是一个数值,用来作为对进程进行排序的参考,不用来反映进程真实执行时间,别像我一样看书看到这一部分时一样陷入理解误区)

我们仍然用之前的过程模拟一下:

linux内核调度器 调度原理(2.6.24笔记整理)_第9张图片

 

a)     初始的时候两个个进程都没有运行,runtime都等于0

进程

A

B

weight

1

2

runtime(ms)

0

0

vruntime

0

0

按vruntime进行排序

A B

       

b)   假设初始顺序为A在前,调度器选择了A,它运行了4ms

进程

A

B

 

weight

1

2

 

runtime(ms)

4

0

 

vruntime

4

0

 

按vruntime进行排序

B A

         

c)    Bvruntime最小,选择B运行,运行了4ms

进程

A

B

weight

1

2

runtime(ms)

4

4

vruntime

4

2

按vruntime进行排序

B A

d)    Bvruntime依旧最小,选择B运行,运行了4ms

进程

A

B

 

weight

1

2

 

runtime(ms)

4

8

 

vruntime

4

4

 

按vruntime进行排序

A B

         

e)    vruntime相同,但是A在前,选择A运行,运行了4ms

进程

A

B

weight

1

2

runtime(ms)

8

8

vruntime

8

4

按vruntime进行排序

B A

f)    Bvruntime依旧最小,选择B运行,运行了4ms

进程

A

B

weight

1

2

runtime(ms)

8

12

vruntime

4

6

按vruntime进行排序

B A

通过简单的模拟,我们发现vruntime的引入,果然达到了目的。

那么从现在起忘掉之前提出的runtime,取而代之的是vruntime:每个进程有一个vruntime值,调度器总是选择vruntime值最小的进程,放到CPU上执行,并且vruntime增长的速度和weight值成反比(为了节约篇幅,这里就不证明为什么vruntime的增长速度要这样设定了)。

D.   period模型

原理概述

早期版本中用到了时间片模型,从这个版本的内核开始就没有使用时间片的概念了。

在之前的模型中,每当进程运行4ms后(或者进程还未执行完4ms,就有特殊情况产生了:比如进程要睡眠)主调度器(schedule)就会重新选择一个vruntime值最小的进程来执行。现在我们不用时间片的概念了,那么主调度器(schedule)应该在什么时候启动并选择一个新的进程执行呢?

为了解决这个问题,我们引入一个叫period的参数。

这个模型也很简单:

系统设定了一个period(它表示一段时间),每个进程对应一个ideal_runtime值(称为理想欲运行时间)每个进程的ideal_runtime值是这样设定的:所有可运行进程的ideal_runtime值之和等于period,每个进程的ideal_runtime值的大小与它的权重weight成正比。

这个模型规定:每个进程每次获得CPU使用权,最多执行它对应的ideal_runtime这样长的时间。

例如:如果period=20ms,当前系统中只有A,B,C,D四个进程,它们的权重分别是:1234。那么Aideal_runtime=2ms,B,C,Dideal_runtime依次为4ms,6ms,8ms

我们就模拟一下这例子中各个进程的运行过程吧:

linux内核调度器 调度原理(2.6.24笔记整理)_第10张图片       

a)    初始情况如下:

进程

A

B

C

D

weight

1

2

3

4

ideal_runtime(ms)

2

4

6

8

runtime(ms)

0

0

0

0

vruntime

0

0

0

0

按vruntime进行排序

A B C D

           

b)    和前一个模型一样,vruntime的行走速度和权重值长反比,假设权重为1A进程的vruntime和实际runtime行走速度相同。A先执行,它执行了2ms,此时:

进程

A

B

C

D

weight

1

2

3

4

ideal_runtime(ms)

2

4

6

8

runtime(ms)

2

0

0

0

vruntime

2

0

0

0

按vruntime进行排序

B C D A

           

c)    A执行时间等于它的ideal_runtime了,调度器被激活,它重新选择一个vruntime值最小的进程(即B)执行,假设B运行了3ms

进程

A

B

C

D

weight

1

2

3

4

ideal_runtime(ms)

2

4

6

8

runtime(ms)

2

3

0

0

vruntime

2

1.5

0

0

按vruntime进行排序

C D B A

           

d)    假设此时某特殊事件激活了调度器,调度器进行重新调度,它仍然选择vruntime最小的,C进程,C进程运行了3ms:

进程

A

B

C

D

weight

1

2

3

4

ideal_runtime(ms)

2

4

6

8

runtime(ms)

2

3

3

0

vruntime

2

1.5

1

0

按vruntime进行排序

D C B  A

           

e)    假设此时又有某特殊事件产生,激活了调度器,调度器进行重新调度,选择进程D,它运行了8ms:

进程

A

B

C

D

weight

2

2

3

4

ideal_runtime(ms)

2

4

6

8

runtime(ms)

2

3

3

8

vruntime

2

1.5

1

2

按vruntime进行排序

C B  A D 

           

细节

f)    进程D运行的时间等于它的ideal_runtime,调度器被激活,重新选择一个进程运行,接着进程C被选中。

这一点很关键:C可以运行多长时间呢?也许你有可能跟我一样认为C最多可以运行3ms,因为它的ideal_runtime6ms,而它之前已经运行了3ms

如果是这样的你可能就误解了,在看看之前对ideal_runtime的定义,它只是要求,每个进程每次没调度的时候,运行时间不能超过它对应的ideal_runtime,上次进程C被调度的时候它只执行了3ms,没有超过它的ideal_runtime(6ms);但是,这次它又可以获得CPU使用权了,是新的一次调度了,与之前无关。因此,进程C最多可以运行6ms,那么接下来进程C运行了6ms:

a.       进程

A

B

C

D

weight

1

2

3

4

ideal_runtime(ms)

2

4

6

8

runtime(ms)

2

3

9

8

vruntime

2

1.5

3

2

按vruntime进行排序

B  A D C 

           

上面模拟了这么多步执行过程,最关键的就是第f)步。如果你理解错了(多半是前面的对period定义误导你了),认为C这次被调度最多可以执行3ms,那么我想你很可能认为:系统将CPU时间划分成一段一段的,每片长度为period,并且将他们按权重值分配给各个进程,并且规定它们在该period内最多执行ideal_runtime限定的时间,进入下一个period时间段后,系统又重新为各个进程分配ideal_runtime

如果这么理解的,那就有问题了。注意:CPU并没有把时间分成长度为period的时间段,系统仅仅限定了每个进程每次执行时不能超过它对应的ideal_time指定的时间长度。

我们这个简单的机制起到了这样一个作用:每个进程在就绪队列中等待的时间不会超过period,因为每个人进程获得CPU使用权后,如果它执行的时间等于它的ideal_runtime,那么它的vruntime基本上也就比其他所有进程的vruntime值高了,自然会排到队列的后面。(例如:上述A,B,C,D进程,如果A进程执行了2ms,B执行了4ms,C执行了6ms,他们的vruntime都等于2,即他们的vruntime肯定大于队列中那些还没被调度执行的进程的vruntime。注明:这里先不考虑睡眠的问题,睡眠的问题放到后面再讨论)

小结

     这个模型实现了这样一个效果:

a)    每个进程每次获得CPU使用权最多可以执行与它对应的ideal_runtime那么长的时间。

b)   如果,每个进程每次获得CPU使用权时它都执行了它对应的ideal_runtime那么长的时间,整个就绪队列的顺序会保持不变。

c)    如果某个进程某几次获得CPU使用权时运行的时间小于它ideal_time指定的时间(即它被调度时没有享用完它可以享用的最大时间),按照vruntime进行排序的机制会使得它尽量排在其他进程前,让它尽快把没有享用完的CPU时间弥补起来。

 

每个进程每次被调度不再受4ms(之前提到的这个4ms并非是早期版本中时间片的大小,而是为了说明问题随便指定的)的限制了,而是可以运行"任意"长时间。

要注意:我们讨论的这些模型都是针对普通进程的,与实时进程不相关。

总结

1.1.1.     抽象模型的总结

在下面的内容中,我们将上面的“period模型”成为"抽象模型",将上述模型简单总结起来基本上就是对该版本内核运用的整个机制的一个抽象了。我们对上述的机制进行一个总结(暂不考虑睡眠,抢占等细节):

a)    每个进程有一个权重值(weight),值越大,表示该进程越优先。

b)   每个进程还对应一个vruntime(虚拟时间),它是根据进程实际运行的时间runtime计算出来的。vruntime值不能反映进程执行的真实时间,只是用来作为系统判断接下来应该将CPU使用权交给哪个进程的依据——调度器总是选择vruntime值最小的进程执行。

c)    vruntime行走的速度和进程的weight成反比。

d)    为了保证每个进程在某段时间(period)内每个进程至少能执行一次,操作系统引入了ideal_runtime的概念,规定每个进程每次获得CPU使用权时,执行时间不能超过它对应的ideal_runtime值。达到该值就会激活调度器,让调度器再选择一个vruntime值最小的进程执行。

e)    每个进程的ideal_runtime长度与它的weight成正比。如果有N个进程那么:


1.1.1.     真实模型概述

细节

总结完抽象模型,我们接下来看看内核代码究竟是如何实现的吧。

先来从宏观上来看看,下图是一个(完全公平)就绪队列,就绪队列上有三个进程。


cfs_rq结构体

接下来看看内部,先看看就绪队列的结构体和调度相关的成员变量(蓝色的是与之前谈到的抽象模型有对应参数的):

linux内核调度器 调度原理(2.6.24笔记整理)_第11张图片

成员的定义如下:

a)   min_vruntime

我想你应该还记得,在讲min_runtime公平模型中遗留留下了一个问题:如果进程A,B,C在系统中运行了一段时间他们的runtime分别是100ms,150ms,200ms,新进程D被创建时它的runtime应该设置成什么?另外,如果进程D不是新创建的,而是睡眠很久之后醒来的,也会引发类似的问题。而这个变量就是用来解决这两个问题的(注意:并不是把进程Dvruntime值直接设置为该值,具体细节后续内容会讲到)。min_vruntime用来“跟踪”可运行进程中最小的vruntime值。之所以用“跟踪”一词,是因为它在某些情况下有的进程的vruntime值会比它小。min_vruntime的值初始的时候为0,周期性调度器负责周期性地用min_vruntime的值和可运行的进程中最小的vruntime值比较,如果该进程的vruntime值比min_vruntime,那么就将min_vruntime值更新为较大值。也就是说:min_vruntime的值只会随着时间的推移增加,不会减小。(再次提醒:这并不意味着min_vruntime的值比所有进程的vruntime都小。)

b)  load

先看看这样一个结构体structload_weight,它包含两个成员:第一个是weight,它就对应我们抽象模型中的权重值weight。第二个成员inv_weight是为了方便计算用的(它其实可以用weight计算得到),暂时不用去管他。

structload_weight {

 unsigned long weight, inv_weight; 

};

load.weight保存着cfs_rq队列上所有进程(包括当前正在运行的进程)的权重值weight的总和。

c)   nr_running

可运行进程的总数,即队列上进程的总数加上正在运行的那个进程(正在运行的进程不在就绪队列上)。

d)    tasks_timeline

之前讲过,cfs就绪队列是用红黑树来组织的,tasks_timeline指向红黑树的根节点。(只要记住它是根节点就行了,关于红黑树的细节暂时不需要了解,这里不会涉及到)。

e)    rb_leftmost

指向红黑树种最左边那个节点。即,vruntime值最小的那个进程对应的节点。

f)    curr

指向当前正在执行的可调度实体。调度器的调度单位不是进程,而是可调度实体。每个task_struct都嵌入了一个可调度实体sched_entity,所以每个进程是一个可调度实体。可以将多个进程捆绑在一起作为一个调度单位(即,调度实体)进行调度。因此,可调度实体可以是一个进程,也可以是多个进程构成的一个组。以免把事情弄复杂,在这里我们就把调度实体和进程看做是一样的吧,暂时把curr理解为指向当前正在运行的进程就可以了。

sched_entity结构体

sched_entity是可调度实体,暂时不管它的定义,姑且把它看做是一个进程吧。sched_entity与调度相关的结构有如下几个(蓝色的是与之前谈到的抽象模型有对应参数的):

linux内核调度器 调度原理(2.6.24笔记整理)_第12张图片

a)    loadstruct load_wegiht结构体,结构体中的weight值表示该进程的权重。

b)   vruntime,该进程已运行的虚拟时间,就是抽象模型中的vruntime

c)    sum_exec_runtime,表示进程已经执行的实际时间,对应抽象模型中的runtime

d)    pre_sum_exec_runtime,进程在本获得CPU使用权前,总的执行时间。pre_sum_exec_runtime-sum_exec_runtime等于进程本次获得CPU使用权后执行的总时间。

e)    exec_start,看它的命名好像以为它表示进程开始执行的时间,其实不是这样的。它跟周期性调度器有关。假设一个进程获得CPU使用权并连续执行10ms,在这100ms内,周期性调度器每10ms启动一次并且将进程的exec_start设置为当前时间。例如:周期性调度器在第20ms启动,并执行exec_start=now(now是当前时间)。那么到30ms时周期性再启动一次,它就可以用now-exec_start得到在这10ms时间内,进程已经消耗的CPU时间,然后再更新exec_start的值为当前时间。(这是为了准确计算进程已经消耗的CPU时间,因为理论上时间间隔是10ms,但是由于系统有延时程序真正执行的时间可能没有这么多)。

f)    on_rq,用来标识该实体是否在就绪队列上,如果在就绪队列上则该值为非零,反之则为0(其实细节不是这样的,后续会讲到)

1.1.2.     对应关系

原理概述

 我们关注的是“真实模型”是如何实现的,只需关注“抽象模型”是如何和“真实模型”映射起来的就行了,因此cfs_rq结构体和task_struct结构体中不是很相关的成员暂时不用去管它,只需要做一个简单了解就行了,因为待会引用代码时会涉及到。

 “抽象模型”和“真实模型”的对应关系如下(在这里,"->"仅仅是表示所属关系,不要和C语言中的操作符联系起来)

 抽象模型和真实模型的对应关系

抽象模型

真实模型

说明

task->runtime

task->se->sum_exec_runtime

每个进程对应一个可调度实体,在task_struct的结构体中,该实体就是成员变量se。

task->weight

task->se.load.weight

 

task->vruntime

task->se.vruntime

 

cfs_rq->load.weight

在抽象模型中,我们计算ideal_runtime的时候需要求所有进程的权重值的和,在实现的时候,没有求和的过程,而是把该值记录在就绪队列的load.weight中。

 

向就绪队列中添加新进程时,就加上新进程的权重值,进程被移出就绪队列时则减去被移除的进程的权重值。

 

cfs_rq->min_vruntime

该值用来解决之前在抽象模型中遗留的问题,所以在抽象模型中没有与之对应的值。

task->ideal_runtime

sched_slice( )函数

每个进程的ideal_runtime并没有用变量保存起来,而是在需要用到时用函数shed_slice( )计算得到。

公式跟抽象模型中公式一样:

period

__sched_period()函数

period也没有用变量来保存,也是在需要用到时由函数计算得到:

    在默认情况下period的值是20ms,当可运行进程数目超过5个时,period就等于:nr_running*4ms(nr_running是可运行进程的数目。

 

    上面提到的“20ms”由sysctl_sched_latency指定;“5”个由sched_nr_latency指定;“4ms”是由sysctl_sched_min_granularity指定的。

 

这样设定有它的目的,不与深究,以免迷失在细节里。

1.1.1.     真实模型详述

原理概述

讲完了变量的对应关系,我们来看看真实模型是怎么运作的。在抽象模型中vruntime决定了进程被调度的先后顺序,在真实模型中决定被调度的先后顺序的参数是entity_key(实例的键值),也是值最小的最先被调度。

这个值没有用变量保存起来,而是在需要的时候由函数entity_key()计算得到:

kernel/sched_fair.c

static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se) { 

return se->vruntime - cfs_rq->min_vruntime; 

}

entity_key()的定义可以看出,key=vruntime-cfs_rq->min_vruntime

其实它本质和我们的抽象模型一样的,只不过对进程进行排序时不是比较各个进程的vruntime值,而是将各个进程的vruntime减去一个相同的数再比较大小。

就好像要比较AB两个数的大小,等同于比较A-CB-C的大小一样。(遗留问题。其实我现在还不理解它为什么要将两个数减去一个共同的值再比较大小,这不多此一举吗,是历史的原因还是有其他考虑呢?)

在这里只需要知道真实模型中比较的是键值就行了,不用深究,其本质也只是比较每个进程对应的vruntime值,vruntime小的排在队列(红黑树)前面。红黑树是二叉搜索树的一种,树上的成员的先后顺序是在插入每个成员时就决定的(为了控制树的高度,系统需要对红黑树做一些调整,但是这并不改变各个成员的先后顺序)

__enqueue_entity()函数用于将各个执行完毕的进程(调度器选择某个进程执行前,会先把它从就绪队列中移除,执行完毕后再放回),或新的进程或刚睡眠的进程加入到就绪队列中。__enqueue_entity()函数将进程(准确地说应该是可调度实例)放在就绪队列中的位置决定了被调度的先后顺序:

kernel/sched_fair.c

static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)

{

......

    

    //key是要插入的进程的键值

    s64 key =entity_key(cfs_rq,se);

    while (*link) {

        //link是指向红黑树根节点的指针

        parent = *link;

        

        //不用深究这句话的细节,只需要知道

        //entry的是parent节点对应的可调度实例

        entry = rb_entry(parent, struct sched_entity, run_node);

        s64 key =entity_key(cfs_rq, se);

        

        //这句才是我们要关心的,它比较要插入的进程的键值

        //parent节点对应的进程的键值的大小。要插入的进程

        //的键值更小,则继续遍历左子树

        //反之,则遍历右子树

        if (key <entity_key(cfs_rq, entry)) {

            link = &parent->rb_left;

        } else {

            link = &parent->rb_right;

        ......

        }

    }

......

}

注意看下划线标注的地方,要插入的进程的可调度实例separent节点指向的可调度实例(entry)的键值大小都是通过entity_key()函数获得的。对他们键值的比较实际上是在比较se.vruntime-cfs_rq.min_vruntime,entry.vruntime-cfs_rq.min_vruntime的大小,这正如前面所述,完全等同于比较se.vruntimeentry.vruntime的大小(除非被减项中的cfs_rq不是同一个就绪队列)

 

weight

原理概述

普通进程(非实时进程)被分为40个等级,每个等级的进程对应一个权重值,权重值用一个整数来表示。这些权重值被定义在下述数组中:

kernel/sched.c

static const int prio_to_weight[40] = {

 /* -20 */    88761,     71755,     56483,     46273,     36291,

 /* -15 */    29154,     23254,     18705,     14949,     11916,

 /* -10 */     9548,      7620,      6100,      4904,      3906,

 /*  -5 */      3121,      2501,      1991,      1586,      1277,

 /*   0 */      1024,       820,       655,       526,       423,

 /*   5 */       335,       272,       215,       172,       137,

 /*  10 */       110,        87,        70,        56,        45,

 /*  15 */        36,        29,        23,        18,        15,

};

普通进程的权重值最大为88761,最小为15。默认情况下,普通进程的权重值为1024(由NICE_O_LOAD指定)。顺便说一下实时进程,实时进程也有权重值,它们的权重值为普通进程最大权重值的两倍(即,2X88761)。

相邻两个等级的普通进程的权重值比值约等于1.25(之所以设定这个值,是通过简单计算出来的,系统想达到这个目的:如果只有等级相邻的两个进程在运行,那么它们获得的CPU时间差值为10%)。也就是说,只要知道最小权重值是15,其他进程的权重值也就能计算出来了。但是内核为了节约计算时间,选择把已经计算好的结果存放到上述数组里了。

vruntime行走速度

原理概述

权重值有了,我们还需要知道各个进程的vruntime行走的速度。系统规定:默认权重值(1024)对应的进程的vruntime行走时间与实际运行时间runtime1:1的关系。由于vruntime的行走速度和权重值成反比,那么其他进程的vruntime行走速度都可以通过:1.该进程的权重值,2.默认进程的权重值两个参数计算得到。

例如:权重值为15的进程它的vruntime行走速度:


权重值为3906的进程的vruntime行走速度为:


"真实时钟速度"即为runtime(英文资料成为wall clock)行走的速度。

为了更好的认识“真实模型”是如何运作的,我们来看看update_curr()这个函数(我只把相关的几条语句选出来了):

update_curr 

linux内核调度器 调度原理(2.6.24笔记整理)_第13张图片

在进程执行期间周期性调度器周期性地启动,它的工作只是更新一些相关数据,并不负责进程之间的切换。它会调用update_curr()函数,完成相关数据的更新。

第一条语句:

delta_exec= (unsigned long)(now - curr->exec_start);

是计算周期性调度器上次执行时到周期性这次执行之间,进程实际执行的CPU时间(如果周期性调度器每1ms执行一次,delta_exec就表示没1ms内进程消耗的CPU时间,这个在前面讲了),它是一个实际运行时间。

update_curr()函数内只负责计算delta_exec以及更新exec_start。更新其他相关数据的任务交给了__update_curr()函数。

__update_curr

          

linux内核调度器 调度原理(2.6.24笔记整理)_第14张图片

__update_curr()函数主要完成了三个任务:1.更新当前进程的实际运行时间(抽象模型中的runtime)2.更新当前进程的虚拟时间vruntime3.更新cfs_rq->min_vruntime

a)    更新当前实际运行时间之前已经讲过了,见前面sum_exec_runtime的定义。

b)   更新当前进程的vruntime

delta_exec_weighted表示虚拟时间vruntime的增量。权重值为1024NICE_0_LOAD)的进程,vruntime行走速度和runtime行走速度相同,那么vruntime的增量也就等于runtime的增量。所以先将 delta_exec_weighted设置为delta_exec

下一句是判断当前进程的权重值是否是1024,如果不是1024则需要计算。这个计算任务交给calc_delta_fair()函数完成,它又定义为calc_delta_mine()函数,这个函数的实现过程稍有些复杂,这个我们暂时不关心,我们只需要知道calc_delta_fair()返回的是:


原理很简单,就是按照我们之前定义的,vruntime的增长速度与权重值成反比计算出来的。

至于为什么calc_delta_mine()函数看起来这么复杂,这里简单讨论一下吧。在上面的公式中,curr->load.weight出现在分母上,就意味着要做除法。除法的效率很低,如果我们把curr->load.weight的倒数保存起来,那么“除以weight”的计算就可以转换为乘以它倒数的计算了。就是基于这个想法(实际实现时并不是存的weight的倒数,只是基于这个想法而已,因为weight的倒数是小数,涉及到浮点运算,消耗更大),load_weight结构体中除了weight成员之外,还多了一个inv_weight成员。每个进程的inv_weight值约等于 4294967268/weight。当然每个进程对应的inv_weight值也不是临时计算的,而是提前计算好了放到数组里的:

kernel/sched.c

static const u32 prio_to_wmult[40] = {

 /* -20 */    48388,     59856,     76040,     92818,    118348,

 /* -15 */   147320,    184698,    229616,    287308,    360437,

 /* -10 */   449829,    563644,    704093,    875809,   1099582,

 /*  -5 */   1376151,   1717300,   2157191,   2708050,   3363326,

 /*   0 */   4194304,   5237765,   6557202,   8165337,  10153587,

 /*   5 */  12820798,  15790321,  19976592,  24970740,  31350126,

 /*  10 */  39045157,  49367440,  61356676,  76695844,  95443717,

 /*  15 */ 119304647, 148102320, 186737708, 238609294, 286331153,

};

c)    更新cfs_rq->min_vruntime。在当前进程和下一个将要被调度的进程中选择vruntime较小的值(因为下一个要执行的进程的vruntime是就绪队列中vruntime值最小的,那么在它和当前进程中选择vruntime更小的意味着选出的是可运行进程中vruntime最小的值)。然后用该值和cfs_rq->min_vruntime比较,如果比min_vruntime,则更新cfs_rq为它(保证了min_vruntime值单调增加)。

place_entity

细节

来分析一下place_entity()这个函数,通过它可以更进一步了解进程在就绪队列中排序的机制。下面是它的相关源代码,实际代码中会根据sched_feature查询的结果来执行部分代码,这里我把它们去掉了,这不影响我们对基本原理的研究。

kernel/Sched_fair.c

place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)

{

......

   vruntime = cfs_rq->min_vruntime;

    if (initial)

       vruntime += sched_vslice_add(cfs_rq, se);

       

    if (!initial) {

       /* sleeps upto asingle latency don't count. */

           vruntime -= sysctl_sched_latency;

       /* ensure we nevergain time by being placed backwards. */

        vruntime = max_vruntime(se->vruntime, vruntime);

    }

   se->vruntime = vruntime;

......

}

place_entity()函数的功能是调整进程的虚拟时间。当新进程被创建或者进程被唤醒时都需要调整它的vruntime值(新进程在创建时需要合理设置它的vruntime值,这个我们在抽象模型中就讨论过,跟新进程一样,进程被唤醒时也是需要调整vruntime值的。)。那我们看看真实模型中是怎么实现的。

新进程被创建

原理概述

进程的ideal_time长度和weight成正比,vruntime行走速度和weight值成反比。那么,如果每个进程在period时间内,都执行了自己对应的ideal_time这么长的时间,那么他们的vruntime的增量delta_vruntime相等。又由于nice值等于0(weight值等于1024)的进程vruntime行走速度等于runtime行走速度,如果每个进程都运行他自己对应的ideal_runtime那么长时间,那么他们vruntime的增量都等于nice值为0的进程的ideal_runtime

例如之前举过的例子:A,B,C,D四个进程,weight值分别为1234period长度为20ms。那么他们的ideal_time分别为2ms,4ms,6ms,8ms。如果令weight值为1的进程的vruntime行走速度和runtime相同。那么进程A运行2ms,它的delta_vruntime便是2ms;进程B执行4ms,它的delta_vruntime也是2ms;同样的,进程C,D各自分别执行6ms8ms它们的delta_vruntime也都是2ms

那么,假设初始情况下A,B,C,D四个进程就按这个顺序排在就绪队列上的,它们的vruntime值都等于0。当A执行了2ms(即它的ideal_runtime),它的vruntime值变成了2ms,后果是:它直接被排到就绪队列的最后,只要其它进程运行的总时间没有达到自己对应的ideal_runtime,那么它始终是排在进程A前面的。

如果初始的时候,人为地给Avruntime加上2ms,那会起到什么效果呢?效果是:意味着它被标记为"它在本period内已经执行了它对应的ideal_time那么长的时间",只有等其他进程都执行了它们各自对应的ideal_runtime这么长的时间之后它才能被调度。对于新创建的进程,系统就是这么做的。

如果是新进程,那么initial参数为1if(initial)中的语句会执行,它的vruntime值被设置为min_vruntime+sched_vslice_add(cfs_rq,se)sched_vslice_add()函数便是计算nice值为0的进程的ideal_runtime(,该进程运行它对应的ideal_time这么长时间时的delta_vruntime)。作用是将新加入的进程标记为"它在本period内已经运行了它对应的ideal_time那么长的时间",这致使它理论上会被排到就绪队列的最后,意思就是说,新加入的进程理论上(如果有所进程都执行它对应的ideal_runtime那么长的时间,且没有发生睡眠,进程终止等等特殊事件的情况下)只有等待period之后才能被调度。

细节

sched_vslice_add()调用了__sched_vslice(),计算nice值为0的进程的ideal_runtime便是由__sched_vslice()的工作完成的代码如下:

kernel/Sched_fair.c

static u64 sched_vslice_add(struct cfs_rq *cfs_rq, struct

你可能感兴趣的:(linux内核调度器 调度原理(2.6.24笔记整理))