02进程调度

1、Linux的进程调度

绝大多数系统(包括Linux)是抢占式多任务系统。进程在被抢占之前能运行的时间是分配好的,叫进程的时间片。Linux的调度算法采用完全公平调度算法CFS。

2、进程调度策略

(1)进程调度策略的平衡点

进程可以被分为I/O消耗型和处理器消耗型。I/O消耗型进程的大部分时间用来提交I/O请求或等待I/O请求;处理器消耗型进程把时间大多用在执行代码上。调度策略通常要在两个矛盾的目标中间寻找平衡:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量);Linux更倾向于优先调度I/O消耗型进程,但并未忽视处理器消耗型进程。

(2)进程的优先级

Linux采用两种不同的优先级范围:

  • nice值:nice是代表静态优先级的数值,范围从-20到+19,默认值为0。越大的nice值意味更低的优先级,在Linux系统中nice值代表时间片的比例;
  • 实时优先级:默认情况下它的变化范围是从0到99(包括0和99),越高的实时优先级数值意味着进程优先级越高,实时进程的优先级高于普通进程;

(3)时间片

CFS调度器并没有直接分配时间片到进程,它是将处理器的使用比分给了进程。进程所获得的处理器时间其实是和系统负载密切相关的,还受nice值的影响。Linux中使用新的CFS调度器后,抢占时机取决于新的可运行程序消耗了多少处理器使用比,如果消耗的使用比例比当前进程小,则新进程立刻投入运行抢占当前进程,否则推迟运行。例如:对于一个文字编辑程序和一个视频处理程序两个程序,具有相同的nice值,那么处理器的使用比都是50%,它们平分处理器时间,但是当文字编辑程序要使用处理器时,CFS发现其时间没到50%,会抢占视频处理程序执行。

3、进程调度算法

(1)调度器类

linux调度器是以模块方式提供的,允许不同类型的进程有针对地选择调度算法。完全公平调度(CFS)是一个针对普通进程的调度类。

(2)CFS进程调度思想核心

Linux的CFS对时间片分配方式进行了根本性重新设计,为了避免将nice值直接映射到时间片的不便之处,摒弃时间片而是分配给进程一个处理器使用比,通过这种方式,CFS确保了进程调度中能有恒定的公平性,而将切换频率置于不断变动中。

CFS的核心理念是每个进程都按照权重在全部可运行进程中所占比例的“时间片”来运行。CFS的最小时间片长度为1ms,时间片分配时间根据目标延迟以及nice值决定的比例计算而得。任何进程所获得的处理器时间是由它自己和其他所有可运行进程nice值的相对差值决定的。

CFS进程调度算法公平之处在于:调度器总是选择vruntime值最低的进程执行,而vruntime= 实际运行时间 * 1024 / 进程权重,所以优先级高的进程权重大vruntime增长慢,以至于它能得到更多的运行时间,同时它每次运行的时间片也更大,分配给进程的时间片 = 调度周期 * 进程权重 / 全部进程权重之和,但是当优先级高进程权重大的进程得到更多的实际运行时间之后,vruntime也变大了,那么 实际运行时间少的进程又可以运行了,这样就体现了所谓的完全公平。从下面的公式推导也可以看出进程在一个调度周期内的vruntime值大小与进程权重无关,所有进程的vruntime值在一个周期内增长是一致的。这样既能公平选择进程,又能保证高优先级进程获得较多运行时间,就是cfs的主要思想了,可以简单概括为:机会平等、时间差异。

分配给进程的时间 = 调度周期 * 进程权重 / 全部进程权重之和
vruntime = 实际运行时间 * 1024 / 进程权重
vruntime = (调度周期 * 进程权重 / 全部进程权重之和) * 1024 / 进程权重
vruntime = (调度周期 / 全部进程权重之和) * 1024

当CFS选择下一个运行进程时,选择vruntime最小的进程,也就是红黑树的最左子叶子节点,根据进程的nice值获得进程权重然后计算出该进程的时间片,分配给进程的时间片 = 调度周期 * 进程权重 / 全部进程权重之和,该进程运行完之后,更新该进程的实际运行时间sum_exec_runtime,然后计算出该进程的当前vruntime= 实际运行时间 * 1024 / 进程权重,然后调整红黑树保持处于二叉平衡树状态。

