linux内核调度 - 图文(7)

2019-04-23 20:18

2,如果没有等待资源,则将该任务加入到就绪队列中。

3,调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+rt_priority),选择权值最高的任务使用cpu,该FIFO任务将一直占有cpu直到有优先级更高的任务就绪(即使优先级相同也不行)或者主动放弃(等待资源)。

4,调度程序发现有优先级更高的任务到达(高优先级任务可能被中断或定时器任务唤醒,再或被当前运行的任务唤醒,等等),则调度程序立即在当前任务 堆栈中保存当前cpu寄存器的所有数据,重新从高优先级任务的堆栈中加载寄存器数据到cpu,此时高优先级的任务开始运行。重复第3步。

5,如果当前任务因等待资源而主动放弃cpu使用权,则该任务将从就绪队列中删除,加入等待队列,此时重复第3步。

所有任务都采用RR调度策略时

1,创建任务时指定调度参数为RR,并设置任务的实时优先级和nice值(nice值将会转换为该任务的时间片的长度)。

2,如果没有等待资源,则将该任务加入到就绪队列中。

3,调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+rt_priority),选择权值最高的任务使用cpu。

4,如果就绪队列中的RR任务时间片为0,则会根据nice值设置该任务的时间片,同时将该任务放入就绪队列的末尾。重复步骤3。

5,当前任务由于等待资源而主动退出cpu,则其加入等待队列中。重复步骤3。

系统中既有分时调度,又有时间片轮转调度和先进先出调度

1,RR调度和FIFO调度的进程属于实时进程,以分时调度的进程是非实时进程。 2,当实时进程准备就绪后,如果当前cpu正在运行非实时进程,则实时进程立即抢占非实时进程。 3,RR进程和FIFO进程都采用实时优先级做为调度的权值标准,RR是FIFO的一个延伸。FIFO时,如果两个进程的优先级一样,则这两个优先 级一样的进程具体执行哪一个是由其在队列中的位置决定的,这样导致一些不公正性(优先级是一样的,为什么要让你一直运行?),如果将两个优先级一样的任务 的调度策略都设为RR,则保证了这两个任务可以循环执行,保证了公平。

调度?咋这熟悉,我们是不是常在哪里听到。没错,是的,调度我们时常听过,比如交通管制调度啦等。这不,夏天这热, 标语贴的好:相应国电电力调度,做文明市民,好别扭啊!不管了。你要是还是不懂,再啰嗦讲个事,过年回家,和漂亮的GF回家,为了张普通的硬座票还要排老久对,甚至还可能被坑拿到黄牛票,这时你嘴里咧咧的啥:XX,啥火车站,做的啥春运调度啊!唉,这次你说到点上了。

总结一下:调度就是通过调度程序的合理调度,实现系统资源的最大限度发挥作用。多

进程的并发正是这样的效果。其实原理一点也不复杂,道理也一样简单:只要又可以执行的进程,那么就总是会有进程正在执行。但简单的问题也会复杂化,比如:我们买票为啥抱怨调度,归根接地感谢当年的人海战术(多说一句,其实现实的很多问题,一个人海战术解决所有,这战术中国人用起来最得心应手)。好么,一般系统中进程的数目总会比处理器的个数多,所以竞争在所难免,调度的问题就集中在解决竞争的问题。

种类问题不多说:抢占和非抢占。linux提供了抢占式的多任务模式,下一阶段谁得到CPU的执行时间由调度程序决定。这怎么决定,是不是请个客,喝个酒啥的。对不起,linux无情的说,我是开源的,对所有人公平,哥不吃这一套。我有自己的一套原则(策略,这个我们待会儿再讲)。接着来术语,上面的过程叫做抢占,进程得到CPU的执行机会这段时间叫时间片,是由系统定义好的。后面我们会看到,linux调度程序采用动态方法计算时间片。说了抢占,再说说非抢占,就是说没人跟你抢,除非自己放弃,否则你自己运行下去吧。进程主动放弃挂起自己的行为叫让步。这种机制看似很和谐(不要被和谐哦),但问题挺多,比如系统不知道每个进程执行多长时间。而且一旦一个悬挂进程不愿意或者忘了做出让步,那系统崩了咋办,我的money,我的future,我的lover,从此一去不复返。

