linux调度器源码分析 (一)- 概述

本系列文章源码基于linux3.10.

1概述

linux从其初始版本稳步发展到今天,从2.4的非抢占内核发展到今天的可抢占内核,调度器无论从代码结构还是设计思想上也都发生了翻天覆地的变化,其普通进程的调度算法也从O(1)到现在的CFS。

在linux3.10版本上整个调度系统至少包含两种调度算法,是分别针对实时进程和普通进程,所以在整个linux内核中,实时进程和普通进程是并存的,但它们使用的调度算法并不相同,普通进程使用的是CFS调度算法(基于红黑树)。

进程

在linux中,进程主要分为两种,一种为实时进程,一种为普通进程。

(1)实时进程:对系统的响应时间要求很高,它们需要短的响应时间,并且这个时间的变化非常小,典型的实时进程有音乐播放器,视频播放器等。

(2)普通进程:包括交互进程和非交互进程,交互进程如文本编辑器,它会不断的休眠,又不断地通过鼠标键盘进行唤醒,而非交互进程就如后台维护进程,他们对IO,响应时间没有很高的要求,比如编译器。

它们在linux内核运行时是共存的,实时进程的优先级为0~99,实时进程优先级不会在运行期间改变(静态优先级),而普通进程的优先级为100~139,普通进程的优先级会在内核运行期间进行相应的改变(动态优先级)。

调度策略

在linux系统中,调度策略分为

  • SCHED_NORMAL:普通进程使用的调度策略,现在此调度策略使用的是CFS调度器。
  • SCHED_BATCH:也是普通的非实时进程调度策略
  • SCHED_FIFO:实时进程使用的调度策略,此调度策略的进程一旦使用CPU则一直运行,直到有比其更高优先级的实时进程进入队列,或者其自动放弃CPU,适用于时间性要求比较高,但每次运行时间比较短的进程,没有时间片,在被调度器选择之后,可以运行任意长时间。
  • SCHED_RR:实时进程使用的时间片轮转法策略,实时进程的时间片用完后,调度器将其放到队列末尾,这样每个实时进程都可以执行一段时间。适用于每次运行时间比较长的实时进程。这确保了在有几个优先级相同的SCHED_RR进程的情况下,它们总是依次执行。
  • SCHED_IDLE :则在系统空闲时调用idle进程.
  • SCHED_DEADLINE: 也是实时调度策略

其所属进程的优先级顺序为

stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class

调度

  首先,我们需要清楚,什么样的进程会进入调度器进行选择,就是处于TASK_RUNNING状态的进程,而其他状态下的进程都不会进入调度器进行调度。

当前linux的调度程序又可以分为两类,一种是周期性调度,还有一种是特定时刻调度。

周期性调度:

系统启动调度器初始化时会初始化一个调度定时器,调度定时器每隔一定时间执行一个中断,在中断会对当前运行进程运行时间进行更新,如果进程需要被调度,在调度定时器中断中会设置一个调度标志位,而不会真正的调度时机将会推迟到特定时刻调度。

特定时刻调度:

内核在特定时刻显示或隐示的调度schedule()函数,这个时候进程才会真正的去切换,一般linux有如下调度点:

  • 在内核程序中显式调用schedule()函数,放弃当前cpu。当然也可能不是这个函数名,但所调用函数都是对__schedule()的封装
  • 从中断上下文或者系统调用返回用户空间时
  • 在内核中运行时,从中断上下文返回内核线程上下文时(这是2.4版本以后新加的支持内核抢占功能)
  • 当内核代码再一次具有可抢占性的时候,如解锁(spin_unlock_bh)及使能软中断(local_bh_enable)等, 此时当kernel code从不可抢占状态变为可抢占状态时(preemptible again)。也就是preempt_count从正整数变为0时。这也是隐式的调用schedule()函数。

2 数据结构

2.6版本以后的内核普通进程使用的调度算法为CFS调度算法,它是以红黑树为基础的调度算法,其相比与实时进程的调度算法复杂很多。而之前老的版本是以时间片轮转的方式进行调度的。