(3)两个重要数据结构

完全公平队列cfs_rq:描述运行在一个cpu上的处于TASK_RUNNING状态的普通进程的各种运行信息

struct cfs_rq {
    struct load_weight load;  //运行队列总的进程权重
    unsigned int nr_running, h_nr_running; //进程的个数

    u64 exec_clock;  //运行的时钟
    u64 min_vruntime; //该cpu运行队列的vruntime推进值, 一般是红黑树中最小的vruntime值

    struct rb_root tasks_timeline; //红黑树的根结点
    struct rb_node *rb_leftmost;  //指向vruntime值最小的结点
    //当前运行进程, 下一个将要调度的进程, 马上要抢占的进程, 
    struct sched_entity *curr, *next, *last, *skip;

    struct rq *rq; //系统中有普通进程的运行队列, 实时进程的运行队列, 这些队列都包含在rq运行队列中  
    ...
    };

调度实体sched_entity:记录一个进程的运行状态信息

struct sched_entity {
    struct load_weight  load; //进程的权重
    struct rb_node      run_node; //运行队列中的红黑树结点
    struct list_head    group_node; //与组调度有关
    unsigned int        on_rq; //进程现在是否处于TASK_RUNNING状态

    u64         exec_start; //一个调度tick的开始时间
    u64         sum_exec_runtime; //进程从出生开始, 已经运行的实际时间
    u64         vruntime; //虚拟运行时间
    u64         prev_sum_exec_runtime; //本次调度之前, 进程已经运行的实际时间
    struct sched_entity *parent; //组调度中的父进程
    struct cfs_rq       *cfs_rq; //进程此时在哪个运行队列中
};

(4)三个有意思的问题

新进程的vruntime的初始值是不是0?

假如新进程的vruntime初值为0的话,比老进程的值小很多,那么它在相当长的时间内都会保持抢占CPU的优势,老进程就要饿死了,这显然是不公平的。所以CFS是这样做的:每个CPU的运行队列cfs_rq都维护一个 min_vruntime 字段,记录该运行队列中所有进程的vruntime最小值,新进程的初始vruntime值就以它所在运行队列的min_vruntime为基础来设置,与老进程保持在合理的差距范围内。

休眠进程的vruntime的值一直保持不变吗?

如果休眠进程的 vruntime 保持不变,而其他运行进程的 vruntime 一直在推进,那么等到休眠进程终于唤醒的时候,它的vruntime比别人小很多,会使它获得长时间抢占CPU的优势,其他进程就要饿死了。这显然是另一种形式的不公平。CFS是这样做的:在休眠进程被唤醒时重新设置vruntime值,以min_vruntime值为基础,给予一定的补偿,但不能补偿太多。

进程从一个CPU迁移至另外一个CPU的时候vruntime会变化吗?

当进程从一个CPU的运行队列中出来 (dequeue_entity) 的时候,它的vruntime要减去队列的min_vruntime值; 而当进程加入另一个CPU的运行队列 ( enqueue_entiry) 时,它的vruntime要加上该队列的min_vruntime值。 这样,进程从一个CPU迁移到另一个CPU之后,vruntime保持相对公平。

4、进程调度实现

CFS总共有四个组成部分:时间记账、进程选择、调度器入口、睡眠和唤醒。

(1)时间记账

CFS使用调度器实体结构struct_sched_entity来对进程运行时间做记账。调度器实体作为一个se的成员变量,嵌入在struct task_struct内。CFS使用vruntime变量来存放进程的虚拟运行时间,该运行时间是实际运行时间经过了所有可运行进程总数进行加权运算后的结果。vruntime可以通过update_curr()更新。vruntime = 实际运行时间 * 1024 / 进程权重,其中NICE_0_LOAD=1024即nice值为0时对应的weight的值为1024, 实际运行时间是调度器实体结构中的sum_exec_runtime成员。进程权重根据进程的nice值得来,nice值和进程的权重的关系存储在数组prio_to_weight中,nice值越小,进程的权重越大,如下所示:

/*prio_to_weight数组反应的是nice值与权重的对应关系*/
static const int prio_to_weight[40] = {
     /* -20 */     88761,     71755,     56483,     46273,     36291,
     /* -15 */     29154,     23254,     18705,     14949,     11916,
     /* -10 */      9548,      7620,      6100,      4904,      3906,
     /*  -5 */      3121,      2501,      1991,      1586,      1277,
     /*   0 */      1024,       820,       655,       526,       423,
     /*   5 */       335,       272,       215,       172,       137,
     /*  10 */       110,        87,        70,        56,        45,
     /*  15 */        36,        29,        23,        18,        15,
     };

(2)进程选择

当CFS需要选择下一个运行进程时,调用函数pick_next_entity(),它会挑一个具有最小vruntime的进程。CFS使用红黑树来组织可运行进程队列,并利用其迅速找到最小的vruntime值的进程。

  • 挑选下一个任务:就是在红黑树中查找最左叶子节点;通过函数__pick_next_entity()实现。
  • 向树中加入进程:将进程插入红黑树,并且缓存最左节点;这个过程发生在进程变为可运行状态(被唤醒)或是通过fork()调用第一次创建进程时。通过enqueue_entity()函数实现。
  • 从树中删除进程:删除动作发生在进程堵塞或者终止时;通过函数__dequeue_entity()实现,它是借用了红黑树的rb_erase()来完成所有删除工作的,最后更新rb_leftmost。

(3)调度器入口

进程调度的入口点是函数schedule(),找一个最高优先级的调度类,选择最高优先级的进程。通过函数pick_next_task()按照优先级从高到低,依次轮询调度类,每个调度类又维护着自己的运行队列,于是又查找运行队列里优先级最高的进程。CFS中pick_next_task()会调用pick_next_entity()。

(4)睡眠和唤醒

休眠(被阻塞)进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程。唤醒的过程刚好相反,进程被设置为可执行状态,然后再从等待队列中插入到可执行红黑树中。

5、抢占和上下文切换

(1)上下文切换

上下文切换就是从一个可执行进程切换到另一个可执行进程,每当一个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数:

  • 调用switch_mm(),该函数负责把虚拟内存从上一个进程映射到新进程中
  • 调用switch_to(),该函数负责从上一个进程的处理器状态切换到新进程的处理器状态,这包括保存、恢复栈信息和寄存器信息,还有其他任何与体系结构相关的状态信息,都必须以每个进程为对象进行管理和保存。

内核提供了一个need_resched标志来表明是否需要重新执行一次调度。

(2)抢占

用户抢占:内核返回用户空间时,如果need_resched被置位,会导致schedule()被调用。用户抢占产生的情况:

  • 从系统调用返回用户空间时
  • 从中断处理程序返回用户空间时

为了适应抢占,每个进程thread_info引入 preempt_count计数器。计数器初始为0,每当使用锁的时候数值加1,释放减1。当数值为0时,内核可以抢占。内核抢占会发生的情况:

  • 中断处理程序正在执行,且返回内核空间之前。
  • 内核代码再一次具有可抢占性的时候。
  • 如果内核中的任务显示地调用schedule()
  • 如果内核中的任务阻塞(同样会调用schedule())

6、实时调度策略

Linux提供两种调度策略:SCHED_FIFO和SCHED_RR。SCHED_RR与SCHED_FIFO优先级范围[0,99]。而普通的、非实时的调度使用SCHED_NORMAL策略,SCHED_NORMAL使用nice值确定优先级。

  • SCHED_FIFO:一种简单的、先入先出的调度算法。
  • SCHED_RR:一种实时轮流调度算法,在耗尽事先分配给它的时间后就不能再继续执行了。

7、与调度相关的系统调用

Linux提供了一个系统调用族,用于管理与调度程序的相关参数。这些系统调用可以用来操作和处理进程优先级、调度策略及处理器绑定,同时还提供了显式地将处理器交给其他进程的机制。

(1)与调度策略和优先级相关的系统调用

sched_setscheduler()和sched_getscheduler()用于设置和获取进程的调度策略和实时优先级。sched_setparam()和sched_getparam()用于设置和获取进程的实时优先级。

(2)与处理器绑定有关的系统调用

Linux调度程序提供强制的处理器绑定机制。

(3)放弃处理器时间

Linux通过sched_yield()系统调用,提供了一种让进程显式地将处理器时间让给其他等待执行进程的机制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值