【思维重现】进程调度

Abstract

        linux内核2.6.11算是很老的一个版本了,现在也没什么人会去找出来用,本文目的不为技术普及,而是想讨论内核一些设计的思想,一起体会当年是怎么设计内核的,相信对以后更深入研究也很有帮助。限于本人能力有限,有不足之处,还请轻点拍。

        本文主要内容可以分成两个部分,Part 2从最简单的设计开始,一点点改进得到一个比较完美的解决方案。Part3则是一部分技术的实现过程。

        本文讨论不深入源码,那些经典书籍的讨论绝对比我精辟。

        本文主要参考《linux内核设计与实现 第二版》和《unstanding the linux kernel 3rd》,看的时候可以参考这两本书。

        《unstanding the linux kernel 3rd》http://download.csdn.net/detail/wearenoth/4325419

        《linux内核设计与实现 第二版》http://download.csdn.net/detail/wearenoth/4325428

        为了偷懒,里面很多引用书本的内容,毕竟我再怎么说也不如书本说的好。

Part 1

        这部分内容主要介绍一些基本的概念,目的是为后面内容做铺垫。

1.1 什么是进程

        这个问题其实很难回答。个人认为:进程(processing)是一组任务的有序集合;它和程序(program)不同,程序是一组或多组任务的有序集合。进程是从更微观更底层的层面出发思考,程序则是比较宏观而且更倾向于用户的一个概念。

1.2 进程的组织方式

1.2.1 描述一个进程

        linux下使用进程描述符(PCB)进行描述(注:其实就是一个很大的结构体结构体名称task_struct),这个结构体中包含了一个进程从诞生到死亡整个过程中所有可能用到的数据结构。

        task_struct结构体定义:http://my.csdn.net/my/code/detail/5824


        注:上述部分更详细内容见

        《linux内核设计与实现 第二版》 P17

        《unstanding the linux kernel 3rd》chap3.1 & 3.2

1.2.2 组织一组进程描述符(task_struct)

        在linux中首先使用一个双向链表将一组相关的进程描述符(PCB)的task_struct数据结构串起来,然后在将这些链表用不同的形式(如散列表)组织起来。


        首先看看这个双向链表是怎么回事,如上图显示(截取自<understanding the linux kernel> chap 3.2),内核中定义了一个数据结构list_head,它本质上就是一个双向链表,定义这个类型的数据结构就是因为在内核中很多地方需要用到链表进行数据组织,如果专门为不同数据设计链表,无疑增加了开发难度,而且也不利于代码的阅读。


        上表中显示了对这个链表的一系列操作(截取自<understanding the linux kernel> chap 3.2),这些操作都是一些宏定义,从宏定义名一般都能猜到其语义是什么,顺便说明下除了最后两个宏定义时间复杂度都是O(1),速度非常快。

        注:上述部分更详细内容见《unstanding the linux kernel 3rd》chap 3.2

        另外,对于用户而言,我们还对进程有父进程、子进程、和兄弟进程以及线程的区分,内核也针对这些进行了详细的区分和组织方案,但是在调度过程中,所有的进程是平等的,这些区分没有任何意义,所以这里就不给出相关的说明。

1.3 进程的状态

        在《linux内核设计与实现 第二版》P19~20给出进程的状态可以分成如下5种。



        而在《unstanding the linux kernel 3rd》chap3.2中是却是有7种(如果算上调试用的TASK_TRACED)。

TASK_RUNNING
The process is either executing on a CPU or waiting to be executed.

TASK_INTERRUPTIBLE
The process is suspended (sleeping) until some condition becomes true. Raising a hardware interrupt, releasing a system resource the process is waiting for, or delivering a signal are examples of conditions that might wake up the process (put its state back to TASK_RUNNING).

TASK_UNINTERRUPTIBLE
Like TASK_INTERRUPTIBLE, except that delivering a signal to the sleeping process leaves its state unchanged. This process state is seldom used. It is valuable, however, under certain specific conditions in which a process must wait until a given event occurs without being interrupted. For instance, this state may be used when a process opens a device file and the corresponding device driver starts probing for a corresponding hardware device. The device driver must not be interrupted until the probing is complete, or the hardware device could be left in an unpredictable state.
TASK_STOPPED
Process execution has been stopped; the process enters this state after receiving aSIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU signal.

