linux进程调度

312 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

简介

进程调度是对TASK_RUNNING状态的进程进行调度。如果进程不可执行,那么它跟进程调度没多大关系。调度程序即(scheduler)决定了多个程序运行策略,调度程序的最大原则在于能够最大限度的利用计算资源。 多任务系统可以划分为两类:非抢占式多任务, 抢占式多任务。

进程优先级

进程提供了两种优先级:普通进程优先级和实时优先级。普通优先级调度特点:根据动态优先级调度,动态优先级由静态优先级调整而来。实时优先级采用两种调度算法:SCHED_FIFO(先入先出调度算法)、SCHED_RR(时间片轮询调度算法)。实时优先级调度特点:只有静态优先级,不会调整优先级,默认优先级0-99(MAX_RT_PRIO=100)。nice值只影响100~100+40的进程优先级。

不同调度策略的实时进程只有在相同优先级时才有可比性:

  1. 对于FIFO的进程,意味着只有当前进程执行完毕才会轮到其他进程执行。

  2. 对于RR的进程。一旦时间片消耗完毕,则会将该进程置于队列的末尾,然后运行其他相同优先级的进程,如果没有其他相同优先级的进程,则该进程会继续执行。

调度策略

调度策略是模块化设计的,调度器根据不同的进程依次遍历不同的调度策略,找到进程对应的调度策略,调度的结果即为选出一个可运行的进程指针,并将其加入到进程可运行队列中。如下图: 调度.PNG CFS完全公平调度: CFS并不采用严格规则来为一个优先级分配某个长度的时间片,而是为每个任务分配一定比例的CPU处理时间。每个任务分配的具体比例是根据友好值来计算的。友好值的范围从-20到+19,数值较低的友好值表示较高的相对优先级。具有较低友好值的任务,与具有较高友好值的任务相比,会得到更高比例的处理器处理时间。默认友好值为0。CFS的出发点基于一个简单的理念:即所有进程实际占用处理器CPU的时间应为一致,目的是确保每个进程公平的处理器使用比。

FIFO先入先出队列:不基于时间片调度,处于可运行状态的SCHED_FIFO级别的进程比SCHED_NORMAL有更高优先级得到调度,一旦SCHED_FIFO级别的进程处于可执行的状态,它就会一致运行,直到进程阻塞或者主动释放。

RR(Round-Robin):SCHED_RR级别的进程在耗尽事先分配的时间片之后就不会继续执行。即可以理解将RR调度理解为带有时间片的SCHED_FIFO。 FIFO和RR调度算法都为静态优先级。内核不为实时进程计算动态优先级,保证了优先级别高的实时进程总能抢找优先级比它低的进程。

Linux进程调度的实现

主要讲一下CFS调度实现,4个点:时间记录,选择进程,睡眠和唤醒。 task_struct:为进程任务基础数据结构,存储着进程相关信息 sched_entity:存储着进程调度相关的信息,其中run_node为可执行红黑树的节点 ofs_rq: 存储着rb_root,红黑树的根节点task_timeline slab: linux内核对于对象内存的一种高效的管理机制, 可以有效的降低内存碎片。task_struct这类数据结构由slab分配并管理。

时间记录

CFS 没有使用离散的时间片,而是采用目标延迟,这是每个可运行任务应当运行一次的时间间隔。根据目标延迟,按比例分配 CPU 时间。除了默认值和最小值外,随着系统内的活动任务数量超过了一定阈值,目标延迟可以增加。CFS 调度程序没有直接分配优先级。相反,它通过每个任务的变量 vruntime 以便维护虚拟运行时间,进而记录每个任务运行多久。虚拟运行时间与基于任务优先级的衰减因子有关,更低优先级的任务比更高优先级的任务具有更高衰减速率。对于正常优先级的任务(友好值为 0),虚拟运行时间与实际物理运行时间是相同的。即CFS的vruntime += 处理器运行时间 * nice对应的权重。

// sched.h
struct sched_entity {
 struct load_weight load;  /* for load-balancing 用来做负载均衡 */ 
 struct rb_node  run_node;   /* 红黑树运行节点 */
 struct list_head group_node;
 unsigned int  on_rq;      /* 表明是否在可运行的队列中 */

 u64   exec_start;
 u64   sum_exec_runtime;
 u64   vruntime;           /* 虚拟运行时间,通常是加权的虚拟运行时间 */
 u64   prev_sum_exec_runtime;

 u64   last_wakeup;
 u64   avg_overlap;

 u64   nr_migrations;

 u64   start_runtime;
 u64   avg_wakeup;
}

选择进程

当CFS调度器选择下一个要进行调度的进程时,就会选择具有最小vruntime的任务。涉及到获取最小值,以及有序数据结构,在各种场景下都很适用的红黑树就发挥了其作用。即用红黑树维护以vruntime为排序条件,存储着任务的运行情况。 进程的维护都在红黑树上进行相关操作:

  1. 选择下一个任务 执行__pick_next_entity函数即获取了红黑树最左的节点(最小值)。

  2. 向红黑树中加入进程 这一步骤发生在进程变成可运行态,或者通过fork系统调用第一次创建进程时。

  3. 从红黑树中删除进程 这一步操作发生在进程阻塞,即进程变成不可运行状态或者当进程终止时。

睡眠和唤醒

休眠(被阻塞)状态的进程处于不可执行的状态。进程休眠的原因有多种多样,但通常来说都是等待某一事件的发生,例如等待I/O, 等待设备输入等等。

休眠:进程首先把自己标记为休眠状态(TASK_INTERRUPTIBLE),然后从可执行红黑树中移除该进程,并将进程放入等待队列 唤醒:进程被置为可执行状态(TASK_RUNNING),进程从等待队列移入可执行红黑树中 休眠或者阻塞状态有两种:可中断休眠(TASK_INTERRUPTIBLE), 不可中断休眠(TASK_UNINTERRUPTIBLE). 通常进程的休眠,为可中断休眠,即进程进入休眠,等待某一事件发生。一旦事件发生,或者满足条件,内核将会把进程状态置为运行,并将进程从等待队列中移除。

进程进入等待休眠队列如下:

void wait(){ 
 // 创建一个等待队列的项 'q'
 DEFINE_WAIT(wait);
 // 把自己加入到等待队列中
 add_wait_queue(q, &wait);
 while(!condition){ //condition 为等待的事件
 prepare_to_wait(&q, &wait, TASK_INTERRUPTIBLE);
 if(signal_pending(current)){
 // 处理信号
        }
  // 进行一次调度
  schedule(); 
    }
 finish_wait(q, &wait);
}

总结

CFS是动态计算程序优先级的一种调度算法,其内部算法核心是选取vruntime最小的进程进行调度运行,而维护最小的进程,使用了红黑树,而计算vruntime使用了所有进程数以及nice值的加权。