进程调度程序(常简称调度程序)可看做在运行态进程之间分配有限的处理器时间资源的内核子系统。
最大限度地利用处理器时间的原则是:只要有可执行的进程,那么就总会有进程正在执行。
但是只要系统中可运行的进程的数量比处理器的个数多,就注定某一给定时刻会有一些进程不能执行。这些进程在等待运行。在一组处于可运行状态的进程中选择一个来执行,是调度程序所需完成的基本工作。
对Linux而言,不特别区分线程与进程,线程只不过是一种特殊的进程
内核调度对象是线程,而不是进程
Linux 内核的调度算法,是根据task_struct结构体来进行调度的。
但是对linux操作系统来说,调度解决的对象是线程而不是进程,很大一部分原因在于linux认为线程和进程是一样的。
其中一种理想情况:有两个进程参与调度,两个进程都只有一个线程,调度俩线程,和调度两进程一样。
多任务操作系统就是能同时并发的交互执行多个进程的操作系统。
在单处理器机器上,这会产生多个进程在同时运行的幻觉。
在多处理器机器上,这会使多个进程在不同的处理器上正真的同时、并行地运行。
多任务系统可以划分为两类:非抢占式多任务和抢占式多任务。
强制挂起的动作叫抢占。调度程序决定什么时候停止一个进程的运行,以便其他进程能够得到执行机会。进程在被抢占之前能够运行的时间是预先设置好的,叫做进程时间片。
进程主动挂起自己的操作叫让步。在非抢占式任务模式下,除非进程自己主动停止运行,否则它会一直执行。绝大数系统采用抢占式多任务
Linux2.5的调度程序是O(1)调度算法
O(1)调度算法在拥有数以十计的多处理器的大服务器下尚能表现出近乎完美的性能和可扩展性,但是对于运行在桌面系统上,响应时间短的交互式程序,O(1)调度算法表现不佳。
Linux2.6的调度程序是完全公平调度算法,简称CFS
开发人员为了提高对交互程序的调度性能,引入了完全公平调度算法
1)I/O消耗型和处理器消耗型的进程
I/O消耗型:指进程的大部分时间用来提交I/O请求或是等待I/O请求
处理器消耗型:指进程把大部分时间用在代码执行上。对于处理器消耗型的进程,调度策略往往是尽量降低它们的调度频率,而延长其运行时间。
进程可以同时具有I/O消耗型和处理器消耗型两种特性,即时而表现出I/O消耗型特性,时而表现出处理器消耗型特性
调读策略通常要在两个矛盾的目标中间寻找平衡:进程响应迅速和最大系统利用率。
调读策略要考虑进程切换所消耗的极短处理器时间
Linux2.6倾向于优先调度I/O消耗型进程,但是并未忽略处理器消耗型的进程。
2)进程优先级
普通进程的优先级nice,取值范围-20~+19,nice越大优先级越低,
不同的系统对nice的运用有差异Mac OS X的进程nice值代表时间片的绝对值,Linux的进程nice值代表时间片的比例
实时进程的实时优先级,取值范围0~99,实时优先级越大优先级越高,
任何实时进程的优先级都高于普通进程
3)时间片
时间片表明进程在被抢占前能持续运行的时间。
时间片过长系统对交互的响应表现差劲,时间片太短会明显增大进程切换带来的处理器损耗。
I/O消耗型用短的时间片合适,处理器消耗型用长的时间片合适
Linux2.6默认时间片是10ms,CFS调度器会根据不同进程的特性以10ms为基础,按比例二次划分后再给进程。例如:比例进程nice值越高,进程的时间片二次划分被赋予低权重
Linux2.62的CFS调度器的抢占时机看消耗的处理器使用比,使用比越小越先投入运行。
4)调度策略的活动
想象Linux2.6上有两个可运行进程:一个文字编辑程序和一个视频编码程序。
a)对于文字编辑器而言,有两个目标
一是希望系统给它更多的处理器时间,这并发因为它能消耗的处理器时间多,是因为我们希望在它需要处理器时总能得到处理器。
二是我们希望文本编辑器能在其被唤醒时(也就是当用户打字时),抢占视频编码器程序。这样才能确保文本编辑器具有很好的交互性能,以便能响应用户输入。
文本编辑器能抢占所以两个目标能实现:文本编辑器消耗得到时间片速度慢,视频编码器消耗得到时间片速度快,Linux2.62的CFS调度器的抢占时机看消耗的处理器使用比,文本编辑器的处理器使用比更小更先投入运行。
b)视频编码器而言,有一个希望快速完成编码任务的目标
文本编辑器能抢占,但是文本编辑器处理了用户的击键输入后,时间片都没有用完就进入睡眠了,视频编码器会被唤醒,所有虽然文本编辑器能抢占,但是它抢了很快会递处理器给视频编码器,总之来也匆匆,去也匆匆。处理器最终大多在视频编码器手里
1)调度器类
Linux2.6中:完全公平调度(CFS)是针对普通进程的调度类。
Linux2.6调度器允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。调度器按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调度器类胜出,去选择接下来要执行的那一个程序
2)Unix系统中的进程调度
现代进程调度器有两个通用的概念:进程优先级和时间片。
进程一旦启动就会有一个默认时间片。具有更高优先级的进程将运行得更频繁而且在大多数系统上也会被赋予更多的时间片
现实生活中使用nice值的形式的优先级存在不合理,导致的反常问题:
问题一:
Linux的进程nice值代表时间片的比例,
时间片基础值105ms ---- 高优先级的nice值为0 ---- 时间片的比例20/21 ---- 进程获得的处理器时间100ms
时间片基础值105ms ---- 低优先级的nice值为+20 ---- 时间片的比例1/21 ---- 进程获得的处理器时间5ms
低优先级的进程往往是后台计算密集型的进程,高优先级的进程往往是前台用户任务。nice越大优先级越低,所以此时计算密集型的进程获得的处理器时间5ms,就与处理器消耗型获得长的处理器时间相背
问题二:
nice值为0---- 时间片的比例20/21----进程映射的处理器时间100ms
nice值为1---- 时间片的比例19/21----进程映射的处理器时间95ms
nice值为2---- 时间片的比例18/21----进程映射的处理器时间90ms
… … … … … …
… … … … … …
nice值为17---- 时间片的比例3/21----进程映射的处理器时间15ms
nice值为18---- 时间片的比例2/21----进程映射的处理器时间10ms
nice值为19---- 时间片的比例1/21—进程映射的处理器时间5ms
nice值为0与nice值为1的进程映射的处理器时间相差微乎其微
nice值为18与nice值为19的进程映射的处理器时间相差两倍
此时nice值减少1的效果时而大,时而微乎其微
解决方法是改nice值不加减1这样的算数变换为几何变换
问题三:
时间片需要硬件如定时器,定时器有最小刻度。所以时间片是定时器最小刻度的整数倍,所以时间片按比例二次划分后也要是定时器最小刻度的整数倍。定时器节拍偶尔出现波动也会影响时间片的大小
3)公平调度
CFS在时间片上体现的公平理念:
理想情况:所有进程的时间片保持一样,这样的绝对公平,不实用不现实。
为了相对公平公平,先定一个目标延时,即一个进程开始运行到运行完时间片,其他所有就绪进程运行完,到这个进程再次运行的这段延时。用目标延时÷进程数=各个进程的时间片。
考虑进程数量多时,计算出的各个进程的时间片趋近于0,要设置一个最小只能是多少的常数。此常数叫最小粒度
再考虑进程I/O消耗型用短的时间片合适,处理器消耗型用长的时间片合适,再用nice值做权重,对用公式目标延时÷进程数=各个进程的时间片计算出的时间片进行再调整。
1)时间记账
Linux2.6的CFS的时间片,不再是时间片,而是时间记账。即用时间记账来确保进程只在公平分配给它的处理器时间内运行。
CFS使用调度器结构体sched_entity来追踪进程运行记账。进程描述符结构体中有调度器结构体类型的成员se
Linux2.6的CFS使用vruntime变量来记录一个程序到低运行了多长时间以及它还应该在运行多久。
update_curr()是个由系统定时器周期性调用的函数。update_curr()计算当前进程的执行时间,并且将其存放在变量delta_exec中,然后再将运行时间传递给__update_curr(),__update_curr()根据当前进程总数对运行时间进行加权计算,最终将计算结果与当前运行进程的vruntime相加,和赋给vruntime
2)进程选择
当CFS需要选择下一个运行进程时,它会挑一个具有最小vruntime的进程。
如何实现选择具有最小vruntime值的进程:CFS使用红黑树来组织可运行进程队列,并利用红黑树迅速找到最小vruntime值的进程。
Linux2.6中,红黑树称为rbtree,它是一个自平衡二叉树搜索树。
向树中加入进程的情况:在进程变为可运行状态(被唤醒)或者是通过fork()调用第一次创建进程时
从树中删除进程的情况:在进程堵塞(变为不可运行态)或者终止时(结束运行)
3)调度器入口
Linux2.6的进程调度的主要入口点是函数schedule()。schedule()需要和一个具体的调度类相关联,调度类需要有自己的可运行队列。
schedule()会调用pick_next_task(),pick_next_task()会以优先级为序,从高到低,依次检查每一个调度类,并且从最高优先级的调度类中,选择最高优先级的进程
进程休眠有多种原因,但肯定都是为了等待一些事件。事件可能是一段时间从文件I/O读更多数据,或者是某个硬件事件。无论哪种情况,内核的操作都相同:进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程。
唤醒的过程是,进程被设置为可执行状态,然后再从等待队列中移到可执行红黑树中。
等待队列是由等待某写事件发生的进程组成的简单链表。
进程调用DEFINE_WAIT()创建一个等待队列的项,调用add_wait_queue()自己加入队列中。我们必须在其他地方撰写相关代码,在事件发生时,对等待队列执行wake_up()操作。
其他相关函数:prepare_to_wait()可变更进程的状态为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。
finish_wait()可把进程移出等待队列
上下文切换,也就是从一个可执行进程切换到另一个可执行进程,有定义在kernel/sched.c中的context_switch()函数负责处理。每当一个新的进程被选出来准备投入运行的时候,schedule()就会调用context_switch()函数。
内核提供了一个need_resched标志来表明是否需要重新执行一次调度。即上一次调度去执行的进程开始执行后,此时出现别进程需要立马运行。
所以置need_resched标志为1,表示有其他进程应当被运行了,要尽快调用调度程序。
因为访问进程描述符内的数值要比访问一个全局变量快(因为current宏速度很快并且描述符通常都在高速缓存中),所以2.2以前need_resched标志是一个全局变量,2.2到2.4版内核中它在task_struct中,2.6版中need_resched标志被移到thread_info结构体里,用一个特别的标志变量中的一位来表示。
1)用户抢占
内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。
用户抢占产生的情况:a)从系统调用返回用户空间时,检查到need_resched标志被设置 b)从中断处理程序返回用户空间时检查到need_resched标志被设置
2)内核抢占
只要重新调度是安全的,内核就可以在任何时间抢占正在执行的任务。只要没有持有锁,内核就可以进行抢占。内核进程持有锁时,抢占不安全。
Linux2.6为了支持内核抢占,为每个进程的thread_info引入preempt_count计数器。计数器初值0,使用锁时加一,释放锁时减一。当数值为0的时候,内核就可以执行抢占。
如果内核进程的代码自己清楚某时刻被抢占是安全的,自己调用了schedule(),肯定在所以操作系统中都是可以的。
实时调度策略不被完全公平调度器管理,而是被一个特殊的实时调度器管理。
实时调度策略1是SCHED_FIFO:
处于可运行状态的SCHED_FIFO级的进程会比任何CHED_NORMAL级的进程都先得到调度。只要有SCHED_FIFO级的进程在执行,其他级别较低的进程就只能等待它变为不可运行态才有机会执行。两个或更多SCHED_FIFO级的进程之间会轮流执行,但是占有处理器的SCHED_FIFO级的进程可以想给就给,不想给就不给。
实时调度策略2是SCHED_RR:
处于可运行状态的SCHED_FIFO级的进程会比任何CHED_NORMAL级的进程都先得到调度。只要有SCHED_FIFO级的进程在执行,其他级别较低的进程就只能等待它变为不可运行态才有机会执行。两个或更多SCHED_FIFO级的进程之间会轮流执行,占有处理器的SCHED_FIFO级的进程时间片耗尽,重新调度一个同一优先级的进程去执行。
普通的非实时的调度策略是CHED_NORMAL:
Linux2.6的默认情况下,实时进程的优先级是0到99,CHED_NORMAL级进程的优先级是100到139。值越大代表优先级越低。
Linux提供了一个系统调用族,用于管理与调度程序相关的参数。这些系统调用可以用来操作和处理进程 优先级、调度策略即处理器绑定,同时还提供了显式地将处理器交给其他进程的机制。
1)与调度策略和优先级相关的系统调用
sched_setscheduler()获取进程的调度策略和sched_getscheduler()获取进程的实时优先级,主要工作是读取或修改进程描述符的policy和rt_priority的值。(policy n.政策 priority n.优先级)
sched_setparam()设置进程实时优先级和sched_getparam()获取进程实时优先级
nice()对给定的进程的静态优先级增加一个给定的量。主要工作是设置进程的描述符的static_prio和prio值
2)与处理器绑定有关的系统调用
Linux默认是让一个进程尽量在同一个处理器上运行,但是也允许用户强制指定某个进程必须在某些处理器上允许。
进程描述符中的位掩码标志cpus_allows,置某位进程就可以在对应此位的处理器上运行
3)放弃处理器时间
过期队列:
a)过期队列和活动队列结构一摸一样
b)过期队列上放置的进程,都是时间片耗尽的进程
c)当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
普通的非实时进程执行语句sched_yield()进程会被放入过期队列中。
实时进程执行语句sched_yield(),由于实时进程不会过期,所以无法放入过期队列,就放入优先级队列的最后面。