下面以一个图来概括cfs调度算法的原理。

 从上图中可以看出来,每个CPU对应包含一个运行队列结构(struct rq),而每个运行队列又包含有其自己的实时进程运行队列(struct rt_rq)、普通进程运行队列(struct cfs_rq)、和一个我自己也不太知道有什么用的dl队列(struct dl_rq),也就是说每个CPU都有他们自己的实时进程运行队列及普通进程运行队列,为了方便,我们在图中只描述普通进程的组织结构(最复杂的也是普通进程的组织结构),而红色se则为当前CPU上正在执行的程序,蓝色为下个将要执行的程序,其实图中并不规范,实际上当进程运行时,会从红黑树中剥离出来,然后设定下一个调度进程,当进程运行时间结束时,再重新放入红黑树中。而为什么CPU0上有两个蓝色将被调度进程,将在组调度中解释。而为什么红黑树中又有一个子红黑树,我们将在调度实体中解释。

2.1 组调度(struct task_group)

  我们知道,linux是一个多用户系统,如果有两个进程分别属于两个用户,而进程的优先级不同,会导致两个用户所占用的CPU时间不同,这样显然是不公平的(如果优先级差距很大,低优先级进程所属用户使用CPU的时间就很小),所以内核引入组调度。如果基于用户分组,即使进程优先级不同,这两个用户使用的CPU时间都为50%。这就是为什么图1 中CPU0有两个蓝色将被调度的程序,如果task_group1中的运行时间还没有使用完,而当前进程运行时间使用完后,会调度task_group1中的下一个被调度进程;相反,如果task_group1的运行时间使用结束,则调用上一层的下一个被调度进程。需要注意的是,一个组调度中可能会有一部分是实时进程,一部分是普通进程,这也导致这种组要能够满足即能在实时调度中进行调度,又可以在CFS调度中进行调度。
  linux可以以以下两种方式进行进程的分组:

  • 用户ID:按照进程的USER ID进行分组,在对应的/sys/kernel/uid/目录下会生成一个cpu.share的文件,可以通过配置该文件来配置用户所占CPU时间比例。
  • cgourp(control group):生成组用于限制其所有进程,比如我生成一个组(生成后此组为空,里面没有进程),设置其CPU使用率为10%,并把一个进程丢进这个组中,那么这个进程最多只能使用CPU的10%,如果我们将多个进程丢进这个组,这个组的所有进程平分这个10%。

    注意的是,这里的进程组概念和fork调用所产生的父子进程组概念不一样,文章所使用的进程组概念全为组调度中进程组的概念。为了管理组调度,内核引进了struct task_group结构,如下:

/* 进程组,用于实现组调度 */
struct task_group {
     /* 用于进程找到其所属进程组结构 */
     struct cgroup_subsys_state css;
 
 #ifdef CONFIG_FAIR_GROUP_SCHED
     /* CFS调度器的进程组变量,在 alloc_fair_sched_group() 中进程初始化及分配内存 */
     /* 该进程组在每个CPU上都有对应的一个调度实体,因为有可能此进程组同时在两个CPU上运行(它的A进程在CPU0上运行,B进程在CPU1上运行) */
     struct sched_entity **se;
     /* 进程组在每个CPU上都有一个CFS运行队列(为什么需要,稍后解释) */
     struct cfs_rq **cfs_rq;
     /* 用于保存优先级默认为NICE 0的优先级 */
     unsigned long shares;
 
 #ifdef    CONFIG_SMP
     atomic_long_t load_avg;
     atomic_t runnable_avg;
 #endif
 #endif
 
 #ifdef CONFIG_RT_GROUP_SCHED
     /* 实时进程调度器的进程组变量,同 CFS */
     struct sched_rt_entity **rt_se;
     struct rt_rq **rt_rq;
 
     struct rt_bandwidth rt_bandwidth;
 #endif
 
     struct rcu_head rcu;
     /* 用于建立进程链表(属于此调度组的进程链表) */
     struct list_head list;
 
     /* 指向其上层的进程组,每一层的进程组都是它上一层进程组的运行队列的一个调度实体,在同一层中,进程组和进程被同等对待 */
     struct task_group *parent;
     /* 进程组的兄弟结点链表 */
     struct list_head siblings;
     /* 进程组的儿子结点链表 */
     struct list_head children;
 
 #ifdef CONFIG_SCHED_AUTOGROUP
     struct autogroup *autogroup;
 #endif
 
     struct cfs_bandwidth cfs_bandwidth;
 };

在struct task_group结构中,对于普通进程来说最重要的成员为 struct sched_entity ** se 和 struct cfs_rq ** cfs_rq。在上图中,root_task_group与task_group1都只有一个,它们在初始化时会根据CPU个数为se和cfs_rq分配空间,即在task_group1和root_task_group中会为每个CPU分配一个se和cfs_rq,同理用于实时进程的 struct sched_rt_entity ** rt_se 和 struct rt_rq ** rt_rq也是一样。为什么这样呢,原因就是在多核多CPU的情况下,同一进程组的进程有可能在不同CPU上同时运行,所以每个进程组都必须对每个CPU分配它的调度实体(struct sched_entity 和 struct sched_rt_entity)和运行队列(struct cfs_rq 和 struct rt_rq)。

2.2调度实体

普通进程调度实体(struct sched_entity)

普通进程的调度实体,它的结构为struct sched_entity(简称se),就是上图红黑树中的se。其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组。对于根的红黑树而言,一个进程组就相当于一个调度实体,一个进程也相当于一个调度实体。我们可以先看看其结构,如下:

/* 一个调度实体(红黑树的一个结点),其包含一组或一个指定的进程,包含一个自己的运行队列,一个父亲指针,一个指向需要调度的运行队列指针 */
struct sched_entity {
    /* 权重,在数组prio_to_weight[]包含优先级转权重的数值 */
    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;
    /* 虚拟运行时间,在时间中断或者任务状态发生改变时会更新
     * 其会不停增长,增长速度与load权重成反比,load越高,增长速度越慢,就越可能处于红黑树最左边被调度
     * 每次时钟中断都会修改其值
     * 具体见calc_delta_fair()函数
     */
    u64            vruntime;
    /* 进程在切换进CPU时的sum_exec_runtime值 */
    u64            prev_sum_exec_runtime;

    /* 此调度实体中进程移到其他CPU组的数量 */
    u64            nr_migrations;

#ifdef CONFIG_SCHEDSTATS
    /* 用于统计一些数据 */
    struct sched_statistics statistics;
#endif

#ifdef CONFIG_FAIR_GROUP_SCHED
    /* 代表此进程组的深度,每个进程组都比其parent调度组深度大1 */
    int            depth;
    /* 父亲调度实体指针,如果是进程则指向其运行队列的调度实体,如果是进程组则指向其上一个进程组的调度实体
     * 在 set_task_rq 函数中设置
     */
    struct sched_entity    *parent;
    /* 实体所处红黑树运行队列 */
    struct cfs_rq        *cfs_rq;        
    /* 实体的红黑树运行队列,如果为NULL表明其是一个进程,若非NULL表明其是调度组 */
    struct cfs_rq        *my_q;
#endif

#ifdef CONFIG_SMP
    /* Per-entity load-tracking */
    struct sched_avg    avg;
#endif
};

 实际上,红黑树是根据 struct rb_node 建立起关系的,不过 struct rb_node 与 struct sched_entity 是一一对应关系,也可以简单看为一个红黑树结点就是一个调度实体。可以看出,在 struct sched_entity 结构中,包含了一个进程(或进程组)调度的全部数据,其被包含在 struct task_struct 结构中的se中,如下:

struct task_struct {
    ........
    /* 表示是否在运行队列 */
    int on_rq;

    /* 进程优先级 
     * prio: 动态优先级,范围为100~139,与静态优先级和补偿(bonus)有关
     * static_prio: 静态优先级,static_prio = 100 + nice + 20 (nice值为-20~19,所以static_prio值为100~139)
     * normal_prio: 没有受优先级继承影响的常规优先级,具体见normal_prio函数,跟属于什么类型的进程有关
     */
    int prio, static_prio, normal_prio;
    /* 实时进程优先级 */
    unsigned int rt_priority;
    /* 调度类,调度处理函数类 */
    const struct sched_class *sched_class;
    /* 调度实体(红黑树的一个结点) */
    struct sched_entity se;
    /* 调度实体(实时调度使用) */
    struct sched_rt_entity rt;
#ifdef CONFIG_CGROUP_SCHED
    /* 指向其所在进程组 */
    struct task_group *sched_task_group;
#endif
    ........
}