TASK_TRACED
Process execution has been stopped by a debugger. When a process is being monitored by another (such as when a debugger executes aptrace( )system call to monitor a test program), each signal may put the process in theTASK_TRACED state.
EXIT_ZOMBIE
Process execution is terminated, but the parent process has not yet issued await4( )or waitpid( )system call to return information about the dead process.Before the wait( )-like call is issued, the kernel cannot discard the data contained in the dead process descriptor because the parent might need it. (See the section "Process Removal" near the end of this chapter.)
EXIT_DEAD
The final state: the process is being removed by the system because the parent process has just issued await4( ) or waitpid( ) system call for it. Changing its state fromEXIT_ZOMBIE to EXIT_DEAD avoids race conditions due to other threads of execution that executewait( )-like calls on the same process (see Chapter 5).


        进程的几种状态很容易就看明白。不再多说明。

Part 2

        这部分内容希望从简单到复杂,我们先考虑下手上拥有的资源和迫切需要解决的问题,然后给出一个最基本简单的解决方案,然后再把这套方案一步步改进和完善。

        手上的资源:

         1、一个有多处理器的CPU  

         2、每个进程都有一个可以描述进程所有信息的进程描述符(PCB,以后方案的改变可以修改这个描述符中的参数),每个描述符之间是相互独立的

         3、其他的功能(主要是内存管理和文件系统)是完备的,意思就是每次只要切换不同的task_struct到一个处理器上就可以自动执行,进程调度是不需要考虑它需要执行的任务是否能够实现。

         需要解决的问题:

         调度策略的选定:

         1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         2、每个处理器之间是否会存在竞争?如何解决这个竞争

         3、每个进程需要执行,哪些进程不需要执行-2.2

         4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         6、是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         9、都使用动态优先级是最高效的么?-2.7

         10、如何处理多处理器下的进程调度-2.8

        针对上述的几个问题,我们先将问题勒紧,做出几个很强的假设,将问题变成一个很容易实现的问题,然后一个个的放松假设,逐步改进和完善我们的调度方案

         (a)在一个处理器上只能执行一个进程-problem1

         (b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         (c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿-problem5

         (f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10


单处理器下的调度方案

         需要解决的问题:

         调度策略的选定:

         1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         (讨论)2、每个处理器之间是否会存在竞争?如何解决这个竞争

         3、每个进程需要执行,哪些进程不需要执行-2.2

         4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         6、是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         9、都使用动态优先级是最高效的么?-2.7

         10、如何处理多处理器下的进程调度-2.8

         假定条件:

         (a)在一个处理器上只能执行一个进程-problem1

         (准备放松)(b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         ( c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿-problem5

         (f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10

         放松条件(b)的可行性:一个进程只有一个进程描述符,也就是唯一的一个task_struct,它每次只能被一个处理器获得,其他进程不会拿到这个进程的任务开始执行。不过有一个问题,就是其他处理器虽然不会获得该task_struct,但是因为进程有父子关系,父子之间可能会对同一块内存的访问,但因为有条件(j)的保护,所以我们可以只在一个处理器上考虑一个进程的执行情况而不去考察那种复杂的情况。(注:同一个处理器上也会产生竞争,假定在条件(j)的情况下这种竞争也不存在了)

         所以可以先讨论单处理器下进程调度的设计,然后引入多处理器下如何设计进程的调度方案。所以在2.1到2.7之间我们考虑的都是单处理器下的调度实现,到2.8开始考虑多处理器下的情况。

调度策略

2.1 一个糟糕的实现 —— 轮询+定长时间片

         需要解决的问题:

         调度策略的选定:

         (讨论)1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         (OK)2、每个处理器之间是否会存在竞争?如何解决这个竞争

         3、每个进程需要执行,哪些进程不需要执行-2.2

         4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         6、我们如何知道一个任务的优先级,是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         9、都使用动态优先级是最高效的么?-2.7

         10、如何处理多处理器下的进程调度-2.8

         假定条件:

         (准备放松)(a)在一个处理器上只能执行一个进程-problem1

         (已经放松)(b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         (c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿,如何区分一个进程的紧急程度-problem5

         (f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10

         放松条件(a)的可行性:处理器是计算机中执行效率最快的硬件设备,它可以在很短时间内处理用户的请求的任务(例如输入),假设用户无法忍受计算机完成的一个任务的最低时间为t,而执行这个任务的时间为T,这个T>>t的,这样可以选择在完成用户任务后的(t-T)的时间内去做其他的任务。这样用户就会感觉计算机可以对其的所有请求都得到执行。


        基本思路:如上图所示

step 1:把所有的的进程用一个链表串联起来。

        step 2:轮询链表中的每个进程描述符(PCB),每次从队列头部取出,执行定长时间片T,然后插入到双向链表尾部

        这种思路中最需要解决的问题就是时间片T大小是多少,而用户的最大忍受时间t又是多少

2.2 一点点改进 —— 轮询+定长时间片+进程状态

         需要解决的问题:

         调度策略的选定:

         (OK)1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         (OK)2、每个处理器之间是否会存在竞争?如何解决这个竞争

        (讨论) 3、每个进程需要执行,哪些进程不需要执行-2.2

         4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         6、是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         9、都使用动态优先级是最高效的么?-2.7

         10、如何处理多处理器下的进程调度-2.8

         假定条件:

         (已经放松)(a)在一个处理器上只能执行一个进程-problem1

         (已经放松)(b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         (准备放松)(c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿-problem5

         (f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10

         放松条件(c)的可行性:这很好想想,比如有一个进程只处理用户敲打键盘输入的数据。用户的速度和处理器相比肯定是非常慢的,这样这个进程可能在很多时间内并不需要去争用处理器。所以可以让这些进程在不需要的时候“挂起”,主动让出争用处理器的权限,只有在需要的时候唤醒。这里涉及到调度时机的问题,但是因为有条件(h)的假设存在,我们现在可以不去考虑这些进程什么时候被唤醒。什么时候又应该去挂起。


        基本思路:在2.1的基础上。如上图所示

        step1:把需要运行的用一个可运行队列(run_queue)管理起来,然后将不需要运行(例如睡眠、阻塞)的进程用一个等待队列(wait_queue)组织起来。

        step2:对于可运行队列(run_queue)中的进程,按照2.1中的方法进行调度,而对于等待队列(wait_queue)中的进程,则不考虑。

        这种思路可以通过进程状态进行区别进程是否需要争用处理器(注:参见1.3),而且细致的进程状态可以更精细的区分哪些进程需要挂起(等待),哪些需要运行。

The runqueue lists group all processes in a TASK_RUNNING state. When it comes to grouping processes in other states, the various states call for different types of treatment, with Linux opting for one of the choices shown in the following list.

	Processes in a TASK_STOPPED, EXIT_ZOMBIE, or EXIT_DEAD state are not linked in specific lists. There is no need to group processes in any of these three states, because stopped, zombie, and dead processes are accessed only via PID or via linked lists of the child processes for a particular parent.

	Processes in a TASK_INTERRUPTIBLE or TASK_UNINTERRUPTIBLE state are subdivided into many classes, each of which corresponds to a specific event. In this case, the process state does not provide enough information to retrieve the process quickly, so it is necessary to introduce additional lists of processes. These are called wait queues and are discussed next.
        注:上述部分更详细内容见 《unstanding the linux kernel 3rd》chap 3.2.4

2.3 一点点改进 —— 轮询+定长时间片+进程状态+静态优先级

         需要解决的问题:

         调度策略的选定:

         (OK)1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         (OK)2、每个处理器之间是否会存在竞争?如何解决这个竞争

        (OK) 3、每个进程需要执行,哪些进程不需要执行-2.2

         (讨论)4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         6、是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         9、都使用动态优先级是最高效的么?-2.7

         10、如何处理多处理器下的进程调度-2.8

         假定条件:

         (已经放松)(a)在一个处理器上只能执行一个进程-problem1

         (已经放松)(b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         (已经放松)(c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (准备放松)(d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿-problem5

         (f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10

         放松条件(d)的可行性:同样是需要执行的进程,但是每个进程的执行紧急度是不一样的,例如内核中一个进程发生了一个错误需要紧急处理,如果不处理可能会引起其他进程产生错误,这样就需要保证这些进程得到优先处理。如我们去银行一样,VIP用户就可以比普通用户先得到服务。


        基本思路1:在2.2的基础上。如上图所示

        step1:对每个进程赋一个静态优先级。

        step2:把可执行队列按照优先级从高到低进行组织,依次轮询。


        基本思路2:在2.2的基础上,如上图所示

        step1:对每个进程赋一个静态优先级。

        step2:设计一个优先级数组,每个数组组织每种不同优先级的可执行队列,利用bit_map来寻找最高优先级的进程。然后依次轮询

(注:这两个思路中看着貌似还是在遍历,没有实现一个优先的效果,但是需要知道我们此时已经有假设条件(g)和(h),这样每次调度的时候内核总是能够找到最需要立刻执行的那个进程)

        两种思路都是可行思路,哪种思路更好呢?考虑一种情况,当一个wait_queue中的进程被唤醒的时候,需要插入到可执行数组中,如果根据思路1,在一个链表中插入数组的时间复杂度为O(n);如果根据思路2,根据bit_map查找位于数组的下标时间复杂度为O(1),而插入操作时间复杂度也是O(1),所以思路2中的时间复杂度为O(1)。这样考虑的话,使用思路2更好。

(注:bit_map的实现可以参考《linux内核设计与实现 第二版》P37

2.4.1 一点点改进 —— 轮询+变长时间片+进程状态+静态优先级+进程类型

         需要解决的问题:

         调度策略的选定:

         (OK)1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         (OK)2、每个处理器之间是否会存在竞争?如何解决这个竞争

        (OK) 3、每个进程需要执行,哪些进程不需要执行-2.2

         (OK)4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         (讨论)5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         6、是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         9、都使用动态优先级是最高效的么?-2.7

         10、如何处理多处理器下的进程调度-2.8

         假定条件:

         (已经放松)(a)在一个处理器上只能执行一个进程-problem1

         (已经放松)(b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         (已经放松)(c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (已经放松)(d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (准备放松)(e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿-problem5

         (f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10

放松条件(e)的可行性:根据2.3中的设计,进程总是会执行最紧急的那个进程,但是一种情况,如果紧急进程是那种一直需要处理的(如大规模数学运算),这样就会一直占用处理器,而使那些不紧急但是又需要处理的进程无法得到处理器。所以需要一种可行的方案区分那些进程是最迫切需要处理的。而且这个紧急的进程不会一直占用处理器

         基本思路:

         引入概念:交互性进程和批处理进程,其说明如下,第三个实时进程暂时不考虑,后面放松假设(i)的时候会使用到它。

         tip1:将那些交互性强的进程设置为紧急程度高(优先级更高),而那些需要大量运算的进程设置为紧急程度低(低优先级),这样如果有一个用户请求,内核就可以立马得到响应,而且它占用CPU的时间很少,这样就可以在一定程度上保证进程间的公平(注:这种公平只是相对而言的,只是一种折中的方案。)

         tip2:强交互性的进程经常只需要比较短的时间就可以完成,但是可能固定的时间片又往往不够,所以也采用了一种变长时间分配方式(注:这算是一种优化不是放松这个假设的必备条件)

Interactive processes

These interact constantly with their users, and therefore spend a lot of time waiting for keypresses and mouse operations. When input is received, the process must be woken up quickly, or the user will find the system to be unresponsive. Typically, the average delay must fall between 50 and 150 milliseconds. The variance of such delay must also be bounded, or the user will find the system to be erratic. Typical interactive programs are command shells, text editors, and graphical applications.

Batch processes

These do not need user interaction, and hence they often run in the background. Because such processes do not need to be very responsive, they are often penalized by the scheduler. Typical batch programs are programming language compilers, database search engines, and scientific computations.

Real-time processes

These have very stringent scheduling requirements. Such processes should never be blocked by lower-priority processes and should have a short guaranteed response time with a minimum variance. Typical real-time programs are video and sound applications, robot controllers, and programs that collect data from physical sensors.

(注:上述内容见《unstanding the linux kernel 3rd》chap7.1)

2.4.2一点点改进—— 轮询+变长时间片+进程状态+静态优先级+动态优先级+进程类型

         需要解决的问题:

         调度策略的选定:

         (OK)1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         (OK)2、每个处理器之间是否会存在竞争?如何解决这个竞争

        (OK) 3、每个进程需要执行,哪些进程不需要执行-2.2

         (OK)4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         (OK)5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         (讨论)6、是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         9、都使用动态优先级是最高效的么?-2.7

         10、如何处理多处理器下的进程调度-2.8

         假定条件:

         (已经放松)(a)在一个处理器上只能执行一个进程-problem1

         (已经放松)(b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         (已经放松)(c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (已经放松)(d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (已经放松)(e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿-problem5

         (准备放松)(f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10

         放松条件(f)的可行性:我们很少会单独为输入数据(交互性强的程序)写一个程序,也很少专门写一个程序用来大型数学运算(批处理),所以一个进程可能一会是一个强交互性的,一会又编程一个批处理的。所以使用2.4.1中的方案仍然不够。需要有一份方案能够判断一个进程什么时候是交互进程,什么时候是批处理进程。还好这个方案还是可以找到的。

         基本思路:通过判断一个进程的平均睡眠时间,如果这个进程平均睡眠时间大于一个临界值(通常选为200),就说明它是一个强交互性的进程,就要去提高它的优先级,如果小,就要降低它的优先级。(注:这种方案会隐藏一个bug,所以才会有假设(i)这个很令人诡异的假设,对于这个bug是怎么回事可以参考2.7中的说明)

         扩充:如下图所示:bonus就是一个惩罚因子。动态优先级的计算公式:

dynamic priority = max (100, min (  static priority - bonus + 5, 139))

(注:上述表格见《understanding the linux kernel 3rd 》chap7.2)

2.5一点点改进——轮询+变长时间片+进程状态+静态优先级+动态优先级+进程类型+活期/过期数组

         需要解决的问题:

         调度策略的选定:

         (OK)1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         (OK)2、每个处理器之间是否会存在竞争?如何解决这个竞争

        (OK) 3、每个进程需要执行,哪些进程不需要执行-2.2

         (OK)4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         (OK)5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         (OK)6、是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         (讨论)7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         9、都使用动态优先级是最高效的么?-2.7

         10、如何处理多处理器下的进程调度-2.8

         假定条件:

         (已经放松)(a)在一个处理器上只能执行一个进程-problem1

         (已经放松)(b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         (已经放松)(c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (已经放松)(d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (已经放松)(e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿-problem5

         (已经放松)(f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (准备放松)(g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10

         放松条件(g)的可行性:这个条件很容易放松,因为有很多方法可以标记一个进程是否已经被执行过。


         基本思路:如上图所示(注:在2.6版本的linux内核中用的这种方式,上图详细内容见《understanding the linux kernel 3rd》chap7.3)

        tip1:引入两个数组,一个活期数组(active)用于标识还需要进行调度的进程,一个过期数组(expire)用于标识已经被调度过的进程。这两个数组都按2.3中的方式组织进程,active中也是使用bit_map(位图)进行快速查找第一个优先级最高的可执行进程。(注:这两个数组组织的都是TASK_RUNNING状态的进程)

        tip2:对于active中的一个进程在使用完时间片后就插入到expire数组相应优先级的双向链表的队尾中。如果active中没有需要调用的进程,说明这一轮结束,交换active和expire的指针,这样就可以在O(1)的复杂度内实现新一轮的开始。

2.6 完善与优化

         需要解决的问题:

         调度策略的选定:

         (OK)1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         (OK)2、每个处理器之间是否会存在竞争?如何解决这个竞争

        (OK) 3、每个进程需要执行,哪些进程不需要执行-2.2

         (OK)4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         (OK)5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         (OK)6、是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         (OK)7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         (讨论)8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         9、都使用动态优先级是最高效的么?-2.7

         10、如何处理多处理器下的进程调度-2.8

         假定条件:

         (已经放松)(a)在一个处理器上只能执行一个进程-problem1

         (已经放松)(b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         (已经放松)(c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (已经放松)(d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (已经放松)(e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿-problem5

         (已经放松)(f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (已经放松)(g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (准备放松)(h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10

           放松条件(h)的可行性:要放松这个条件就是要问进程的调度时机。好在这个条件也是很好放松的(当然,实现起来就很复杂了)在2.6.1到2.6.3中讨论几种重要的调度时机(注:我只知道这几种,别的暂时没找到)
2.6.1 确定调度时机——显示调用

        想到这种时机最为正常。一个进程显示调用调度函数--schdule(),意味着它已经完成自己所需要做的事情,主动放弃对处理器的使用。这个“时间”自然就是一个调度时机。下面是书中对schdule()函数的一个解释(见《understanding the linux kernel 》chap7.4)

The schedule( ) function implements the scheduler. Its objective is to find a process in 
the runqueue list and then assign the CPU to it. It is invoked, directly or in a lazy (deferred) way,
 by several kernel routines.

2.6.2 确定调度时机——系统调用

         有一些系统调用会产生调度时间发生,这些与调度相关的系统调度其实也很好去理解,凡是对task_struct中与调度相关的参数进行操作(例如改变优先级)的时候,这就意味着有调度的期望,所以这些调度函数返回(执行完操作)的时候就是一个必要的调度时机。下图列出了一些与调度相关的函数API,像nice(),sched_getscheduler(),sched_set_param()这样的函数在返回的时候就会进行重新调度(注:这些函数会引发调用不意味着绝对会被调度)。


(注:上述表格见《understanding the linux kernel 3rd》chap 7.1)

2.6.3 确定调度时机——中断和时钟中断

        中断——产生一个中断就很可能意味着发生了一件很紧急的事情,它很可能意味着出现了一个更高优先级的进程迫切需要执行,所以在中断返回的时候就需要考虑是否有进程需要进行调度。如下图,在《understanding the linux kernel 3rd》chap4.9.12就对这种情况进行的描述。


        时钟中断——之所以把它单独提出讲,是因为它太重要了,之前所有的讨论一直在忽略一个很强的条件,就是每个进程获取到处理器的时间都是恰好的,在那种时间分配下可以实现性能的最优化,但是这明显是做不到的。所以为了提高效率,引入时钟中断,提高调度时机的粒度(注:当然这种时钟中断粒度不能太高,否则因为调度引起的资源消耗会抵消效率提高甚至得不偿失),同时也可以应对不同优先级下变长时间片的要求。所以在时钟中断的中断处理程序就是一个调度时机。下图是《understanding  the linux kernel 3rd》chap6.4.1中对时钟中断最后如何更新进程的计时器,并进行调度的说明。



2.7 完善与优化——轮询+定长时间片+进程状态+静态优先级+动态优先级+活期/过期数组+实时进程(FIFO/RR)

         需要解决的问题:

         调度策略的选定:

         (OK)1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         (OK)2、每个处理器之间是否会存在竞争?如何解决这个竞争

        (OK) 3、每个进程需要执行,哪些进程不需要执行-2.2

         (OK)4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         (OK)5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         (OK)6、是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         (OK)7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         (OK)8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         (讨论)9、都使用动态优先级是最高效的么?-2.7

         10、如何处理多处理器下的进程调度-2.8

         假定条件:

         (已经放松)(a)在一个处理器上只能执行一个进程-problem1

         (已经放松)(b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         (已经放松)(c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (已经放松)(d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (已经放松)(e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿-problem5

         (已经放松)(f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (已经放松)(g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (已经放松)(h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (准备放松)(i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10

         放松条件(i)的可能性:进行了这么久的讨论,其实还有一个条件被我们忽略了,就是在2.4.2中我们说明动态优先级是根据平均睡眠时间不是很长(例如等于200ms),但是又是很紧急的任务,这样它的优先级总是在一个很“尴尬”(不大不小)的范围内,它明明需要尽快被执行,但是又总是无法境况得到执行。所以之前的方案不一定是最高效的。

        基本思路:

        tip:增加SCHE_FIFO和SCHE_RR两种调度类型,增加实时进程(real-time process 见2.4.1中的描述)

        这个假设条件其实一开始不一定能够像的到,从直觉很难去发觉还有更好调度类型,当然首先需要解释下什么是调度类型,如下图所示。(注:下图内容见《understanding the linux kernel 3rd》chap7.2)



多处理器下的调度方案

2.8 完善与优化——多处理器下的内核同步技术

         需要解决的问题:

         调度策略的选定:

         (OK)1、能够让多个task_struct在处理器上并发执行,让用户感觉不到电脑是线性运行的-2.1

         (OK)2、每个处理器之间是否会存在竞争?如何解决这个竞争

        (OK) 3、每个进程需要执行,哪些进程不需要执行-2.2

         (OK)4、需要区分进程的紧急情况,让需要更紧急执行的进程能够尽快执行-2.3

         (OK)5、如果让紧急的任务一直执行,会不会让不紧急的任务产生饥饿,如何区分一个进程的紧急程度-2.4.1

         (OK)6、是不是一个进程一直是紧急的?如果不是要怎么办?-2.4.2

         (OK)7、如何区分已经处理过和没处理过的任务-2.5

         调度时机的选定:

         (OK)8、什么时候执行调度(调度时机)-2.6

         怎么让系统更高效:

         (OK)9、都使用动态优先级是最高效的么?-2.7

         (讨论)10、如何处理多处理器下的进程调度-2.8

         假定条件:

         (已经放松)(a)在一个处理器上只能执行一个进程-problem1

         (已经放松)(b)多处理器下,每个处理器下会对同一个进程产生竞争,所以在考虑一个处理器上执行一个进程的时候需要考虑其他处理器对其产生的影响-problem2

         (已经放松)(c)每个进程都是需要立即执行的,它们都希望能够立刻得到处理器的执行权限-problem3

         (已经放松)(d)每个进程的紧急情况是一样的,不会有一个进程需要立马执行-problem4

         (已经放松)(e)让一个紧急的进程执行不会让一个不紧急的进程产生饥饿-problem5

         (已经放松)(f)一个进程如果是紧急的,那它一直都是紧急的-problem6

         (已经放松)(g)内核有一个很有效的机制可以区别哪些进程已经处理过,哪些进程没有被处理过-problem7

         (已经放松)(h)内核懂的在什么时候进行进程调度,它总是在最需要调度的时候进行调度-problem8

         (已经放松)(i)使用一种调度类型已经是最高效的,不需要引入其他的调度类型-problem9

         (准备放松)(j)内核有一个很有效的机制可以避免其他处理器对当前任务访问的数据进行保护-problem10

         放松条件(j)的可行性:从2.1到2.7下我们都强制使用了(j)这个条件,但是现在使用的基本都是多处理器,所以这个条件也不得不放松。多处理器最大的问题就是会产生共享内存的竞争,所以需要提供一些非常好的机制保护。当然单处理器下也会产生竞争,例如,父子进程先后在同一个处理器上执行,父进程准备访问某个内存的时候被子进程抢占了,子进程又对这段内存进行了修改,当父进程返回处理器准备继续读取的时候访问的数据就不是原来的数据了。所以放松这个假设势在必行。


        基本思路:如上图所示,表中列举出了用到了一些内核同步(防止竞争)技术以及他们适用的范围。

(注:上述表格见《understanding the linux kernel 3rd》chap5.2)

2.9 结束语

        本章内容是我的脑海中对进程调度实现设计过程的一个重现,中间尽量少去将那些实现的细节,主要是希望能够有一个对系统调度有一个全局的概念,知道一个个东西的由来,当然实现方案肯定不只是上面这种,为了能够和书本和内核真实实现情况靠拢,所以每种情况的设计都是现实中的实现方式(注:这个调度方案是2.6版本的实现,现在linux内核使用的不是这种O(1)方案,而是CFQ),在接下来的部分更深入的介绍一些关键技术的具体实现,当然还是不会涉及源码。

Part 3




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值