说了那多,开始正题。原则,一切都有原则,linux的原则。开始原则以前,再来认识一下我们前边说过的进程,其实进程远不如以前那样简单。进程也有种类的,至少分为IO型和CPU型。IO型指那种大部分时间用来提交I/O请求或是等待I/O请求的进程。这样的进程挺讨人厌的,他通常都是运行一小小会儿,因为它总是在等待更多的I/O请求时最后总会阻塞。与之对应的则是CPU型进程。这样的进程把时间大多放在执行代码上,除非被抢占,他们就一直贪得无厌的运行。因为它们没有太多的IO请求。由于它们不是IO驱动类型,也就不存在用户交互的问题(IO驱动不仅仅指磁盘,还指键盘鼠标等),所有从系统交互响应速度而言,调度器不应该经常让他们运行。即降低它们的运行频率,而将它们的运行时间拖长一些。这里所说的原则就是:调度策略要在进程响应迅速(相应时间短)和进程吞吐量高之间做出艰难的决定。这个原则有时不公平,很残酷。残酷的让人不懂,不懂低优先级的有时不能公平对待。由于交互式进程属于IO型进程,所以linux为了保证交互式应用,所以调度策略会向IO型进行倾斜。

上面提到优先级,调度算法中最基本的一类当然就是基于优先级的调度。挺简单:优先级高的先运行,相同优先级的按轮转式进行调度。优先级高的进程使用的时间片也长。调度程序总是选择时间片未用尽且优先级最高的进程运行。这句话就是说用户和系统可以通过设置进程的优先级来响应系统的调度。基于此,linux设计上一套动态优先级的调度方法。一开始,先为进程设置一个基本的优先级,然而它允许调度程序根据需要来加减优先级。linux内核提供了两组独立的优先级范围。第一种是nice值,范围从-20到19,默认为0.nice值越大优先级越小。另外nice值也用来决定分配给进程时间片的长短。第二种范围是实时优先级。默认范围是从0到99.任何实时的优先级都高于普通优先级。这个我们后面再说。 说了那么多的时间片,说起来不费劲,做起来那不是一个麻烦可以说清楚的。时间片就是一个数值,它表明进程在被抢占前所能持续运行的时间。默认的时间片是20ms,很短。linux调度程序还能根据进程的优先级动态调整分配给他的时间片。特别说明的是,进程不一定要一次用完所有分配给它的时间片。一旦进程的时间片耗尽后,就认为进程到期了。没有时间片的进程不会再投入运行。除非等到其他的进程都耗尽了它们的时间片,这时,所有进程的时间片就会被重新计算。

前边讲到了抢占的话题,当一个进程进入TASK_RUNNING状态时,内核就会检测它的优先级是否高于当前正在执行的进程。如果是,则调度程序会被唤醒,重新选择新的进程执行(套用书上的话, 应该是刚刚进入可运行状态的这个进程)。此外,当一个进程的时间片变为0时,它会被抢占,调度程序被唤醒以选择一个新的进程。(具体的例子大家可以看看参

考书籍的p28页3.1.5)

linux的调度程序定义于kernel/sched.c中,在2.6内核中的调度程序和以前有了很大的差别,体现在下面:

1.充分实现O(1)调度.这个应该懂得,可以懂的 2.在多核处理器的今天,linux也不落伍,全面实现SMP的扩展性。每个处理器拥有自己的锁和自己的可执行队列 3.强化SMP的亲和力。这是有关多核CPU亲和力的说明,我目前正在研究这个,后面我会专门在一篇博文中详细说明。 4.加强交互能力。 5.保证公平。 6.虽然最常见的优化情况是系统中只有1~2个可运行进程,但是优化也完全有能力扩展到具有多处理器且每个处理器上运行多个进程的系统中。 上面第二条可执行队列,它是调度程序中最基本的数据结构,定义于kernel/sched.c中,由结构runquene表示。它是给定处理器上的可执行进程的链表,每个处理器维护一个。每个可投入运行得进程都唯一的归属于一个可执行队列。此外,可执行队列中还包含每个处理器的调度信息。具体的结构这里就不给了,自己要有看源代码的能力哦。宏cpu_rq(processor)用于返回给定处理器可执行队列的指针,this_rq()用于来返回当前处理器的可执行队列,task_rq(task)返回给定任务所在的队列指针。 在其拥有者读取或改写其队列成员的时候,可执行队列包含的锁用来防止队列被其他代码改动,由于每个可执行队列唯一地对应一个处理器,所以很少出现一个处理器需要锁其他处理器的可执行队列的情况,但这种情况是事实存在的。最常见的情况是发生在一个特定的任务在运行队列上执行时。此时需要用到task_rq_lock()和task_rq_unlock()函数,或者可以用this_rq_lock()来锁住当前的可执行队列,用rq_unlock(struct runqueue *rq)释放给定队列上的锁。关于锁的操作,我们再次飘过,为啥?一方面,这超出了本节的重点,二者我在linux驱动理论帖中对加解锁做了详细说明,三者,我后面可能还会详细说。所以,那句话真是太真理了:暂时的放下,是再次的拿起。