当然,在task_struct中,也包含了实时进程的调度实体sched_rt_entity,方便把实时进程挂载进实时进程调度队列中。

在 struct sched_entity 结构中,值得我们注意的成员是:

  • load:权重,通过优先级转换而成,是vruntime计算的关键。
  • on_rq:表明是否处于CFS红黑树运行队列中,需要明确一个观点就是,CFS运行队列里面包含有一个红黑树,但这个红黑树并不是CFS运行队列的全部,因为红黑树仅仅是用于选择出下一个调度程序的算法。很简单的一个例子,普通程序运行时,其并不在红黑树中,但是还是处于CFS运行队列中,其on_rq为真。只有准备退出、即将睡眠等待和转为实时进程的进程其CFS运行队列的on_rq为假。
  • vruntime:虚拟运行时间,调度的关键,其计算公式:一次调度间隔的虚拟运行时间 = 实际运行时间 * (NICE_0_LOAD / 权重)。可以看出跟实际运行时间和权重有关,红黑树就是以此作为排序的标准,优先级越高的进程在运行时其vruntime增长的越慢,其可运行时间相对就长,而且也越有可能处于红黑树的最左结点,调度器每次都选择最左边的结点为下一个调度进程。注意其值为单调递增,在每个调度器的时钟中断时当前进程的虚拟运行时间都会累加。单纯的说就是进程们都在比谁的vruntime最小,最小的将被调度。
  • cfs_rq:此调度实体所处于的CFS运行队列。
  • my_q:如果此调度实体代表的是一个进程组,那么此调度实体就包含有一个自己的CFS运行队列,其CFS运行队列中存放的是此进程组中的进程,这些进程就不会在其他CFS运行队列的红黑树中被包含(包括顶层红黑树也不会包含他们,他们只属于这个进程组的红黑树)。

  对于怎么理解一个进程组有它自己的CFS运行队列,其实很好理解,比如在根CFS运行队列的红黑树上有一个进程A一个进程组B,各占50%的CPU,对于根的红黑树而言,他们就是两个调度实体。调度器调度的不是进程A就是进程组B,而如果调度到进程组B,进程组B自己选择一个程序交给CPU运行就可以了,而进程组B怎么选择一个程序给CPU,就是通过自己的CFS运行队列的红黑树选择,如果进程组B还有个子进程组C,原理都一样,就是一个层次结构。

实时进程调度实体(sched_rt_entity)

struct sched_rt_entity {
    struct list_head run_list; //用于挂载进调度队列
    unsigned long timeout;----------------------------------------watchdog计数,用于判断当前进程时间是否超过RLIMIT_RTTIME。
    unsigned long watchdog_stamp;
    unsigned int time_slice;--------------------------------------针对RR调度策略的调度时隙,相当于轮转调度实时进程每个进程一次调度周期可运行的时间

    struct sched_rt_entity *back;---------------------------------dequeue_rt_stack()中作为临时变量使用
#ifdef CONFIG_RT_GROUP_SCHED
    struct sched_rt_entity    *parent;----------------------------指向上一层调度实体
    /* rq on which this entity is (to be) queued: */
    struct rt_rq        *rt_rq;-----------------------------------当前实时调度实体所在的就绪队列
    /* rq "owned" by this entity/group: */
    struct rt_rq        *my_q;------------------------------------当前实时调度实体的子调度实体所在的就绪队列
#endif
}

2.3 调度类

  而在 struct task_struct 结构中,我们注意到有个调度类,里面包含的是调度处理函数,它具体如下:

struct sched_class {
    /* 下一优先级的调度类
     * 调度类优先级顺序: stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
     */
    const struct sched_class *next;

