>

这个数取决于运行队列当前的进程数sbf282.com:,

- 编辑:澳门博发娱乐官网 -

这个数取决于运行队列当前的进程数sbf282.com:,

在抽象模型中vruntime决定了进程被调度的先后顺序,在真实模型中决定被调度的先后顺序的参数是由函数entity_key决定的。   
static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    return se->vruntime - cfs_rq->min_vruntime;
}
enqueue_task_fair---->enqueue_entity---->__enqueue_entity---->entity_key决定插入就绪队列的位置。

一、概述

本篇来探究下cgroup对cpu的限制机制,前文提到过cgroup也是通过进程调度子系统来达到限制cpu的目的,因此需要了解下进程调度子系统.

完全公平调度CFS

CFS(Completely Fair Scheduler)试图按照对 CPU 时间的 “最大需求(gravest need)” 运行任务;这有助于确保每个进程可以获得对 CPU 的公平共享。

进程调度所使用到的数据结构:

普通进程分为40个等级,每个等级对应一个权重值,权重值用一个整数来标示。权重值定义在数组prio_to_weight[40]中;普通进程的权重值最大为88761,最小为15。默认情况下,普通进程的权重值为1024(由NICE_0_LOAD指定)。weight是由进程的静态优先级static_prio决定的,静态优先级越高(static_prio值越小)weight值越大。普通进程的默认 nice值为0,即默认静态优先级为120,它的weight值为prio_to_weight[20],即1024。因此NICE_0_LOAD的值就 为1024。

首先简介一下主要的设计思路,
CFS思路非常easy。就是依据各个进程的权重分配执行时间(权重怎么来的后面再说)。
进程的执行时间计算公式为:
分配给进程的执行时间 = 调度周期 * 进程权重 / 全部进程权重之和   (公式1)
调度周期非常好理解。就是将全部处于TASK_RUNNING态进程都调度一遍的时间,
几乎相同相当于O(1)调度算法中执行队列和过期队列切换一次的时间
(我对O(1)调度算法看得不是非常熟,如有错误还望各位大虾指出)。
举个样例。比方仅仅有两个进程A, B,权重分别为1和2,
调度周期设为30ms,那么分配给A的CPU时间为
30ms * (1/(1+2)) = 10ms
而B的CPU时间为
 
30ms * (2/(1+2)) = 20ms
那么在这30ms中A将执行10ms。B将执行20ms。
公平怎么体现呢?它们的执行时间并不一样阿?
事实上公平是体如今另外一个量上面。叫做virtual runtime(vruntime)。它记录着进程已经执行的时间,
可是并非直接记录,而是要依据进程的权重将执行时间放大或者缩小一个比例。
我们来看下从实际执行时间到vruntime的换算公式
vruntime = 实际执行时间 * 1024 / 进程权重。 (公式2)
为了不把大家搞晕。这里我直接写1024。实际上它等于nice为0的进程的权重,代码中是NICE_0_LOAD。
也就是说。全部进程都以nice为0的进程的权重1024作为基准。计算自己的vruntime添加速度。
还以上面AB两个进程为例。B的权重是A的2倍,那么B的vruntime添加速度仅仅有A的一半。

因为是介绍cgroup的文章,因此只介绍进程调度中与cgroup密切关联的部分,详细完成的进程调度实现可参考进程调度的相关资料.

CFS初探

CFS 调度程序使用安抚(appeasement)策略确保公平性。当某个任务进入运行队列后,将记录当前时间,当某个进程等待 CPU 时,将对这个进程的 wait_runtime 值加一个数,这个数取决于运行队列当前的进程数。当执行这些计算时,也将考虑不同任务的优先级值。 将这个任务调度到 CPU 后,它的 wait_runtime 值开始递减,当这个值递减到其他任务成为红黑树的最左侧任务时,当前任务将被抢占。通过这种方式,CFS 努力实现一种理想 状态,即 wait_runtime 值为 0!

CFS 维护任务运行时(相对于运行队列级时钟,称为 fair_clock(cfs_rq->fair_clock)),它在某个实际时间的片段内运行,因此,对于单个任务可以按照理想的速度运行。

例如,如果具有 4 个可运行的任务,那么 fair_clock 将按照实际时间速度的四分之一增加。每个任务将设法跟上这个速度。这是由分时多任务处理的量子化特性决定的。也就是说,在任何一个时间段内只有一个任务可以运行;因此, 其他进程在时间上的拖欠将增大(wait_runtime)。因此,一旦某个任务进入调度,它将努力赶上它所欠下的时间(并且要比所欠时间多一点,因为在追赶时间期间,fair_clock 不会停止计时)。

粒度和延迟如何关联?

关联粒度和延迟的简单等式为: 

gran = (lat/nr) - (lat/nr/nr)

其中 gran = 粒度,

lat = 延迟,而 

nr = 运行中的任务数。

加权任务引入了优先级。假设我们有两个任务:其中一个任务占用 CPU 的时间量是另一个任务的两倍,比例为 2:1。执行数学转换后,对于权重为 0.5 的任务,时间流逝的速度是以前的两倍。我们根据 fair_clock 对树进行排队。

请注意,CFS 没有使用时间片(time slices),至少,没有优先使用。CFS 中的时间片具有可变的长度并且动态确定。

1.就绪队列

vruntime行走速度:
    系统规定:默认权重值(1024)对应的进程的vruntime行走时间与实际运行时间runtime是1:1的关系。由于vruntime的行走速度和权重值成反比,那么其它进程的vruntime行走速度都通过以下两个参数计算得到:1、该进程的权重值2、默认进程的权重值。
    例如权重为3096的进程的vruntime行走速度为:1024/3096 * (wall clock)。
    “真实时钟速度”即为runtime(即 wall clock)行走的速度。

如今我们把公式2中的实际执行时间用公式1来替换。能够得到这么一个结果:
vruntime = (调度周期 * 进程权重 / 全部进程总权重) * 1024 / 进程权重=调度周期 * 1024 / 全部进程总权重
看出什么眉目没有?没错,尽管进程的权重不同,可是它们的vruntime增长速度应该是一样的(这里所说的增长速度一样,是从宏观上来看的。从上一篇文章能够看出来。而在上一篇文章中说vruntime的增量不同,是从公式分析得到的,算是局部分析,在公式2中,假设实际执行时间都是一样。非常显然权重小的增长的多。权重大的增长的小,我个人认为正是虚拟时钟的存在。转换了思想。才有了这个CFS,事实上还是依据权重来决定一个进程在一个调用周期内执行了多长时间,可是虚拟时钟决定了怎么调度这个过程,这就是思想),与权重无关。
好,既然全部进程的vruntime增长速度宏观上看应该是同一时候推进的。
那么就能够用这个vruntime来选择执行的进程。谁的vruntime值较小就说明它曾经占用cpu的时间较短,
受到了“不公平”对待,因此下一个执行进程就是它。

本文分为三个部分,首先介绍进程调度中的调度算法,在该基础上引入组调度,最后结合前面文章(cgroup原理简析:vfs文件系统)来说明上层通过echo pid >> tasks, echo n > cpu.shares等操作影响调度器对进程的调度,从而控制进程对cpu的使用,(内核源码版本3.10)

运行时调优选项

引入了重要的 sysctls 来在运行时对调度程序进行调优(以 ns 结尾的名称以纳秒为单位):

sched_latency_ns:针对 CPU 密集型任务进行目标抢占延迟(Targeted preemption latency)。

sched_batch_wakeup_granularity_ns:针对 SCHED_BATCH 的唤醒(Wake-up)粒度。

sched_wakeup_granularity_ns:针对 SCHED_OTHER 的唤醒粒度。

sched_compat_yield:由于 CFS 进行了改动,严重依赖 sched_yield() 的行为的应用程序可以要求不同的性能,因此推荐启用 sysctls。

sched_child_runs_first:child 在 fork 之后进行调度;此为默认设置。如果设置为 0,那么先调度 parent。

sched_min_granularity_ns:针对 CPU 密集型任务执行最低级别抢占粒度。

sched_features:包含各种与调试相关的特性的信息。

sched_stat_granularity_ns:收集调度程序统计信息的粒度。

sbf282.com 1

系统中运行时参数的典型值

内核为每一个cpu创建一个进程就绪队列,该队列上的进程均由该cpu执行,代码如下(kernel/sched/core.c)。

    进程执行执行期间周期性调度器周期性地启动,其负责更新一些相关数据,并不负责进程之间的切换:
    timer_tick()---->update_process_times---->schedule_tick()
    schedule_tick---->task_tick_fair---->entity_tick()---->update_curr()
    update_curr()函数完成相关数据的更新。
        update_curr()---->delta_exec = (unsigned long)(now - curr->exec_start)
                              |-->__update_curr()
                              |-->curr_exec_start = now;
    update_curr()函数只负责计算delta_exec以及更新exec_start。其它工作由__update_curr()函数完成:
        1、更新当前进程的实际运行时间(抽象模型中的runtime)。
        2、更新当前进程的虚拟时间vruntime。
        3、更新cfs_rq->min_vruntime。
           在当前进程和下一个将要被调度的进程中选择vruntime较小的值。然后用该值和cfs_rq->min_vruntime比较,如果比min_vruntime大,则更新cfs_rq为的min_vruntime为所求出的值。

这样既能公平选择进程,又能保证高优先级进程
获得较多的执行时间。
这就是CFS的主要思想了。


新的调度程序调试接口

新调度程序附带了一个非常棒的调试接口,还提供了运行时统计信息,分别在 kernel/sched_debug.c 和 kernel/sched_stats.h 中实现。要提供调度程序的运行时信息和调试信息,需要将一些文件添加到 proc pseudo 文件系统:

/proc/sched_debug:显示运行时调度程序可调优选项的当前值、CFS 统计信息和所有可用 CPU 的运行队列信息。当读取这个 proc 文件时,将调用 sched_debug_show() 函数并在 sched_debug.c 中定义。

/proc/schedstat:为所有相关的 CPU 显示特定于运行队列的统计信息以及 SMP 系统中特定于域的统计信息。kernel/sched_stats.h 中定义的 show_schedstat() 函数将处理 proc 条目中的读操作。

/proc/[PID]/sched:显示与相关调度实体有关的信息。在读取这个文件时,将调用 kernel/sched_debug.c 中定义的 proc_sched_show_task() 函数

1 DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);

考虑下当创建新进程或者进程唤醒时,vruntime在真实模型中的处理方式:
I、新建进程
    进程的ideal_time长度和weight成正比,vruntime行走速度与weight值成反比。因此当每个进程在period时间内,都执行了自己对应的ideal_time长时间,那么他们的vruntime的增量相等。而nice为0的进程的vruntime行走速度等于runtime行走速度,所以每个进程都运行它自己对应的ideal_runtime时间,其它进程的vruntime增量都等于nice值为0的进程的ideal_runtime。假设初始情况下,它们的所有进程的vruntime值都等于0,那么当一个进程运行完runtime的时间为ideal_time,那么它的vruntime将为最大,只要其它进程的运行总时间没有达到各自对应的ideal_runtime值,那么它始终排在进程队列的末尾。

再补充一下权重的来源,权重跟进程nice值之间有一一相应的关系,能够通过全局数组prio_to_weight来转换,
nice值越大,权重越低

1.进程调度

CFS内部原理

Linux 内的所有任务都由称为 task_struct 的任务结构表示。该结构(以及其他相关内容)完整地描述了任务并包括了任务的当前状态、其堆栈、进程标识、优先级(静态和动态)等等。可以在 ./linux/include/linux/sched.h 中找到这些内容以及相关结构。 但是因为不是所有任务都是可运行的,在 task_struct 中不会发现任何与 CFS 相关的字段。 相反,会创建一个名为 sched_entity 的新结构来跟踪调度信息。

sbf282.com 2

任务和红黑树的结构层次关系

树的根通过 rb_root 元素通过 cfs_rq 结构(在 ./kernel/sched.c 中)引用。红黑树的叶子不包含信息,但是内部节点代表一个或多个可运行的任务。红黑树的每个节点都由 rb_node 表示,它只包含子引用和父对象的颜色。 rb_node 包含在 sched_entity 结构中,该结构包含rb_node引用、负载权重以及各种统计数据。最重要的是,sched_entity 包含 vruntime(64 位字段),它表示任务运行的时间量,并作为红黑树的索引。 最后,task_struct 位于顶端,它完整地描述任务并包含 sched_entity 结构。

就 CFS 部分而言,调度函数非常简单。 在 ./kernel/sched.c 中,通用 schedule() 函数,它会先抢占当前运行任务(除非它通过 yield() 代码先抢占自己)。注意 CFS 没有真正的时间切片概念用于抢占,因为抢占时间是可变的。当前运行任务(现在被抢占的任务)通过对 put_prev_task 调用(通过调度类)返回到红黑树。当schedule函数开始确定下一个要调度的任务时,它会调用 pick_next_task函数。此函数也是通用的(在./kernel/sched.c 中),但它会通过调度器类调用 CFS 调度器。

CFS调度算法的核心是选择具有最小vruntine的任务。运行队列采用红黑树方式存放,其中节点的键值便是可运行进程的虚拟运行时间。CFS调度器选取待运行的下一个进程,是所有进程中vruntime最小的那个,他对应的便是在树中最左侧的叶子节点。实现选择的函数为pick_next_task_fair,CFS 中的 pick_next_task 函数可以在 ./kernel/sched_fair.c(称为pick_next_task_fair())中找到。 此函数只是从红黑树中获取最左端的任务并返回相关sched_entity。通过此引用,一个简单的 task_of()调用确定返回的task_struct 引用。通用调度器最后为此任务提供处理器。

static struct task_struct *pick_next_task_fair(struct rq *rq)

{

struct task_struct *p;

struct cfs_rq *cfs_rq = &rq->cfs;

struct sched_entity *se;

if (unlikely(!cfs_rq->nr_running))

return NULL;

do {/*此循环为了考虑组调度*/

se = pick_next_entity(cfs_rq);

set_next_entity(cfs_rq, se);/*设置为当前运行进程*/

cfs_rq = group_cfs_rq(se);

} while (cfs_rq);

p = task_of(se);

hrtick_start_fair(rq, p);

return p;

}

实质工作调用__pick_next_entity完成。

/*函数本身并不会遍历数找到最左叶子节点(即就是所有进程中vruntime最小的那个),因为该值已经缓存在rb_leftmost字段中*/

static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)

{

/*rb_leftmost为保存的红黑树的最左边的节点*/

struct rb_node *left = cfs_rq->rb_leftmost;

if (!left)

return NULL;

return rb_entry(left, struct sched_entity, run_node);

}

定义了一个struct rq结构体数组,每个数组元素是一个就绪队列,对应一个cpu。下面看下struct rq结构体(kernel/sched/sched.h):

    对于新进程,task_fork_fair()->place_entity(cfs_rq, se, 1),其intial参数为1。新进程的vruntime值被设置为min_vruntime+sched_vslice(cfs_rq, se), sched_vslice()函数可计算出nice值为0的进程的ideal_runtime。其作用是将新加入的进程的标记为“它在period长时间内已经运行它对应的ideal_time长时间”,那么新加入进程在理论上(所有进程都执行它对应的ideal_runtime时间,没有发生睡眠、进程终止等特殊情况)只有等待period之后才能被调度。
    sched_vslice(cfs_rq, se)---->calc_delta_fair(sched_slice(cfs_rq, se), se), sched_slice()计算新建进程的ideal_runtime,calc_delta_fair()将ideal_runtime转换成vruntime。

以下来分析代码。网上已经有非常多cfs的文章。因此我打算换一个方式来写,选择几个点来进行情景分析,
包含进程创建时。进程被唤醒,主动调度(schedule),时钟中断。

我们知道linux下的进程会有多种状态,已就绪状态的进程在就绪队列中(struct rq),当cpu需要加载新任务时,调度器会从就绪队列中选择一个最优的进程加载执行.

重要的 CFS 数据结构

对于每个 CPU,CFS 使用按时间排序的红黑(red-black)树。

红黑树是一种自平衡二叉搜索树,这种数据结构可用于实现关联数组。对于每个运行中的进程,在红黑树上都有一个节点。红黑树上位于最左侧的进程表示将进行下一次调度的进程。红黑树比较复杂,但它的操作具有良好的最差情况(worst-case)运行时,并且在实际操作中非常高效:它可以在 O(log n) 时间内搜索、插入和删除 ,其中 n 表示树元素的数量。叶节点意义不大并且不包含数据。为节省内存,有时使用单个哨兵(sentinel)节点执行所有叶节点的角色。内部节点到叶节点的所有引用都指向哨兵节点。

该树方法能够良好运行的原因在于:

1.红黑树可以始终保持平衡。

2.由于红黑树是二叉树,查找操作的时间复杂度为对数。但是,除了最左侧查找以外,很难执行其他查找,并且最左侧的节点指针始终被缓存。

3.对于大多数操作,红黑树的执行时间为 O(log n),而以前的调度程序通过具有固定优先级的优先级数组使用 O(1)O(log n) 行为具有可测量的延迟,但是对于较大的任务数无关紧要。Molnar 在尝试这种树方法时,首先对这一点进行了测试。

4.红黑树可通过内部存储实现 — 即不需要使用外部分配即可对数据结构进行维护。

让我们了解一下实现这种新调度程序的一些关键数据结构。

sbf282.com 3sbf282.com 4

II、睡眠进程被唤醒
    将进程的vruntime值设置为cfs_rq->min_vruntime值,然后再进行一下补偿:将vruntime减去与sysctl_sched_latencyd相关的一个数值。进程进入睡眠状态时cfs_rq->min_vruntime就大于或等于该进程的vruntime值,它在睡眠过程中vruntime值是不改变的,但是cfs_rq->min_vruntime的值却是单调增加的,进程醒来后补偿的量由sysctl_sched_latency给出,不管进程受到的不公平待遇大还是小,一律只补偿这么多。

介绍代码之前先介绍一下CFS相关的结构
第一个是调度实体sched_entity,它代表一个调度单位。在组调度关闭的时候能够把他等同为进程。
每个task_struct中都有一个sched_entity,进程的vruntime和权重都保存在这个结构中。
那么全部的sched_entity怎么组织在一起呢?红黑树。全部的sched_entity以vruntime为key
(实际上是以vruntime-min_vruntime为单位,难道是防止溢出?反正结果是一样的)插入到红黑树中,
同一时候缓存树的最左側节点。也就是vruntime最小的节点,这样能够迅速选中vruntime最小的进程。
注意仅仅有等待CPU的就绪态进程在这棵树上,睡眠进程和正在执行的进程都不在树上。
我从ibm developer works上偷过来一张图来展示一下它们的关系:
汗。图片上传功能被关闭了。先盗链一个过来。别怪我没品哈。。。

那么调度器根据规则来选出这个最优的进程呢?这又引出了进程优先级的概念,简单来说,linux将进程分为普通进程和实时进程两个大类,用一个数值范围0-139来表示优先级,值越小优先级越高,其中0-99表示实时进程,100-139(对应用户层nice值-20-19)表示普通进程,实时进程的优先级总是高于普通进程.

struct task_struct 的变化

CFS 去掉了 struct prio_array,并引入调度实体(scheduling entity)和调度类 (scheduling classes),分别由 struct sched_entity 和 struct sched_class 定义。因此,task_struct 包含关于 sched_entity 和 sched_class 这两种结构的信息:

struct task_struct {

/* Defined in 2.6.23:/usr/include/linux/sched.h */....

-  struct prio_array *array;

+  struct sched_entity se;

+  struct sched_class *sched_class;  

 ....   ....

};

  1 struct rq {
  2     /* runqueue lock: */
  3     raw_spinlock_t lock;
  4 
  5     /*
  6      * nr_running and cpu_load should be in the same cacheline because
  7      * remote CPUs use both these fields when doing load calculation.
  8      */
  9     unsigned int nr_running;
 10 #ifdef CONFIG_NUMA_BALANCING
 11     unsigned int nr_numa_running;
 12     unsigned int nr_preferred_running;
 13 #endif
 14     #define CPU_LOAD_IDX_MAX 5
 15     unsigned long cpu_load[CPU_LOAD_IDX_MAX];
 16     unsigned long last_load_update_tick;
 17 #ifdef CONFIG_NO_HZ_COMMON
 18     u64 nohz_stamp;
 19     unsigned long nohz_flags;
 20 #endif
 21 #ifdef CONFIG_NO_HZ_FULL
 22     unsigned long last_sched_tick;
 23 #endif
 24     int skip_clock_update;
 25 
 26     /* capture load from *all* tasks on this cpu: */
 27     struct load_weight load;
 28     unsigned long nr_load_updates;
 29     u64 nr_switches;
 30 
 31     struct cfs_rq cfs;
 32     struct rt_rq rt;
 33     struct dl_rq dl;
 34 
 35 #ifdef CONFIG_FAIR_GROUP_SCHED
 36     /* list of leaf cfs_rq on this cpu: */
 37     struct list_head leaf_cfs_rq_list;
 38 
 39     struct sched_avg avg;
 40 #endif /* CONFIG_FAIR_GROUP_SCHED */
 41 
 42     /*
 43      * This is part of a global counter where only the total sum
 44      * over all CPUs matters. A task can increase this counter on
 45      * one CPU and if it got migrated afterwards it may decrease
 46      * it on another CPU. Always updated under the runqueue lock:
 47      */
 48     unsigned long nr_uninterruptible;
 49 
 50     struct task_struct *curr, *idle, *stop;
 51     unsigned long next_balance;
 52     struct mm_struct *prev_mm;
 53 
 54     u64 clock;
 55     u64 clock_task;
 56 
 57     atomic_t nr_iowait;
 58 
 59 #ifdef CONFIG_SMP
 60     struct root_domain *rd;
 61     struct sched_domain *sd;
 62 
 63     unsigned long cpu_capacity;
 64 
 65     unsigned char idle_balance;
 66     /* For active balancing */
 67     int post_schedule;
 68     int active_balance;
 69     int push_cpu;
 70     struct cpu_stop_work active_balance_work;
 71     /* cpu of this runqueue: */
 72     int cpu;
 73     int online;
 74 
 75     struct list_head cfs_tasks;
 76 
 77     u64 rt_avg;
 78     u64 age_stamp;
 79     u64 idle_stamp;
 80     u64 avg_idle;
 81 
 82     /* This is used to determine avg_idle's max value */
 83     u64 max_idle_balance_cost;
 84 #endif
 85 
 86 #ifdef CONFIG_IRQ_TIME_ACCOUNTING
 87     u64 prev_irq_time;
 88 #endif
 89 #ifdef CONFIG_PARAVIRT
 90     u64 prev_steal_time;
 91 #endif
 92 #ifdef CONFIG_PARAVIRT_TIME_ACCOUNTING
 93     u64 prev_steal_time_rq;
 94 #endif
 95 
 96     /* calc_load related fields */
 97     unsigned long calc_load_update;
 98     long calc_load_active;
 99 
100 #ifdef CONFIG_SCHED_HRTICK
101 #ifdef CONFIG_SMP
102     int hrtick_csd_pending;
103     struct call_single_data hrtick_csd;
104 #endif
105     struct hrtimer hrtick_timer;
106 #endif
107 
108 #ifdef CONFIG_SCHEDSTATS
109     /* latency stats */
110     struct sched_info rq_sched_info;
111     unsigned long long rq_cpu_time;
112     /* could above be rq->cfs_rq.exec_clock + rq->rt_rq.rt_runtime ? */
113 
114     /* sys_sched_yield() stats */
115     unsigned int yld_count;
116 
117     /* schedule() stats */
118     unsigned int sched_count;
119     unsigned int sched_goidle;
120 
121     /* try_to_wake_up() stats */
122     unsigned int ttwu_count;
123     unsigned int ttwu_local;
124 #endif
125 
126 #ifdef CONFIG_SMP
127     struct llist_head wake_list;
128 #endif
129 };

真实模型总结:
    a)进程在就绪队列中用键值key来排序,它没有保存在任何变量中,而是在需要时由函数entity_key()计算得出。它等于
        key = task->vruntime - cfs_rq->min_vruntime
    b)各个进程有不同的重要性(优先等级),越重要的进程权重值weight(task.se.load.weight)越大。
    c)每个进程vruntime行走的速度和weight值成反比。权重值为1024(NICE_0_LOAD)的进程vruntime行走速度和runtime相同。
    d)每个进程每次获得CPU使用权最多执行与该进程对应的ideal_runtime长时间。该时间长度和weight值成正比,它没有用变量来保存,而是需要使用sched_slice()函数计算得出。
    e)ideal_runtime计算的基准是period,它也没有用变量来保存,而是由__sched_period()计算得出。

 

不同优先级类型的进程当然要使用不同的调度策略,普通进程使用完全公平调度(cfs),实时进程使用实时调度(rt),这里内核实现上使用了一个类似面向对象的方式,抽象出一个调度类(struct sched_class)声明同一的钩子函数,完全公平调度实例(fair_sched_class)和实时调度实例(rt_sched_class)各自实现这些钩子.

struct sched_entity

运行实体结构为sched_entity,该结构包含了完整的信息,用于实现对单个任务或任务组的调度。它可用于实现组调度。调度实体可能与进程没有关联。所有的调度器都必须对进程运行时间做记账。CFS不再有时间片的概念,但是他也必须维护每个进程运行的时间记账,因为他需要确保每个进程只在公平分配给他的处理器时间内运行。CFS使用调度器实体结构来最终运行记账。

sbf282.com 5

sched_entity 结构体简介

实现记账功能,由系统定时器周期调用

static void update_curr(struct cfs_rq *cfs_rq)

{

struct sched_entity *curr = cfs_rq->curr;

u64 now = rq_of(cfs_rq)->clock;/*now计时器*/

unsigned long delta_exec;

if (unlikely(!curr))

return;

/*

* Get the amount of time the current task was running

* since the last time we changed load (this cannot

* overflow on 32 bits):

*/

/*获得从最后一次修改负载后当前任务所占用的运行总时间*/

/*即计算当前进程的执行时间*/

delta_exec = (unsigned long)(now - curr->exec_start);

if (!delta_exec)/*如果本次没有执行过,不用重新更新了*/

return;

/*根据当前可运行进程总数对运行时间进行加权计算*/

__update_curr(cfs_rq, curr, delta_exec);

curr->exec_start = now;/*将exec_start属性置为now*/

if (entity_is_task(curr)) {/*下面为关于组调度的,暂时不分析了*/

struct task_struct *curtask = task_of(curr);

trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);

cpuacct_charge(curtask, delta_exec);

account_group_exec_runtime(curtask, delta_exec);

}

}

struct rq

 

sbf282.com 6

对应的,就绪队列里也就分为两个子队列,一个维护普通进程,称之为cfs队列(cfs_rq),一个维护实时进程,称之为rt队列(rt_rq).

struct sched_class

该调度类类似于一个模块链,协助内核调度程序工作。每个调度程序模块需要实现 struct sched_class建议的一组函数。

sbf282.com 7

sched_class 结构体简介

函数功能说明:

enqueue_task:当某个任务进入可运行状态时,该函数将得到调用。它将调度实体(进程)放入红黑树中,并对 nr_running 变量加 1。

dequeue_task:当某个任务退出可运行状态时调用该函数,它将从红黑树中去掉对应的调度实体,并从 nr_running 变量中减 1。

yield_task:在 compat_yield sysctl 关闭的情况下,该函数实际上执行先出队后入队;在这种情况下,它将调度实体放在红黑树的最右端。

check_preempt_curr:该函数将检查当前运行的任务是否被抢占。在实际抢占正在运行的任务之前,CFS 调度程序模块将执行公平性测试。这将驱动唤醒式(wakeup)抢占。

pick_next_task:该函数选择接下来要运行的最合适的进程。

load_balance:每个调度程序模块实现两个函数,load_balance_start() 和load_balance_next(),使用这两个函数实现一个迭代器,在模块的 load_balance 例程中调用。内核调度程序使用这种方法实现由调度模块管理的进程的负载平衡。

set_curr_task:当任务修改其调度类或修改其任务组时,将调用这个函数。

task_tick:该函数通常调用自 time tick 函数;它可能引起进程切换。这将驱动运行时(running)抢占。

task_new:内核调度程序为调度模块提供了管理新任务启动的机会。CFS 调度模块使用它进行组调度,而用于实时任务的调度模块则不会使用这个函数。

该结构体是本地cpu所有进程组成的就绪队列,在linux内核中,进程被分为普通进程和实时进程,这两种进程的调度策略是不同的,因此在31-32行可以看到rq结构体中又内嵌了struct cfs_rq cfs和struct rt_rq rt两个子就绪队列,分别来组织普通进程和实时进程(普通进程将采用完全公平调度策略cfs,而实时进程将采用实时调度策略),第33行struct dl_rq dl调度空闲进程,暂且不讨论。所以,如果咱们研究的是普通进程的调度,需要关心的就是struct cfs_rq cfs队列;如果研究的是实时进程,就只关心struct rt_rq rt队列。

进程的优先等级决定了其权重值,task_struct中与优先级相关数据成员:
    a)static_prio,指普通进程的静态优先级(实时进程没用该参数),值越小则优先级越高。静态优先级是进程启动时分配的优先级。它可以用nice()或者sched_setscheduler()系统调用更改,否则在运行期间一直保持恒定。

 

另外,虽然调度器最终调度的对象是进程,但在这里用一个调度实体(struct sched_entity||struct sched_rt_entity)表示一个要被调度的对象.
对于普通的进程调度来说,一个调度实体对象内嵌在task_struct中,对于组调度(cgroup)来说,其内嵌在task_group中.

运行队列中CFS 有关的字段

对于每个运行队列,都提供了一种结构来保存相关红黑树的信息。

struct cfs_rq {

struct load_weight load;/*运行负载*/

unsigned long nr_running;/*运行进程个数*/

u64 exec_clock;

u64 min_vruntime;/*保存的最小运行时间*/

struct rb_root tasks_timeline;/*运行队列树根*/

struct rb_node *rb_leftmost;/*保存的红黑树最左边的

节点,这个为最小运行时间的节点,当进程

选择下一个来运行时,直接选择这个*/

struct list_head tasks;

struct list_head *balance_iterator;

/*

* 'curr' points to currently running entity on this cfs_rq.

* It is set to NULL otherwise (i.e when none are currently running).

*/

struct sched_entity *curr, *next, *last;

unsigned int nr_spread_over;

#ifdef CONFIG_FAIR_GROUP_SCHED

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.

*/

struct list_head leaf_cfs_rq_list;

struct task_group *tg; /* group that "owns" this runqueue */

#ifdef CONFIG_SMP

/*

* the part of load.weight contributed by tasks

*/

unsigned long task_weight;

/*

*  h_load = weight * f(tg)

*

* Where f(tg) is the recursive weight fraction assigned to

* this group.

*/

unsigned long h_load;

/*

* this cpu's part of tg->shares

*/

unsigned long shares;

/*

* load.weight at the time we set shares

*/

unsigned long rq_weight;

#endif

#endif

};

1.1普通进程的就绪队列struct cfs_rq(kernel/sched/sched.h)

       注意:关于a),注意本文的结尾添加的注释。

 

cfs队列用红黑树组织调度实体,cfs调度算法总是选择最左边的调度实体.

sbf282.com 8sbf282.com 9

    b)rt_priority,表示实时进程的优先级(普通进程没用该参数),它的值介于[0~99]之间。rt_priority的值越大其优先级越高。
    c)normal_prio,由于static_prio和rt_priority与优先级的关联性不相同,因此用normal_prio统一下“单位”,统一成:normal_prio值越小则进程优先级越高。因此,normal_prio也可以理解为:统一了单位的“静态”优先级。
    d)prio,在系统中使用prio判断进程优先级,prio是进程的动态优先级,其表示进程的有效优先级。对于实时进程来说,有效优先级prio就等于它的normal_prio。普通进程可以临时提高优先级,通过改变prio实现,动态优先级的提高不影响进程的静态优先级。父进程的动态优先级不会遗传给子进程,子进程的动态优先级prio初始化为父进程的静态优先级。

 

rt队列的结构是类似rt_queue[100][list]这样的结构,这里99对应实时进程的0-99个优先级,二维是链表,也就是根据实时进程的优先级,将进程挂载相应的list上.

内核 2.6.24 中的变化

新版本中不再追赶全局时钟(fair_clock),任务之间将彼此追赶。将引入每个任务(调度实体)的时钟 vruntime(wall_time/task_weight),并且将使用近似的平均时间初始化新任务的时钟。其他重要改动将影响关键数据结构。下面展示了 struct sched_entity 中的预期变动:

sbf282.com 10

2.6.24 版本中 sched_entity 结构的预期变动

sbf282.com 11

2.6.24 版本中 cfs_rq 结构的预期变动

sbf282.com 12

2.6.24版本中新添加的 task_group 结构

每个任务都跟踪它的运行时,并根据该值对任务进行排队。这意味着运行最少的任务将位于树的最左侧。同样,通过对时间加权划分优先级。每个任务在下面的时间段内力求获得精确调度:

sched_period = (nr_running > sched_nr_latency) ? sysctl_sched_latency : ((nr_running * sysctl_sched_latency) / sched_nr_latency)

其中 sched_nr_latency = (sysctl_sched_latency / sysctl_sched_min_granularity)。这表示,当可运行任务数大于 latency_nr 时,将线性延长调度周期。sched_fair.c 中定义的 sched_slice() 是进行这些计算的位置。因此,如果每个可运行任务运行与 sched_slice() 等价的时间,那么将花费的时间为 sched_period,每个任务将运行与其权重成比例的时间量。此外,在任何时刻,CFS 都承诺超前运行 sched_period,因为最后执行调度的任务将在这个时限内再次运行。

因此,当一个新任务变为可运行状态时,对其位置有严格的要求。在所有其他任务运行之前,此任务不能运行;否则,将破坏对这些任务作出的承诺。然而,由于该任务确实进行了排队,对运行队列的额外权重将缩短其他所有任务的时间片,在 sched_priod 的末尾释放一点位置,刚好满足新任务的需求。这个新的任务就被放在这个位置。

 1 /* CFS-related fields in a runqueue */
 2 struct cfs_rq {
 3     struct load_weight load;
 4     unsigned int nr_running, h_nr_running;
 5 
 6     u64 exec_clock;
 7     u64 min_vruntime;
 8 #ifndef CONFIG_64BIT
 9     u64 min_vruntime_copy;
10 #endif
11 
12     struct rb_root tasks_timeline;
13     struct rb_node *rb_leftmost;
14 
15     /*
16      * 'curr' points to currently running entity on this cfs_rq.
17      * It is set to NULL otherwise (i.e when none are currently running).
18      */
19     struct sched_entity *curr, *next, *last, *skip;
20 
21 #ifdef    CONFIG_SCHED_DEBUG
22     unsigned int nr_spread_over;
23 #endif
24 
25 #ifdef CONFIG_SMP
26     /*
27      * CFS Load tracking
28      * Under CFS, load is tracked on a per-entity basis and aggregated up.
29      * This allows for the description of both thread and group usage (in
30      * the FAIR_GROUP_SCHED case).
31      */
32     unsigned long runnable_load_avg, blocked_load_avg;
33     atomic64_t decay_counter;
34     u64 last_decay;
35     atomic_long_t removed_load;
36 
37 #ifdef CONFIG_FAIR_GROUP_SCHED
38     /* Required to track per-cpu representation of a task_group */
39     u32 tg_runnable_contrib;
40     unsigned long tg_load_contrib;
41 
42     /*
43      *   h_load = weight * f(tg)
44      *
45      * Where f(tg) is the recursive weight fraction assigned to
46      * this group.
47      */
48     unsigned long h_load;
49     u64 last_h_load_update;
50     struct sched_entity *h_load_next;
51 #endif /* CONFIG_FAIR_GROUP_SCHED */
52 #endif /* CONFIG_SMP */
53 
54 #ifdef CONFIG_FAIR_GROUP_SCHED
55     struct rq *rq;    /* cpu runqueue to which this cfs_rq is attached */
56 
57     /*
58      * leaf cfs_rqs are those that hold tasks (lowest schedulable entity in
59      * a hierarchy). Non-leaf lrqs hold other higher schedulable entities
60      * (like users, containers etc.)
61      *
62      * leaf_cfs_rq_list ties together list of leaf cfs_rq's in a cpu. This
63      * list is used during load balance.
64      */
65     int on_list;
66     struct list_head leaf_cfs_rq_list;
67     struct task_group *tg;    /* group that "owns" this runqueue */
68 
69 #ifdef CONFIG_CFS_BANDWIDTH
70     int runtime_enabled;
71     u64 runtime_expires;
72     s64 runtime_remaining;
73 
74     u64 throttled_clock, throttled_clock_task;
75     u64 throttled_clock_task_time;
76     int throttled, throttle_count;
77     struct list_head throttled_list;
78 #endif /* CONFIG_CFS_BANDWIDTH */
79 #endif /* CONFIG_FAIR_GROUP_SCHED */
80 };

注:

如今開始分情景解析CFS。

rt调度算法总是先选优先级最高的调度实体.

优先级和CFS

CFS 不直接使用优先级而是将其用作允许任务执行的时间的衰减系数。 低优先级任务具有更高的衰减系数,而高优先级任务具有较低的衰减系数。 这意味着与高优先级任务相比,低优先级任务允许任务执行的时间消耗得更快。 这是一个绝妙的解决方案,可以避免维护按优先级调度的运行队列。

struct cfs_rq

由于在某些情况下需要暂时提高进程的优先级,因此不仅需要静态优先级和普通优先级,还需要动态优先级prio;

 

这里先贴出这些概念的struct,图1展示了他们之间的关系.

CFS 组调度

考虑一个两用户示例,用户 A 和用户 B 在一台机器上运行作业。用户 A 只有两个作业正在运行,而用户 B 正在运行 48 个作业。组调度使 CFS 能够对用户 A 和用户 B 进行公平调度,而不是对系统中运行的 50 个作业进行公平调度。每个用户各拥有 50% 的 CPU 使用。用户 B 使用自己 50% 的 CPU 分配运行他的 48 个作业,而不会占用属于用户 A 的另外 50% 的 CPU 分配。

CFS 调度模块(在 kernel/sched_fair.c 中实现)用于以下调度策略:SCHED_NORMAL、SCHED_BATCH 和 SCHED_IDLE。对于 SCHED_RR 和 SCHED_FIFO 策略,将使用实时调度模块(该模块在 kernel/sched_rt.c 中实现)。

CFS 另一个有趣的地方是组调度 概念(在 2.6.24 内核中引入)。组调度是另一种为调度带来公平性的方式,尤其是在处理产生很多其他任务的任务时。 假设一个产生了很多任务的服务器要并行化进入的连接(HTTP服务器的典型架构)。不是所有任务都会被统一公平对待,CFS 引入了组来处理这种行为。产生任务的服务器进程在整个组中(在一个层次结构中)共享它们的虚拟运行时,而单个任务维持其自己独立的虚拟运行时。这样单个任务会收到与组大致相同的调度时间。我们会发现 /proc 接口用于管理进程层次结构,让我们对组的形成方式有完全的控制。使用此配置,我们可以跨用户、跨进程或其变体分配公平性。

cfs_rq就绪队列是以红黑树的形式来组织调度实体。第12行tasks_timeline成员就是红黑树的树根。第13行rb_leftmost指向了红黑树最左边的左孩子(下一个可调度的实体)。第19行curr指向当前正运行的实体,next指向将被唤醒的进程,last指向唤醒next进程的进程,next和last用法后边会提到。第55行rq指向了该cfs_rq就绪队列所属的rq队列。

参考《深入Linux内核架构》p70-76、 p_288-290;

二、创建进程 

struct rq {
    unsigned int nr_running;    //就绪进程的总数目
    struct load_weight load;    //当前队列的总负荷
    struct cfs_rq cfs;          //完全公平队列
    struct rt_rq rt;            //实时队列
    struct task_struct *curr, *idle, *stop; //curr指向当前正在执行的task,
    u64 clock;                  //该队列自身的时钟(实际时间,调度算法还有个虚拟时间的概念)
    ...
};

调度类和域

与 CFS 一起引入的是调度类概念。每个任务都属于一个调度类,这决定了任务将如何调度。 调度类定义一个通用函数集(通过sched_class),函数集定义调度器的行为。例如,每个调度器提供一种方式, 添加要调度的任务、调出要运行的下一个任务、提供给调度器等等。每个调度器类都在一对一连接的列表中彼此相连,使类可以迭代(例如,要启用给定处理器的禁用)。一般结构如下图所示。注意,将任务函数加入队列或脱离队列只需从特定调度结构中加入或移除任务。 函数 pick_next_task 选择要执行的下一个任务(取决于调度类的具体策略)。

sbf282.com 13

调度类视图

但是不要忘了调度类是任务结构本身的一部分,这一点简化了任务的操作,无论其调度类具体如何实现。例如, 以下函数用 ./kernel/sched.c 中的新任务抢占当前运行任务(其中 curr 定义了当前运行任务, rq 代表 CFS 红黑树而 p 是下一个要调度的任务):

static inline void check_preempt( struct rq *rq, struct task_struct *p ){ 

 rq->curr->sched_class->check_preempt_curr( rq, p );

}

如果此任务正使用公平调度类,则 check_preempt_curr() 将解析为check_preempt_wakeup()。 我们可以在 ./kernel/sched_rt.c,/kernel/sched_fair.c 和 ./kernel/sched_idle.c 中查看这些关系。

调度类是调度发生变化的另一个有趣的地方,但是随着调度域的增加,功能也在增加。 这些域允许您出于负载平衡和隔离的目的将一个或多个处理器按层次关系分组。 一个或多个处理器能够共享调度策略(并在其之间保持负载平衡)或实现独立的调度策略从而故意隔离任务。

1.2实时进程的就绪队列struct rt_rq(kernel/sched/sched.h)

         linux内核的优先级继承协议(pip)

第一个情景选为进程创建时CFS相关变量的初始化。
我们知道。Linux创建进程使用fork或者clone或者vfork等系统调用,终于都会到do_fork。

就绪队列,每个cpu对应一个就绪队列,后面为描述方便,假定系统cpu核数为1.  

2.6.24 中的组调度有哪些改变

在 2.6.24 中,我们将能够对调度程序进行调优,从而实现对用户或组的公平性,而不是任务公平性。可以将任务进行分组,形成多个实体,调度程序将平等对待这些实体,继而公平对待实体中的任务。要启用这个特性,在编译内核时需要选择 CONFIG_FAIR_GROUP_SCHED。目前,只有 SCHED_NORMAL 和SCHED_BATCH 任务可以进行分组。

可以使用两个独立的方法对任务进行分组,它们分别基于:

1.用户 ID。

2.cgroup pseudo 文件系统:这个选项使管理员可以根据需要创建组。有关更多细节,阅读内核源文档目录中的 cgroups.txt 文件。

3.内核配置参数 CONFIG_FAIR_USER_SCHED 和 CONFIG_FAIR_CGROUP_SCHED 可帮助您进行选择。

通过引入调度类并通过增强调度统计信息来简化调试,这个新的调度程序进一步扩展了调度功能。

sbf282.com 14sbf282.com 15

         进程优先级逆转问题的解决  

假设没有设置CLONE_STOPPED,则会进入wake_up_new_task函数,我们看看这个函数的关键部分

struct cfs_rq {  //删减版
    struct load_weight load;  //该队列的总权重
    unsigned int nr_running, h_nr_running;  //该队列中的任务数
    u64 min_vruntime;    //一个虚拟时间,后面细说
    struct rb_root tasks_timeline;  //该cfs队列的红黑树,所有的进程用它来组织
    struct rb_node *rb_leftmost;  //指向红黑树最左边的一个节点,也就是下次将被调度器装载的
    struct sched_entity *curr, *next, *last, *skip; //curr指向当前正在执行的进程
    struct rq *rq;          //自己所属的rq
    struct task_group *tg;  //该cfs队列所属的task_group(task_group是实现cgroup的基础,后面再说)
    ...
};

其他调度器

继续研究调度,您将发现正在开发中的调度器将会突破性能和扩展性的界限。Con Kolivas 没有被他的 Linux 经验羁绊,他开发出了另一个 Linux 调度器,其缩写为:BFS。该调度器据说在 NUMA 系统以及移动设备上具有更好的性能, 并且被引入了 Android 操作系统的一款衍生产品中。

 1 /* Real-Time classes' related field in a runqueue: */
 2 struct rt_rq {
 3     struct rt_prio_array active;
 4     unsigned int rt_nr_running;
 5 #if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED
 6     struct {
 7         int curr; /* highest queued rt task prio */
 8 #ifdef CONFIG_SMP
 9         int next; /* next highest */
10 #endif
11     } highest_prio;
12 #endif
13 #ifdef CONFIG_SMP
14     unsigned long rt_nr_migratory;
15     unsigned long rt_nr_total;
16     int overloaded;
17     struct plist_head pushable_tasks;
18 #endif
19     int rt_queued;
20 
21     int rt_throttled;
22     u64 rt_time;
23     u64 rt_runtime;
24     /* Nests inside the rq lock: */
25     raw_spinlock_t rt_runtime_lock;
26 
27 #ifdef CONFIG_RT_GROUP_SCHED
28     unsigned long rt_nr_boosted;
29 
30     struct rq *rq;
31     struct task_group *tg;
32 #endif
33 };

        为了在Linux中使用Priority Inheritance Protocol协议来解决优先级反转问题,Linux中引入实时互斥量rt_mutex,在task_struc结构体中也引入了pi_waiters链表,需要注意的流程为:

[cpp] view plaincopy

cfs就绪队列,用红黑树组织,这里有一个虚拟时间(vruntime)的概念,来保证在保证高优先级进程占用更多cpu的前提下,保证所有进程被公平的调度.

展望

对于 Linux 技术而言,惟一不变的就是永恒的变化。今天,CFS 是 2.6 Linux 调度器; 明天可能就会是另一个新的调度器或一套可以被静态或动态调用的调度器。 CFS、RSDL 以及内核背后的进程中还有很多秘密等待我们去研究。

struct rt_rq

         rt_mutex_slowlock() ----> __rt_mutex_slowlock() ---->

  1. /* 
  2.  * wake_up_new_task - wake up a newly created task for the first time. 
  3.  * 
  4.  * This function will do some initial scheduler statistics housekeeping 
  5.  * that must be done for every newly created context, then puts the task 
  6.  * on the runqueue and wakes it. 
  7.  */  
  8. void wake_up_new_task(struct task_struct *p, unsigned long clone_flags)  
  9. {  
  10.     .....  
  11.     if (!p->sched_class->task_new || !current->se.on_rq) {  
  12.         activate_task(rq, p, 0);  
  13.     } else {  
  14.         /* 
  15.          * Let the scheduling class do new task startup 
  16.          * management (if any): 
  17.          */  
  18.         p->sched_class->task_new(rq, p);  
  19.         inc_nr_running(rq);  
  20.     }  
  21.     check_preempt_curr(rq, p, 0);  
  22.     .....  
  23. }  
struct rt_prio_array {  //实时队列用这个二位链表组织进程
    DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1);  
    struct list_head queue[MAX_RT_PRIO];    // MAX_RT_PRIO=100 上面解释过了
};

struct rt_rq {
    struct rt_prio_array active;    //组织所有就绪进程
    unsigned int rt_nr_running;     //就绪进程数目
    int rt_throttled;               //禁止调度标记
    u64 rt_time;                    //当前队列累计运行时间
    u64 rt_runtime;                 //当前队列最大运行时间
    unsigned long rt_nr_boosted;
    struct rq *rq;                  //所属rq
    struct task_group *tg;          //所属cgroup
    ...
};

2.调度实体(include/linux/sched.h)

                 task_blocks_on_rt_mutex() ---->  __rt_mutex_adjust_prio()

 上面那个if语句我不知道什么情况下会为真。我測试了一下。在上面两个分支各加一个计数器,
推断为真的情况仅仅有2次(我毫无依据的推測是idle进程和init进程),而推断为假的情况有近万次。
因此我们仅仅看以下的分支,假设哪位前辈知道真相的话还望告诉我一声,万分感谢。

 

2.1普通进程的调度实体sched_entity

                                                                   |--> rt_mutex_adjust_prio_chain()

再以下就是检測是否可以形成抢占,假设新进程可以抢占当前进程则进行进程切换。

实时就绪队列,用二维链表组织.
因为实时进程优先级总是高于普通进程,又不使用完全公平算法,极端情况下实时进程一直占着cpu,普通进程等不到cpu资源.
因此实现用rt_time,rt_runtime用来限制实时进程占用cpu资源,例如rt_time = 100 rt_runtime = 95,这两个变量对应cgroup下的cpu.rt_period_us, cpu.rt_runtime_us.
那么该rq下,所有的实时进程只能占用cpu资源的95%,剩下的%5的资源留给普通进程使用.

 1 struct sched_entity {
 2     struct load_weight    load;        /* for load-balancing */
 3     struct rb_node        run_node;
 4     struct list_head    group_node;
 5     unsigned int        on_rq;
 6 
 7     u64            exec_start;
 8     u64            sum_exec_runtime;
 9     u64            vruntime;
10     u64            prev_sum_exec_runtime;
11 
12     u64            nr_migrations;
13 
14 #ifdef CONFIG_SCHEDSTATS
15     struct sched_statistics statistics;
16 #endif
17 
18 #ifdef CONFIG_FAIR_GROUP_SCHED
19     int            depth;
20     struct sched_entity    *parent;
21     /* rq on which this entity is (to be) queued: */
22     struct cfs_rq        *cfs_rq;
23     /* rq "owned" by this entity/group: */
24     struct cfs_rq        *my_q;
25 #endif
26 
27 #ifdef CONFIG_SMP
28     /* Per-entity load-tracking */
29     struct sched_avg    avg;
30 #endif
31 };

          __rt_mutex_adjust_prio调整了当前持有锁的进程的动态优先级(继承自等待队列中所有进程的最高优先级),rt_mutex_adjust_prio_chain()如果被调整的动态优先级的进程也在等待某个资源,那么也要链式地调整相应进程的动态优先级。

我们一个一个函数来看
p->sched_class->task_new相应的函数是task_new_fair:

struct sched_entity {
    struct load_weight load;    //该调度实体的权重(cfs算法的关键 >> cgroup限制cpu的关键)
    struct rb_node run_node;    //树节点,用于在红黑树上组织排序
    u64 exec_start;             //调度器上次更新这个实例的时间(实际时间)
    u64 sum_exec_runtime;       //自进程启动起来,运行的总时间(实际时间)
    u64 vruntime;               //该调度实体运行的虚拟时间
    u64 prev_sum_exec_runtime;  //进程在上次被撤销cpu时,运行的总时间(实际时间)
    struct sched_entity *parent;//父调度实体
    struct cfs_rq *cfs_rq;      //自己所属cfs就绪队列
    struct cfs_rq *my_q;        //子cfs队列,组调度时使用,如果该调度实体代表普通进程,该字段为NULL
    ...
};

每个进程描述符中均包含一个该结构体变量,内核使用该结构体来将普通进程组织到采用完全公平调度策略的就绪队列中(struct rq中的cfs队列中,上边提到过),该结构体有两个作用,一是包含有进程调度的信息(比如进程的运行时间,睡眠时间等等,调度程序参考这些信息决定是否调度进程),二是使用该结构体来组织进程,第3行的struct rb_node类型结构体变量run_node是红黑树节点,因此struct sched_entity调度实体将被组织成红黑树的形式,同时意味着普通进程也被组织成红黑树的形式。第18-25行是和组调度有关的成员,需要开启组调度。第20行parent顾名思义指向了当前实体的上一级实体,后边再介绍。第22行的cfs_rq指向了该调度实体所在的就绪队列。第24行my_q指向了本实体拥有的就绪队列(调度组),该调度组(包括组员实体)属于下一个级别,和本实体不在同一个级别,该调度组中所有成员实体的parent域指向了本实体,这就解释了上边的parent,此外,第19行depth代表了此队列(调度组)的深度,每个调度组都比其parent调度组深度大1。内核依赖my_q域实现组调度。

关于Priority Inversion可以参考《Operating System Concepts》9_ed p217-218                                                                                                                       

[cpp] view plaincopy

普通进程使用完全公平算法来保证队列中的进程都可得到公平的调度机会,同时兼顾高优先级的进程占用更多的cpu资源.

2.2实时进程的调度实体 sched_rt_entity

本文由胜博发-操作发布,转载请注明来源:这个数取决于运行队列当前的进程数sbf282.com:,