从运行队列的结构中,我们发现了两个有关优先级的数组(定义在kernel/sched.c中,名字叫prio_array,具体自己去看吧):活跃的和过去的。这两个数组是实现O(1)级算法的数据结构。优先级数组使可运行处理器的每一种优先级都包含一个相应的队列,而这些队列包含对应优先级上的可执行进程链表。结构中的优先级位图主要是为了帮助提高查找当前系统内拥有最高优先级的可执行进程的效率而设计的。关于结构中的定义常量,MAX_PRIO定义了系统拥有的优先级个数,默认值是140,BITMAP_SIZE是优先级位图数组的大小,值是5,这个是可以计算出来的。比如:位图数组的类型是unsigned long类型,长是32位,假定每一位代表一个优先级(4*32 = 128<140),就需要5个这样的长整型才能表示。所以,bitmap就正好含有5个数组项。

开始时,位图成员的所有位都被清0,当某一优先级的进程开始准备执行时,位图中对应的位就置1,这样,查找系统中最高的优先级就变成了查找位图中被设置的第一位。鉴于优先级个数的定值性,查找的时间就不受系统到底有多少可执行进程的影响,是个定值。关于对位图的快速查找算法对应的函数是sched_find_first_bit().

优先级数组的list_head队列是一个链表,这个链表包含了该处理器上相应优先级的全部可运行线程。要找到下一个要运行的任务,直接从该链表中选择下一个元素。对于给定的优先级,按轮转方式调度任务。优先级数组的计数器nr_active,它保存了该优先级数组内可执行进程的数目。

下一话题,我前边反复提到时间片的话题,一旦所有进程的时间片都用完会怎样了,

总不能系统宕掉吧,好在操作系统提供了一种显式的方法来重新计算每个进程的时间片。典型就是循环访问每个进程。借用书上的一个例子: for (系统中的每个任务){ 重新计算优先级 重新计算时间片 } 这种方法简单,但弊端很多: 1.可能耗费相当长的时间。 2.为了保护任务队列和每个进程的描述符,必要要用到锁的机制。这样做很明显会加剧对锁的争用,影响系统性能。 3.重算时间片的时机不确定。 4.实现显得很粗糙。 现在采用的是新的调度方法,每个处理器拥有上面提到的两个优先级数组,活动数组上的进程都有时间片剩余,而过期数组中的进程都耗尽了时间片,当一个进程的时间片耗尽时,它会移至过期数组,但在此之前,时间片已经给它重新计算好了,重新计算时间片现在变的非常简单,只要在活动和过期数组之间来回切换就可以了。又因为数组是通过指针进行访问的,所以交换它们用的时间就是交换指针需要的时间。这个动作由schedule()完成: struct prio_array *array = rq->active; if(!array->nr_active){ rq->active = rq->expired; rq->expired = array; } 这种交换是O(1)级调度程序的核心。O(1)级调度程序根本不需要从头到尾都忙着重新计算时间片,它只要完成一个两个步骤就能实现数组的切换,这种实现完美地解决了前面列举的所有弊端。linux的调度程序是在schedule()函数实现的。当内核代码想要休眠时,会直接调用该函数,如果哪个进程将被抢占,也会被唤起执行。调度的第一步是判断谁是优先级最高的进程: struct task_struct *prev, *next; struct list_head *queue; struct prio_array array; int idx; prev = current; array = rq->active; idx = sched_find_first_bit(array->bitmpa); queue = array->queue + idx; next = list_entry(queue->next, struct task_struct, run_list); 分析上面这段代码,首先在活动优先级数组中找到第一个被设置的bit位,该位对应着最高的可执行进程。然后,调度程序选择这个级别链表里的头一个进程。这个进程就是系统中优先级最高的可执行程序,也是马上就会被调度执行的进程。上面中的prev和next不等,说明被选中的进程不是当前进程,这时就要调用context_switch来进程切换。最后强调一点,不存在任何影响schedule()时间长短的因素,因为前边说过,所用的时间是恒定的。

前边多次说过:调度程序是利用优先级和时间片来做出决定的。下边看看实际代码的实