    /* 将进程加入到运行队列中,即将调度实体(进程)放入红黑树中,并对 nr_running 变量加1 */
    void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
    /* 从运行队列中删除进程,并对 nr_running 变量中减1 */
    void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
    /* 放弃CPU,在 compat_yield sysctl 关闭的情况下,该函数实际上执行先出队后入队;在这种情况下,它将调度实体放在红黑树的最右端 */
    void (*yield_task) (struct rq *rq);
    bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);

    /* 检查当前进程是否可被新进程抢占 */
    void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);

    /*
     * It is the responsibility of the pick_next_task() method that will
     * return the next task to call put_prev_task() on the @prev task or
     * something equivalent.
     *
     * May return RETRY_TASK when it finds a higher prio class has runnable
     * tasks.
     */
    /* 选择下一个应该要运行的进程运行 */
    struct task_struct * (*pick_next_task) (struct rq *rq,
                        struct task_struct *prev);
    /* 将进程放回运行队列 */
    void (*put_prev_task) (struct rq *rq, struct task_struct *p);

#ifdef CONFIG_SMP
    /* 为进程选择一个合适的CPU */
    int (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
    /* 迁移任务到另一个CPU */
    void (*migrate_task_rq)(struct task_struct *p, int next_cpu);
    /* 用于上下文切换后 */
    void (*post_schedule) (struct rq *this_rq);
    /* 用于进程唤醒 */
    void (*task_waking) (struct task_struct *task);
    void (*task_woken) (struct rq *this_rq, struct task_struct *task);
    /* 修改进程的CPU亲和力(affinity) */
    void (*set_cpus_allowed)(struct task_struct *p,
                 const struct cpumask *newmask);
    /* 启动运行队列 */
    void (*rq_online)(struct rq *rq);
    /* 禁止运行队列 */
    void (*rq_offline)(struct rq *rq);
#endif
    /* 当进程改变它的调度类或进程组时被调用 */
    void (*set_curr_task) (struct rq *rq);
    /* 该函数通常调用自 time tick 函数;它可能引起进程切换。这将驱动运行时(running)抢占 */
    void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
    /* 在进程创建时调用,不同调度策略的进程初始化不一样 */
    void (*task_fork) (struct task_struct *p);
    /* 在进程退出时会使用 */
    void (*task_dead) (struct task_struct *p);

    /* 用于进程切换 */
    void (*switched_from) (struct rq *this_rq, struct task_struct *task);
    void (*switched_to) (struct rq *this_rq, struct task_struct *task);
    /* 改变优先级 */
    void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
             int oldprio);

    unsigned int (*get_rr_interval) (struct rq *rq,
                     struct task_struct *task);

    void (*update_curr) (struct rq *rq);

#ifdef CONFIG_FAIR_GROUP_SCHED
    void (*task_move_group) (struct task_struct *p, int on_rq);
#endif
};

这个调度类具体有什么用呢,实际上在内核中不同的调度算法它们的操作都不相同,为了方便修改、替换调度算法,使用了调度类,每个调度算法只需要实现自己的调度类就可以了,CFS算法有它的调度类,SCHED_FIFO也有它自己的调度类,当一个进程创建时,用什么调度算法就将其 task_struct->sched_class 指向其相应的调度类,调度器每次调度处理时,就通过当前进程的调度类函数进程操作,大大提高了可移植性和易修改性。

2.4 运行队列

CFS运行队列(struct cfs_rq)

  我们现在知道,在系统中至少有一个CFS运行队列,其就是根CFS运行队列,而其他的进程组和进程都包含在此运行队列中,不同的是进程组又有它自己的CFS运行队列,其运行队列中包含的是此进程组中的所有进程。当调度器从根CFS运行队列中选择了一个进程组进行调度时,进程组会从自己的CFS运行队列中选择一个调度实体进行调度(这个调度实体可能为进程,也可能又是一个子进程组),就这样一直深入,直到最后选出一个进程进行运行为止。
  对于 struct cfs_rq 结构没有什么好说明的,只要确定其代表着一个CFS运行队列,并且包含有一个红黑树进行选择调度进程即可。

/* CFS调度的运行队列,每个CPU的rq会包含一个cfs_rq,而每个组调度的sched_entity也会有自己的一个cfs_rq队列 */
struct cfs_rq {
    /* CFS运行队列中所有进程的总负载 */
    struct load_weight load;
    /*
     * nr_running: cfs_rq中调度实体数量
     * h_nr_running: 只对进程组有效,其下所有进程组中cfs_rq的nr_running之和
     */
    unsigned int nr_running, h_nr_running;

    u64 exec_clock;
    /* 当前CFS队列上最小运行时间,单调递增
     * 两种情况下更新该值: 
     * 1、更新当前运行任务的累计运行时间时
     * 2、当任务从队列删除去,如任务睡眠或退出,这时候会查看剩下的任务的vruntime是否大于min_vruntime,如果是则更新该值。
     */
    u64 min_vruntime;
#ifndef CONFIG_64BIT
    u64 min_vruntime_copy;
#endif
    /* 该红黑树的root */
    struct rb_root tasks_timeline;
    /* 下一个调度结点(红黑树最左边结点,最左边结点就是下个调度实体) */
    struct rb_node *rb_leftmost;

    /*
     * 'curr' points to currently running entity on this cfs_rq.
     * It is set to NULL otherwise (i.e when none are currently running).
     */
    /*
     * curr: 当前正在运行的sched_entity(对于组虽然它不会在cpu上运行,但是当它的下层有一个task在cpu上运行,那么它所在的cfs_rq就把它当做是该cfs_rq上当前正在运行的sched_entity)
     * next: 表示有些进程急需运行,即使不遵从CFS调度也必须运行它,调度时会检查是否next需要调度,有就调度next
     *
     * skip: 略过进程(不会选择skip指定的进程调度)
     */
    struct sched_entity *curr, *next, *last, *skip;

#ifdef    CONFIG_SCHED_DEBUG
    unsigned int nr_spread_over;
#endif

#ifdef CONFIG_SMP
    /*
     * CFS Load tracking
     * Under CFS, load is tracked on a per-entity basis and aggregated up.
     * This allows for the description of both thread and group usage (in
     * the FAIR_GROUP_SCHED case).
     */
    unsigned long runnable_load_avg, blocked_load_avg;
    atomic64_t decay_counter;
    u64 last_decay;
    atomic_long_t removed_load;

#ifdef CONFIG_FAIR_GROUP_SCHED
    /* Required to track per-cpu representation of a task_group */
    u32 tg_runnable_contrib;
    unsigned long tg_load_contrib;

    /*
     * h_load = weight * f(tg)
     *
     * Where f(tg) is the recursive weight fraction assigned to
     * this group.
     */
    unsigned long h_load;
    u64 last_h_load_update;
    struct sched_entity *h_load_next;
#endif /* CONFIG_FAIR_GROUP_SCHED */
#endif /* CONFIG_SMP */

#ifdef CONFIG_FAIR_GROUP_SCHED
    /* 所属于的CPU rq */
    struct rq *rq;    /* cpu runqueue to which this cfs_rq is attached */

    /*
     * leaf cfs_rqs are those that hold tasks (lowest schedulable entity in
     * a hierarchy). Non-leaf lrqs hold other higher schedulable entities
     * (like users, containers etc.)
     *
     * leaf_cfs_rq_list ties together list of leaf cfs_rq's in a cpu. This
     * list is used during load balance.
     */
    int on_list;
    struct list_head leaf_cfs_rq_list;
    /* 拥有该CFS运行队列的进程组 */
    struct task_group *tg;    /* group that "owns" this runqueue */

#ifdef CONFIG_CFS_BANDWIDTH
    int runtime_enabled;
    u64 runtime_expires;
    s64 runtime_remaining;

    u64 throttled_clock, throttled_clock_task;
    u64 throttled_clock_task_time;
    int throttled, throttle_count;
    struct list_head throttled_list;
#endif /* CONFIG_CFS_BANDWIDTH */
#endif /* CONFIG_FAIR_GROUP_SCHED */
};

实时就绪队列struct rt_rq

struct rt_rq {
    struct rt_prio_array active; //该结构里挂载的就是实时进程的队列
    unsigned int rt_nr_running;
#if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED
    struct {
        int curr; /* highest queued rt task prio */---------------最高实时任务的优先级
#ifdef CONFIG_SMP
        int next; /* next highest */------------------------------下一个实时任务最高优先级
#endif
    } highest_prio;
#endif
#ifdef CONFIG_SMP
    unsigned long rt_nr_migratory;
    unsigned long rt_nr_total;
    int overloaded;
    struct plist_head pushable_tasks;
#endif
    int rt_queued;

