计算机底层知识拾遗(十)理解进程调度

这篇说说内核的进程调度机制,进程调度是内核的一个重要工作,由调度器完毕。


进程状态

内核调度器调度的实体(KSE, kernal schedule entry)是进程和线程。内核必须知道全部进程和线程的状态,比方把时间片给一个堵塞的进程是没有意义的。从内核的角度来看,进程的状态有3种:

1. 执行,表示正在执行的进程

2. 等待,没有执行,可是等待时间片执行的进程

3. 睡眠,也就是堵塞,包含可中断的堵塞和不可中断的堵塞。睡眠的进程在等待一个事件的发生,调度器无法在下一次任务切换时选择睡眠的进程


进程在几种状态中不断的切换

1表示执行的进程要等待某个事件,进入到了睡眠状态

2表示执行的进程交出CPU资源,进入到了等待状态

3表示睡眠的进程等待的事件发生了,它进入到等待状态,不能直接进入到执行状态

4表示等待的进程获得CPU资源,变成执行状态

5表示执行的进程结束,进入到终止状态


内核将全部的进程保存在一个进程表中,不论是执行,等待,睡眠。睡眠的进程会被特别标记出来,调度器知道它们无法马上执行,就不会在下一个任务切换时选择它们。睡眠的进程被分在多个队列中,它们会在适当的时候被唤醒。对于睡眠的进程,又分为两种:

1. TASK_INTERUPTIBLE,可中断的睡眠,当内核发送信号给该进程表示它等待的事情已经发生时,响应信号处理程序把进程状态改成TASK_RUNNING,表示进入可执行状态,仅仅要调度器选中它就能够执行

2. TASK_UNINTERUPTIBLE,不可中断的睡眠,不能由外部信号唤醒,即不响应外部信号,仅仅能由内核亲自唤醒


所谓的僵尸进程是指进程资源已经被释放,可是还保存在进程表中的进程。通常造成僵尸进程的原因是子进程已经被终结,可是父进程没有调用wait4系统调用来确认父进程知道子进程已死亡。这样子进程因为没有被父进程确认死亡,可是已经释放了资源,变成了僵尸进程。


从运行权限的维度来考察进程状态,进程状态分为用户态和核心态。用户态仅仅能訪问进程自身的数据,是受限的。核心态具有无限权限,能够訪问随意数据。

用户态到核心态的切换有两种方式,一种是用户态运行系统调用,会切换到核心态。另外一种是中断,其中断发生时,也会切换到核心态。

对于抢占式调度模型来说,

1. 中断具有最高权限,能够抢占处于用户态或者核心态的进程的时间片

2. 当进程处于核心态并运行系统调用时,不能被其它进程抢占,当然中断除外

3. 在用户态运行的进程能够随时被抢占


调度器

调度器主要解决两个问题

1. 调度策略,即决定为每一个进程分配多少执行时间,何时切换到下一个进程,下一个进程是什么

2. 上下文切换,即从进程A切换到进程B时,要保证进程B的运行环境和上次被撤销运行时全然一致,比方寄存器的内容,虚拟地址空间的各个数据结构。


Linux的调度器和传统基于时间片的调度器有所差别,它考虑的是进程的等待时间,即全部可执行的进程在一个就绪队列里面等待的时间。对CPU时间要求最严格的进程被挑选执行。就绪队列中的进程被组织成一棵红黑树来加速操作。等待最长的进程在最左边。


调度器子系统的主要组件例如以下,

1. 主调度器和周期性调度器称为通用调度器,来确定是否要调度。前者处理进程打算睡眠或者由于某种原因放弃CPU的情况,后者以周期性频率执行,检測是否须要上下文切换

2. 调度器类来挑选下一个执行的进程。调度器类分装了不同的调度算法,比方全然公平调度,实时调度,以模块的方式执行

3. 选中下一个要运行的进程后,就要进行上下文切换,须要和CPU紧密结合

