Linux进程调度基于分时(time sharing),分时依赖于定时中断。调度的依据是优先级(回忆140个双向链表)。传统上,进程可以分为"I/O型"与“CPU型”;按另一种分法可分为:1.交互式进程;2.批处理进程(编译器、科学计算、数据库搜索引擎);3.实时进程(音频、视频)。以上分类一定程序上相互独立,一个批处理进程可能是I/O型,也可能是CPU型。如数据库服务器是I/O型、图像绘制程序是CPU型。Linux调度器可明确认出实时进程。
虽然内核可否抢占在编译内核时确定,但Linux进程是抢占式的。而“分时系统”也是基于时间片到的时候,设置thread_info中的TIF_NEED_RESCHED,让时钟中断返回用户程序时启用调度程序,“抢占”当前进程。当进程进入TASK_RUNNING态,内核查它的动态优先级是否大于当前进程。是,则current执行被中断。要注意的是,被抢占的进程仍处于TASK_RUNNING态。系统中至少有一个进程在执行,就是进程0。每个进程有三种调度类型:1.SCHED_FIFO先进先出实时调度;2.SCHED_RR分时实时调度;3.SCHED_NORMAL。它们作为进程描述符的成员存储。
每个进程首先有个静态优先级。内核用100-139这40个数来标记。越小,优先级越高。新进程继承父进程的优先级。静态优先级的作用是决定进程的基本时间片。若1.小于120,则基本时间片为(140-它)*20;2.>=120,则为(140-它)*5。即静态优先级高,则获CPU时间片长。此外,还有动态优先级。范围从100-139。它表示调度程序选择要执行的进程时的度量。经验上,它=(静态优先级-(bonus-5))。bonus为0-10。表示对动态优先级的奖赏或惩罚,它与“平均睡眠时间”有关。静、动态优先级以及后面的实时优先级也是进程描述符的成员。
设想,某个运行时间需很长的高优先级进程一次时间片用完后,即使算上bonus,它优先级仍可能最高。调度程序再次选中它,这使其它稍低优先级的进程得不到CPU,造成饥饿。为避免此情况,对可运行进程(注意,是可运行进程)维护两个不相交的集合,它们是活动进程与过期进程。只有在活动进程集合中的可运行进程才可被调度程序选中,且一次时间片用完,则放入过期进程中,直到活动进程集合为空。为空时,交换两个集合,继续按优先级进行调度。但为获得较好的用户体验,必须提高交互式进程的性能,所以调度程序在用完一个交互式进程的时间片时,仍将其放入活动进程集合,除非以下情况:1.最先进入过期进程集合的进程已经等待过长的时间(一般是1000节拍乘以可运行进程数+1);2.过期进程静态优先级比当前的交互式进程高。这时,就将交互式进程移入过期进程集合。这样,最终也会让活动进程集合为空,达到设想的状态。例外的情况是实时进程。每个实时进程与一个实时优先级相关(1-99),实时的表现在于,实时进程运行过程中, 不会让低优先级的进程得到运行机会。如前,每个优先级的进程在同一个链表中,当为SCHED_FIFO时,调度程序选中最高优先级的链表队首的实时进程运行前,会仍将它放在链表头,时间片用完时,定时中断返回用户态时会再启动调度程序,如果此时没有更高优先级的实时进程可运行,那么还会从该链表头选中同一进程,并还将它放在表头,再运行它。这就是FIFO。但当它是SCHED_RR调度类型时,被调度程序选中执行前,会将它放到链表尾。时间片到后,会选中新队头进程。无论是哪种情况,调度程序都不会选中比它们低优先级的进程。实现上,就是不像普通进程那样设两个集合,而只有一个集合。
上述活动进程、过期进程集合是与CPU时刻相关的数据结构,它们应存在何处?存在“可运行进程队列”中,它存在“每CPU变量”(per-CPU variable)中,它们在主存中经过精心排列,以对应到高速缓存的不同行。"可运行进程队列"是个结构体,结构中有此集合中进程数量的计数器以及优先级位图,它最重要的成员即是活动进程、过期进程两个集合,每集合有0-139个链表头,指向本集合优先级的可运行进程的描述符指针。另外有active和expire指针指向每个集合。如前所述,当需要时,只要交换这两个指针即可,结构中还有一个指向当前运行进程的描述符指针。
分时,优先级由时间片实现,时间片在哪里?它也在进程描述符中,可运行进程初次被选中时,会根据静态优先级初始化它描述符中的时间片字段。每次定时中断,将描述符中剩余时间片数量减去中断间隔时间,若非0,继续运行,若0,则先按静态优先级设置基本时间片,再设置进程描述符的thread_info->flags为TIF_NEED_RESCHED,使本次中断返回用户程序时调用调度程序,最后改变活动进程链表。一个细节问题是,当一个父进程创建子进程时,子进程的剩余时间片应该设为多少?应当将父进程的剩余时间片一分为二,父子平分。以避免一个进程通过不断创建子进程来霸占CPU。为配合“活动进程过期进程”方案。进程描述符中也应该存有进程最近一次被调度的时间,这通常由64位寄存器tsc赋值。这是可运行进程之间的调度。
调度程序本身由schedule()函数实现。它在以下情况被调用:1.current进程不能获取资源而阻塞。此时会把current插入适当的等待队列,将TASK_RUNNING改为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。再调用schedule(),schedule会将当前不处于RUNNING的进程从可运行队列中删除,关键是schedule()调用完成之后,会检查资源是否可用,不可用则会再调schedule。猜测,等待资源时,会将参数存于某处,资源到,中断来,会按参数唤醒等待进程。否则永远不可执行。schedule()函数关键是设next指向下个要运行进程。并用switch_to来实现相应的上下文更替。schedule先禁用内核抢占、完成当前进程的队列交换处理。若next是内核线程,它仍用pre的地址空间(后面会说),否则,context_switch完成,并完成地址空间的变换,并switch_to()。之后,因为上下文已换,schedule()的剩余指令并不由新进程执行,而由prev的prev执行,以再给它一次被CPU选中的机会。
版权声明:本文为博主原创文章,未经博主允许不得转载。