    int rt_throttled;------------------------------------------------当前队列的实时调度是否受限
    u64 rt_time;-----------------------------------------------------当前队列的累积运行时间
    u64 rt_runtime;--------------------------------------------------当前队列的单个period周期内最大运行时间
    /* Nests inside the rq lock: */
    raw_spinlock_t rt_runtime_lock;

#ifdef CONFIG_RT_GROUP_SCHED
    unsigned long rt_nr_boosted;

    struct rq *rq;
    struct task_group *tg;
#endif
}

我们注意到struct rt_rq中有一个active成员,他是一个rt_prio_array结构。

具有相同优先级的所有实时进程都保存在一个链表中,表头为active.queue[prio],而active.bitmap位图中的每个比特位对应于一个链表,凡包含了进程的链表,对应的比特位则置位。如果链表中没有进程,则对应的比特位不置位。

                                         

struct rt_prio_array是一组链表,每个优先级对应一个链表。还维护一个bitmap,其中实时进程优先级为0~99,再加上1bit的定界符。当某个优先级别上有进程被插入列表时,相应的比特位就被置位。通常用sched_find_first_bit()函数来查询该bitmap,他返回当前被置位的最高优先级的数组下标。在实时调度中,运行进程根据优先级放到对应的队列里面,对于相同的优先级进程后面来的进程放到同一优先级队列的末尾。对于FIFO/RR调度,各自的进程需要设置相关的属性。进程运行时,要根据task中的这些属性判断和设置,放弃CPU的时机。

#define MAX_USER_RT_PRIO	100
#define MAX_RT_PRIO		MAX_USER_RT_PRIO
struct rt_prio_array {
    DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
    struct list_head queue[MAX_RT_PRIO];---------------------------按优先级排列的实时调度器的就绪队列
}

需要注意的是SCHED_FIFO实时进程和SCHED_RR都在同一个实时进程运行队列中,在调度的时候对实体的调度策略police 进行判断,如果SCHED_RR策略,则在周期调度器中不会进行调度,SCHED_FIFO则会根据其运行时间来判断是否选择下一个进程运行。

CPU运行队列(struct rq)

  每个CPU都有自己的 struct rq 结构,其用于描述在此CPU上所运行的所有进程,其包括一个实时进程队列和一个根CFS运行队列,在调度时,调度器首先会先去实时进程队列找是否有实时进程需要运行,如果没有才会去CFS运行队列找是否有进行需要运行,这就是为什么常说的实时进程优先级比普通进程高,不仅仅体现在prio优先级上,还体现在调度器的设计上,至于dl运行队列,我暂时还不知道有什么用处,其优先级比实时进程还高,但是创建进程时如果创建的是dl进程创建会错误(具体见sys_fork)。

/* CPU运行队列,每个CPU包含一个struct rq */
struct rq {
    /* 处于运行队列中所有就绪进程的load之和 */
    raw_spinlock_t lock;

    /*
     * nr_running and cpu_load should be in the same cacheline because
     * remote CPUs use both these fields when doing load calculation.
     */
    /* 此CPU上总共就绪的进程数,包括cfs,rt和正在运行的 */
    unsigned int nr_running;
#ifdef CONFIG_NUMA_BALANCING
    unsigned int nr_numa_running;
    unsigned int nr_preferred_running;
#endif
    #define CPU_LOAD_IDX_MAX 5
    /* 根据CPU历史情况计算的负载,cpu_load[0]一直等于load.weight,当达到负载平衡时,cpu_load[1]和cpu_load[2]都应该等于load.weight */
    unsigned long cpu_load[CPU_LOAD_IDX_MAX];
    /* 最后一次更新 cpu_load 的时间 */
    unsigned long last_load_update_tick;
#ifdef CONFIG_NO_HZ_COMMON
    u64 nohz_stamp;
    unsigned long nohz_flags;
#endif
#ifdef CONFIG_NO_HZ_FULL
    unsigned long last_sched_tick;
#endif
    /* 是否需要更新rq的运行时间 */
    int skip_clock_update;

    /* capture load from *all* tasks on this cpu: */
    /* CPU负载,该CPU上所有可运行进程的load之和,nr_running更新时这个值也必须更新 */
    struct load_weight load;
    unsigned long nr_load_updates;
    /* 进行上下文切换次数,只有proc会使用这个 */
    u64 nr_switches;