4. 每一个进程都属于一个特定的调度器类,由调度器类来管理所属的进程,通用调度器不涉及进程的状态


进程的task_struct结构中和调度相关的属性例如以下

1. prio, static_prio, normal_prio表示进程的优先级信息。static_prio表示静态优先级,也就是进程启动时分配的nice优先级值。normal_prio是基于static_prio和调度策略计算出来的优先级,进程分支时,子进程基础normal_prio。prio是调度器考虑的优先级,有时候内核要暂时提升某个进程的优先级,就是改动prio值,不影响static_prio和normal_prio

2. sched_class表示该进程所属的调度器类

3. sched_entity表示进程所属的调度实体,调度器不仅能够调度进程,还能够调度进程组,线程等调度实体

4. policy表示进程的调度策略,比方SCHED_NORMAL调度普通进程,用全然公平调度器类来处理。SCHED_BATCH,SCHED_IDEL,SCHED_RR,SCHED_FIFO等

5. time_slice指定该进程能够使用的剩余时间片



调度器类必须提供sched_class的实例,指定了调度器类能够进行的操作

1. enqueue_task表示把一个进程增加到就绪队列,当一个进程状态从睡眠变成可执行时,就是发生了这个操作进入到了就绪队列

2. dequeue_task表示把一个进程移出就绪队列,比方一个进程从可执行状态切换到不可执行状态

3. yield_task表示进程自愿放弃CPU控制权的操作

4. check_preempt_curr表示一个新唤醒的进程来抢占当前进程,比方wake_up_new_task唤醒新进程时发生这个操作

5. pick_next_task用于选择下一个运行的进程,向进程提供CPU资源。但在不同进程切换时,还须要运行一个底层的上下文切换

6. task_tick表示激活周期性调度器

7. task_new表示将fork出来的新进程增加到调度器类



用户层程序是无法直接和调度器类交互的,都是通过调度策略常量,比方SCHED_NORMAL,SCHED_BATCH,SCHED_IDEL映射到全然公平调度器类fair_sched_class, SCHED_RR,SCHED_FIFO映射到实时调度器rt_sched_class。


每一个CPU都相应一个就绪队列,一个活动进程仅仅能出如今一个就绪队列中。对于仅仅使用进程的程序来说,一个进程仅仅能同一时候在一个CPU执行。而基于线程的程序来说,源于一个进程的不同线程能够同一时候执行在多个CPU。就绪队列的结构例如以下

1. nr_running和load表示当前就绪队列的负载情况。就绪队列的虚拟时钟的速度就是基于这个信息

2. cfs_rq是全然公平调度器类的子就绪队列,rt_rq是实时调度器类的就绪队列

3. curr指向当前正在执行的进程

4. clock用于实现就绪队列自身的时钟,每次调用周期性调度器都会更新clock值



调度实体的结构例如以下,它表示一般性的能够调度的实体,包含进程,进程组,线程等等

1. load表示负载,表示该实体占用队列总负荷的比例。计算负荷是调度器类的一个重任,它影响到虚拟时钟的速度

2. run_node表示红黑树的节点,让这个实体能够出如今红黑树上

3. on_rq来表示该实体是否处于就绪队列

4. exec_start表示进程每次開始运行的时间,sum_exec_runtime表示该进程总共运行了的时间。每次进程运行開始时,都会记录exec_start值,然后每次调用update_curr系统调用都会用当前时间减去exec_start,把差值加到sum_exec_runtime中。

5. 当进程被撤销CPU时,会把sum_exec_runtime保存到pre_sum_exec_runtime中去。当进程抢占时,sun_exec_time又单调增长。

6. vruntime记录进程执行期间虚拟时钟流式的时间数量



通用调度器有两类,一个是周期性调度器,一个是主调度器。周期性调度器在schedule_tick函数中实现。内核依照频率自己主动调用这个函数,会激活当前进程的调度器类的周期性

调度方法。

