1. 前言
Linux 2.4中使用goodness()函数,给每个处于可运行状态的进程赋予一个权值(Weight),使用这个权值衡量一个处于可运行状态的进程值得运行的程度。调度程序以这个权值作为选择进程的唯一依据。
虽然Linux2.4进程调度程序简单有效,但是也有其缺点。
- 单个就绪队列问题,时间片耗尽的进程依然会放进同一个就绪队列中,在不可被调度的情况下,还会参与调度。
- 多处理器问题,多个处理器上的进程放在同一个就绪队列中,因而调度器对它的所有操作都会因全局自旋锁而导致系统各个处理机之间的等待,使得就绪队列成为一个明显的瓶颈。
- 内核不可抢占问题,如果某个进程,一旦进了内核态那么再高优先级的进程都无法剥夺,只有等进程返回内核态的时候才可以进行调度。缺乏对实时进程的支持。
针对以上问题,Linux 2.6做了较大的改进。针对多处理器问题,为每个CPU设置一个就绪队列。针对单就绪队列问题,设置两个队列组,即active队列组和expired队列组。借助以上队列实现了时间复杂度为O(1)的调度算法。直到Linxu 2.6.23内核版本中,O(1)调度算法才真正替代为CFS(完全公平)调度算法。
2. 就绪队列
O(1)调度器是以进程的动态优先级prio为调度依据的,它总是选择目前就绪队列中优先
级最高的进程作为候选进程 next。由于实时进程的优先级总是比普通进程的优先级高,故能
保证实时进程总是比普通进程先被调度。
Linux2.6 中,优先级 prio 的计算不再集中在调度器选择 next 进程时,而是分散在进程
状态改变的任何时候,这些时机有:
- 进程被创建时;
- 休眠进程被唤醒时;
- 从TASK_INTERRUPTIBLE 状态中被唤醒的进程被调度时;
- 因时间片耗尽或时间片过长而分段被剥夺 CPU 时;
在这些情况下,内核都会调用 effective_prio()重新计算进程的动态优先级 prio并根据计算结果调整它在就绪队列中的位置。
Linux 2.6为每个cpu定义了一个struck runqueue数据结构,每个就绪队列都有一个自旋锁,从而解决了 2.4 中因只有一个就绪队列而造成的瓶颈。
1 | struct runqueue { |
active 是指向活动进程队列的指针;expired 是指向过期进程队列的指针;array[2]是实际的优先级进程队列,其中一个是活跃的一个是过期的,过期数组存放时间片耗完的进程。
每个处理器的就绪队列都有两个优先级数组,它们是 prio_array 类型的结构体。Linux2.6
内核正是因为使用了优先级数组,才实现了 O(1)调度算法。该结构定义在 kernel/sched.c 中:
1 | struct prio_array{ |
3. 调度算法介绍
选择并运行候选进程,确定next,下一个应该占有 CPU 并运行的进程,schedule()函数是完成进程调度的主要函数,并完成进程切换的工作。schedule()用于确定最高优先级进程的代码非常快捷高效,其性能的好坏对系统性能有着直接影响,它在/kernel/sched.c 中的定义如下:
1 | ... |
其中,sched_find_first_bit()能快速定位优先级最高的非空就绪进程链表,运行时间和就绪队列中的进程数无关,是实现O(1)调度算法的一个关键所在。
schedule()的执行流程:
首先,调用 pre_empt_disable(),关闭内核抢占,因为此时要对内核的一些重要数据结构进行操作,所以必须将内核抢占关闭;其次,调用sched_find_first_bit()找到位图中的第1个置1的位,该位正好对应于就绪队列中的最高优先级进程链表;
再者,调用 context_switch()执行进程切换,选择在最高优先级链表中的第 1个进程投入运行;
图中的网格为 140 位优先级数组,queue[7]为优先级为 7 的就绪进程链表。此种算法保证了调度器运行的时间上限,加速了候选进程的定位过程。
Linux2.4 调度系统在所有就绪进程的时间片都耗完以后在调度器中一次性重新计算,其中重算是用for循环相当耗时。
Linux2.6 为每个 CPU 保留 active 和 expired 两个优先级数组, active 数组中包含了有剩余时间片的任务,expired 数组中包含了所有用完时间片的任务。
当一个任务的时间片用完了就会重新计算其时间片,并插入到 expired 队列中,当 active 队列中所有进程用完时间片时,只需交换指向 active 和 expired 队列的指针即可。此交换是实现 O(1)算法的核心,由 schedule()中以下程序来实现:
1 | array = rq ->active; |
4.Linux 2.6调度算法源码
1 | /** |