    /* cfs调度运行队列,包含红黑树的根 */
    struct cfs_rq cfs;
    /* 实时调度运行队列 */
    struct rt_rq rt;
    struct dl_rq dl;

#ifdef CONFIG_FAIR_GROUP_SCHED
    /* list of leaf cfs_rq on this cpu: */
    struct list_head leaf_cfs_rq_list;

    struct sched_avg avg;
#endif /* CONFIG_FAIR_GROUP_SCHED */

    /*
     * This is part of a global counter where only the total sum
     * over all CPUs matters. A task can increase this counter on
     * one CPU and if it got migrated afterwards it may decrease
     * it on another CPU. Always updated under the runqueue lock:
     */
     /* 曾经处于队列但现在处于TASK_UNINTERRUPTIBLE状态的进程数量 */
    unsigned long nr_uninterruptible;

    /*
     * curr: 当前正在此CPU上运行的进程
     * idle: 当前CPU上idle进程的指针,idle进程用于当CPU没事做的时候调用,它什么都不执行
     */
    struct task_struct *curr, *idle, *stop;
    /* 下次进行负载平衡执行时间 */
    unsigned long next_balance;
    /* 在进程切换时用来存放换出进程的内存描述符地址 ,当切换进的线程是个内核线程时,由于内核线程没有mm,需要借用切出去线程的mm,同时为了切进来的线程后面再被切换走时需要减少mm的引用计数,需要把mm放在该结构中*/
    struct mm_struct *prev_mm;

    /* rq运行时间 */
    u64 clock;
    u64 clock_task;

    atomic_t nr_iowait;

#ifdef CONFIG_SMP
    struct root_domain *rd;
    /* 当前CPU所在基本调度域,每个调度域包含一个或多个CPU组,每个CPU组包含该调度域中一个或多个CPU子集,负载均衡都是在调度域中的组之间完成的,不能跨域进行负载均衡 */
    struct sched_domain *sd;

    unsigned long cpu_capacity;

    unsigned char idle_balance;
    /* For active balancing */
    int post_schedule;
    /* 如果需要把进程迁移到其他运行队列,就需要设置这个位 */
    int active_balance;
    int push_cpu;
    struct cpu_stop_work active_balance_work;

    /* 该运行队列所属CPU */
    int cpu;
    int online;

    struct list_head cfs_tasks;

    u64 rt_avg;
    /* 该运行队列存活时间 */
    u64 age_stamp;
    u64 idle_stamp;
    u64 avg_idle;

    /* This is used to determine avg_idle's max value */
    u64 max_idle_balance_cost;
#endif

#ifdef CONFIG_IRQ_TIME_ACCOUNTING
    u64 prev_irq_time;
#endif
#ifdef CONFIG_PARAVIRT
    u64 prev_steal_time;
#endif
#ifdef CONFIG_PARAVIRT_TIME_ACCOUNTING
    u64 prev_steal_time_rq;
#endif

    /* calc_load related fields */
    /* 用于负载均衡 */
    unsigned long calc_load_update;
    long calc_load_active;

#ifdef CONFIG_SCHED_HRTICK
#ifdef CONFIG_SMP
    int hrtick_csd_pending;
    struct call_single_data hrtick_csd;
#endif
    /* 调度使用的高精度定时器 */
    struct hrtimer hrtick_timer;
#endif

#ifdef CONFIG_SCHEDSTATS
    /* latency stats */
    struct sched_info rq_sched_info;
    unsigned long long rq_cpu_time;
    /* could above be rq->cfs_rq.exec_clock + rq->rt_rq.rt_runtime ? */

    /* sys_sched_yield() stats */
    unsigned int yld_count;

    /* schedule() stats */
    unsigned int sched_count;
    unsigned int sched_goidle;

    /* try_to_wake_up() stats */
    unsigned int ttwu_count;
    unsigned int ttwu_local;
#endif

#ifdef CONFIG_SMP
    struct llist_head wake_list;
#endif

#ifdef CONFIG_CPU_IDLE
    /* Must be inspected within a rcu lock section */
    struct cpuidle_state *idle_state;
#endif
};

上面内容整理自这两篇文章:

https://www.cnblogs.com/tolimit/p/4303052.html

https://www.cnblogs.com/arnoldlu/p/9025981.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值