假设当前进程须要被又一次调度,那么调度器类会在task_struct中设置TIF_NEED_RESCHED标志,内核会在适当的时机完毕该调度请求。


主调度器负责把CPU从一个进程交给还有一个进程。主调度器在schedule函数中实现。当系统调用返回时,内核会检查当前进程是否设置了重调度标志TIF_NEED_RESCHED。假设设置了该标志,那么内核会调用schedule函数来切换。

1.schedule函数首先确定当前就绪队列,并在prev指针保存当前还在执行的进程task_struct。改动就绪队列的clock值,取消TIF_NEED_RESCHED标志。

2. 假设当前进程处于可中断睡眠状态,而且接收到唤醒信号,那么把当前进程状态改成可执行,否则使进程停止活动,进入睡眠队列。

3. 调用调度器类的put_prev_task通知调度器类当前进程要被还有一个进程所替换。调用pick_next_task选取下一个进程。不一定会选取新的进程,比方其它进程都处于睡眠状态,就一个能够执行的进程。一旦选择了一个进程,那么就要准备执行硬件级的上下文切换

4. context_switch负责运行硬件级的上下文切换

5. 检查重调度标志,假设当前进程设置了TIF_NEED_SCHED标志,就goto到need_resched运行。



来看看上下文切换做了什么工作

1. 先调用和体系结构相关的prepare_task_switch函数为切换做事先准备

2. mm和oldmm表示下一个进程的用户空间虚拟地址上下文实例和上一个进程的用户空间虚拟地址上下文实例

3. switch_mm函数更换由task_struct的mm描写叙述的用户空间虚拟地址上下文,比方载入的页表,刷出TLB。主要是存放在快速缓存和TLB中的供CPU使用的数据。而很多其它的数据保存在内存中,是不须要切换的的,在须要时从内存载入

4. swtich_to函数切换寄存器和内核栈中的数据,将新进程用户空间程序之前使用到的寄存器内容恢复到寄存器。关于寄存器的内容,有一个要点是当用户态进入到核心态时,会把用户空间程序的寄存器内容保存到内核栈。所以上下文切换时,寄存器内容不须要特别处理。进程总是从核心态開始运行,返回到用户空间时,寄存器的内容会从内核栈恢复。


                      



全然公平调度器类的基本原理是计算进程的虚拟时钟值,虚拟时钟值是度量一个等待的进程能够获得的CPU时间。这个值是由实际时钟和进程的负荷权重计算出来的。全部与虚拟时钟相关的计算都是有update_curr函数相关的。update_curr被周期性调度器触发

就绪队列的红黑树最左边节点是要被选出的节点,红黑树是依照vruntime的值来排序的。红黑树还维护了一个min_vruntime值,表示整棵数最小的虚拟时钟值。当最左边节点的vruntime值大于min_vruntime时,会更新min_vruntime值为最左边节点的vruntime值。

1. vruntime值总是不断增长的,也就是说节点在红黑树中不断右移。当进程进入执行时,它的vruntime会增长。越重要的进程的vruntime值增长越慢,也就是说右移的速度越慢

2. min_vruntime值总是单调增长的。当一个进程进入睡眠时它的vruntime是保持不变的,当它醒来时,它在红黑树中的位置相对左移,被更优先调度。



进程调度是基于就绪队列的,就绪队列中等待的进程都是可执行状态。而睡眠的进程则处于等待队列,在等待队列中的进程不会被调度器选择。而睡眠的进程醒来之后,会进入就绪队列。睡眠的进程有两种,可中断和不可中断。唤醒一个在等待队列中睡眠的进程有几种方式。

1. 等待的信号到达,处理可中断的睡眠进程

2. 等待的事件发生,比方读取网卡数据的进程在网卡没有数据的时候进入不可中断睡眠,当网卡数据达到时,会调用wake_up函数,来唤醒在等待网卡数据的进程




posted @ 2015-05-14 18:06  zfyouxi  阅读(627)  评论(0编辑  收藏  举报