日期 | 内核版本 | 架构 | 作者 | 内容 |
---|---|---|---|---|
2019-4-14 | Linux-2.6.32 | X86 |
Bystander | Linux进程调度架构 |
现代的操作系统是多任务的操作系统,硬件的处理器核心和各种资源越来越多,CPU也是一个资源。为了保证进程合理的使用CPU资源,则需要一个管理单元,负责调度进程,由管理单元来决定下一刻应该由谁使用CPU,这里管理单元就是进程调度器。调度器可以临时分配一个任务在上面执行(单位是时间片)。进程调度器的任务就是合理分配CPU时间给运行的进程,创造一种所有进程并行运行的错觉,使得我们同时执行多个程序成为可能,可以具有各种需求的用户共享CPU。因此调度器必须在各个进程之间尽可能公平地共享CPU时间, 而同时又要考虑不同的任务优先级。调度器的一个重要目标是有效地分配 CPU 时间片,同时提供很好的用户体验。调度器的一般原理是, 按所需分配的计算能力, 向系统中每个进程提供最大的公正性, 或者从另外一个角度上说, 试图确保没有进程被亏待。
传统的Unix操作系统的调度算法必须实现几个互相冲突的目标:
Linux的调度基于分时(time sharing)技术: 多个进程以”时间多路复用”方式运行, 因为CPU的时间被分成”片(slice)”, 给每个可运行进程分配一片CPU时间片, 当然单处理器在任何给定的时刻只能运行一个进程。如果当前可运行进程的时间片耗尽(就是调度器给进程分配的时间), 而该进程还没有运行完毕, 进程切换就可以发生。分时依赖于定时中断, 因此对进程是透明的, 不需要在程序中插入额外的代码来保证CPU分时.
在目前Linux内核中,调度器分成两个层级,在进程中被直接调用的成为通用调度器或者核心调度器,它们作为一个组件和进程其它部分分开,而通用调度器和进程并没有直接关系,其通过第二层的具体的调度器类来直接管理进程。具体架构如图1所示:
图1 Linux内核调度器架构
接下来我们根据图1,由下至上分析Linux内核调度架构。
根据进程运行使用方式和运行场景有两种分类方式:
第一种分类”I/O受限(I/O密集型)”进程或”CPU受限(计算密集型)”进程:
类别 | 描述 |
I/O受限型(I/O密集型) | 频繁的使用I/O设备, 并花费很多时间等待I/O操作的完成 |
CPU受限型(计算密集型) | 需要占用大量CPU时间进行数值计算 |
第二种分类交互式进程,批处理进程和实时进程:
类别 | 描述 |
交互式进程 | 此类进程经常与用户进行交互,等待用户的唤醒, 因此需要花费很多时间等待用户操作. 当接受了用户的输入后, 进程必须很快被唤醒, 如果唤醒太慢用户会感觉系统反应迟钝。比如移动鼠标,输入文字。 |
批处理进程 | 此类进程不必与用户交互,因此经常在后台运行。 因为这样的进程不必很快相应,因此常受到调度程序的慢待。比如编译程序等。 |
实时进程 | 此类进程由很强的调度需要, 这样的进程不会被低优先级的进程阻塞。并且它们的响应时间要短,要及时。比如数据采集,机器人控制等。 |
注意:在linux中, 调度算法可以明确的确认所有实时进程的身份, 但是没办法区分交互式程序和批处理程序(但是有时可以根据平均睡眠时间可以大致确定,下文将进行讲解)。
当主调度器和周期调度器进行进程调度时先查询调度器类,再根据优先级选择进程。由图一可以看出调度器只负责选择进程,不负责进程的管理,进程的管理都是由调度器类来进行。
目前在Linux操作系统中共有6中调度策略,分别是SCHED_NORMAL,SCHED_BATCH,SCHED_IDLE,SCHED_RR,SCHED_FIFO和SCHED_DEADLINE。
调度策略 | 调度器类 | 描述 |
SCHED_NORMAL | CFS | 普通分时进程,通过CFS调度器实现。 |
SCHED_BATCH | CFS | 用于非交互的处理器消耗型进程。 |
SCHED_IDLE | CFS | 优先级最低,在CPU空闲时才执行类进程,此时也会进行一次负载均衡(点击查看)。 |
SCHED_RR | RT | 时间片轮转的实时进程。这种策略下的进程具有相同优先级,按照优先级权重分配运行时间。当调度程序把CPU分配给进程时,会把该进程描述符放在运行队列末尾,所以不会及时执行该进程。 |
SCHED_FIFO | RT | 先进先出实时进程,当调度程序把CPU分配给进程时,会把该进程描述符放在运行队列当前位置,所以会及时执行该进程,而且如果没有比它优先级高的进程,且它不主动放弃CPU,他将一直执行下去。 |
SCHED_RESET_ON_FORK | 为了限制实时调度策略的进程运行,而为调度策略添加了标志flag。设置了标志flag的实时调度策略进程,在执行fork()时,新生成的子进程就成为SCHED_OTHER策略的进程 |
目前linux2.6.32中有三种调度类优先级依次降低排序:rt_sched_class,fair_sched_class,idle_sched_class ;调度类在管理和选择进程时不会直接操作进程,而是通过调度实体进行操作,因为调度器可以操作比进程更一般的实体,所以需要一个数据结构来描述此类实体就是调度实体,调度实体中包含很多关于进程运行调度信息,比如sched_entity中load负载均衡的权重,决定实体占队列总负荷比例,CFS在计算虚拟时间的速度会依赖总负荷;run_node标准树节点,在红黑树上的排序;on_rq实体是否在就绪队列上;sum_exec_runtime在进程运行时,CFS中记录进程消耗CPU时间,也就是每次新进程加入就绪队列或周期性调度器时,当前时间和exec_start差值累加到sum_exec_runtime中,而exec_start则更新为当前时间;prev_sum_exec_runtime在进程调度不再占用CPU时,sum_exec_runtime保存到prev_sum_exec_runtime中;这些信息都是进程调度所必须的,所以每个task_struct中都有一个实体,进程都是可调度实体,但是可调度实体不一定是进程。调度类,调度实体和调度策略关系如下表:
调度器类 | 调度实体 | 调度策略 | 调度器类描述 |
rt_sched_class | sched_rt_entity | SCHED_FIFO, SCHED_RR | 采用提供 Roound-Robin算法或者FIFO算法调度实时进程 |
fair_sched_class | sched_entity | SCHED_NORMAL, SCHED_BATCH | 采用CFS算法调度普通的非实时进程 |
idle_sched_class | 无 | SCHED_IDLE | 采用CFS算法调度idle进程, 每个cup的第一个pid=0线程:swapper,是一个静态线程。调度类属于:idel_sched_class。一般运行在开机过程,cpu异常的时候或者运行进程队列中无可运行进程时执行 |
前面已经讲解一些基本的知识,下面进一步学习进程何时何地发生调度,以下几种情况会发生调度:
注:最后两种情况(紫色字体)是开启内核抢占后才会出现的情况。
调度器的实现是基于两个函数:主调度器函数和周期性调度器函数。这两个函数会根据进程的策略和优先级进行调度。上面可能说的有点抽象,那我们结合图1 Linux内核调度器架构来进行讲解,上面发生的调度的方式,根据图1来说第一种是显示或间接的调用主调度器,或者处于其他原因放弃CPU;第二种通过周期调度器机制,以固定的周期频率运行,不时的检测是否有必要进行进程切换。为了方便查看图1,复制一份到此处:
图1 Linux内核调度器架构
图1中主调度器由schedule()函数(点击查看)实现,在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule()。
图1中周期性调度器由scheduler_tick()函数实现,如果系统正在活动中,内核会按照频率HZ自动调用该函数。该函数主要有如下两个任务:
在进程进行调度时,调度器会从具有可运行进程的类中选择,优先级最高调的度类中选择一个优先级最高的进程进行调度。进程优先级(点击查看)与进程是否被调度,占用CPU时间多久息息相关。
静态优先级:
每个普通进程均具有自己静态优先级,调度程序使静态优先级来评估系统中这个进程与其他普通进程之间调度的程度。内核用从100(最高优先级)到139(最低优先级)的数表示普通进程的静态优先级。值越大静态优先级越低。
动态优先级:
其值范围100(最高优先级)到139(最低优先级)的数表示普通进程的静态优先级。值越大静态优先级越低。
每个实时进程都有一个实时优先级,范围从1(最高优先级)- 99(最低优先级)的值。调度程序总是让优先级高的进程运行,禁止低优先级运行。