概述
多任务系统可分为非抢占式所任务和抢占式多任务。
调度策略
通常情况下,使用什么样的调度策略与进程的具体类型有关。进程的一种分类方法是CPU消耗型和I/O消耗型。前者需要大量的CPU时间用于数值计算,
后者则需要花费很多的时间等待I/O操作的完成。
另一种分类方法把进程区分为3种。
交互式进程。这类进程需要大量的人机交互,因此会不断地休眠,等待键盘和鼠标输入操作将其唤醒。此类进程对系统响应时间要求比较高,否则用户
将会觉得系统反应非常迟钝。典型的应用为文本编辑器等。
批处理进程。这类进程因为不需要与用户交互,因此经常在后台运行,能够忍受响应的延迟,比如编译器。
实时进程。实时进程对系统的响应时间有很高的要求,它们需要短的响应时间,并且这个时间的变化非常小。典型的实时进程为音视频播放软件等。
SCHED_NORMAL、SCHED_BATCH与SCHED_IDLE都适用于普通进程,它们之间的区别如下。
(1)SCHED_NORMAL是默认采用的调度策略,SCHED_BATCH适用于批处理进程,SCHED_IDLE适用于运行在极低优先级的后台进程。
(2)SCHED_BATCH与SCHED_NORMAL比较相似,只是在唤醒时有所区别,唤醒比较频繁的进程不适合采用SCHED_BATCH。与SCHED_BATCH与
SCHED_NORMAL调度策略的两个进程,如果它们的nice值相同,将会得到调度程序公平的对待(每个进程占用CPU的时间相等)。
(3)SCHED_BATCH仍然取-20~19之间的nice值,SCHED_IDLE则溢出了-20~19的nice值范围。如果进程采用了SCHED_IDLE调度策略,意味着将会有
"super idle"的工作量。
进程所采用的调度策略反映在进程描述符的policy字段,它的值可以取上述的调度策略之一。在使用fork()创建进程时,子进程会继承父进程的policy值,
另外我们也可以通过系统调用sched_setscheduler来修改它。
进程的nice值
nice值是每个进程都会具有的属性,它不像常常被误解的那样是进程的优先级,而仅仅是一个能够影响进程优先级的数字。
nice值不同于进程的优先级,这可以通过命令"ps -el"的执行结果体现出来。
目前的内核不再存储nice值,而代之以static_prio(静态优先级)。nice值用户可见,静态优先级则隐藏在内核里,nice值与静态优先级之间通过一定
的关系进行换算。因此准确地说,nice值仅仅影响进程的静态优先级,但是对于普通进程来说,最终的动态优先级是基于静态优先级计算出来的。
优先级
1.静态优先级
之所以成为静态,是因为它从不随时间而改变,内核不贵主动去修改它,只能通过系统调用nice去修改。
2.动态优先级
调度程序通过增加或减少进程静态优先级的值来奖励I/O消耗型进程或惩罚CPU消耗型进程,调整后的优先级即称之为动态优先级,存储在进程描述符
的prio字段。我们通常所说的进程优先级指的即是进程的动态优先级。
进程的动态优先级在0~MAX_PRIO-1之间取值(MAX_PRIO定义为140),其中0~MAX_TR_PRIO-1对应实时进程,MAX_RT_PRIO~MX_PRIO-1对应普通
进程,数值越大,表示进程优先级越小。
普通进程的优先级通过一个关于静态优先级和进程交互性的函数关系计算得来,随任务的实际运行情况调整。
实时进程的优先级与它的实时优先级成线性关系,不随进程的运行而改变。
3.实时优先级
实时优先级只对于实时进程有意义,存储在进程描述符的rt_priority字段,取值范围为0~MAX_RT_PRIO-1。
时间片
在完全公平调度器CFS被正式合并入内核之前,时间片是各种调度算法中一个很重要的概念,它指定了进程在被抢占前所能持续运行的时间。调度器的
一个重要目标便是有效地分配CPU时间片,以便提供良好的用户体验。
但是这个目标实现起来显然不会容易,时间片过长会导致对交互式进程的响应不佳,过短则会明显增加进程间切换所带来的消耗,因为肯定会有相当一部分
CPU时间用在不断的进程切换上。为了解决这个矛盾,内核采取下面的措施
(1)提高交互式进程的优先级,同时提供给它们较长的默认时间片。
(2)不需要进程一次用完自己所有的时间片,它可以分成多次使用。
8.2进程调度器的发展历史
O(1)调度器
(1)运行队列
对于O(1)调度,每一个CPU都维护一个自己的运行队列,这大大减小了竞争。
运行队列由一个复杂的数据结构struct runqueue表示,其中包含两个优先级数组,一个活跃优先级数组(active)和一个过期优先级数组(expired)。
active指向时间片没用完、当前可被调度的可运行进程,expired指向时间片已经用完的可运行进程。
(2)时间片的影响
(3)优先级计算的时机
(4)支持内核抢占
(5)负载均衡
SD调度器
楼梯调度(SD)算法主要用于提升Linux内核对交互式进程的及时响应能力,其目标是应用于桌面系统。
RSDL调度器
CFS调度器
O(1)调度器
运行队列
运行队列是O(1)调度器中最基本的数据结构,也是整个调度算法构建的基础。
运行队列是给定处理器上可运行进程的链表,每个CPU都会维护一个。每个运行队列包含两个优先级数组,一个活跃优先级数组和一个过期优先级
数组。所有的可运行进程都会首先进入active数组,当它们耗尽了自己的时间片之后,会被转移到expired数组,进程的时间片在这个转移过程中
被重新计算。当active数组中的所有进程都耗尽了自己的时间片时,active与expired指针进行交换,原来的过期数组成为活跃数组。
优先级数组
O(1)调度器总是选择最高优先级的进程运行,如果有多个进程具有同样的优先级,则它们被轮流调度。优先级数组在这个过程中提供了O(1)级
的算法复杂度。
计算时间片
函数task_timeslice()用于计算进程的时间片。
平均休眠时间
O(1)调度器通过记录进程用于休眠和用于执行的时间,来实现对进程交互性的判断。如果一个进程的大部分时间都在休眠,那么它就是I/O消耗型。
如果一个进程执行的时间比休眠的时间长,那它就是CPU消耗型。
判断交互性
宏TASK_INTERACTIVE用于判断进程的交互性,定义在kernel/sched.c文件中。
计算优先级
动态优先级的计算由函数effective_prio()完成,该函数定义在kernel/sched.c文件中。
休眠和唤醒
1.为什么休眠
进程并不总是希望运行,这个时候,它就需要进入休眠状态。休眠的原因有很多种,但肯定都是为了等待某些事件,比如等待I/O操作完成。
休眠是一个特殊的状态,休眠的进程不能够被调度运行,否则调度程序就会可能选出一个并不愿意被执行的进程。
2.TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态
当一个进程进入休眠状态时,它应该处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE两种状态之一。这两种状态的唯一区别就是处于
TASK_UNINTERRUPTIBLE的进程不能够被信号唤醒。
3.等待队列
等待队列是由等待某些事件发生的进程所组成的链表。当与等待队列相关的事件发生时,队列上的所有进程会被唤醒。
4.进入休眠
进程通过如下的一些步骤将自己加入到一个等待队列中。
调用DECLARE_WAITQUEUE()创建一个等待队列。
调用add_wait_queue()将自己添加到新创建的等待队列中。当等待的条件满足时,该进程会被唤醒。
该进程将自己的状态变更为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。
开始一个循环,并检查等待的条件是否满足,如果是的话,就没必要休眠了。否则,调用schedule()放弃CPU,该进程将不再被调度。
当进程被唤醒时,再次检查条件是否满足,如果是,就退出循环,否则再次调用schedule()并一直重复这步操作。
一旦条件满足,进程的状态变更为TASK_RUNNING,并调用remove_wait_queue()将自己从等待队列中删除。
5.唤醒
函数schedule()是主要的调度器函数,它的工作是选定下一个进程并切换到它去运行。当一个进程自愿放弃CPU时,或者有某个进程将被抢占时,
schedule()都会被调用。
schedule()定义在kernel/sched.c文件,代码中的关键部分如下:
(1)下一个被调度运行的进程描述符next
(2)禁止内核抢占并获取本地CPU的运行队列
(3)获得运行队列的自旋锁
(4)从运行队列中选择下一个被调度运行的进程。
CFS调度器
完全公平与进程的权重
CFS是“Completely Fair Scheduler”(完全公平调度器)的缩写,这里的公平并不意味着绝对的公平,即简单地将系统内所有进程都一视同仁。这种
绝对的公平也是不可能实现的,因为进程之间本身就不平等,比如,许多内核线程用来应付某些紧急的情况,它们理应比其他进程更为优越一些。
CFS使用权重来区分进程之间并不平等的地位,进程的权重也是CFS实现公平的依据。
权重可简单理解为进程的重要性,它由进程的优先级所决定,优先级越高,权重也就越高,但进程的优先级与权重之间并不是一个简单的线性
关系,内核提供了一些经验数值来进行它们之间的转化。定义了kernel/sched.c文件中的数组prio_to_weight即包含了这些经验数值,它里面的
40个数值一一对应了40个优先级(40个nice值)所应有的权重。
模块化
内核通过引入调度类来增加调度程序的可扩展性。调度类将调度策略模块化,封装了对不同调度策略的具体实现。核心的调度程序代码只需调用
调度类提供的接口,完成具体的调度任务,并不需要考虑调度策略的实现细节。因此,调度类可以看作是核心的调度程序代码与具体的调度策略实现
之间的桥梁。
调度实体
调度实体是伴随CFS引入内核的另一个重要概念,它代表被调度的对象,保存了该对象的调度信息,这个对象可以是一个进程,也可以是一个进程组
(支持组调度)。
内核中共有两种调度实体,分别是CFS调度实体(struct sched_entity)和实时调度实体(struct sched_rt_entity),它们在include/linux/sched.h定义。
schedule()
(1)下一个被调度运行的进程描述符next
(2)禁止内核抢占并获取本地CPU的运行队列
(3)获得运行队列的自旋锁
(4)如果prev->on_rq为1,则将prev放入运行队列
(6)切换到所选择的进程
进程抢占与切换
内核抢占发生的时机:
从中断处理程序返回内核空间的时候。此时,内核户检查preempt_count和TIF_NEED_RESCHED标志,如果TIF_NEED_RESCHED被设置,并且
preempt_count等于0,就会调用schedule()。
当内核代码再一次具有可抢占性的时候。如果当前进程所持有的所有锁都已经被释放,那么preempt_count会变为0,内核再一次具有可抢占性。此时
释放锁的代码会检查TIF_NEED_RESCHED标志是否被设置,如果是的话,就会调用schedule()
内核中的进程显示调用schedule()的时候
内核中的进程被阻塞的时候
从一个可运行进程切换到另一个可运行进程的过程称为进程切换,也可以称为上下文切换。
1.硬件上下文 2.任务状态段TSS 3.硬件上下文的存放 4.context_switch()