现方式。进程结构体告诉我们进程拥有一个初始的优先级,叫做nice值,最低是19,最高是20,结构体的static_prio域就存放这个值,static就是说这个一开始就由用户指定好了,不能改变。调度程序要用的动态优先级用prio域表示,两者的关系在于通过一个关于静态优先级和进程交互性的函数计算而来。啥叫进程交互性?这是个新词,第一次提到,这样说吧,我们前边说过I/O消耗性和CPU消耗性的进程区别,进程交互性就是指IO消耗性的,因为和用户进行交互就是电脑IO和用户交互,像鼠标键盘等。但我们怎么确定一个进程的交互性的强弱呢?最明显的标准莫过于通过计算进程休眠的时间长短来做预测了。大部分时间都在休眠的进程当然是IO消耗型的,如果一个进程把所有的时间几乎都放在了CPU执行处理上,那..不说了。在linux中,用值tast_struct中的sleep_avg域记录了一个进程用于休眠和用于执行的时间,范围为0~MAX_SLEEP_AVG,默认值是10ms。当一个进程从休眠状态恢复到执行状态时,sleep_avg会根据它休眠时间的长短而增长直到最大。相反,进程没运行一个时钟节拍,sleep_avg就做相应的递减,到0为止。这种方法不仅会奖励交互性强的进程,还会惩罚处理器耗时量的进程。再加之奖励和处罚都是建立在作为基数的nice值上,所有用户仍然能够通过改变nice指来调整程序的调度。effective_prio()函数可以返回一个进程的动态优先数,该函数以nice值为基数,再加上我们上面提到的从-5到+5的进程交互性型奖励处罚分。

一旦有了上面的动态优先级,再来重新计算时间片就方便多了。另外,当一个进程创建的时候,新建的子进程和父进程均分父进程剩余的时间片。这样的分配很平均并且防止用户通过不断的创建新进程来不断攫取时间片。task_timeslice()函数为给定任务返回一个新的时间片,时间片的计算只需要把优先级按比例缩放,使其符合时间片的数值范围要求就可以了。优先级最高的进程能获得的最大时间片长度(MAX_TIMESLICE)是200ms,最短时间片(MIN_TIMESLICE)是10ms。默认优先级(nice值为0,没有交互性奖罚分)的进程得到的时间片长度为100ms。上面我们也提到过,活动数组内的可执行队列上的进程的时间片耗尽时,它就会移植到过期数组。但是,如果一个进程的交互性很强,特别特别强,那么调度程序支持另外一种机制来支持这种交互进程:当时间片用完后,它会被再放置到活动数组而不是过期数组中。回忆一下上面O(1)调度器的实现机制:进程用尽其时间片后,都会被移动到过期数组,当活动数组中没有剩余进程的时候,这个时候两个数组就会被交换。但是在发生这种交换以前,交互性很强的一个进程有可能已经处于过期数组中了,当他需要交互的时候,这时可能数组交互还没有发生,所以交互也就无法进行,这时对于这种交互特别特别强的进程重新插入到活动数组就可以避免这种问题。注意,虽然被放回到活动数组,但不会立即就运行,它还要和其他具有相同优先级的进程轮流运行。该逻辑在scheduler_tick()中实现,该函数会被定时器中断调用。看下面的实现代码: struct task_struct *task = current; struct runqueue *rq = this_rq(); if(!—task->time_slice){ if(!TASK_INTERACTIVE(task) || (EXPIRED_STARVING(rq))) enqueue_task(task, rq->expired); else enqueue_task(task, rq->active); } 这段代码先减少进程时间片的值。宏TASK_INTERACTIVE主要是基于进程的nice值来查看这个进程是否是交互性很强的进程。宏EXPIRED_STARVING负责检查过期数组内的进程是否处于饥饿状态,是否有相当长的时间没有发生数组交换了。如果最近一直没有发生切换,那么再把当前的进程放置到活动数组会进一步拖延切换--过期数组内的进程会来越来越饿.


linux内核调度 - 图文(7).doc 将本文的Word文档下载到电脑 下载失败或者文档不完整,请联系客服人员解决!

下一篇:Unit 2 Space Invaders课文翻译综合教程四

相关阅读
本类排行
× 注册会员免费下载(下载后可以自由复制和排版)

马上注册会员

注:下载文档有可能“只有目录或者内容不全”等情况,请下载之前注意辨别,如果您已付费且无法下载或内容有问题,请联系我们协助你处理。
微信: QQ: