diff --git a/Makefile b/Makefile index 3748524..d9c6044 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ all:github -COMMIT="print_task_struct中增加了对进程优先级/调度器类的实验示例..." +COMMIT="更新了CFS调度器类的学习实例和博客信息..." .PHONY : github github: diff --git a/study/kernel/01-process/05-schedule/07-cfs/01-cfs/README.md b/study/kernel/01-process/05-schedule/07-cfs/01-cfs/README.md index 8ab7abb..b0659e6 100644 --- a/study/kernel/01-process/05-schedule/07-cfs/01-cfs/README.md +++ b/study/kernel/01-process/05-schedule/07-cfs/01-cfs/README.md @@ -1,317 +1,311 @@ -Linux进程调度CFS调度器 -======= - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | - - -#前景回顾 -------- - - -##进程调度 -------- - -linux把进程区分为实时进程和非实时进程, 其中非实时进程进一步划分为交互式进程和批处理进程 - - - -根据进程的不同分类Linux采用不同的调度策略. - -对于实时进程,采用FIFO或者Round Robin的调度策略. - -对于普通进程,则采用CFS完全功能公平调度器 - -##linux调度器的演变 -------- - -一开始的调度器是复杂度为**$O(n)$的始调度算法**(实际上每次会遍历所有任务,所以复杂度为O(n)), 这个算法的缺点是当内核中有很多任务时,调度器本身就会耗费不少时间,所以,从linux2.5开始引入赫赫有名的**$O(1)$调度器** - - - -| 字段 | 版本 | -| ------------- |:-------------:| -| O(n)的始调度算法 | linux-0.11~2.4 | -| O(1)调度器 | linux-2.5 | -| CFS调度器 | linux-2.6~至今 | - - -#Linux的调度器组成 -------- - - -##2个调度器 -------- - -可以用两种方法来激活调度 - -* 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU - -* 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 - -因此当前linux的调度程序由两个调度器组成:**主调度器**,**周期性调度器**(两者又统称为**通用调度器(generic scheduler)**或**核心调度器(core scheduler)**) - -并且每个调度器包括两个内容:**调度框架**(其实质就是两个函数框架)及**调度器类** - - - -##6种调度策略 -------- - - -linux内核目前实现了6中调度策略(即调度算法), 用于对不同类型的进程进行调度, 或者支持某些特殊的功能 - -比如SCHED_NORMAL和SCHED_BATCH调度普通的非实时进程, SCHED_FIFO和SCHED_RR和SCHED_DEADLINE则采用不同的调度策略调度实时进程, SCHED_IDLE则在系统空闲时调用idle进程. - - - - -##5个调度器类 -------- - -而依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程. - - - -其所属进程的优先级顺序为 -````c -stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class -``` - -##3个调度实体 -------- - -调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度: 可用的CPUI时间首先在一半的进程组(比如, 所有进程按照所有者分组)之间分配, 接下来分配的时间再在组内进行二次分配. - -这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组. - -linux中针对当前可调度的实时和非实时进程, 定义了类型为seched_entity的3个调度实体 - - -* sched_dl_entity 采用EDF算法调度的DEADLINE实时调度实体 - -* sched_rt_entity 采用Roound-Robin或者FIFO算法调度的实时调度实体 - -* sched_entity 采用CFS算法调度的普通非实时进程的调度实体 - - -每个进程都属于某个调度器类(由字段task_struct->sched_class标识), 由调度器类采用进程对应的调度策略调度(由task_struct->policy )进行调度, task_struct也存储了其对应的调度实体标识 - -linux实现了6种调度策略, 依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程. - - -调度器类, 调度策略, 调度实体他们之间的关系如下表 - -| 调度器类 | 调度策略 | 调度策略对应的调度算法 | 调度实体 | 调度实体对应的调度对象 | -| ------- |:-------:|:-------:|:-------:||:-------:| -| stop_sched_class | 无 | 无 | 无 | 特殊情况, 发生在cpu_stop_cpu_callback 进行cpu之间任务迁移migration或者HOTPLUG_CPU的情况下关闭任务 | -| dl_sched_class | SCHED_DEADLINE | Earliest-Deadline-First最早截至时间有限算法 | sched_dl_entity | 采用DEF最早截至时间有限算法调度实时进程 | -| rt_sched_class | SCHED_RR

SCHED_FIFO | Roound-Robin时间片轮转算法

FIFO先进先出算法 | sched_rt_entity | 采用Roound-Robin或者FIFO算法调度的实时调度实体 | -| fair_sched_class | SCHED_NORMAL

SCHED_BATCH | CFS完全公平懂调度算法 |sched_entity | 采用CFS算法普通非实时进程 | -| idle_sched_class | SCHED_IDLE | 无 | 无 | 特殊进程, 用于cpu空闲时调度空闲进程idle | - -#cfs完全公平调度器 -------- - -##CFS调度器类fair_sched_class -------- - -CFS完全公平调度器的调度器类叫fair_sched_class, 其定义在[kernel/sched/fair.c, line 8521](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L8521), 它是我们熟知的是struct sched_class调度器类类型, 将我们的CFS调度器与一些特定的函数关联起来 - -```c -/* - * All the scheduling class methods: - */ -const struct sched_class fair_sched_class = { - .next = &idle_sched_class, /* 下个优先级的调度类, 所有的调度类通过next链接在一个链表中*/ - .enqueue_task = enqueue_task_fair, - .dequeue_task = dequeue_task_fair, - .yield_task = yield_task_fair, - .yield_to_task = yield_to_task_fair, - - .check_preempt_curr = check_preempt_wakeup, - - .pick_next_task = pick_next_task_fair, - .put_prev_task = put_prev_task_fair, - -#ifdef CONFIG_SMP - .select_task_rq = select_task_rq_fair, - .migrate_task_rq = migrate_task_rq_fair, - - .rq_online = rq_online_fair, - .rq_offline = rq_offline_fair, - - .task_waking = task_waking_fair, - .task_dead = task_dead_fair, - .set_cpus_allowed = set_cpus_allowed_common, -#endif - - .set_curr_task = set_curr_task_fair, - .task_tick = task_tick_fair, - .task_fork = task_fork_fair, - - .prio_changed = prio_changed_fair, - .switched_from = switched_from_fair, - .switched_to = switched_to_fair, - - .get_rr_interval = get_rr_interval_fair, - - .update_curr = update_curr_fair, - -#ifdef CONFIG_FAIR_GROUP_SCHED - .task_move_group = task_move_group_fair, -#endif -}; -``` - -| 成员 | 描述 | -| ------------- |:-------------:| -| enqueue_task | 向就绪队列中添加一个进程, 某个任务进入可运行状态时,该函数将得到调用。它将调度实体(进程)放入红黑树中,并对 nr_running 变量加 1 | -| dequeue_task | 将一个进程从就就绪队列中删除, 当某个任务退出可运行状态时调用该函数,它将从红黑树中去掉对应的调度实体,并从 nr_running 变量中减 1 | -| yield_task | 在进程想要资源放弃对处理器的控制权的时, 可使用在sched_yield系统调用, 会调用内核API yield_task完成此工作. compat_yield sysctl 关闭的情况下,该函数实际上执行先出队后入队;在这种情况下,它将调度实体放在红黑树的最右端 | -| check_preempt_curr | 该函数将检查当前运行的任务是否被抢占。在实际抢占正在运行的任务之前,CFS 调度程序模块将执行公平性测试。这将驱动唤醒式(wakeup)抢占 | -| pick_next_task | 该函数选择接下来要运行的最合适的进程 | -| put_prev_task | 用另一个进程代替当前运行的进程 | -| set_curr_task | 当任务修改其调度类或修改其任务组时,将调用这个函数 | -| task_tick | 在每次激活周期调度器时, 由周期性调度器调用, 该函数通常调用自 time tick 函数;它可能引起进程切换。这将驱动运行时(running)抢占 | -| task_new | 内核调度程序为调度模块提供了管理新任务启动的机会, 用于建立fork系统调用和调度器之间的关联, 每次新进程建立后, 则用new_task通知调度器, CFS 调度模块使用它进行组调度,而用于实时任务的调度模块则不会使用这个函数 | - - -##cfs的就绪队列 -------- - -就绪队列是全局调度器许多操作的起点, 但是进程并不是由就绪队列直接管理的, 调度管理是各个调度器的职责, 因此在各个就绪队列中嵌入了特定调度类的子就绪队列(cfs的顶级调度就队列 [struct cfs_rq](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L359), 实时调度类的就绪队列[struct rt_rq](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L449)和deadline调度类的就绪队列[struct dl_rq](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L490) - -```c -/* CFS-related fields in a runqueue */ -/* 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 - */ - struct sched_avg avg; - u64 runnable_load_sum; - unsigned long runnable_load_avg; -#ifdef CONFIG_FAIR_GROUP_SCHED - unsigned long tg_load_avg_contrib; -#endif - atomic_long_t removed_load_avg, removed_util_avg; -#ifndef CONFIG_64BIT - u64 load_last_update_time_copy; -#endif - -#ifdef CONFIG_FAIR_GROUP_SCHED - /* - * 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 */ -}; -``` - - -| 成员 | 描述 | -| ------------- |:-------------:| -| nr_running | 队列上可运行进程的数目 -| load | 就绪队列上可运行进程的累计负荷权重 | -| min_vruntime | 跟踪记录队列上所有进程的最小虚拟运行时间. 这个值是实现与就绪队列相关的虚拟时钟的基础 | -| tasks_timeline | 用于在按时间排序的红黑树中管理所有进程 | -| rb_leftmost | 总是设置为指向红黑树最左边的节点, 即需要被调度的进程. 该值其实可以可以通过病例红黑树获得, 但是将这个值存储下来可以减少搜索红黑树花费的平均时间 | -| curr | 当前正在运行的sched_entity(对于组虽然它不会在cpu上运行,但是当它的下层有一个task在cpu上运行,那么它所在的cfs_rq就把它当做是该cfs_rq上当前正在运行的sched_entity | -| next | 表示有些进程急需运行,即使不遵从CFS调度也必须运行它,调度时会检查是否next需要调度,有就调度next | -| skip | 略过进程(不会选择skip指定的进程调度) | - - -[Linux进程组调度机制分析](http://www.oenhan.com/task-group-sched) - -[ Linux内核学习笔记(一)CFS完全公平调度类 ](http://blog.chinaunix.net/uid-24757773-id-3266304.html) - -[CFS 调度器学习笔记](http://blog.csdn.net/melong100/article/details/6329201) - - -[linux调度器(五)——进程管理与CFS](http://blog.csdn.net/wudongxu/article/details/8574737) - -[CFS进程调度](http://blog.csdn.net/arriod/article/details/7033895) - - -http://www.360doc.com/content/15/1006/18/18252487_503643269.shtml - -http://linuxperf.com/?p=42 +Linux进程调度CFS调度器 +======= + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | + + +1 前景回顾 +------- + + +##1.1 进程调度 +------- + + +内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来. + + +**调度器**面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为两个不同的部分, 其中一个涉及**调度策略**, 另外一个涉及**上下文切换**. + + + + +##1.2 进程的分类 +------- + +linux把进程区分为**实时进程**和**非实时进程**, 其中非实时进程进一步划分为交互式进程和批处理进程 + +根据进程的不同分类Linux采用不同的调度策略. + +对于实时进程,采用FIFO, Round Robin或者Earliest Deadline First (EDF)最早截止期限优先调度算法|的调度策略. + + + +##1.3 linux调度器的演变 +------- + + +| 字段 | 版本 | +| ------------- |:-------------:| +| O(n)的始调度算法 | linux-0.11~2.4 | +| O(1)调度器 | linux-2.5 | +| CFS调度器 | linux-2.6~至今 | + + +##1.4 Linux的调度器组成 +------- + + +**2个调度器** + +可以用两种方法来激活调度 + +* 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU + +* 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 + +因此当前linux的调度程序由两个调度器组成:**主调度器**,**周期性调度器**(两者又统称为**通用调度器(generic scheduler)**或**核心调度器(core scheduler)**) + +并且每个调度器包括两个内容:**调度框架**(其实质就是两个函数框架)及**调度器类** + + + +**6种调度策略** + +linux内核目前实现了6中调度策略(即调度算法), 用于对不同类型的进程进行调度, 或者支持某些特殊的功能 + +* SCHED_NORMAL和SCHED_BATCH调度普通的非实时进程 + +* SCHED_FIFO和SCHED_RR和SCHED_DEADLINE则采用不同的调度策略调度实时进程 + +* SCHED_IDLE则在系统空闲时调用idle进程. + + + +**5个调度器类** + +而依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程. + + +其所属进程的优先级顺序为 + +```c +stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class +``` + +**3个调度实体** + +调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度. + +这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组. + +linux中针对当前可调度的实时和非实时进程, 定义了类型为seched_entity的3个调度实体 + +* sched_dl_entity 采用EDF算法调度的实时调度实体 + +* sched_rt_entity 采用Roound-Robin或者FIFO算法调度的实时调度实体 rt_sched_class + +* sched_entity 采用CFS算法调度的普通非实时进程的调度实体 + + + + +#2 cfs完全公平调度器 +------- + +##2.1 CFS调度器类fair_sched_class +------- + +CFS完全公平调度器的调度器类叫fair_sched_class, 其定义在[kernel/sched/fair.c, line 8521](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L8521), 它是我们熟知的是struct sched_class调度器类类型, 将我们的CFS调度器与一些特定的函数关联起来 + +```c +/* + * All the scheduling class methods: + */ +const struct sched_class fair_sched_class = { + .next = &idle_sched_class, /* 下个优先级的调度类, 所有的调度类通过next链接在一个链表中*/ + .enqueue_task = enqueue_task_fair, + .dequeue_task = dequeue_task_fair, + .yield_task = yield_task_fair, + .yield_to_task = yield_to_task_fair, + + .check_preempt_curr = check_preempt_wakeup, + + .pick_next_task = pick_next_task_fair, + .put_prev_task = put_prev_task_fair, + +#ifdef CONFIG_SMP + .select_task_rq = select_task_rq_fair, + .migrate_task_rq = migrate_task_rq_fair, + + .rq_online = rq_online_fair, + .rq_offline = rq_offline_fair, + + .task_waking = task_waking_fair, + .task_dead = task_dead_fair, + .set_cpus_allowed = set_cpus_allowed_common, +#endif + + .set_curr_task = set_curr_task_fair, + .task_tick = task_tick_fair, + .task_fork = task_fork_fair, + + .prio_changed = prio_changed_fair, + .switched_from = switched_from_fair, + .switched_to = switched_to_fair, + + .get_rr_interval = get_rr_interval_fair, + + .update_curr = update_curr_fair, + +#ifdef CONFIG_FAIR_GROUP_SCHED + .task_move_group = task_move_group_fair, +#endif +}; +``` + +| 成员 | 描述 | +| ------------- |:-------------:| +| enqueue_task | 向就绪队列中添加一个进程, 某个任务进入可运行状态时,该函数将得到调用。它将调度实体(进程)放入红黑树中,并对 nr_running 变量加 1 | +| dequeue_task | 将一个进程从就就绪队列中删除, 当某个任务退出可运行状态时调用该函数,它将从红黑树中去掉对应的调度实体,并从 nr_running 变量中减 1 | +| yield_task | 在进程想要资源放弃对处理器的控制权的时, 可使用在sched_yield系统调用, 会调用内核API yield_task完成此工作. compat_yield sysctl 关闭的情况下,该函数实际上执行先出队后入队;在这种情况下,它将调度实体放在红黑树的最右端 | +| check_preempt_curr | 该函数将检查当前运行的任务是否被抢占。在实际抢占正在运行的任务之前,CFS 调度程序模块将执行公平性测试。这将驱动唤醒式(wakeup)抢占 | +| pick_next_task | 该函数选择接下来要运行的最合适的进程 | +| put_prev_task | 用另一个进程代替当前运行的进程 | +| set_curr_task | 当任务修改其调度类或修改其任务组时,将调用这个函数 | +| task_tick | 在每次激活周期调度器时, 由周期性调度器调用, 该函数通常调用自 time tick 函数;它可能引起进程切换。这将驱动运行时(running)抢占 | +| task_new | 内核调度程序为调度模块提供了管理新任务启动的机会, 用于建立fork系统调用和调度器之间的关联, 每次新进程建立后, 则用new_task通知调度器, CFS 调度模块使用它进行组调度,而用于实时任务的调度模块则不会使用这个函数 | + + +##2.2 cfs的就绪队列 +------- + +就绪队列是全局调度器许多操作的起点, 但是进程并不是由就绪队列直接管理的, 调度管理是各个调度器的职责, 因此在各个就绪队列中嵌入了特定调度类的子就绪队列(cfs的顶级调度就队列 [struct cfs_rq](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L359), 实时调度类的就绪队列[struct rt_rq](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L449)和deadline调度类的就绪队列[struct dl_rq](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L490) + +```c +/* CFS-related fields in a runqueue */ +/* 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 + */ + struct sched_avg avg; + u64 runnable_load_sum; + unsigned long runnable_load_avg; +#ifdef CONFIG_FAIR_GROUP_SCHED + unsigned long tg_load_avg_contrib; +#endif + atomic_long_t removed_load_avg, removed_util_avg; +#ifndef CONFIG_64BIT + u64 load_last_update_time_copy; +#endif + +#ifdef CONFIG_FAIR_GROUP_SCHED + /* + * 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 */ +}; +``` + + +| 成员 | 描述 | +| ------------- |:-------------:| +| nr_running | 队列上可运行进程的数目 +| load | 就绪队列上可运行进程的累计负荷权重 | +| min_vruntime | 跟踪记录队列上所有进程的最小虚拟运行时间. 这个值是实现与就绪队列相关的虚拟时钟的基础 | +| tasks_timeline | 用于在按时间排序的红黑树中管理所有进程 | +| rb_leftmost | 总是设置为指向红黑树最左边的节点, 即需要被调度的进程. 该值其实可以可以通过病例红黑树获得, 但是将这个值存储下来可以减少搜索红黑树花费的平均时间 | +| curr | 当前正在运行的sched_entity(对于组虽然它不会在cpu上运行,但是当它的下层有一个task在cpu上运行,那么它所在的cfs_rq就把它当做是该cfs_rq上当前正在运行的sched_entity | +| next | 表示有些进程急需运行,即使不遵从CFS调度也必须运行它,调度时会检查是否next需要调度,有就调度next | +| skip | 略过进程(不会选择skip指定的进程调度) | + + +#3 参考 +------- + +[Linux进程组调度机制分析](http://www.oenhan.com/task-group-sched) + +[ Linux内核学习笔记(一)CFS完全公平调度类 ](http://blog.chinaunix.net/uid-24757773-id-3266304.html) + +[CFS 调度器学习笔记](http://blog.csdn.net/melong100/article/details/6329201) + + +[linux调度器(五)——进程管理与CFS](http://blog.csdn.net/wudongxu/article/details/8574737) + +[CFS进程调度](http://blog.csdn.net/arriod/article/details/7033895) + + +[理解CFS组调度](http://www.360doc.com/content/15/1006/18/18252487_503643269.shtml) + +[从几个问题开始理解CFS调度器](http://linuxperf.com/?p=42) diff --git a/study/kernel/01-process/05-schedule/07-cfs/02-load_weight/README.md b/study/kernel/01-process/05-schedule/07-cfs/02-load_weight/README.md index 17d7997..bc9b540 100644 --- a/study/kernel/01-process/05-schedule/07-cfs/02-load_weight/README.md +++ b/study/kernel/01-process/05-schedule/07-cfs/02-load_weight/README.md @@ -1,615 +1,583 @@ -Linux CFS调度器之负荷权重load_weight -======= - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | - - - -前面我们详细的了解了linux下进程优先级的表示以及其计算的方法, 我们了解到linux针对普通进程和实时进程分别使用静态优先级static_prio和实时优先级rt_priority来指定其默认的优先级别, 然后通过normal_prio函数将他们分别转换为普通优先级normal_prio, 最终换算出动态优先级prio, 动态优先级prio才是内核调度时候有限考虑的优先级字段 - -但是CFS完全公平调度器在调度进程的时候, 进程的重要性不仅是由优先级指定的, 而且还需要考虑保存在task_struct->se.load的负荷权重. - - - -#前景回顾 -------- - - -##进程调度 -------- - -内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来. - -**调度器**面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为两个不同的部分, 其中一个涉及**调度策略**, 另外一个涉及**上下文切换**. - - -内核必须提供一种方法, 在各个进程之间尽可能公平地共享CPU时间, 而同时又要考虑不同的任务优先级. - -调度器的一般原理是, 按所需分配的计算能力, 向系统中每个进程提供最大的公正性, 或者从另外一个角度上说, 他试图确保没有进程被亏待. - - -##进程的分类 -------- - -linux把进程区分为实时进程和非实时进程, 其中非实时进程进一步划分为交互式进程和批处理进程 - -| 类型 | 描述 | 示例 | -| ------- |:-------:|:-------:|:-------:| -| 交互式进程(interactive process) | 此类进程经常与用户进行交互, 因此需要花费很多时间等待键盘和鼠标操作. 当接受了用户的输入后, 进程必须很快被唤醒, 否则用户会感觉系统反应迟钝 | shell, 文本编辑程序和图形应用程序 | -| 批处理进程(batch process) | 此类进程不必与用户交互, 因此经常在后台运行. 因为这样的进程不必很快相应, 因此常受到调度程序的怠慢 | 程序语言的编译程序, 数据库搜索引擎以及科学计算 | -| 实时进程(real-time process) | 这些进程由很强的调度需要, 这样的进程绝不会被低优先级的进程阻塞. 并且他们的响应时间要尽可能的短 | 视频音频应用程序, 机器人控制程序以及从物理传感器上收集数据的程序| - - -##不同进程采用不同的调度策略 -------- - -根据进程的不同分类Linux采用不同的调度策略. - -对于实时进程,采用FIFO, Round Robin或者Earliest Deadline First (EDF)最早截止期限优先调度算法|的调度策略. - -但是普通进程的调度策略就比较麻烦了, 因为普通进程不能简单的只看优先级, 必须公平的占有CPU, 否则很容易出现进程饥饿, 这种情况下用户会感觉操作系统很卡, 响应总是很慢,因此在linux调度器的发展历程中经过了多次重大变动, linux总是希望寻找一个最接近于完美的调度策略来公平快速的调度进程. - - -##linux调度器的演变 -------- - - -一开始的调度器是复杂度为**$O(n)$的始调度算法**(实际上每次会遍历所有任务,所以复杂度为O(n)), 这个算法的缺点是当内核中有很多任务时,调度器本身就会耗费不少时间,所以,从linux2.5开始引入赫赫有名的**$O(1)$调度器** - -然而,linux是集全球很多程序员的聪明才智而发展起来的超级内核,没有最好,只有更好,在$O(1)$调度器风光了没几天就又被另一个更优秀的调度器取代了,它就是**CFS调度器Completely Fair Scheduler**. 这个也是在2.6内核中引入的,具体为2.6.23,即从此版本开始,内核使用CFS作为它的默认调度器,$O(1)$调度器被抛弃了, 其实CFS的发展也是经历了很多阶段,最早期的楼梯算法(SD), 后来逐步对SD算法进行改进出RSDL(Rotating Staircase Deadline Scheduler), 这个算法已经是"完全公平"的雏形了, 直至CFS是最终被内核采纳的调度器, 它从RSDL/SD中吸取了完全公平的思想,不再跟踪进程的睡眠时间,也不再企图区分交互式进程。它将所有的进程都统一对待,这就是公平的含义。CFS的算法和实现都相当简单,众多的测试表明其性能也非常优越 - - - -| 字段 | 版本 | -| ------------- |:-------------:|:-------------:| -| O(n)的始调度算法 | linux-0.11~2.4 | -| O(1)调度器 | linux-2.5 | -| CFS调度器 | linux-2.6~至今 | - - -##Linux的调度器组成 -------- - - -**2个调度器** - -可以用两种方法来激活调度 - -* 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU - -* 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 - -因此当前linux的调度程序由两个调度器组成:**主调度器**,**周期性调度器**(两者又统称为**通用调度器(generic scheduler)**或**核心调度器(core scheduler)**) - -并且每个调度器包括两个内容:**调度框架**(其实质就是两个函数框架)及**调度器类** - - - -**6种调度策略** - -linux内核目前实现了6中调度策略(即调度算法), 用于对不同类型的进程进行调度, 或者支持某些特殊的功能 - -* SCHED_NORMAL和SCHED_BATCH调度普通的非实时进程 - -* SCHED_FIFO和SCHED_RR和SCHED_DEADLINE则采用不同的调度策略调度实时进程 - -* SCHED_IDLE则在系统空闲时调用idle进程. - - - -**5个调度器类** - -而依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程. - - -其所属进程的优先级顺序为 -````c -stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class -``` - -**3个调度实体** - -调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度. - -这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组. - -linux中针对当前可调度的实时和非实时进程, 定义了类型为seched_entity的3个调度实体 - -* sched_dl_entity 采用EDF算法调度的实时调度实体 - -* sched_rt_entity 采用Roound-Robin或者FIFO算法调度的实时调度实体 - -* sched_entity 采用CFS算法调度的普通非实时进程的调度实体 - - -**调度器整体框架** - -每个进程都属于某个调度器类(由字段task_struct->sched_class标识), 由调度器类采用进程对应的调度策略调度(由task_struct->policy )进行调度, task_struct也存储了其对应的调度实体标识 - -linux实现了6种调度策略, 依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程. - - -| 调度器类 | 调度策略 | 调度策略对应的调度算法 | 调度实体 | 调度实体对应的调度对象 | -| ------- |:-------:|:-------:|:-------:||:-------:| -| stop_sched_class | 无 | 无 | 无 | 特殊情况, 发生在cpu_stop_cpu_callback 进行cpu之间任务迁移migration或者HOTPLUG_CPU的情况下关闭任务 | -| dl_sched_class | SCHED_DEADLINE | Earliest-Deadline-First最早截至时间有限算法 | sched_dl_entity | 采用DEF最早截至时间有限算法调度实时进程 | -| rt_sched_class | SCHED_RR

SCHED_FIFO | Roound-Robin时间片轮转算法

FIFO先进先出算法 | sched_rt_entity | 采用Roound-Robin或者FIFO算法调度的实时调度实体 | -| fair_sched_class | SCHED_NORMAL

SCHED_BATCH | CFS完全公平懂调度算法 |sched_entity | 采用CFS算法普通非实时进程 | -| idle_sched_class | SCHED_IDLE | 无 | 无 |特殊进程, 用于cpu空闲时调度空闲进程idle | - -它们的关系如下图 - -![调度器的组成](../images/level.jpg) - - -##优先级的内核表示 -------- - - -内核使用一些简单的数值范围0~139表示内部优先级, 数值越低, 优先级越高。 - -从0~99的范围专供实时进程使用, nice的值[-20,19]则映射到范围100~139 - - ->实时优先级范围是0到MAX_RT_PRIO-1(即99),而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX_PRIO-1(即100到139)。 - -| 优先级范围 | 描述 | -| ------------- |:-------------:| -| 0——99 | 实时进程 | -| 100——139 | 非实时进程 | - -![内核优先级标度](../images/priority.jpg) - -##进程的优先级表示 -------- - - -```c -struct task_struct -{ - /* 进程优先级 - * 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; -} -``` - -**动态优先级 静态优先级 实时优先级** - - -其中task_struct采用了三个成员表示进程的优先级:prio和normal_prio表示动态优先级, static_prio表示进程的静态优先级. - -此外还用了一个字段rt_priority保存了实时进程的优先级 - - -| 字段 | 描述 | -| ------------- |:-------------:| -| static_prio | 用于保存静态优先级, 是进程启动时分配的优先级, ,可以通过nice和sched_setscheduler系统调用来进行修改, 否则在进程运行期间会一直保持恒定 | -| prio | 保存进程的动态优先级 | -| normal_prio | 表示基于进程的静态优先级static_prio和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级 | -| rt_priority | 用于保存实时优先级 | - - -实时进程的优先级用实时优先级rt_priority来表示 - - - -#负荷权重 -------- - - -##负荷权重结构struct load_weight -------- - -负荷权重用struct load_weight数据结构来表示, 保存着进程权重值weight。其定义在[/include/linux/sched.h, v=4.6, L1195](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1195), 如下所示 - -```c -struct load_weight { - unsigned long weight; /* 存储了权重的信息 */ - u32 inv_weight; /* 存储了权重值用于重除的结果 weight * inv_weight = 2^32 */ -}; -``` - - - -##调度实体的负荷权重load -------- - -既然struct load_weight保存着进程的权重信息, 那么作为进程调度的实体, 必须将这个权重值与特定的进程task_struct, 更一般的与通用的调度实体sched_entity相关联 - -struct sched_entity作为进程调度的实体信息, 其内置了load_weight结构用于保存当前调度实体的权重, 参照http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1195 - -```c -struct sched_entity { - struct load_weight load; /* for load-balancing */ - /* ...... */ -}; -``` - -##进程的负荷权重 - -而进程可以被作为一个调度的实时, 其内部通过存储struct sched_entity se而间接存储了其load_weight信息, 参照http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1415 - -```c -struct task_struct -{ - /* ...... */ - struct sched_entity se; - /* ...... */ -} -``` - -因此我们就可以通过task_statuct->se.load获取负荷权重的信息, 而set_load_weight负责根据进程类型及其静态优先级计算符合权重. - - -#优先级和权重的转换 -------- - - -##优先级->权重转换表 -------- - - -一般这个概念是这样的, 进程每降低一个nice值(优先级提升), 则多获得10%的CPU时间, 没升高一个nice值(优先级降低), 则放弃10%的CPU时间. - -为执行该策略, 内核需要将优先级转换为权重值, 并提供了一张优先级->权重转换表sched_prio_to_weight, 内核不仅维护了负荷权重自身, 还保存另外一个数值, 用于负荷重除的结果, 即sched_prio_to_wmult数组, 这两个数组中的数据是一一对应的. - - -其中相关的数据结构定义在[kernel/sched/sched.h?v=4.6, L1132](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1132) - -```c -// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1132 -/* - * To aid in avoiding the subversion of "niceness" due to uneven distribution - * of tasks with abnormal "nice" values across CPUs the contribution that - * each task makes to its run queue's load is weighted according to its - * scheduling class and "nice" value. For SCHED_NORMAL tasks this is just a - * scaled version of the new time slice allocation that they receive on time - * slice expiry etc. - */ - - -#define WEIGHT_IDLEPRIO 3 /* SCHED_IDLE进程的负荷权重 */ -#define WMULT_IDLEPRIO 1431655765 /* SCHED_IDLE进程负荷权重的重除值 */ - - -extern const int sched_prio_to_weight[40]; -extern const u32 sched_prio_to_wmult[40]; - - -// http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L8484 -/* -* Nice levels are multiplicative, with a gentle 10% change for every -* nice level changed. I.e. when a CPU-bound task goes from nice 0 to -* nice 1, it will get ~10% less CPU time than another CPU-bound task -* that remained on nice 0. -* -* The "10% effect" is relative and cumulative: from _any_ nice level, -* if you go up 1 level, it's -10% CPU usage, if you go down 1 level -* it's +10% CPU usage. (to achieve that we use a multiplier of 1.25. -* If a task goes up by ~10% and another task goes down by ~10% then -* the relative distance between them is ~25%.) -*/ -const int sched_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, -}; - - -/* -* Inverse (2^32/x) values of the sched_prio_to_weight[] array, precalculated. -* -* In cases where the weight does not change often, we can use the -* precalculated inverse to speed up arithmetics by turning divisions -* into multiplications: -*/ -const u32 sched_prio_to_wmult[40] = { -/* -20 */ 48388, 59856, 76040, 92818, 118348, -/* -15 */ 147320, 184698, 229616, 287308, 360437, -/* -10 */ 449829, 563644, 704093, 875809, 1099582, -/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326, -/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587, -/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126, -/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717, -/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153, -}; -``` - -对内核使用的范围[-20, 19]中的每个nice级别, sched_prio_to_weight数组都有一个对应项 - -nice [-20, 19] -=> 下标 [0, 39] - -而由于权重`weight` 用`unsigned long` 表示, 因此内核无法直接存储1/weight, 而必须借助于乘法和位移来执行除法的技术. sched_prio_to_wmult数组就存储了这些值, 即sched_prio_to_wmult每个元素的值是2^32/prio_to_weight$每个元素的值. - -可以验证 - -$$sched\_prio\_to\_wmult[i] = \frac{2^{32}}{sched\_prio\_to\_weight[i]}$$ - -同时我们可以看到其定义了两个宏WEIGHT_IDLEPRIO和WMULT_IDLEPRIO这两个宏对应的就是SCHED_IDLE调度的进程的负荷权重信息, 因为要保证SCHED_IDLE进程的最低优先级和最低的负荷权重. 这点信息我们可以在后面分析set_load_weight函数的时候可以看到 - -可以验证 - -$$\frac{2^{32}}{WEIGHT_IDLEPRIO} = WMULT_IDLEPRIO$$ - - -##linux-4.4之前的shced_prio_to_weight和sched_prio_to_wmult -------- - -关于优先级->权重转换表sched_prio_to_weight - -在linux-4.4之前的内核中, 优先级权重转换表用prio_to_weight表示, 定义在[kernel/sched/sched.h, line 1116](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.4#L1116), 与它一同定义的还有prio_to_wmult, 在[kernel/sched/sched.h, line 1139](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.4#L1139) -均被定义为static const - -但是其实这种能够方式不太符合规范的编码风格, 因此常规来说, 我们的头文件中不应该存储结构的定义, 即为了是程序的模块结构更加清晰, 头文件中尽量只包含宏或者声明, 而将具体的定义, 需要分配存储空间的代码放在源文件中. - -否则如果在头文件中定义全局变量,并且将此全局变量赋初值,那么在多个引用此头文件的C文件中同样存在相同变量名的拷贝,关键是此变量被赋了初值,所以编译器就会将此变量放入DATA段,最终在连接阶段,会在DATA段中存在多个相同的变量,它无法将这些变量统一成一个变量,也就是仅为此变量分配一个空间,而不是多份空间,假定这个变量在头文件没有赋初值,编译器就会将之放入BSS段,连接器会对BSS段的多个同名变量仅分配一个存储空间 - -因此在新的内核中, 内核黑客们将这两个变量存放在了[kernel/sched/core.c](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L8472), 并加上了sched_前缀, 以表明这些变量是在进程调度的过程中使用的, 而在[kernel/sched/sched.h, line 1144](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1144)中则只包含了他们的声明. - - -下面我们列出优先级权重转换表定义更新后对比项 - - -| 内核版本 | 实现 | 地址 | -| ------------- |:-------------:|:-------------:| -| <= linux-4.4 | static const int prio_to_weight[40] | [kernel/sched/sched.h, line 1116](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.4#L1116) | -| >=linux-4.5 | const int sched_prio_to_weight[40] | 声明在[kernel/sched/sched.h, line 1144](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1144), 定义在[kernel/sched/core.c](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L8472) - -其定义并没有发生变化, 依然是一个一对一NICE to WEIGHT的转换表 - - -##1.25的乘积因子 -------- - - -各数组之间的乘积因子是1.25. 要知道为何使用该因子, 可考虑下面的例子 - -两个进程A和B在nice级别0, 即静态优先级120运行, 因此两个进程的CPU份额相同, 都是50%, nice级别为0的进程, 查其权重表可知是1024. 每个进程的份额是1024/(1024+1024)=0.5, 即50% - -如果进程B的优先级+1(优先级降低), 成为nice=1, 那么其CPU份额应该减少10%, 换句话说进程A得到的总的CPU应该是55%, 而进程B应该是45%. 优先级增加1导致权重减少, 即1024/1.25=820, 而进程A仍旧是1024, 则进程A现在将得到的CPU份额是1024/(1024+820=0.55, 而进程B的CPU份额则是820/(1024+820)=0.45. 这样就正好产生了10%的差值. - - -#进程负荷权重的计算 -------- - - -set_load_weight负责根据非实时进程类型极其静态优先级计算符合权重 -而实时进程不需要CFS调度, 因此无需计算其负荷权重值 - - ->早期的代码中实时进程也是计算其负荷权重的, 但是只是采用一些方法保持其权重值较大 -> ->在早期有些版本中, set_load_weight中实时进程的权重是普通进程的两倍, 后来又设置成0, 直到后来linux-2.6.37开始不再设置实时进程的优先级, 因此这本身就是一个无用的工作 -> ->而另一方面, SCHED_IDLE进程的权值总是非常小, 普通非实时进程则根据其静态优先级设置对应的负荷权重 - - -##set_load_weight依据静态优先级设置进程的负荷权重 -------- - - -```c -static void set_load_weight(struct task_struct *p) -{ - /* 由于数组中的下标是0~39, 普通进程的优先级是[100~139] - 因此通过static_prio - MAX_RT_PRIO将静态优先级转换成为数组下标 - */ - int prio = p->static_prio - MAX_RT_PRIO; - /* 取得指向进程task负荷权重的指针load, - 下面修改load就是修改进程的负荷权重 */ - struct load_weight *load = &p->se.load; - - /* - * SCHED_IDLE tasks get minimal weight: - * 必须保证SCHED_IDLE进程的负荷权重最小 - * 其权重weight就是WEIGHT_IDLEPRIO - * 而权重的重除结果就是WMULT_IDLEPRIO - */ - if (p->policy == SCHED_IDLE) { - load->weight = scale_load(WEIGHT_IDLEPRIO); - load->inv_weight = WMULT_IDLEPRIO; - return; - } - - /* 设置进程的负荷权重weight和权重的重除值inv_weight */ - load->weight = scale_load(prio_to_weight[prio]); - load->inv_weight = prio_to_wmult[prio]; -} -``` - -##scale_load取得负荷权重的值 -------- - -其中scale_load是一个宏, 定义在[include/linux/sched.h, line 785](http://lxr.free-electrons.com/source/include/linux/sched.h?v=3.9?v=4.6#L785) - - -```c -#if 0 /* BITS_PER_LONG > 32 -- currently broken: it increases power usage under light load */ -# define SCHED_LOAD_RESOLUTION 10 -# define scale_load(w) ((w) << SCHED_LOAD_RESOLUTION) -# define scale_load_down(w) ((w) >> SCHED_LOAD_RESOLUTION) -#else -# define SCHED_LOAD_RESOLUTION 0 -# define scale_load(w) (w) -# define scale_load_down(w) (w) -#endif -``` - -我们可以看到目前版本的scale_load其实什么也没做就是简单取了个值, 但是我们注意到负荷权重仍然保留了SCHED_LOAD_RESOLUTION不为0的情形, 只不过目前因为效率原因和功耗问题没有启用而已 - - -##set_load_weight的演变 -------- - -linux内核的调度器经过了不同阶段的发展, 但是即使是同一个调度器其算法也不是一成不变的, 也在不停的改进和优化 - -| 内核版本 | 实现 | 地址 | -| ------------- |:-------------:|:-------------:| -| 2.6.18~2.6.22 | 实时进程的权重用RTPRIO_TO_LOAD_WEIGHT(p->rt_priority);转换 | [kernel/sched.c#L746](http://lxr.linux.no/linux+v2.6.18/kernel/sched.c#L746) | -| 2.6.23~2.6.34 | 实时进程的权重为非实时权重的二倍 | [kernel/sched.c#L1836](http://lxr.linux.no/linux+v2.6.32/kernel/sched.c#1836) | -| 2.6.35~2.6.36| 实时进程的权重设置为0, 重除值设置为WMULT_CONST | [kernel/sched.c, L1859](http://lxr.linux.no/linux+v2.6.36/kernel/sched.c#L1859) | -| 2.6.37~至今4.6 | 实时进程不再设置权重 | 其中<= linux-3.2时, 代码在sched.c中
3.3~4.4之后, 增加了[sched/core.c](http://lxr.linux.no/linux+v4.4/kernel/sched/core.c#L812)文件调度的核心代码在此存放
4.5~至今, [修改prio_to_weight为sched_prio_to_weight](http://lxr.linux.no/linux+v4.5/kernel/sched/core.c#L813), 并将声明存放头文件中 | -#就绪队列的负荷权重 -------- - -不仅进程, 就绪队列也关联到一个负荷权重. 这个我们在前面讲[Linux进程调度器的设计--Linux进程的管理与调度(十七)](http://blog.csdn.net/gatieme/article/details/51702662)的时候提到过了在cpu的就绪队列rq和cfs调度器的就绪队列cfs_rq中都保存了其load_weight. - -这样不仅确保就绪队列能够跟踪记录有多少进程在运行, 而且还能将进程的权重添加到就绪队列中. - - - -##cfs就绪队列的负荷权重 -------- - -```c -// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L596 -struct rq -{ - /* ...... */ - /* capture load from *all* tasks on this cpu: */ - struct load_weight load; - /* ...... */ -}; - -// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L361 -/* CFS-related fields in a runqueue */ -struct cfs_rq -{ - struct load_weight load; - unsigned int nr_running, h_nr_running; - /* ...... */ -}; - -// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L596 -struct rt_rq中不需要负荷权重 - -// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L490 -struct dl_rq中不需要负荷权重 -``` - -由于负荷权重仅用于调度普通进程(非实时进程), 因此只在cpu的就绪队列队列rq和cfs调度器的就绪队列cfs_rq上需要保存其就绪队列的信息, 而实时进程的就绪队列rt_rq和dl_rq -是不需要保存负荷权重的. - - -##就绪队列的负荷权重计算 -------- - - -就绪队列的负荷权重存储的其实就是队列上所有进程的负荷权重的总和, 因此每次进程被加到就绪队列的时候, 就需要在就绪队列的负荷权重中加上进程的负荷权重, 同时由于就绪队列的不是一个单独被调度的实体, 也就不需要优先级到负荷权重的转换, 因而其不需要负荷权重的重除字段, 即inv_weight = 0; - -因此进程从就绪队列上入队或者出队的时候, 就绪队列的负荷权重就加上或者减去进程的负荷权重, 但是 - - -```c -//struct load_weight { - /* 就绪队列的负荷权重 +/- 入队/出队进程的负荷权重 */ - unsigned long weight +/- task_struct->se->load->weight; - /* 就绪队列负荷权重的重除字段无用途,所以始终置0 */ - u32 inv_weight = 0; -//}; -``` - -因此内核为我们提供了增加/减少/重置就绪队列负荷权重的的函数, 分别是update_load_add, update_load_sub, update_load_set - -```c -/* 使得lw指向的负荷权重的值增加inc, 用于进程进入就绪队列时调用 - * 进程入队 account_entity_enqueue kernel/sched/fair.c#L2422 - */ - -static inline void update_load_add(struct load_weight *lw, unsigned long inc) -{ - lw->weight += inc; - lw->inv_weight = 0; -} - -/* 使得lw指向的负荷权重的值减少inc, 用于进程调出就绪队列时调用 - * 进程出队 account_entity_dequeue kernel/sched/fair.c#L2422*/ -static inline void update_load_sub(struct load_weight *lw, unsigned long dec) -{ - lw->weight -= dec; - lw->inv_weight = 0; -} - -static inline void update_load_set(struct load_weight *lw, unsigned long w) -{ - lw->weight = w; - lw->inv_weight = 0; -} -```` - - -| 函数 | 描述 | 调用时机 | 定义位置 | 调用位置 | -| ------------- |:-------------:|:-------------:| -| update_load_add | 使得lw指向的负荷权重的值增加inc | 用于进程进入就绪队列时调用 | [kernel/sched/fair.c, L117](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L117) | [account_entity_enqueue两处](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L2420), [sched_slice](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L640) | -| update_load_sub | 使得lw指向的负荷权重的值减少inc | 用于进程调出就绪队列时调用 | [update_load_sub, L123](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L123) | [account_entity_dequeue两处](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L2437) | -| update_load_set | - - ->其中sched_slice函数计算当前进程在调度延迟内期望的运行时间, 它根据cfs就绪队列中进程数确定一个最长时间间隔,然后看在该时间间隔内当前进程按照权重比例执行 - - -#总结 -------- - - -**负荷权重load_weight** -CFS完全公平调度器在调度非实时进程的时候, 进程的重要性不仅是由优先级指定的, 还需要考虑保存在task_struct->se.load的负荷权重. - - -**转换表prio_to_weight和重除表ched_prio_to_wmult** -这个负荷权重用struct load_weight, 其包含了名为weight的负荷权重信息, 为了方便快速的将静态优先级转换成权重值, 内核提供了一个长为40的prio_to_weight数组方便转换, 静态优先级[100~139], 对应nice值[-20, 19], 对应数组中的下标[0, 39] - -由于权重`weight` 用`unsigned long` 表示, 因此内核无法直接存储1/weight, 而必须借助于乘法和位移来执行除法的技术. sched_prio_to_wmult数组就存储了这些值, 即sched_prio_to_wmult每个元素的值是2^32/prio_to_weight$每个元素的值. - - -对于SCHED_IDLE进程其优先级最低, 因此其负荷权重也要尽可能的小, 因此内核用WEIGHT_IDLEPRIO( = 3)和WMULT_IDLEPRIO分别表示了SCHED_IDLE进程的负荷权重和重除值. - - -**调度实体负荷权重的计算** - ->既然CFS把负荷权重作为进程调度的一个重要依据, 那么我们就需要了解调度器是如何计算进程或者调度实体的负荷权重的. - - -有了prio_to_weight和ched_prio_to_wmult这两个转换表, 我们就可以很方便的将非实时进程的静态优先级转换成负荷权重, 这个其实就是一个很简单的查表得过程, 内核用set_load_weight完成这个工作, 同时也保证了SCHED_LDLE进程的负荷权重最小 - - -* 将进程的静态优先级[100, 139]转换成数组下标[0, 39] - -* 如果进程是SCHED_IDLE调度, 则负荷权重直赋值为WEIGHT_IDLEPRIO( = 3)和WMULT_IDLEPRIO - -* 对于普通进程, 从prio_to_weight和sched_prio_to_wmult中查找出对应优先级的负荷权重值和重除值 - - -现在的内核中是实时进程是不依靠负荷权重的, 因此也就不需要计算实时进程的负荷权重, 但是早期的内核中实时进程的负荷权重设置为普通进程的两倍, 以保证权重比非实时进程大 - -**调度实体的负荷权重** - -既然load_weight保存着进程的权重信息, 那么作为进程调度的实体, 必须将这个权重值与特定的进程task_struct, 更一般的与通用的调度实体sched_entity相关联 - -sched_entity作为进程调度的实体信息, 其内置了load_weight结构用于保存当前调度实体的权重, [参照](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1195) - - -同时进程作为调度实体的最一般单位, 其load_weight就存储在其struct sched_entity *se成员中, 可以通过task_struct->se.load访问进程的负荷权重. - - -**就绪队列的负荷权重** - -然后内核也在全局cpu就绪队列rq中cfs的就绪队列cfs_rq中保存了load_weight, 这就可以很方便的统计出整个就绪队列的负荷权重总和, 为进程调度提供参考, 因此在每次进程入队或者出队的时候就需要通过修改就绪队列的负荷权重, 内核为我们提供了增加/减少/重置就绪队列负荷权重的的函数, 分别是update_load_add, update_load_sub, update_load_set, 而由于就绪队列的负荷权重只关心权重值, 因此其重除字段inv_weight恒为0 - -同时需要**注意**的是, 由于实时进程不依赖于负荷权重的, 因此实时进程的就绪队列rt_qt和dl_rq中不需要存储load_weight. - - - +Linux CFS调度器之负荷权重load_weight +======= + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-07-29 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/details/51456569) | + + +Linux内核使用CFS是来调度我们最常见的普通进程, 其所属调度器类为fair_sched_class, 使用的调度策略包括SCHED_NORMAL和SCHED_BATCH, 进程task_struct中struct sched_entity se;字段标识的就是CFS调度器类的调度实体. + + +前面我们详细的了解了linux下进程优先级的表示以及其计算的方法, 我们了解到linux针对普通进程和实时进程分别使用静态优先级static_prio和实时优先级rt_priority来指定其默认的优先级别, 然后通过normal_prio函数将他们分别转换为普通优先级normal_prio, 最终换算出动态优先级prio, 动态优先级prio才是内核调度时候有限考虑的优先级字段 + +但是CFS完全公平调度器在调度进程的时候, 进程的重要性不仅是由优先级指定的, 而且还需要考虑保存在task_struct->se.load的负荷权重. + + + +#1 前景回顾 +------- + +##1.1 linux调度器的演变 +------- + + +一开始的调度器是复杂度为**$O(n)$的始调度算法**(实际上每次会遍历所有任务,所以复杂度为O(n)), 这个算法的缺点是当内核中有很多任务时,调度器本身就会耗费不少时间,所以,从linux2.5开始引入赫赫有名的**$O(1)$调度器** + +然而,linux是集全球很多程序员的聪明才智而发展起来的超级内核,没有最好,只有更好,在$O(1)$调度器风光了没几天就又被另一个更优秀的调度器取代了,它就是**CFS调度器Completely Fair Scheduler**. 这个也是在2.6内核中引入的,具体为2.6.23,即从此版本开始,内核使用CFS作为它的默认调度器,$O(1)$调度器被抛弃了, 其实CFS的发展也是经历了很多阶段,最早期的楼梯算法(SD), 后来逐步对SD算法进行改进出RSDL(Rotating Staircase Deadline Scheduler), 这个算法已经是"完全公平"的雏形了, 直至CFS是最终被内核采纳的调度器, 它从RSDL/SD中吸取了完全公平的思想,不再跟踪进程的睡眠时间,也不再企图区分交互式进程。它将所有的进程都统一对待,这就是公平的含义。CFS的算法和实现都相当简单,众多的测试表明其性能也非常优越 + + + +| 字段 | 版本 | +| --- |:----:| +| O(n)的始调度算法 | linux-0.11~2.4 | +| O(1)调度器 | linux-2.5 | +| CFS调度器 | linux-2.6~至今 | + + +##1.2 Linux的调度器组成 +------- + + +**2个调度器** + +可以用两种方法来激活调度 + +* 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU + +* 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 + +因此当前linux的调度程序由两个调度器组成:**主调度器**,**周期性调度器**(两者又统称为**通用调度器(generic scheduler)**或**核心调度器(core scheduler)**) + +并且每个调度器包括两个内容:**调度框架**(其实质就是两个函数框架)及**调度器类** + + + +**6种调度策略** + +linux内核目前实现了6中调度策略(即调度算法), 用于对不同类型的进程进行调度, 或者支持某些特殊的功能 + +* SCHED_NORMAL和SCHED_BATCH调度普通的非实时进程 + +* SCHED_FIFO和SCHED_RR和SCHED_DEADLINE则采用不同的调度策略调度实时进程 + +* SCHED_IDLE则在系统空闲时调用idle进程. + + + +**5个调度器类** + +而依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程. + + +其所属进程的优先级顺序为 +```c +stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class +``` + +**3个调度实体** + +调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度. + +这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组. + +linux中针对当前可调度的实时和非实时进程, 定义了类型为seched_entity的3个调度实体 + +* sched_dl_entity 采用EDF算法调度的实时调度实体 + +* sched_rt_entity 采用Roound-Robin或者FIFO算法调度的实时调度实体 + +* sched_entity 采用CFS算法调度的普通非实时进程的调度实体 + + +**调度器整体框架** + +每个进程都属于某个调度器类(由字段task_struct->sched_class标识), 由调度器类采用进程对应的调度策略调度(由task_struct->policy )进行调度, task_struct也存储了其对应的调度实体标识 + +linux实现了6种调度策略, 依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程. + + +| 调度器类 | 调度策略 | 调度策略对应的调度算法 | 调度实体 | 调度实体对应的调度对象 | +| ------- |:-------:|:-------:|:-------:||:-------:| +| stop_sched_class | 无 | 无 | 无 | 特殊情况, 发生在cpu_stop_cpu_callback 进行cpu之间任务迁移migration或者HOTPLUG_CPU的情况下关闭任务 | +| dl_sched_class | SCHED_DEADLINE | Earliest-Deadline-First最早截至时间有限算法 | sched_dl_entity | 采用DEF最早截至时间有限算法调度实时进程 | +| rt_sched_class | SCHED_RR

SCHED_FIFO | Roound-Robin时间片轮转算法

FIFO先进先出算法 | sched_rt_entity | 采用Roound-Robin或者FIFO算法调度的实时调度实体 | +| fair_sched_class | SCHED_NORMAL

SCHED_BATCH | CFS完全公平懂调度算法 |sched_entity | 采用CFS算法普通非实时进程 | +| idle_sched_class | SCHED_IDLE | 无 | 无 |特殊进程, 用于cpu空闲时调度空闲进程idle | + +它们的关系如下图 + +![调度器的组成](../../images/level.jpg) + + +##1.3 优先级的内核表示 +------- + + +内核使用一些简单的数值范围0~139表示内部优先级, 数值越低, 优先级越高。 + +从0~99的范围专供实时进程使用, nice的值[-20,19]则映射到范围100~139 + + +>实时优先级范围是0到MAX_RT_PRIO-1(即99),而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX_PRIO-1(即100到139)。 + +| 优先级范围 | 描述 | +| ------------- |:-------------:| +| 0——99 | 实时进程 | +| 100——139 | 非实时进程 | + +![内核优先级标度](../../images/priority.jpg) + +##1.4 进程的优先级表示 +------- + + +```c +struct task_struct +{ + /* 进程优先级 + * 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; +} +``` + +**动态优先级 静态优先级 实时优先级** + + +其中task_struct采用了三个成员表示进程的优先级:prio和normal_prio表示动态优先级, static_prio表示进程的静态优先级. + +此外还用了一个字段rt_priority保存了实时进程的优先级 + + +| 字段 | 描述 | +| ------------- |:-------------:| +| static_prio | 用于保存静态优先级, 是进程启动时分配的优先级, ,可以通过nice和sched_setscheduler系统调用来进行修改, 否则在进程运行期间会一直保持恒定 | +| prio | 保存进程的动态优先级 | +| normal_prio | 表示基于进程的静态优先级static_prio和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级 | +| rt_priority | 用于保存实时优先级 | + + +实时进程的优先级用实时优先级rt_priority来表示 + + + +#2 负荷权重 +------- + + +##2.1 负荷权重结构struct load_weight +------- + +负荷权重用struct load_weight数据结构来表示, 保存着进程权重值weight。其定义在[/include/linux/sched.h, v=4.6, L1195](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1195), 如下所示 + +```c +struct load_weight { + unsigned long weight; /* 存储了权重的信息 */ + u32 inv_weight; /* 存储了权重值用于重除的结果 weight * inv_weight = 2^32 */ +}; +``` + + + +##2.2 调度实体的负荷权重load +------- + +既然struct load_weight保存着进程的权重信息, 那么作为进程调度的实体, 必须将这个权重值与特定的进程task_struct, 更一般的与通用的调度实体sched_entity相关联 + +struct sched_entity作为进程调度的实体信息, 其内置了load_weight结构用于保存当前调度实体的权重, 参照http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1195 + +```c +struct sched_entity { + struct load_weight load; /* for load-balancing */ + /* ...... */ +}; +``` + +##2.3 进程的负荷权重 + +而进程可以被作为一个调度的实时, 其内部通过存储struct sched_entity se而间接存储了其load_weight信息, 参照http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1415 + +```c +struct task_struct +{ + /* ...... */ + struct sched_entity se; + /* ...... */ +} +``` + +因此我们就可以通过task_statuct->se.load获取负荷权重的信息, 而set_load_weight负责根据进程类型及其静态优先级计算符合权重. + + +#3 优先级和权重的转换 +------- + + +##3.1 优先级->权重转换表 +------- + + +一般这个概念是这样的, 进程每降低一个nice值(优先级提升), 则多获得10%的CPU时间, 没升高一个nice值(优先级降低), 则放弃10%的CPU时间. + +为执行该策略, 内核需要将优先级转换为权重值, 并提供了一张优先级->权重转换表sched_prio_to_weight, 内核不仅维护了负荷权重自身, 还保存另外一个数值, 用于负荷重除的结果, 即sched_prio_to_wmult数组, 这两个数组中的数据是一一对应的. + + +其中相关的数据结构定义在[kernel/sched/sched.h?v=4.6, L1132](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1132) + +```c +// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1132 +/* + * To aid in avoiding the subversion of "niceness" due to uneven distribution + * of tasks with abnormal "nice" values across CPUs the contribution that + * each task makes to its run queue's load is weighted according to its + * scheduling class and "nice" value. For SCHED_NORMAL tasks this is just a + * scaled version of the new time slice allocation that they receive on time + * slice expiry etc. + */ + + +#define WEIGHT_IDLEPRIO 3 /* SCHED_IDLE进程的负荷权重 */ +#define WMULT_IDLEPRIO 1431655765 /* SCHED_IDLE进程负荷权重的重除值 */ + + +extern const int sched_prio_to_weight[40]; +extern const u32 sched_prio_to_wmult[40]; + + +// http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L8484 +/* +* Nice levels are multiplicative, with a gentle 10% change for every +* nice level changed. I.e. when a CPU-bound task goes from nice 0 to +* nice 1, it will get ~10% less CPU time than another CPU-bound task +* that remained on nice 0. +* +* The "10% effect" is relative and cumulative: from _any_ nice level, +* if you go up 1 level, it's -10% CPU usage, if you go down 1 level +* it's +10% CPU usage. (to achieve that we use a multiplier of 1.25. +* If a task goes up by ~10% and another task goes down by ~10% then +* the relative distance between them is ~25%.) +*/ +const int sched_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, +}; + + +/* +* Inverse (2^32/x) values of the sched_prio_to_weight[] array, precalculated. +* +* In cases where the weight does not change often, we can use the +* precalculated inverse to speed up arithmetics by turning divisions +* into multiplications: +*/ +const u32 sched_prio_to_wmult[40] = { +/* -20 */ 48388, 59856, 76040, 92818, 118348, +/* -15 */ 147320, 184698, 229616, 287308, 360437, +/* -10 */ 449829, 563644, 704093, 875809, 1099582, +/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326, +/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587, +/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126, +/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717, +/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153, +}; +``` + +对内核使用的范围[-20, 19]中的每个nice级别, sched_prio_to_weight数组都有一个对应项 + +nice [-20, 19] -=> 下标 [0, 39] + +而由于权重`weight` 用`unsigned long` 表示, 因此内核无法直接存储1/weight, 而必须借助于乘法和位移来执行除法的技术. sched_prio_to_wmult数组就存储了这些值, 即sched_prio_to_wmult每个元素的值是2^32/prio_to_weight$每个元素的值. + +可以验证 + +$$sched\_prio\_to\_wmult[i] = \frac{2^{32}}{sched\_prio\_to\_weight[i]}$$ + +同时我们可以看到其定义了两个宏WEIGHT_IDLEPRIO和WMULT_IDLEPRIO这两个宏对应的就是SCHED_IDLE调度的进程的负荷权重信息, 因为要保证SCHED_IDLE进程的最低优先级和最低的负荷权重. 这点信息我们可以在后面分析set_load_weight函数的时候可以看到 + +可以验证 + +$$\frac{2^{32}}{WEIGHT_IDLEPRIO} = WMULT_IDLEPRIO$$ + + +##3.2 linux-4.4之前的shced_prio_to_weight和sched_prio_to_wmult +------- + +关于优先级->权重转换表sched_prio_to_weight + +在linux-4.4之前的内核中, 优先级权重转换表用prio_to_weight表示, 定义在[kernel/sched/sched.h, line 1116](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.4#L1116), 与它一同定义的还有prio_to_wmult, 在[kernel/sched/sched.h, line 1139](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.4#L1139) +均被定义为static const + +但是其实这种能够方式不太符合规范的编码风格, 因此常规来说, 我们的头文件中不应该存储结构的定义, 即为了是程序的模块结构更加清晰, 头文件中尽量只包含宏或者声明, 而将具体的定义, 需要分配存储空间的代码放在源文件中. + +否则如果在头文件中定义全局变量,并且将此全局变量赋初值,那么在多个引用此头文件的C文件中同样存在相同变量名的拷贝,关键是此变量被赋了初值,所以编译器就会将此变量放入DATA段,最终在连接阶段,会在DATA段中存在多个相同的变量,它无法将这些变量统一成一个变量,也就是仅为此变量分配一个空间,而不是多份空间,假定这个变量在头文件没有赋初值,编译器就会将之放入BSS段,连接器会对BSS段的多个同名变量仅分配一个存储空间 + +因此在新的内核中, 内核黑客们将这两个变量存放在了[kernel/sched/core.c](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L8472), 并加上了sched_前缀, 以表明这些变量是在进程调度的过程中使用的, 而在[kernel/sched/sched.h, line 1144](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1144)中则只包含了他们的声明. + + +下面我们列出优先级权重转换表定义更新后对比项 + + +| 内核版本 | 实现 | 地址 | +| ------------- |:-------------:|:-------------:| +| <= linux-4.4 | static const int prio_to_weight[40] | [kernel/sched/sched.h, line 1116](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.4#L1116) | +| >=linux-4.5 | const int sched_prio_to_weight[40] | 声明在[kernel/sched/sched.h, line 1144](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1144), 定义在[kernel/sched/core.c](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L8472) + +其定义并没有发生变化, 依然是一个一对一NICE to WEIGHT的转换表 + + +##3.3 1.25的乘积因子 +------- + + +各数组之间的乘积因子是1.25. 要知道为何使用该因子, 可考虑下面的例子 + +两个进程A和B在nice级别0, 即静态优先级120运行, 因此两个进程的CPU份额相同, 都是50%, nice级别为0的进程, 查其权重表可知是1024. 每个进程的份额是1024/(1024+1024)=0.5, 即50% + +如果进程B的优先级+1(优先级降低), 成为nice=1, 那么其CPU份额应该减少10%, 换句话说进程A得到的总的CPU应该是55%, 而进程B应该是45%. 优先级增加1导致权重减少, 即1024/1.25=820, 而进程A仍旧是1024, 则进程A现在将得到的CPU份额是1024/(1024+820=0.55, 而进程B的CPU份额则是820/(1024+820)=0.45. 这样就正好产生了10%的差值. + + +#4 进程负荷权重的计算 +------- + + +set_load_weight负责根据非实时进程类型极其静态优先级计算符合权重 +而实时进程不需要CFS调度, 因此无需计算其负荷权重值 + + +>早期的代码中实时进程也是计算其负荷权重的, 但是只是采用一些方法保持其权重值较大 +> +>在早期有些版本中, set_load_weight中实时进程的权重是普通进程的两倍, 后来又设置成0, 直到后来linux-2.6.37开始不再设置实时进程的优先级, 因此这本身就是一个无用的工作 +> +>而另一方面, SCHED_IDLE进程的权值总是非常小, 普通非实时进程则根据其静态优先级设置对应的负荷权重 + + +##4.1 set_load_weight依据静态优先级设置进程的负荷权重 +------- + + +```c +static void set_load_weight(struct task_struct *p) +{ + /* 由于数组中的下标是0~39, 普通进程的优先级是[100~139] + 因此通过static_prio - MAX_RT_PRIO将静态优先级转换成为数组下标 + */ + int prio = p->static_prio - MAX_RT_PRIO; + /* 取得指向进程task负荷权重的指针load, + 下面修改load就是修改进程的负荷权重 */ + struct load_weight *load = &p->se.load; + + /* + * SCHED_IDLE tasks get minimal weight: + * 必须保证SCHED_IDLE进程的负荷权重最小 + * 其权重weight就是WEIGHT_IDLEPRIO + * 而权重的重除结果就是WMULT_IDLEPRIO + */ + if (p->policy == SCHED_IDLE) { + load->weight = scale_load(WEIGHT_IDLEPRIO); + load->inv_weight = WMULT_IDLEPRIO; + return; + } + + /* 设置进程的负荷权重weight和权重的重除值inv_weight */ + load->weight = scale_load(prio_to_weight[prio]); + load->inv_weight = prio_to_wmult[prio]; +} +``` + +##4.2 scale_load取得负荷权重的值 +------- + +其中scale_load是一个宏, 定义在[include/linux/sched.h, line 785](http://lxr.free-electrons.com/source/include/linux/sched.h?v=3.9?v=4.6#L785) + + +```c +#if 0 /* BITS_PER_LONG > 32 -- currently broken: it increases power usage under light load */ +# define SCHED_LOAD_RESOLUTION 10 +# define scale_load(w) ((w) << SCHED_LOAD_RESOLUTION) +# define scale_load_down(w) ((w) >> SCHED_LOAD_RESOLUTION) +#else +# define SCHED_LOAD_RESOLUTION 0 +# define scale_load(w) (w) +# define scale_load_down(w) (w) +#endif +``` + +我们可以看到目前版本的scale_load其实什么也没做就是简单取了个值, 但是我们注意到负荷权重仍然保留了SCHED_LOAD_RESOLUTION不为0的情形, 只不过目前因为效率原因和功耗问题没有启用而已 + + +##4.3 set_load_weight的演变 +------- + +linux内核的调度器经过了不同阶段的发展, 但是即使是同一个调度器其算法也不是一成不变的, 也在不停的改进和优化 + +| 内核版本 | 实现 | 地址 | +| ------------- |:-------------:|:-------------:| +| 2.6.18~2.6.22 | 实时进程的权重用RTPRIO_TO_LOAD_WEIGHT(p->rt_priority);转换 | [kernel/sched.c#L746](http://lxr.linux.no/linux+v2.6.18/kernel/sched.c#L746) | +| 2.6.23~2.6.34 | 实时进程的权重为非实时权重的二倍 | [kernel/sched.c#L1836](http://lxr.linux.no/linux+v2.6.32/kernel/sched.c#1836) | +| 2.6.35~2.6.36| 实时进程的权重设置为0, 重除值设置为WMULT_CONST | [kernel/sched.c, L1859](http://lxr.linux.no/linux+v2.6.36/kernel/sched.c#L1859) | +| 2.6.37~至今4.6 | 实时进程不再设置权重 | 其中<= linux-3.2时, 代码在sched.c中
3.3~4.4之后, 增加了[sched/core.c](http://lxr.linux.no/linux+v4.4/kernel/sched/core.c#L812)文件调度的核心代码在此存放
4.5~至今, [修改prio_to_weight为sched_prio_to_weight](http://lxr.linux.no/linux+v4.5/kernel/sched/core.c#L813), 并将声明存放头文件中 | + + +#5 就绪队列的负荷权重 +------- + +不仅进程, 就绪队列也关联到一个负荷权重. 这个我们在前面讲[Linux进程调度器的设计--Linux进程的管理与调度(十七)](http://blog.csdn.net/gatieme/article/details/51702662)的时候提到过了在cpu的就绪队列rq和cfs调度器的就绪队列cfs_rq中都保存了其load_weight. + +这样不仅确保就绪队列能够跟踪记录有多少进程在运行, 而且还能将进程的权重添加到就绪队列中. + + + +##5.1 cfs就绪队列的负荷权重 +------- + +```c +// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L596 +struct rq +{ + /* ...... */ + /* capture load from *all* tasks on this cpu: */ + struct load_weight load; + /* ...... */ +}; + +// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L361 +/* CFS-related fields in a runqueue */ +struct cfs_rq +{ + struct load_weight load; + unsigned int nr_running, h_nr_running; + /* ...... */ +}; + +// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L596 +struct rt_rq中不需要负荷权重 + +// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L490 +struct dl_rq中不需要负荷权重 +``` + +由于负荷权重仅用于调度普通进程(非实时进程), 因此只在cpu的就绪队列队列rq和cfs调度器的就绪队列cfs_rq上需要保存其就绪队列的信息, 而实时进程的就绪队列rt_rq和dl_rq +是不需要保存负荷权重的. + + +##5.2 就绪队列的负荷权重计算 +------- + + +就绪队列的负荷权重存储的其实就是队列上所有进程的负荷权重的总和, 因此每次进程被加到就绪队列的时候, 就需要在就绪队列的负荷权重中加上进程的负荷权重, 同时由于就绪队列的不是一个单独被调度的实体, 也就不需要优先级到负荷权重的转换, 因而其不需要负荷权重的重除字段, 即inv_weight = 0; + +因此进程从就绪队列上入队或者出队的时候, 就绪队列的负荷权重就加上或者减去进程的负荷权重, 但是 + + +```c +//struct load_weight { + /* 就绪队列的负荷权重 +/- 入队/出队进程的负荷权重 */ + unsigned long weight +/- task_struct->se->load->weight; + /* 就绪队列负荷权重的重除字段无用途,所以始终置0 */ + u32 inv_weight = 0; +//}; +``` + +因此内核为我们提供了增加/减少/重置就绪队列负荷权重的的函数, 分别是update_load_add, update_load_sub, update_load_set + +```c +/* 使得lw指向的负荷权重的值增加inc, 用于进程进入就绪队列时调用 + * 进程入队 account_entity_enqueue kernel/sched/fair.c#L2422 + */ + +static inline void update_load_add(struct load_weight *lw, unsigned long inc) +{ + lw->weight += inc; + lw->inv_weight = 0; +} + +/* 使得lw指向的负荷权重的值减少inc, 用于进程调出就绪队列时调用 + * 进程出队 account_entity_dequeue kernel/sched/fair.c#L2422*/ +static inline void update_load_sub(struct load_weight *lw, unsigned long dec) +{ + lw->weight -= dec; + lw->inv_weight = 0; +} + +static inline void update_load_set(struct load_weight *lw, unsigned long w) +{ + lw->weight = w; + lw->inv_weight = 0; +} +```` + + +| 函数 | 描述 | 调用时机 | 定义位置 | 调用位置 | +| ------------- |:-------------:|:-------------:| +| update_load_add | 使得lw指向的负荷权重的值增加inc | 用于进程进入就绪队列时调用 | [kernel/sched/fair.c, L117](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L117) | [account_entity_enqueue两处](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L2420), [sched_slice](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L640) | +| update_load_sub | 使得lw指向的负荷权重的值减少inc | 用于进程调出就绪队列时调用 | [update_load_sub, L123](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L123) | [account_entity_dequeue两处](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L2437) | +| update_load_set | + + +>其中sched_slice函数计算当前进程在调度延迟内期望的运行时间, 它根据cfs就绪队列中进程数确定一个最长时间间隔,然后看在该时间间隔内当前进程按照权重比例执行 + + +#6 总结 +------- + + +**负荷权重load_weight** +CFS完全公平调度器在调度非实时进程的时候, 进程的重要性不仅是由优先级指定的, 还需要考虑保存在task_struct->se.load的负荷权重. + + +**转换表prio_to_weight和重除表ched_prio_to_wmult** +这个负荷权重用struct load_weight, 其包含了名为weight的负荷权重信息, 为了方便快速的将静态优先级转换成权重值, 内核提供了一个长为40的prio_to_weight数组方便转换, 静态优先级[100~139], 对应nice值[-20, 19], 对应数组中的下标[0, 39] + +由于权重`weight` 用`unsigned long` 表示, 因此内核无法直接存储1/weight, 而必须借助于乘法和位移来执行除法的技术. sched_prio_to_wmult数组就存储了这些值, 即sched_prio_to_wmult每个元素的值是2^32/prio_to_weight$每个元素的值. + + +对于SCHED_IDLE进程其优先级最低, 因此其负荷权重也要尽可能的小, 因此内核用WEIGHT_IDLEPRIO( = 3)和WMULT_IDLEPRIO分别表示了SCHED_IDLE进程的负荷权重和重除值. + + +**调度实体负荷权重的计算** + +>既然CFS把负荷权重作为进程调度的一个重要依据, 那么我们就需要了解调度器是如何计算进程或者调度实体的负荷权重的. + + +有了prio_to_weight和ched_prio_to_wmult这两个转换表, 我们就可以很方便的将非实时进程的静态优先级转换成负荷权重, 这个其实就是一个很简单的查表得过程, 内核用set_load_weight完成这个工作, 同时也保证了SCHED_LDLE进程的负荷权重最小 + + +* 将进程的静态优先级[100, 139]转换成数组下标[0, 39] + +* 如果进程是SCHED_IDLE调度, 则负荷权重直赋值为WEIGHT_IDLEPRIO( = 3)和WMULT_IDLEPRIO + +* 对于普通进程, 从prio_to_weight和sched_prio_to_wmult中查找出对应优先级的负荷权重值和重除值 + + +现在的内核中是实时进程是不依靠负荷权重的, 因此也就不需要计算实时进程的负荷权重, 但是早期的内核中实时进程的负荷权重设置为普通进程的两倍, 以保证权重比非实时进程大 + +**调度实体的负荷权重** + +既然load_weight保存着进程的权重信息, 那么作为进程调度的实体, 必须将这个权重值与特定的进程task_struct, 更一般的与通用的调度实体sched_entity相关联 + +sched_entity作为进程调度的实体信息, 其内置了load_weight结构用于保存当前调度实体的权重, [参照](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1195) + + +同时进程作为调度实体的最一般单位, 其load_weight就存储在其struct sched_entity *se成员中, 可以通过task_struct->se.load访问进程的负荷权重. + + +**就绪队列的负荷权重** + +然后内核也在全局cpu就绪队列rq中cfs的就绪队列cfs_rq中保存了load_weight, 这就可以很方便的统计出整个就绪队列的负荷权重总和, 为进程调度提供参考, 因此在每次进程入队或者出队的时候就需要通过修改就绪队列的负荷权重, 内核为我们提供了增加/减少/重置就绪队列负荷权重的的函数, 分别是update_load_add, update_load_sub, update_load_set, 而由于就绪队列的负荷权重只关心权重值, 因此其重除字段inv_weight恒为0 + +同时需要**注意**的是, 由于实时进程不依赖于负荷权重的, 因此实时进程的就绪队列rt_qt和dl_rq中不需要存储load_weight. + + + diff --git a/study/kernel/01-process/05-schedule/07-cfs/03-vruntime/README.md b/study/kernel/01-process/05-schedule/07-cfs/03-vruntime/README.md index 94fb67f..8a8a012 100644 --- a/study/kernel/01-process/05-schedule/07-cfs/03-vruntime/README.md +++ b/study/kernel/01-process/05-schedule/07-cfs/03-vruntime/README.md @@ -1,475 +1,530 @@ -Linux CFS调度器之虚拟时钟与调度延迟 -======= - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | - - - -CFS负责处理普通非实时进程, 这类进程是我们linux中最普遍的进程, 今天我们把注意力转向CFS的虚拟时钟 - - -#CFS虚拟时钟 -------- - - -完全公平调度算法CFS依赖于虚拟时钟, 用以度量等待进程在完全公平系统中所能得到的CPU时间. 但是数据结构中任何地方都没有找到虚拟时钟. 这个是由于所有的必要信息都可以根据现存的实际时钟和每个进程相关的负荷权重推算出来. - -假设现在系统有A,B,C三个进程,A.weight=1,B.weight=2,C.weight=3.那么我们可以计算出整个公平调度队列的总权重是cfs_rq.weight = 6,很自然的想法就是,公平就是你在重量中占的比重的多少来拍你的重要性,那么,A的重要性就是1/6,同理,B和C的重要性分别是2/6,3/6.很显然C最重要就应改被先调度,而且占用的资源也应该最多,即假设A,B,C运行一遍的总时间假设是6个时间单位的话,A占1个单位,B占2个单位,C占三个单位。这就是CFS的公平策略. - -**CFS调度算法的思想**:理想状态下每个进程都能获得相同的时间片,并且同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。也就是说,当一个进程占用CPU时,其他进程就必须等待。CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度. - -具体实现时,CFS通过每个进程的**虚拟运行时间(vruntime)**来衡量哪个进程最值得被调度. CFS中的就绪队列是一棵以vruntime为键值的红黑树,虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的vruntime最小. - - -虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。而,CFS调度器中的权重在内核是对用户态进程的优先级nice值, 通过prio_to_weight数组进行nice值和权重的转换而计算出来的 - -#虚拟时钟相关的数据结构 -------- - -## 调度实体的虚拟时钟信息 -------- - -为了实现完全公平调度,内核引入了虚拟时钟(virtual clock)的概念,实际上我觉得这个虚拟时钟为什叫虚拟的,是因为这个时钟与具体的时钟晶振没有关系,他只不过是为了公平分配CPU时间而提出的一种时间量度,它与进程的权重有关,这里就知道权重的作用了,权重越高,说明进程的优先级比较高,进而该进程虚拟时钟增长的就慢 - -既然虚拟时钟是用来衡量调度实体(一个或者多个进程)的一种时间度量, 因此必须在调度实体中存储其虚拟时钟的信息 - -```c -struct sched_entity -{ - struct load_weight load; /* for load-balancing负荷权重,这个决定了进程在CPU上的运行时间和被调度次数 */ - struct rb_node run_node; - unsigned int on_rq; /* 是否在就绪队列上 */ - - u64 exec_start; /* 上次启动的时间*/ - - u64 sum_exec_runtime; - u64 vruntime; - u64 prev_sum_exec_runtime; - /* rq on which this entity is (to be) queued: */ - struct cfs_rq *cfs_rq; - ... -}; -``` - - -**sum_exec_runtime**是用于记录该进程的CPU消耗时间,这个是真实的CPU消耗时间。在进程撤销时会将sum_exec_runtime保存到**prev_sum_exec_runtime**中 - -**vruntime**是本进程生命周期中在CPU上运行的虚拟时钟。那么何时应该更新这些时间呢?这是通过调用**update_curr**实现的, 该函数在多处调用. - - -## 就绪队列上的虚拟时钟信息 -------- - -完全公平调度器类sched_fair_class主要负责管理普通进程, 在全局的CPU就读队列上存储了在CFS的就绪队列struct cfs_rq cfs - -进程的就绪队列中就存储了CFS相关的虚拟运行时钟的信息, struct cfs_rq定义如下: - -```c -struct cfs_rq -{ - struct load_weight load; /*所有进程的累计负荷值*/ - unsigned long nr_running; /*当前就绪队列的进程数*/ - - // ======================== - u64 min_vruntime; // 队列的虚拟时钟, - // ======================= - struct rb_root tasks_timeline; /*红黑树的头结点*/ - struct rb_node *rb_leftmost; /*红黑树的最左面节点*/ - - struct sched_entity *curr; /*当前执行进程的可调度实体*/ - ... -}; -``` - - - -#update_curr函数计算进程虚拟时间 -------- - -所有与虚拟时钟有关的计算都在update_curr中执行, 该函数在系统中各个不同地方调用, 包括周期性调度器在内. - -update_curr的流程如下 - -* 首先计算进程当前时间与上次启动时间的差值 - -* 通过负荷权重和当前时间模拟出进程的虚拟运行时钟 - -* 重新设置cfs的min_vruntime保持其单调性 - - -##计算时间差 - -首先, 该函数确定就绪队列的当前执行进程, 并获取主调度器就绪队列的实际时钟值, 该值在每个调度周期都会更新 - -```c - /* 确定就绪队列的当前执行进程curr */ - struct sched_entity *curr = cfs_rq->curr; -``` - -其中辅助函数[rq_of](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L248)用于确定与CFS就绪队列相关的struct rq实例, 其定义在[kernel/sched/fair.c, line 248](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L248) - -cfs_rq就绪队列中存储了指向就绪队列的实例,参见[kernel/sched/sched.h, line412](http://lxr.free-electrons.com/source/kernel/sched/sched.h#L412), 而rq_of就返回了这个指向rq的指针, rq_of定义在[kernel/sched/fair.c, line 249](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L249) - - -[rq_clock_task](http://lxr.free-electrons.com/source/kernel/sched/sched.h#L735)函数返回了运行队列的clock_task成员. - -```c - /* rq_of -=> return cfs_rq->rq 返回cfs队列所在的全局就绪队列 - * rq_clock_task返回了rq的clock_task */ - u64 now = rq_clock_task(rq_of(cfs_rq)); - u64 delta_exec; -``` - 如果就队列上没有进程在执行, 则显然无事可做, 否则内核计算当前和上一次更新负荷权重时两次的时间的差值 - -```c - /* 如果就队列上没有进程在执行, 则显然无事可做 */ - if (unlikely(!curr)) - return; - - /* 内核计算当前和上一次更新负荷权重时两次的时间的差值 */ - delta_exec = now - curr->exec_start; - if (unlikely((s64)delta_exec <= 0)) - return; -``` - -然后重新更新更新启动时间exec_start为now, 以备下次计算时使用 - -最后将计算出的时间差, 加到了先前的统计时间上 - -```c - /* 重新更新启动时间exec_start为now */ - curr->exec_start = now; - - schedstat_set(curr->statistics.exec_max, - max(delta_exec, curr->statistics.exec_max)); - - /* 将时间差加到先前统计的时间即可 */ - curr->sum_exec_runtime += delta_exec; - schedstat_add(cfs_rq, exec_clock, delta_exec); - ``` - -##模拟虚拟时钟 -------- - - -有趣的事情是如何使用给出的信息来模拟不存在的虚拟时钟. 这一次内核的实现仍然是非常巧妙地, 针对最普通的情形节省了一些时间. 对于运行在nice级别0的进程来说, 根据定义虚拟时钟和物理时间相等. 在使用不同的优先级时, 必须根据进程的负荷权重重新衡定时间 - -```c - curr->vruntime += calc_delta_fair(delta_exec, curr); - update_min_vruntime(cfs_rq); - -``` - -其中calc_delta_fair函数是计算的关键 - -```c -// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L596 -/* - * delta /= w - */ -static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se) -{ - if (unlikely(se->load.weight != NICE_0_LOAD)) - delta = __calc_delta(delta, NICE_0_LOAD, &se->load); - - return delta; -} -``` - -忽略舍入和溢出检查, calc_delta_fair函数所做的就是根据下列公式计算: - - -$$delta =delta \times \dfrac{NICE\_0\_LOAD}{curr->se->load.weight}$$ - - -每一个进程拥有一个vruntime, 每次需要调度的时候就选运行队列中拥有最小vruntime的那个进程来运行, vruntime在时钟中断里面被维护, 每次时钟中断都要更新当前进程的vruntime, 即vruntime以如下公式逐渐增长 - -那么`curr->vruntime += calc_delta_fair(delta_exec, curr);` 即相当于如下操作 - - - - -| 条件 | 公式 | -|:-------:|:-------:| -| curr.nice != NICE_0_LOAD | $curr->vruntime += delta\_exec \times \dfrac{NICE\_0\_LOAD}{curr->se->load.weight}$| -| curr.nice == NICE_0_LOAD | $ curr->vruntime += delta $ | - - -在该计算中可以派上用场了, 回想一下子, 可知越重要的进程会有越高的优先级(即, 越低的nice值), 会得到更大的权重, 因此累加的虚拟运行时间会小一点, - -根据公式可知, nice = 0的进程(优先级120), 则虚拟时间和物理时间是相等的, 即current->se->load.weight等于NICE_0_LAD的情况. - -##重新设置cfs_rq->min_vruntime - -接着内核需要重新设置`min_vruntime`. 必须小心保证该值是单调递增的, 通过update_min_vruntime函数来设置 - -```c -// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L457 - -static void update_min_vruntime(struct cfs_rq *cfs_rq) -{ - /* 初始化vruntime的值, 相当于如下的代码 - if (cfs_rq->curr != NULL) - vruntime = cfs_rq->curr->vruntime; - else - vruntime = cfs_rq->min_vruntime; - */ - u64 vruntime = cfs_rq->min_vruntime; - - if (cfs_rq->curr) - vruntime = cfs_rq->curr->vruntime; - - - /* 检测红黑树是都有最左的节点, 即是否有进程在树上等待调度 - * cfs_rq->rb_leftmost(struct rb_node *)存储了进程红黑树的最左节点 - * 这个节点存储了即将要被调度的结点 - * */ - if (cfs_rq->rb_leftmost) - { - /* 获取最左结点的调度实体信息se, se中存储了其vruntime - * rb_leftmost的vruntime即树中所有节点的vruntiem中最小的那个 */ - struct sched_entity *se = rb_entry(cfs_rq->rb_leftmost, - struct sched_entity, - run_node); - /* 如果就绪队列上没有curr进程 - * 则vruntime设置为树种最左结点的vruntime - * 否则设置vruntiem值为cfs_rq->curr->vruntime和se->vruntime的最小值 - */ - if (!cfs_rq->curr) /* 此时vruntime的原值为cfs_rq->min_vruntime*/ - vruntime = se->vruntime; - else /* 此时vruntime的原值为cfs_rq->curr->vruntime*/ - vruntime = min_vruntime(vruntime, se->vruntime); - } - - /* ensure we never gain time by being placed backwards. - * 为了保证min_vruntime单调不减 - * 只有在vruntime超出的cfs_rq->min_vruntime的时候才更新 - */ - cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime); -#ifndef CONFIG_64BIT - smp_wmb(); - cfs_rq->min_vruntime_copy = cfs_rq->min_vruntime; -#endif -} -``` -我们通过分析update_min_vruntime函数设置cfs_rq->min_vruntime的流程如下 - -* 首先检测cfs就绪队列上是否有活动进程curr, 以此设置vruntime的值 - 如果cfs就绪队列上没有活动进程curr, 就设置vruntime为curr->vruntime; - 否则又活动进程就设置为vruntime为cfs_rq的原min_vruntime; - -* 接着检测cfs的红黑树上是否有最左节点, 即等待被调度的节点, 重新设置vruntime的值为curr进程和最左进程rb_leftmost的vruntime较小者的值 - -* 为了保证min_vruntime单调不减, 只有在vruntime超出的cfs_rq->min_vruntime的时候才更新 - -update_min_vruntime依据当前进程和待调度的进程的vruntime值, 设置出一个可能的vruntime值, 但是只有在这个可能的vruntime值大于就绪队列原来的min_vruntime的时候, 才更新就绪队列的min_vruntime, 利用该策略, 内核确保min_vruntime只能增加, 不能减少. - -update_min_vruntime函数的流程等价于如下的代码 - -```c -// 依据curr进程和待调度进程rb_leftmost找到一个可能的最小vruntime值 -if (cfs_rq->curr != NULL cfs_rq->rb_leftmost == NULL) - vruntime = cfs_rq->curr->vruntime; -else if(cfs_rq->curr == NULL && cfs_rq->rb_leftmost != NULL) - vruntime = cfs_rq->rb_leftmost->se->vruntime; -else if (cfs_rq->curr != NULL cfs_rq->rb_leftmost != NULL) - vruntime = min(cfs_rq->curr->vruntime, cfs_rq->rb_leftmost->se->vruntime); -else if(cfs_rq->curr == NULL cfs_rq->rb_leftmost == NULL) - vruntime = cfs_rq->min_vruntime; - -// 每个队列的min_vruntime只有被树上某个节点的vruntime((curr和程rb_leftmost两者vruntime的较小值)超出时才更新 -cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime); -``` - -其中寻找可能vruntime的策略我们采用表格的形式可能更加直接 - - -| 活动进程curr | 待调度进程rb_leftmost | 可能的vruntime值 | cfs_rq | -| ------- |:-------:|:-------:| -| NULL | NULL | cfs_rq->min_vruntime | 维持原值 | -| NULL | 非NULL | rb_leftmost->se->vruntime | max(可能值vruntime, 原值) | -| 非NULL | NULL | curr->vruntime | max(可能值vruntime, 原值) | -| 非NULL | 非NULL | min(curr->vruntime, rb_leftmost->se->vruntime) | max(可能值vruntime, 原值) | - - -#红黑树的键值entity_key和entity_before -------- - - -完全公平调度调度器CFS的真正关键点是, 红黑树的排序过程是进程的vruntime来进行计算的, 准确的来说同一个就绪队列所有进程(或者调度实体)依照其键值se->vruntime - cfs_rq->min_vruntime进行排序. - -键值通过entity_key计算, 该函数在linux-2.6之中被定义, 但是后来的内核中移除了这个函数, 但是我们今天仍然讲解它, 因为它对我们理解CFS调度器和虚拟时钟vruntime有很多帮助, 我们也会讲到为什么这么有用的一个函数会被移除 - -我们可以在早期的linux-2.6.30(仅有[entity_key函数](http://lxr.linux.no/linux+v2.6.30/kernel/sched_fair.c#L269))和linux-2.6.32(定义了[entity_key](http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L283)和[entity_befire函数](http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L277))来查看 -```c -static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se) -{ - return se->vruntime - cfs_rq->min_vruntime; -} -``` - -键值较小的结点, 在CFS红黑树中排序的位置就越靠左, 因此也更快地被调度. 用这种方法, 内核实现了下面两种对立的机制 - -* 在程序运行时, 其vruntime稳定地增加, 他在红黑树中总是向右移动的. - - 因为越重要的进程vruntime增加的越慢, 因此他们向右移动的速度也越慢, 这样其被调度的机会要大于次要进程, 这刚好是我们需要的 - -* 如果进程进入睡眠, 则其vruntime保持不变. 因为每个队列min_vruntime同时会单调增加, 那么当进程从睡眠中苏醒, 在红黑树中的位置会更靠左, 因为其键值相对来说变得更小了. - - -好了我们了解了entity_key计算了红黑树的键值, 他作为CFS对红黑树中结点的排序依据. 但是在新的内核中entity_key函数却早已消失不见, 这是为什么呢? - -[sched: Replace use of entity_key](http://lkml.iu.edu/hypermail/linux/kernel/1107.2/01692.html) - -[sched: Replace use of entity_key](http://marc.info/?l=linux-kernel&m=131127311326308) - -我们在[linux-2.6.32的kernel/sched_fair.c](http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L269)中搜索entity_key函数关键字, 会发现内核仅在__enqueue_entity(定义在[linux-2.6.32的kernel/sched_fair.c, line 309](http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L309))函数中使用了entity_key函数用来比较两个调度实体的虚拟时钟键值的大小 - -即相当于如下代码 - -```c -if (entity_key(cfs_rq, se) < entity_key(cfs_rq, entry)) - -等价于 -if (se->vruntime-cfs_rq->min_vruntime < entry->vruntime-cfs_rq->min_vruntime) - -进一步化简为 - -if (se->vruntime < entry->vruntime) -```` - -即整个过程等价于比较两个调度实体vruntime值得大小 - -因此内核定义了函数entity_before来实现此功能, 函数定义在[linux+v2.6.32/kernel/sched_fair.c, line 269](http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L269), 在我们新的linux-4.6内核中定义在[kernel/sched/fair.c, line 452](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L452) - -```c -static inline int entity_before(struct sched_entity *a, - struct sched_entity *b) -{ - return (s64)(a->vruntime - b->vruntime) < 0; -} -``` - -# 延迟跟踪(调度延迟)与虚拟时间在调度实体内部的再分配 -------- - -## 调度延迟与其控制字段 -------- - -内核有一个固定的概念, 称之为良好的**调度延迟**, 即保证每个可运行的进程都应该至少运行一次的某个时间间隔. 它在sysctl_sched_latency给出, 可通过/proc/sys/kernel/sched_latency_ns控制, 默认值为20000000纳秒, 即20毫秒. - -第二个控制参数sched_nr_latency, 控制在一个**延迟周期中处理的最大活动进程数目**. 如果挥动进程的数目超过该上限, 则延迟周期也成比例的线性扩展.sched_nr_latency可以通过sysctl_sched_min_granularity间接的控制, 后者可通过/procsys/kernel/sched_min_granularity_ns设置. 默认值是4000000纳秒, 即4毫秒, 每次sysctl_sched_latency/sysctl_sched_min_granularity之一改变时, 都会重新计算sched_nr_latency. - -__sched_period确定延**迟周期的长度**, 通常就是sysctl_sched_latency, 但如果有更多的进程在运行, 其值有可能按比例线性扩展. 在这种情况下, 周期长度是 - -__sched_period = sysctl_sched_latency * nr_running / sched_nr_latency - - -## 虚拟时间在调度实体内的分配 - -调度实体是内核进行调度的基本实体单位, 其可能包含一个或者多个进程, 那么调度实体分配到的虚拟运行时间, 需要在内部对各个进程进行再次分配. - -通过考虑各个进程的相对权重, 将一个延迟周期的时间在活动进程之前进行分配. 对于由某个调度实体标识的给定进程, 分配到的时间通过sched_slice函数来分配, 其实现在[kernel/sched/fair.c, line 626](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L626), 计算方式如下 - -```c -/* - * We calculate the wall-time slice from the period by taking a part - * proportional to the weight. - * - * s = p*P[w/rw] - */ -static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se) -{ - u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq); - - for_each_sched_entity(se) { - struct load_weight *load; - struct load_weight lw; - - cfs_rq = cfs_rq_of(se); - load = &cfs_rq->load; - - if (unlikely(!se->on_rq)) { - lw = cfs_rq->load; - - update_load_add(&lw, se->load.weight); - load = &lw; - } - slice = __calc_delta(slice, se->load.weight, load); - } - return slice; -} -``` -回想一下子, 就绪队列的负荷权重是队列是那个所有活动进程负荷权重的总和, 结果时间段是按实际时间给出的, 但内核有时候也需要知道等价的虚拟时间, 该功能通过sched_vslice函数来实现, 其定义在[kernel/sched/fair.c, line 626](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L626) - -```c -/* - * We calculate the vruntime slice of a to-be-inserted task. - * - * vs = s/w - */ -static u64 sched_vslice(struct cfs_rq *cfs_rq, struct sched_entity *se) -{ - return calc_delta_fair(sched_slice(cfs_rq, se), se); -} -``` - -相对于权重weight的进程来说, 其实际时间段time相对应的虚拟时间长度为 - -time * NICE_0_LOAD / weight - -该公式通过calc_delta_fair函数计算, 在sched_vslice函数中也被用来转换分配到的延迟时间间隔. - -#总结 -------- - - -**CFS调度算法的思想** - -理想状态下每个进程都能获得相同的时间片,并且同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。也就是说,当一个进程占用CPU时,其他进程就必须等待。CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度. - -**虚拟时钟是红黑树排序的依据** - -具体实现时,CFS通过每个进程的**虚拟运行时间(vruntime)**来衡量哪个进程最值得被调度. CFS中的就绪队列是一棵以vruntime为键值的红黑树,虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的vruntime最小. - -**优先级计算负荷权重, 负荷权重和当前时间计算出虚拟运行时间** - -虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。而,CFS调度器中的权重在内核是对用户态进程的优先级nice值, 通过prio_to_weight数组进行nice值和权重的转换而计算出来的 - - -**虚拟时钟相关公式** - - linux内核采用了计算公式: - -| 属性 | 公式 | 描述 | -|:-------:|:-------:| -| ideal_time | sum_runtime *se.weight/cfs_rq.weight | 每个进程应该运行的时间 | -| sum_exec_runtime | | 运行队列中所有任务运行完一遍的时间 | -| se.weight | | 当前进程的权重 | -| cfs.weight | | 整个cfs_rq的总权重 | - -这里se.weight和cfs.weight根据上面讲解我们可以算出, sum_runtime是怎们计算的呢,linux内核中这是个经验值,其经验公式是 - -| 条件 | 公式 | -|:-------:|:-------:| -| 进程数 > sched_nr_latency | sum_runtime=sysctl_sched_min_granularity *nr_running | -| 进程数 <=sched_nr_latency | sum_runtime=sysctl_sched_latency = 20ms | - ->注:sysctl_sched_min_granularity =4ms -> ->sched_nr_latency是内核在一个延迟周期中处理的最大活动进程数目 - -linux内核代码中是通过一个叫vruntime的变量来实现上面的原理的,即: - -每一个进程拥有一个vruntime,每次需要调度的时候就选运行队列中拥有最小vruntime的那个进程来运行,vruntime在时钟中断里面被维护,每次时钟中断都要更新当前进程的vruntime,即vruntime以如下公式逐渐增长: - - -| 条件 | 公式 | -|:-------:|:-------:| -| curr.nice!=NICE_0_LOAD | vruntime += delta * NICE_0_LOAD/se.weight; | -| curr.nice=NICE_0_LOAD | vruntime += delta; | - - +Linux CFS调度器之虚拟时钟与调度延迟 +======= + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-07-29 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/details/51456569) | + + + +CFS负责处理普通非实时进程, 这类进程是我们linux中最普遍的进程, 今天我们把注意力转向CFS的虚拟时钟 + + +#1 前景回顾 +------- + +##1.1 CFS调度器类 +------- + +Linux内核使用CFS是来调度我们最常见的普通进程, 其所属调度器类为fair_sched_class, 使用的调度策略包括SCHED_NORMAL和SCHED_BATCH, 进程task_struct中struct sched_entity se;字段标识的就是CFS调度器类的调度实体. + + +##1.2 进程的优先级 +------- + +前面我们详细的了解了linux下进程优先级的表示以及其计算的方法, 我们了解到linux针对普通进程和实时进程分别使用静态优先级static_prio和实时优先级rt_priority来指定其默认的优先级别, 然后通过normal_prio函数将他们分别转换为普通优先级normal_prio, 最终换算出动态优先级prio, 动态优先级prio才是内核调度时候有限考虑的优先级字段 + +##1.3 CFS调度的普通进程的负荷权重 +------- + +但是CFS完全公平调度器在调度进程的时候, 进程的重要性不仅是由优先级指定的, 而且还需要考虑保存在task_struct->se.load的负荷权重. + +##1.4 CFS算法的基本思想 +------- + +简单说一下CFS调度算法的思想:理想状态下每个进程都能获得相同的时间片,并且同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。 也就是说,当一个进程占用CPU时,其他进程就必须等待。 + + + +#2 虚拟运行时间(今日内容提醒) +------- + + +##2.1 虚拟运行时间的引入 +------- + +CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度。 + + +具体实现时,CFS通过每个进程的虚拟运行时间(vruntime)来衡量哪个进程最值得被调度。 + +CFS中的就绪队列是一棵以vruntime为键值的红黑树,虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的vruntime最小 + +虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。 + +在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。 +那么,在用户态进程的优先级nice值与CFS调度器中的权重又有什么关系?在内核中通过prio_to_weight数组进行nice值和权重的转换。 + + +##2.2 CFS虚拟时钟 +------- + + +完全公平调度算法CFS依赖于虚拟时钟, 用以度量等待进程在完全公平系统中所能得到的CPU时间. 但是数据结构中任何地方都没有找到虚拟时钟. 这个是由于所有的必要信息都可以根据现存的实际时钟和每个进程相关的负荷权重推算出来. + +假设现在系统有A,B,C三个进程,A.weight=1,B.weight=2,C.weight=3.那么我们可以计算出整个公平调度队列的总权重是cfs_rq.weight = 6,很自然的想法就是,公平就是你在重量中占的比重的多少来拍你的重要性,那么,A的重要性就是1/6,同理,B和C的重要性分别是2/6,3/6.很显然C最重要就应改被先调度,而且占用的资源也应该最多,即假设A,B,C运行一遍的总时间假设是6个时间单位的话,A占1个单位,B占2个单位,C占三个单位。这就是CFS的公平策略. + +**CFS调度算法的思想**:理想状态下每个进程都能获得相同的时间片,并且同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。也就是说,当一个进程占用CPU时,其他进程就必须等待。CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度. + +具体实现时,CFS通过每个进程的**虚拟运行时间(vruntime)**来衡量哪个进程最值得被调度. CFS中的就绪队列是一棵以vruntime为键值的红黑树,虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的vruntime最小. + + +虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。而,CFS调度器中的权重在内核是对用户态进程的优先级nice值, 通过prio_to_weight数组进行nice值和权重的转换而计算出来的 + +#3 虚拟时钟相关的数据结构 +------- + +##3.1 调度实体的虚拟时钟信息 +------- + +为了实现完全公平调度,内核引入了虚拟时钟(virtual clock)的概念,实际上我觉得这个虚拟时钟为什叫虚拟的,是因为这个时钟与具体的时钟晶振没有关系,他只不过是为了公平分配CPU时间而提出的一种时间量度,它与进程的权重有关,这里就知道权重的作用了,权重越高,说明进程的优先级比较高,进而该进程虚拟时钟增长的就慢 + +既然虚拟时钟是用来衡量调度实体(一个或者多个进程)的一种时间度量, 因此必须在调度实体中存储其虚拟时钟的信息 + +```c +struct sched_entity +{ + struct load_weight load; /* for load-balancing负荷权重,这个决定了进程在CPU上的运行时间和被调度次数 */ + struct rb_node run_node; + unsigned int on_rq; /* 是否在就绪队列上 */ + + u64 exec_start; /* 上次启动的时间*/ + + u64 sum_exec_runtime; + u64 vruntime; + u64 prev_sum_exec_runtime; + /* rq on which this entity is (to be) queued: */ + struct cfs_rq *cfs_rq; + ... +}; +``` + + +**sum_exec_runtime**是用于记录该进程的CPU消耗时间,这个是真实的CPU消耗时间。在进程撤销时会将sum_exec_runtime保存到**prev_sum_exec_runtime**中 + +**vruntime**是本进程生命周期中在CPU上运行的虚拟时钟。那么何时应该更新这些时间呢?这是通过调用**update_curr**实现的, 该函数在多处调用. + + +##3.2 就绪队列上的虚拟时钟信息 +------- + +完全公平调度器类sched_fair_class主要负责管理普通进程, 在全局的CPU就读队列上存储了在CFS的就绪队列struct cfs_rq cfs + +进程的就绪队列中就存储了CFS相关的虚拟运行时钟的信息, struct cfs_rq定义如下: + +```c +struct cfs_rq +{ + struct load_weight load; /*所有进程的累计负荷值*/ + unsigned long nr_running; /*当前就绪队列的进程数*/ + + // ======================== + u64 min_vruntime; // 队列的虚拟时钟, + // ======================= + struct rb_root tasks_timeline; /*红黑树的头结点*/ + struct rb_node *rb_leftmost; /*红黑树的最左面节点*/ + + struct sched_entity *curr; /*当前执行进程的可调度实体*/ + ... +}; +``` + + + +#4 update_curr函数计算进程虚拟时间 +------- + +所有与虚拟时钟有关的计算都在update_curr中执行, 该函数在系统中各个不同地方调用, 包括周期性调度器在内. + +update_curr的流程如下 + +* 首先计算进程当前时间与上次启动时间的差值 + +* 通过负荷权重和当前时间模拟出进程的虚拟运行时钟 + +* 重新设置cfs的min_vruntime保持其单调性 + + +##4.1 计算时间差 +------- + +首先, 该函数确定就绪队列的当前执行进程, 并获取主调度器就绪队列的实际时钟值, 该值在每个调度周期都会更新 + +```c +/* 确定就绪队列的当前执行进程curr */ +struct sched_entity *curr = cfs_rq->curr; +``` + +其中辅助函数[rq_of](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L248)用于确定与CFS就绪队列相关的struct rq实例, 其定义在[kernel/sched/fair.c, line 248](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L248) + +cfs_rq就绪队列中存储了指向就绪队列的实例,参见[kernel/sched/sched.h, line412](http://lxr.free-electrons.com/source/kernel/sched/sched.h#L412), 而rq_of就返回了这个指向rq的指针, rq_of定义在[kernel/sched/fair.c, line 249](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L249) + + +[rq_clock_task](http://lxr.free-electrons.com/source/kernel/sched/sched.h#L735)函数返回了运行队列的clock_task成员. + +```c +/* rq_of -=> return cfs_rq->rq 返回cfs队列所在的全局就绪队列 +* rq_clock_task返回了rq的clock_task */ +u64 now = rq_clock_task(rq_of(cfs_rq)); +u64 delta_exec; +``` + +如果就队列上没有进程在执行, 则显然无事可做, 否则内核计算当前和上一次更新负荷权重时两次的时间的差值 + +```c +/* 如果就队列上没有进程在执行, 则显然无事可做 */ +if (unlikely(!curr)) + return; + +/* 内核计算当前和上一次更新负荷权重时两次的时间的差值 */ +delta_exec = now - curr->exec_start; +if (unlikely((s64)delta_exec <= 0)) + return; +``` + +然后重新更新更新启动时间exec_start为now, 以备下次计算时使用 + +最后将计算出的时间差, 加到了先前的统计时间上 + +```c + /* 重新更新启动时间exec_start为now */ + curr->exec_start = now; + + schedstat_set(curr->statistics.exec_max, + max(delta_exec, curr->statistics.exec_max)); + + /* 将时间差加到先前统计的时间即可 */ + curr->sum_exec_runtime += delta_exec; + schedstat_add(cfs_rq, exec_clock, delta_exec); +``` + +##4.2 模拟虚拟时钟 +------- + + +有趣的事情是如何使用给出的信息来模拟不存在的虚拟时钟. 这一次内核的实现仍然是非常巧妙地, 针对最普通的情形节省了一些时间. 对于运行在nice级别0的进程来说, 根据定义虚拟时钟和物理时间相等. 在使用不同的优先级时, 必须根据进程的负荷权重重新衡定时间 + +```c + curr->vruntime += calc_delta_fair(delta_exec, curr); + update_min_vruntime(cfs_rq); + +``` + +其中calc_delta_fair函数是计算的关键 + +```c +// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L596 +/* + * delta /= w + */ +static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se) +{ + if (unlikely(se->load.weight != NICE_0_LOAD)) + delta = __calc_delta(delta, NICE_0_LOAD, &se->load); + + return delta; +} +``` + +忽略舍入和溢出检查, calc_delta_fair函数所做的就是根据下列公式计算: + + +$$delta =delta \times \dfrac{NICE\_0\_LOAD}{curr->se->load.weight}$$ + + +每一个进程拥有一个vruntime, 每次需要调度的时候就选运行队列中拥有最小vruntime的那个进程来运行, vruntime在时钟中断里面被维护, 每次时钟中断都要更新当前进程的vruntime, 即vruntime以如下公式逐渐增长 + +那么`curr->vruntime += calc_delta_fair(delta_exec, curr);` 即相当于如下操作 + + + + +| 条件 | 公式 | +|:-------:|:-------:| +| curr.nice != NICE_0_LOAD | $curr->vruntime += delta\_exec \times \dfrac{NICE\_0\_LOAD}{curr->se->load.weight}$| +| curr.nice == NICE_0_LOAD | $ curr->vruntime += delta $ | + + +在该计算中可以派上用场了, 回想一下子, 可知越重要的进程会有越高的优先级(即, 越低的nice值), 会得到更大的权重, 因此累加的虚拟运行时间会小一点, + +根据公式可知, nice = 0的进程(优先级120), 则虚拟时间和物理时间是相等的, 即current->se->load.weight等于NICE_0_LAD的情况. + +##4.3 重新设置cfs_rq->min_vruntime +------- + +接着内核需要重新设置`min_vruntime`. 必须小心保证该值是单调递增的, 通过update_min_vruntime函数来设置 + +```c +// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L457 + +static void update_min_vruntime(struct cfs_rq *cfs_rq) +{ + /* 初始化vruntime的值, 相当于如下的代码 + if (cfs_rq->curr != NULL) + vruntime = cfs_rq->curr->vruntime; + else + vruntime = cfs_rq->min_vruntime; + */ + u64 vruntime = cfs_rq->min_vruntime; + + if (cfs_rq->curr) + vruntime = cfs_rq->curr->vruntime; + + + /* 检测红黑树是都有最左的节点, 即是否有进程在树上等待调度 + * cfs_rq->rb_leftmost(struct rb_node *)存储了进程红黑树的最左节点 + * 这个节点存储了即将要被调度的结点 + * */ + if (cfs_rq->rb_leftmost) + { + /* 获取最左结点的调度实体信息se, se中存储了其vruntime + * rb_leftmost的vruntime即树中所有节点的vruntiem中最小的那个 */ + struct sched_entity *se = rb_entry(cfs_rq->rb_leftmost, + struct sched_entity, + run_node); + /* 如果就绪队列上没有curr进程 + * 则vruntime设置为树种最左结点的vruntime + * 否则设置vruntiem值为cfs_rq->curr->vruntime和se->vruntime的最小值 + */ + if (!cfs_rq->curr) /* 此时vruntime的原值为cfs_rq->min_vruntime*/ + vruntime = se->vruntime; + else /* 此时vruntime的原值为cfs_rq->curr->vruntime*/ + vruntime = min_vruntime(vruntime, se->vruntime); + } + + /* ensure we never gain time by being placed backwards. + * 为了保证min_vruntime单调不减 + * 只有在vruntime超出的cfs_rq->min_vruntime的时候才更新 + */ + cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime); +#ifndef CONFIG_64BIT + smp_wmb(); + cfs_rq->min_vruntime_copy = cfs_rq->min_vruntime; +#endif +} +``` +我们通过分析update_min_vruntime函数设置cfs_rq->min_vruntime的流程如下 + +* 首先检测cfs就绪队列上是否有活动进程curr, 以此设置vruntime的值 + 如果cfs就绪队列上没有活动进程curr, 就设置vruntime为curr->vruntime; + 否则又活动进程就设置为vruntime为cfs_rq的原min_vruntime; + +* 接着检测cfs的红黑树上是否有最左节点, 即等待被调度的节点, 重新设置vruntime的值为curr进程和最左进程rb_leftmost的vruntime较小者的值 + +* 为了保证min_vruntime单调不减, 只有在vruntime超出的cfs_rq->min_vruntime的时候才更新 + +update_min_vruntime依据当前进程和待调度的进程的vruntime值, 设置出一个可能的vruntime值, 但是只有在这个可能的vruntime值大于就绪队列原来的min_vruntime的时候, 才更新就绪队列的min_vruntime, 利用该策略, 内核确保min_vruntime只能增加, 不能减少. + +update_min_vruntime函数的流程等价于如下的代码 + +```c +// 依据curr进程和待调度进程rb_leftmost找到一个可能的最小vruntime值 +if (cfs_rq->curr != NULL cfs_rq->rb_leftmost == NULL) + vruntime = cfs_rq->curr->vruntime; +else if(cfs_rq->curr == NULL && cfs_rq->rb_leftmost != NULL) + vruntime = cfs_rq->rb_leftmost->se->vruntime; +else if (cfs_rq->curr != NULL cfs_rq->rb_leftmost != NULL) + vruntime = min(cfs_rq->curr->vruntime, cfs_rq->rb_leftmost->se->vruntime); +else if(cfs_rq->curr == NULL cfs_rq->rb_leftmost == NULL) + vruntime = cfs_rq->min_vruntime; + +// 每个队列的min_vruntime只有被树上某个节点的vruntime((curr和程rb_leftmost两者vruntime的较小值)超出时才更新 +cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime); +``` + +其中寻找可能vruntime的策略我们采用表格的形式可能更加直接 + + +| 活动进程curr | 待调度进程rb_leftmost | 可能的vruntime值 | cfs_rq | +| ------- |:-------:|:-------:| +| NULL | NULL | cfs_rq->min_vruntime | 维持原值 | +| NULL | 非NULL | rb_leftmost->se->vruntime | max(可能值vruntime, 原值) | +| 非NULL | NULL | curr->vruntime | max(可能值vruntime, 原值) | +| 非NULL | 非NULL | min(curr->vruntime, rb_leftmost->se->vruntime) | max(可能值vruntime, 原值) | + + +#5 红黑树的键值entity_key和entity_before +------- + + +完全公平调度调度器CFS的真正关键点是, 红黑树的排序过程是进程的vruntime来进行计算的, 准确的来说同一个就绪队列所有进程(或者调度实体)依照其键值se->vruntime - cfs_rq->min_vruntime进行排序. + +键值通过entity_key计算, 该函数在linux-2.6之中被定义, 但是后来的内核中移除了这个函数, 但是我们今天仍然讲解它, 因为它对我们理解CFS调度器和虚拟时钟vruntime有很多帮助, 我们也会讲到为什么这么有用的一个函数会被移除 + +我们可以在早期的linux-2.6.30(仅有[entity_key函数](http://lxr.linux.no/linux+v2.6.30/kernel/sched_fair.c#L269))和linux-2.6.32(定义了[entity_key](http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L283)和[entity_befire函数](http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L277))来查看 +```c +static inline s64 entity_key(struct cfs_rq *cfs_rq, struct sched_entity *se) +{ + return se->vruntime - cfs_rq->min_vruntime; +} +``` + +键值较小的结点, 在CFS红黑树中排序的位置就越靠左, 因此也更快地被调度. 用这种方法, 内核实现了下面两种对立的机制 + +* 在程序运行时, 其vruntime稳定地增加, 他在红黑树中总是向右移动的. + + 因为越重要的进程vruntime增加的越慢, 因此他们向右移动的速度也越慢, 这样其被调度的机会要大于次要进程, 这刚好是我们需要的 + +* 如果进程进入睡眠, 则其vruntime保持不变. 因为每个队列min_vruntime同时会单调增加, 那么当进程从睡眠中苏醒, 在红黑树中的位置会更靠左, 因为其键值相对来说变得更小了. + + +好了我们了解了entity_key计算了红黑树的键值, 他作为CFS对红黑树中结点的排序依据. 但是在新的内核中entity_key函数却早已消失不见, 这是为什么呢? + +[sched: Replace use of entity_key](http://lkml.iu.edu/hypermail/linux/kernel/1107.2/01692.html) + +[sched: Replace use of entity_key](http://marc.info/?l=linux-kernel&m=131127311326308) + +我们在[linux-2.6.32的kernel/sched_fair.c](http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L269)中搜索entity_key函数关键字, 会发现内核仅在__enqueue_entity(定义在[linux-2.6.32的kernel/sched_fair.c, line 309](http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L309))函数中使用了entity_key函数用来比较两个调度实体的虚拟时钟键值的大小 + +即相当于如下代码 + +```c +if (entity_key(cfs_rq, se) < entity_key(cfs_rq, entry)) + +等价于 +if (se->vruntime-cfs_rq->min_vruntime < entry->vruntime-cfs_rq->min_vruntime) + +进一步化简为 + +if (se->vruntime < entry->vruntime) +``` + +即整个过程等价于比较两个调度实体vruntime值得大小 + +因此内核定义了函数entity_before来实现此功能, 函数定义在[linux+v2.6.32/kernel/sched_fair.c, line 269](http://lxr.linux.no/linux+v2.6.32/kernel/sched_fair.c#L269), 在我们新的linux-4.6内核中定义在[kernel/sched/fair.c, line 452](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L452) + +```c +static inline int entity_before(struct sched_entity *a, + struct sched_entity *b) +{ + return (s64)(a->vruntime - b->vruntime) < 0; +} +``` + +#6 延迟跟踪(调度延迟)与虚拟时间在调度实体内部的再分配 +------- + +##6.1 调度延迟与其控制字段 +------- + +内核有一个固定的概念, 称之为良好的**调度延迟**, 即保证每个可运行的进程都应该至少运行一次的某个时间间隔. 它在sysctl_sched_latency给出, 可通过/proc/sys/kernel/sched_latency_ns控制, 默认值为20000000纳秒, 即20毫秒. + +第二个控制参数sched_nr_latency, 控制在一个**延迟周期中处理的最大活动进程数目**. 如果挥动进程的数目超过该上限, 则延迟周期也成比例的线性扩展.sched_nr_latency可以通过sysctl_sched_min_granularity间接的控制, 后者可通过/procsys/kernel/sched_min_granularity_ns设置. 默认值是4000000纳秒, 即4毫秒, 每次sysctl_sched_latency/sysctl_sched_min_granularity之一改变时, 都会重新计算sched_nr_latency. + +__sched_period确定延**迟周期的长度**, 通常就是sysctl_sched_latency, 但如果有更多的进程在运行, 其值有可能按比例线性扩展. 在这种情况下, 周期长度是 + +__sched_period = sysctl_sched_latency * nr_running / sched_nr_latency + + +##6.2 虚拟时间在调度实体内的分配 +------- + +调度实体是内核进行调度的基本实体单位, 其可能包含一个或者多个进程, 那么调度实体分配到的虚拟运行时间, 需要在内部对各个进程进行再次分配. + +通过考虑各个进程的相对权重, 将一个延迟周期的时间在活动进程之前进行分配. 对于由某个调度实体标识的给定进程, 分配到的时间通过sched_slice函数来分配, 其实现在[kernel/sched/fair.c, line 626](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L626), 计算方式如下 + +```c +/* + * We calculate the wall-time slice from the period by taking a part + * proportional to the weight. + * + * s = p*P[w/rw] + */ +static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se) +{ + u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq); + + for_each_sched_entity(se) { + struct load_weight *load; + struct load_weight lw; + + cfs_rq = cfs_rq_of(se); + load = &cfs_rq->load; + + if (unlikely(!se->on_rq)) { + lw = cfs_rq->load; + + update_load_add(&lw, se->load.weight); + load = &lw; + } + slice = __calc_delta(slice, se->load.weight, load); + } + return slice; +} +``` + + +回想一下子, 就绪队列的负荷权重是队列是那个所有活动进程负荷权重的总和, 结果时间段是按实际时间给出的, 但内核有时候也需要知道等价的虚拟时间, 该功能通过sched_vslice函数来实现, 其定义在[kernel/sched/fair.c, line 626](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L626) + + +```c +/* + * We calculate the vruntime slice of a to-be-inserted task. + * + * vs = s/w + */ +static u64 sched_vslice(struct cfs_rq *cfs_rq, struct sched_entity *se) +{ + return calc_delta_fair(sched_slice(cfs_rq, se), se); +} +``` + + +相对于权重weight的进程来说, 其实际时间段time相对应的虚拟时间长度为 + +time * NICE_0_LOAD / weight + +该公式通过calc_delta_fair函数计算, 在sched_vslice函数中也被用来转换分配到的延迟时间间隔. + + +#7 总结 +------- + + +**CFS调度算法的思想** + +理想状态下每个进程都能获得相同的时间片,并且同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。也就是说,当一个进程占用CPU时,其他进程就必须等待。CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度. + +**虚拟时钟是红黑树排序的依据** + +具体实现时,CFS通过每个进程的**虚拟运行时间(vruntime)**来衡量哪个进程最值得被调度. CFS中的就绪队列是一棵以vruntime为键值的红黑树,虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的vruntime最小. + +**优先级计算负荷权重, 负荷权重和当前时间计算出虚拟运行时间** + +虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。而,CFS调度器中的权重在内核是对用户态进程的优先级nice值, 通过prio_to_weight数组进行nice值和权重的转换而计算出来的 + + +**虚拟时钟相关公式** + + linux内核采用了计算公式: + +| 属性 | 公式 | 描述 | +|:-------:|:-------:| +| ideal_time | sum_runtime *se.weight/cfs_rq.weight | 每个进程应该运行的时间 | +| sum_exec_runtime | | 运行队列中所有任务运行完一遍的时间 | +| se.weight | | 当前进程的权重 | +| cfs.weight | | 整个cfs_rq的总权重 | + +这里se.weight和cfs.weight根据上面讲解我们可以算出, sum_runtime是怎们计算的呢,linux内核中这是个经验值,其经验公式是 + +| 条件 | 公式 | +|:-------:|:-------:| +| 进程数 > sched_nr_latency | sum_runtime=sysctl_sched_min_granularity *nr_running | +| 进程数 <=sched_nr_latency | sum_runtime=sysctl_sched_latency = 20ms | + +>注:sysctl_sched_min_granularity =4ms +> +>sched_nr_latency是内核在一个延迟周期中处理的最大活动进程数目 + +linux内核代码中是通过一个叫vruntime的变量来实现上面的原理的,即: + +每一个进程拥有一个vruntime,每次需要调度的时候就选运行队列中拥有最小vruntime的那个进程来运行,vruntime在时钟中断里面被维护,每次时钟中断都要更新当前进程的vruntime,即vruntime以如下公式逐渐增长: + + +| 条件 | 公式 | +|:-------:|:-------:| +| curr.nice!=NICE_0_LOAD | vruntime += delta * NICE_0_LOAD/se.weight; | +| curr.nice=NICE_0_LOAD | vruntime += delta; | + + diff --git a/study/kernel/01-process/05-schedule/07-cfs/04-queue/README.md b/study/kernel/01-process/05-schedule/07-cfs/04-queue/README.md index ea4d447..a7bc818 100644 --- a/study/kernel/01-process/05-schedule/07-cfs/04-queue/README.md +++ b/study/kernel/01-process/05-schedule/07-cfs/04-queue/README.md @@ -1,562 +1,536 @@ -Linux CFS调度器之队列操作 -======= - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-29 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/details/51456569) | - - - -CFS负责处理普通非实时进程, 这类进程是我们linux中最普遍的进程 - - -#1 前景回顾 -------- - -##1.1 CFS调度算法 -------- - -**CFS调度算法的思想** - -理想状态下每个进程都能获得相同的时间片,并且同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。也就是说,当一个进程占用CPU时,其他进程就必须等待。CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度. - -## 负荷权重和虚拟时钟 - -**虚拟时钟是红黑树排序的依据** - -具体实现时,CFS通过每个进程的**虚拟运行时间(vruntime)**来衡量哪个进程最值得被调度. CFS中的就绪队列是一棵以vruntime为键值的红黑树,虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的vruntime最小. - -**优先级计算负荷权重, 负荷权重和当前时间计算出虚拟运行时间** - -虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。而,CFS调度器中的权重在内核是对用户态进程的优先级nice值, 通过prio_to_weight数组进行nice值和权重的转换而计算出来的 - - -**虚拟时钟相关公式** - - linux内核采用了计算公式: - -| 属性 | 公式 | 描述 | -|:-------:|:-------:| -| ideal_time | sum_runtime *se.weight/cfs_rq.weight | 每个进程应该运行的时间 | -| sum_exec_runtime | | 运行队列中所有任务运行完一遍的时间 | -| se.weight | | 当前进程的权重 | -| cfs.weight | | 整个cfs_rq的总权重 | - -这里se.weight和cfs.weight根据上面讲解我们可以算出, sum_runtime是怎们计算的呢,linux内核中这是个经验值,其经验公式是 - -| 条件 | 公式 | -|:-------:|:-------:| -| 进程数 > sched_nr_latency | sum_runtime=sysctl_sched_min_granularity *nr_running | -| 进程数 <=sched_nr_latency | sum_runtime=sysctl_sched_latency = 20ms | - ->注:sysctl_sched_min_granularity =4ms -> ->sched_nr_latency是内核在一个延迟周期中处理的最大活动进程数目 - -linux内核代码中是通过一个叫vruntime的变量来实现上面的原理的,即: - -每一个进程拥有一个vruntime,每次需要调度的时候就选运行队列中拥有最小vruntime的那个进程来运行,vruntime在时钟中断里面被维护,每次时钟中断都要更新当前进程的vruntime,即vruntime以如下公式逐渐增长: - - -| 条件 | 公式 | -|:-------:|:-------:| -| curr.nice!=NICE_0_LOAD | vruntime += delta* NICE_0_LOAD/se.weight; | -| curr.nice=NICE_0_LOAD | vruntime += delta; | - - -##1.2 今日内容--CFS进程入队和出队 -------- - - -完全公平调度器CFS中有两个函数可用来增删队列的成员:enqueue_task_fair和dequeue_task_fair分别用来向CFS就绪队列中添加或者删除进程 - - -#2 enqueue_task_fair入队操作 -------- - -##2.1 enque_task_fair函数 -------- - -向就绪队列中放置新进程的工作由函数enqueue_task_fair函数完成, 该函数定义在[kernel/sched/fair.c, line 5442](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5442), 其函数原型如下 - -该函数将task_struct *p所指向的进程插入到rq所在的就绪队列中, 除了指向所述的就绪队列rq和task_struct的指针外, 该函数还有另外一个参数wakeup. 这使得可以指定入队的进程是否最近才被唤醒并转换为运行状态(此时需指定wakeup = 1), 还是此前就是可运行的(那么wakeup = 0). - -```c -static void -enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags) -``` -enqueue_task_fair的执行流程如下 - -* 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做. - -* 否则, 具体的工作委托给enqueue_entity完成, 其中内核会借机用update_curr更新统计量 - 在enqueue_entity内部如果需要会调用__enqueue_entity将进程插入到CFS红黑树中合适的结点 - - - -##2.2 enque_task_fair完全函数 -------- - - -```c -/* - * The enqueue_task method is called before nr_running is - * increased. Here we update the fair scheduling stats and - * then put the task into the rbtree: - */ -static void -enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags) -{ - struct cfs_rq *cfs_rq; - struct sched_entity *se = &p->se; - - for_each_sched_entity(se) { - if (se->on_rq) - break; - cfs_rq = cfs_rq_of(se); - enqueue_entity(cfs_rq, se, flags); - - /* - * end evaluation on encountering a throttled cfs_rq - * - * note: in the case of encountering a throttled cfs_rq we will - * post the final h_nr_running increment below. - */ - if (cfs_rq_throttled(cfs_rq)) - break; - cfs_rq->h_nr_running++; - - flags = ENQUEUE_WAKEUP; - } - - for_each_sched_entity(se) { - cfs_rq = cfs_rq_of(se); - cfs_rq->h_nr_running++; - - if (cfs_rq_throttled(cfs_rq)) - break; - - update_load_avg(se, 1); - update_cfs_shares(cfs_rq); - } - - if (!se) - add_nr_running(rq, 1); - - hrtick_update(rq); -} - -```` - - -##2.3 for_each_sched_entity -------- - -首先内核查找到待天机进程p所在的调度实体信息, 然后通过for_each_sched_entity循环所有调度实体, - -```c -// enqueue_task_fair函数 - struct cfs_rq *cfs_rq; - struct sched_entity *se = &p->se; - - for_each_sched_entity(se) - { - /* ...... */ - } -``` - -但是有个疑问是, 进程p所在的调度时提就一个为嘛要循环才能遍历啊, 这是因为为了支持组调度.组调度下调度实体是有层次结构的, 我们将进程加入的时候, 同时要更新其父调度实体的调度信息, 而非组调度情况下, 就不需要调度实体的层次结构 - -linux对组调度的支持可以通过CONFIG_FAIR_GROUP_SCHED来启用, 在启用和不启用的条件下, 内核对很多函数的实现也会因条件而异, 这点对for_each_sched_entity函数尤为明显, 参见[启用CONFIG_FAIR_GROUP_SCHED](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L246)和[不启用CONFIG_FAIR_GROUP_SCHED](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L367) - -```c -#ifdef CONFIG_FAIR_GROUP_SCHED - -/* An entity is a task if it doesn't "own" a runqueue */ -#define entity_is_task(se) (!se->my_q) - -// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L266 -/* Walk up scheduling entities hierarchy */ -#define for_each_sched_entity(se) \ - for (; se; se = se->parent) - - #else /* !CONFIG_FAIR_GROUP_SCHED */ - -#define entity_is_task(se) 1 - -// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L381 -#define for_each_sched_entity(se) \ - for (; se; se = NULL) -``` - -* 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做. - -* 否则, 具体的工作委托给enqueue_entity完成, 其中内核会借机用update_curr更新统计量. - -```c -// enqueue_task_fair函数 - /* 如果当前进程已经在就绪队列上 */ - if (se->on_rq) - break; - - /* 获取到当前进程所在的cfs_rq就绪队列 */ - cfs_rq = cfs_rq_of(se); - /* 内核委托enqueue_entity完成真正的插入工作 */ - enqueue_entity(cfs_rq, se, flags); -```` - - -##2.4 enqueue_entity插入进程 -------- - -enqueue_entity完成了进程真正的入队操作, 其具体流程如下所示 - -* 更新一些统计统计量, update_curr, update_cfs_shares等 - -* 如果进程此前是在睡眠状态, 则调用place_entity中首先会调整进程的虚拟运行时间 - -* 最后如果进程最近在运行, 其虚拟运行时间仍然有效, 那么则直接用__enqueue_entity加入到红黑树 - -首先如果进程最近正在运行, 其虚拟时间时间仍然有效, 那么(除非它当前在执行中)它可以直接用__enqueue_entity插入到红黑树, 该函数徐娅萍处理一些红黑树的机制, 这可以依靠内核的标准实现, 参见[__enqueue_entity函数, kernel/sched/fair.c, line483](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#483) - - - -```c -static void -enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) -{ - /* - * Update the normalized vruntime before updating min_vruntime - * through calling update_curr(). - * - * 如果当前进程之前已经是可运行状态不是被唤醒的那么其虚拟运行时间要增加 - */ - if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING)) - se->vruntime += cfs_rq->min_vruntime; - - /* - * Update run-time statistics of the 'current'. - * 更新进程的统计量信息 - */ - update_curr(cfs_rq); - enqueue_entity_load_avg(cfs_rq, se); - account_entity_enqueue(cfs_rq, se); - update_cfs_shares(cfs_rq); - - /* 如果当前进行之前在睡眠刚被唤醒 */ - if (flags & ENQUEUE_WAKEUP) - { - /* 调整进程的虚拟运行时间 */ - place_entity(cfs_rq, se, 0); - if (schedstat_enabled()) - enqueue_sleeper(cfs_rq, se); - } - - check_schedstat_required(); - if (schedstat_enabled()) { - update_stats_enqueue(cfs_rq, se); - check_spread(cfs_rq, se); - } - - /* 将进程插入到红黑树中 */ - if (se != cfs_rq->curr) - __enqueue_entity(cfs_rq, se); - se->on_rq = 1; - - if (cfs_rq->nr_running == 1) { - list_add_leaf_cfs_rq(cfs_rq); - check_enqueue_throttle(cfs_rq); - } -} -``` - -##2.5 place_entity处理睡眠进程 -------- - -如果进程此前在睡眠, 那么则调用place_entity处理其虚拟运行时间 - -设想一下子如果休眠进程的vruntime保持不变, 而其他运行进程的 vruntime一直在推进, 那么等到休眠进程终于唤醒的时候, 它的vruntime比别人小很多, 会使它获得长时间抢占CPU的优势, 其他进程就要饿死了. 这显然是另一种形式的不公平,因此CFS是这样做的:在休眠进程被唤醒时重新设置vruntime值,以min_vruntime值为基础,给予一定的补偿,但不能补偿太多. 这个重新设置其虚拟运行时间的工作就是就是通过place_entity来完成的, 另外新进程创建完成后, 也是通过place_entity完成其虚拟运行时间vruntime的设置的. place_entity通过其第三个参数initial来标识新进程创建和休眠进程苏醒两种不同情形的. - - - -place_entity函数定义在[kernel/sched/fair.c, line 3135](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L3135)中首先会调整进程的虚拟运行时间 - - -```c -// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3134 -static void -place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial) -{ - u64 vruntime = cfs_rq->min_vruntime; - - /* - * The 'current' period is already promised to the current tasks, - * however the extra weight of the new task will slow them down a - * little, place the new task so that it fits in the slot that - * stays open at the end. - * - * 如果是新进程第一次要入队, 那么就要初始化它的vruntime - * 一般就把cfsq的vruntime给它就可以 - * 但是如果当前运行的所有进程被承诺了一个运行周期 - * 那么则将新进程的vruntime后推一个他自己的slice - * 实际上新进程入队时要重新计算运行队列的总权值 - * 总权值显然是增加了,但是所有进程总的运行时期并不一定随之增加 - * 则每个进程的承诺时间相当于减小了,就是减慢了进程们的虚拟时钟步伐。 - */ - /* initial标识了该进程是新进程 */ - if (initial && sched_feat(START_DEBIT)) - vruntime += sched_vslice(cfs_rq, se); - - /* sleeps up to a single latency don't count. - * 休眠进程 */ - if (!initial) - { - /* 一个调度周期 */ - unsigned long thresh = sysctl_sched_latency; - - /* - * Halve their sleep time's effect, to allow - * for a gentler effect of sleepers: - */ - /* 若设了GENTLE_FAIR_SLEEPERS */ - if (sched_feat(GENTLE_FAIR_SLEEPERS)) - thresh >>= 1; /* 补偿减为调度周期的一半 */ - - vruntime -= thresh; - } - - /* ensure we never gain time by being placed backwards. - * 如果是唤醒已经存在的进程,则单调附值 - */ - se->vruntime = max_vruntime(se->vruntime, vruntime); -} -``` - -我们可以看到enqueue_task_fair调用place_entity传递的initial参数为0 - -```c -place_entity(cfs_rq, se, 0); -``` - -所以会执行if (!initial)后的语句。因为进程睡眠后,vruntime就不会增加了,当它醒来后不知道过了多长时间,可能vruntime已经比 min_vruntime小了很多,如果只是简单的将其插入到就绪队列中,它将拼命追赶min_vruntime,因为它总是在红黑树的最左面。如果这 样,它将会占用大量的CPU时间,导致红黑树右边的进程被饿死。但是我们又必须及时响应醒来的进程,因为它们可能有一些工作需要立刻处理,所以系统采取了 一种折衷的办法,将当前cfs_rq->min_vruntime时间减去sysctl_sched_latency赋给vruntime,这时它 会被插入到就绪队列的最左边。这样刚唤醒的进程在当前执行进程时间耗尽时就会被调度上处理器执行。当然如果进程没有睡眠那么多时间,我们只需保留原来的时 间vruntime = max_vruntime(se->vruntime, vruntime)。这有什么好处的,我觉得它可以将所有唤醒的进程排个队,睡眠越久的越快得到响应。 - - -对于新进程创建时initial为1,所以它会执行`vruntime += sched_vslice(cfs_rq, se);`这句,而这里的vruntime就是当前CFS就绪队列的min_vruntime,新加进程应该在最近很快被调度,这样减少系统的响应时间,我们已经知道当前进程的vruntime越小,它在红黑树中就会越靠左,就会被很快调度到处理器上执行。但是,Linux内核需要根据新加入的进程的权重决策一下应该何时调度该进程,而不能任意进程都来抢占当前队列中靠左的进程,因为必须保证就绪队列中的所有进程尽量得到他们应得的时间响应, sched_vslice函数就将其负荷权重转换为等价的虚拟时间, 其定义在[kernel/sched/fair.c, line 626](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L626) - - -函数就是根据initial的值来区分两种情况, 一般来说只有在新进程被加到系统中时,才会首次设置该参数, 但是这里的情况并非如此: - -由于内核已经承诺在当前的延迟周期内使所有活动进程都至少运行一次, 队列的min_vruntime用作基准虚拟时间, 通过减去sysctl_sched_latency, 则可以确保新唤醒新唤醒的进程只有在当前延迟周期结束后才能运行. - -但是如果进程在睡眠的过程中累积了比较大的不公平值(即se->vruntime值比较大), 则内核必须考虑这一点. 如果se->vruntime比先前的差值更大, 则将其作为进程的vruntime, 这会导致高进程在红黑树中处于靠左的位置, 而具有较小vruntime值得进程可以更早调度执行. - - - - -##2.6 __enqueue_entity完成红黑树的插入 -------- - -如果进程最近在运行, 其虚拟时间是有效的, 那么它可以直接通过__enqueue_entity加入到红黑树 - -```c -// enqueue_entity函数解析 - /* 将进程插入到红黑树中 */ - if (se != cfs_rq->curr) - __enqueue_entity(cfs_rq, se); - se->on_rq = 1; -``` - -__enqueue_entity函数定义在[kernel/sched/fair.c, line 486](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L486)中, 其实就是一个机械性地红黑树插入操作 - -```c -/* - * Enqueue an entity into the rb-tree: - */ -static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) -{ - struct rb_node **link = &cfs_rq->tasks_timeline.rb_node; - struct rb_node *parent = NULL; - struct sched_entity *entry; - int leftmost = 1; - - /* - * Find the right place in the rbtree: - * 从红黑树中找到se所应该在的位置 - * 同时leftmost标识其位置是不是最左结点 - * 如果在查找结点的过程中向右走了, 则置leftmost为0 - * 否则说明一直再相左走, 最终将走到最左节点, 此时leftmost恒为1 - */ - while (*link) { - parent = *link; - entry = rb_entry(parent, struct sched_entity, run_node); - /* - * We dont care about collisions. Nodes with - * the same key stay together. - * 以se->vruntime值为键值进行红黑树结点的比较 - */ - if (entity_before(se, entry)) { - link = &parent->rb_left; - } else { - link = &parent->rb_right; - leftmost = 0; - } - } - /* - * Maintain a cache of leftmost tree entries (it is frequently - * used): - * 如果leftmost为1, 说明se是红黑树当前的最左结点, 即vruntime最小 - * 那么把这个节点保存在cfs就绪队列的rb_leftmost域中 - */ - if (leftmost) - cfs_rq->rb_leftmost = &se->run_node; - - /* 将新进程的节点加入到红黑树中 */ - rb_link_node(&se->run_node, parent, link); - /* 为新插入的结点进行着色 */ - rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline); -} -```` - -#3 dequeue_task_fair出队操作 -------- - -dequeue_task_fair函数在完成睡眠等情况下调度, 将任务从就绪队列中移除 - -其执行的过程正好跟enqueue_task_fair的思路相同, 只是操作刚好相反 - - -enqueue_task_fair的执行流程如下 - -* 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做. - -* 否则, 具体的工作委托给dequeue_entity完成, 其中内核会借机用update_curr更新统计量 - 在enqueue_entity内部如果需要会调用__dequeue_entity将进程插入到CFS红黑树中合适的结点 - -##3.1 dequeue_task_fair函数 -------- - - -```c -/* - * The dequeue_task method is called before nr_running is - * decreased. We remove the task from the rbtree and - * update the fair scheduling stats: - */ -static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags) -{ - struct cfs_rq *cfs_rq; - struct sched_entity *se = &p->se; - int task_sleep = flags & DEQUEUE_SLEEP; - - for_each_sched_entity(se) { - cfs_rq = cfs_rq_of(se); - /* 将se调度实体所在的进程从队列中移除 */ - dequeue_entity(cfs_rq, se, flags); - - /* - * end evaluation on encountering a throttled cfs_rq - * - * note: in the case of encountering a throttled cfs_rq we will - * post the final h_nr_running decrement below. - */ - if (cfs_rq_throttled(cfs_rq)) - break; - - /* 进程移除后, 队列上的可运行程序数目减少1 */ - cfs_rq->h_nr_running--; - - /* Don't dequeue parent if it has other entities besides us - * 如果 - */ - if (cfs_rq->load.weight) { - /* - * Bias pick_next to pick a task from this cfs_rq, as - * p is sleeping when it is within its sched_slice. - */ - if (task_sleep && parent_entity(se)) - set_next_buddy(parent_entity(se)); - - /* avoid re-evaluating load for this entity */ - se = parent_entity(se); - break; - } - - // 设置 - flags |= DEQUEUE_SLEEP; - } - - for_each_sched_entity(se) { - cfs_rq = cfs_rq_of(se); - cfs_rq->h_nr_running--; - - if (cfs_rq_throttled(cfs_rq)) - break; - - update_load_avg(se, 1); - update_cfs_shares(cfs_rq); - } - - if (!se) - sub_nr_running(rq, 1); - - hrtick_update(rq); -} -``` - -## dequeue_entity -------- - -```c -static void -dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) -{ - /* - * Update run-time statistics of the 'current'. - */ - update_curr(cfs_rq); - dequeue_entity_load_avg(cfs_rq, se); - - if (schedstat_enabled()) - update_stats_dequeue(cfs_rq, se, flags); - - clear_buddies(cfs_rq, se); - - if (se != cfs_rq->curr) - __dequeue_entity(cfs_rq, se); - se->on_rq = 0; - account_entity_dequeue(cfs_rq, se); - - /* - * Normalize the entity after updating the min_vruntime because the - * update can refer to the ->curr item and we need to reflect this - * movement in our normalized position. - */ - if (!(flags & DEQUEUE_SLEEP)) - se->vruntime -= cfs_rq->min_vruntime; - - /* return excess runtime on last dequeue */ - return_cfs_rq_runtime(cfs_rq); - - update_min_vruntime(cfs_rq); - update_cfs_shares(cfs_rq); -} -``` - -## __dequeue_entity -------- - - -```c -static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) -{ - if (cfs_rq->rb_leftmost == &se->run_node) { - struct rb_node *next_node; - - next_node = rb_next(&se->run_node); - cfs_rq->rb_leftmost = next_node; - } - - rb_erase(&se->run_node, &cfs_rq->tasks_timeline); -} +Linux CFS调度器之队列操作 +======= + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-29 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/details/51456569) | + + + +CFS负责处理普通非实时进程, 这类进程是我们linux中最普遍的进程 + + +#1 前景回顾 +------- + +##1.1 CFS调度算法 +------- + +**CFS调度算法的思想** + +理想状态下每个进程都能获得相同的时间片,并且同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。也就是说,当一个进程占用CPU时,其他进程就必须等待。CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度. + +##1.2 负荷权重和虚拟时钟 + +**虚拟时钟是红黑树排序的依据** + +具体实现时,CFS通过每个进程的**虚拟运行时间(vruntime)**来衡量哪个进程最值得被调度. CFS中的就绪队列是一棵以vruntime为键值的红黑树,虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的vruntime最小. + +**优先级计算负荷权重, 负荷权重和当前时间计算出虚拟运行时间** + +虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。而,CFS调度器中的权重在内核是对用户态进程的优先级nice值, 通过prio_to_weight数组进行nice值和权重的转换而计算出来的 + + +**虚拟时钟相关公式** + + linux内核采用了计算公式: + +| 属性 | 公式 | 描述 | +|:-------:|:-------:| +| ideal_time | sum_runtime *se.weight/cfs_rq.weight | 每个进程应该运行的时间 | +| sum_exec_runtime | | 运行队列中所有任务运行完一遍的时间 | +| se.weight | | 当前进程的权重 | +| cfs.weight | | 整个cfs_rq的总权重 | + +这里se.weight和cfs.weight根据上面讲解我们可以算出, sum_runtime是怎们计算的呢,linux内核中这是个经验值,其经验公式是 + +| 条件 | 公式 | +|:-------:|:-------:| +| 进程数 > sched_nr_latency | sum_runtime=sysctl_sched_min_granularity *nr_running | +| 进程数 <=sched_nr_latency | sum_runtime=sysctl_sched_latency = 20ms | + +>注:sysctl_sched_min_granularity =4ms +> +>sched_nr_latency是内核在一个延迟周期中处理的最大活动进程数目 + +linux内核代码中是通过一个叫vruntime的变量来实现上面的原理的,即: + +每一个进程拥有一个vruntime,每次需要调度的时候就选运行队列中拥有最小vruntime的那个进程来运行,vruntime在时钟中断里面被维护,每次时钟中断都要更新当前进程的vruntime,即vruntime以如下公式逐渐增长: + + +| 条件 | 公式 | +|:-------:|:-------:| +| curr.nice!=NICE_0_LOAD | vruntime += delta* NICE_0_LOAD/se.weight; | +| curr.nice=NICE_0_LOAD | vruntime += delta; | + + +##1.3 今日内容--CFS进程入队和出队 +------- + + +完全公平调度器CFS中有两个函数可用来增删队列的成员:enqueue_task_fair和dequeue_task_fair分别用来向CFS就绪队列中添加或者删除进程 + + +#2 enqueue_task_fair入队操作 +------- + +##2.1 enque_task_fair函数 +------- + +向就绪队列中放置新进程的工作由函数enqueue_task_fair函数完成, 该函数定义在[kernel/sched/fair.c, line 5442](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5442), 其函数原型如下 + +该函数将task_struct *p所指向的进程插入到rq所在的就绪队列中, 除了指向所述的就绪队列rq和task_struct的指针外, 该函数还有另外一个参数wakeup. 这使得可以指定入队的进程是否最近才被唤醒并转换为运行状态(此时需指定wakeup = 1), 还是此前就是可运行的(那么wakeup = 0). + +```c +static void +enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags) +``` +enqueue_task_fair的执行流程如下 + +* 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做. + +* 否则, 具体的工作委托给enqueue_entity完成, 其中内核会借机用update_curr更新统计量 + 在enqueue_entity内部如果需要会调用__enqueue_entity将进程插入到CFS红黑树中合适的结点 + + + +##2.2 enque_task_fair完全函数 +------- + + +```c +/* + * The enqueue_task method is called before nr_running is + * increased. Here we update the fair scheduling stats and + * then put the task into the rbtree: + */ +static void +enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags) +{ + struct cfs_rq *cfs_rq; + struct sched_entity *se = &p->se; + + for_each_sched_entity(se) { + if (se->on_rq) + break; + cfs_rq = cfs_rq_of(se); + enqueue_entity(cfs_rq, se, flags); + + /* + * end evaluation on encountering a throttled cfs_rq + * + * note: in the case of encountering a throttled cfs_rq we will + * post the final h_nr_running increment below. + */ + if (cfs_rq_throttled(cfs_rq)) + break; + cfs_rq->h_nr_running++; + + flags = ENQUEUE_WAKEUP; + } + + for_each_sched_entity(se) { + cfs_rq = cfs_rq_of(se); + cfs_rq->h_nr_running++; + + if (cfs_rq_throttled(cfs_rq)) + break; + + update_load_avg(se, 1); + update_cfs_shares(cfs_rq); + } + + if (!se) + add_nr_running(rq, 1); + + hrtick_update(rq); +} +``` + + +##2.3 for_each_sched_entity +------- + +首先内核查找到待天机进程p所在的调度实体信息, 然后通过for_each_sched_entity循环所有调度实体, + +```c +// enqueue_task_fair函数 +{ + struct cfs_rq *cfs_rq; + struct sched_entity *se = &p->se; + + for_each_sched_entity(se) + { + /* ...... */ + } +} +``` + +但是有个疑问是, 进程p所在的调度时提就一个为嘛要循环才能遍历啊, 这是因为为了支持组调度.组调度下调度实体是有层次结构的, 我们将进程加入的时候, 同时要更新其父调度实体的调度信息, 而非组调度情况下, 就不需要调度实体的层次结构 + +linux对组调度的支持可以通过CONFIG_FAIR_GROUP_SCHED来启用, 在启用和不启用的条件下, 内核对很多函数的实现也会因条件而异, 这点对for_each_sched_entity函数尤为明显, 参见[启用CONFIG_FAIR_GROUP_SCHED](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L246)和[不启用CONFIG_FAIR_GROUP_SCHED](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L367) + +```c +#ifdef CONFIG_FAIR_GROUP_SCHED + +/* An entity is a task if it doesn't "own" a runqueue */ +#define entity_is_task(se) (!se->my_q) + +// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L266 +/* Walk up scheduling entities hierarchy */ +#define for_each_sched_entity(se) \ + for (; se; se = se->parent) + + #else /* !CONFIG_FAIR_GROUP_SCHED */ + +#define entity_is_task(se) 1 + +// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L381 +#define for_each_sched_entity(se) \ + for (; se; se = NULL) +``` + +* 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做. + +* 否则, 具体的工作委托给enqueue_entity完成, 其中内核会借机用update_curr更新统计量. + +```c +// enqueue_task_fair函数 +{ + /* 如果当前进程已经在就绪队列上 */ + if (se->on_rq) + break; + + /* 获取到当前进程所在的cfs_rq就绪队列 */ + cfs_rq = cfs_rq_of(se); + /* 内核委托enqueue_entity完成真正的插入工作 */ + enqueue_entity(cfs_rq, se, flags); +} +``` + + +##2.4 enqueue_entity插入进程 +------- + +enqueue_entity完成了进程真正的入队操作, 其具体流程如下所示 + +* 更新一些统计统计量, update_curr, update_cfs_shares等 + +* 如果进程此前是在睡眠状态, 则调用place_entity中首先会调整进程的虚拟运行时间 + +* 最后如果进程最近在运行, 其虚拟运行时间仍然有效, 那么则直接用__enqueue_entity加入到红黑树 + +首先如果进程最近正在运行, 其虚拟时间时间仍然有效, 那么(除非它当前在执行中)它可以直接用__enqueue_entity插入到红黑树, 该函数徐娅萍处理一些红黑树的机制, 这可以依靠内核的标准实现, 参见[__enqueue_entity函数, kernel/sched/fair.c, line483](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#483) + + + +```c +static void +enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) +{ + /* + * Update the normalized vruntime before updating min_vruntime + * through calling update_curr(). + * + * 如果当前进程之前已经是可运行状态不是被唤醒的那么其虚拟运行时间要增加 + */ + if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING)) + se->vruntime += cfs_rq->min_vruntime; + + /* + * Update run-time statistics of the 'current'. + * 更新进程的统计量信息 + */ + update_curr(cfs_rq); + enqueue_entity_load_avg(cfs_rq, se); + account_entity_enqueue(cfs_rq, se); + update_cfs_shares(cfs_rq); + + /* 如果当前进行之前在睡眠刚被唤醒 */ + if (flags & ENQUEUE_WAKEUP) + { + /* 调整进程的虚拟运行时间 */ + place_entity(cfs_rq, se, 0); + if (schedstat_enabled()) + enqueue_sleeper(cfs_rq, se); + } + + check_schedstat_required(); + if (schedstat_enabled()) { + update_stats_enqueue(cfs_rq, se); + check_spread(cfs_rq, se); + } + + /* 将进程插入到红黑树中 */ + if (se != cfs_rq->curr) + __enqueue_entity(cfs_rq, se); + se->on_rq = 1; + + if (cfs_rq->nr_running == 1) { + list_add_leaf_cfs_rq(cfs_rq); + check_enqueue_throttle(cfs_rq); + } +} +``` + +##2.5 place_entity处理睡眠进程 +------- + +如果进程此前在睡眠, 那么则调用place_entity处理其虚拟运行时间 + +设想一下子如果休眠进程的vruntime保持不变, 而其他运行进程的 vruntime一直在推进, 那么等到休眠进程终于唤醒的时候, 它的vruntime比别人小很多, 会使它获得长时间抢占CPU的优势, 其他进程就要饿死了. 这显然是另一种形式的不公平,因此CFS是这样做的:在休眠进程被唤醒时重新设置vruntime值,以min_vruntime值为基础,给予一定的补偿,但不能补偿太多. 这个重新设置其虚拟运行时间的工作就是就是通过place_entity来完成的, 另外新进程创建完成后, 也是通过place_entity完成其虚拟运行时间vruntime的设置的. place_entity通过其第三个参数initial来标识新进程创建和休眠进程苏醒两种不同情形的. + + + +place_entity函数定义在[kernel/sched/fair.c, line 3135](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L3135)中首先会调整进程的虚拟运行时间 + + +```c +// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3134 +static void +place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial) +{ + u64 vruntime = cfs_rq->min_vruntime; + + /* + * The 'current' period is already promised to the current tasks, + * however the extra weight of the new task will slow them down a + * little, place the new task so that it fits in the slot that + * stays open at the end. + * + * 如果是新进程第一次要入队, 那么就要初始化它的vruntime + * 一般就把cfsq的vruntime给它就可以 + * 但是如果当前运行的所有进程被承诺了一个运行周期 + * 那么则将新进程的vruntime后推一个他自己的slice + * 实际上新进程入队时要重新计算运行队列的总权值 + * 总权值显然是增加了,但是所有进程总的运行时期并不一定随之增加 + * 则每个进程的承诺时间相当于减小了,就是减慢了进程们的虚拟时钟步伐。 + */ + /* initial标识了该进程是新进程 */ + if (initial && sched_feat(START_DEBIT)) + vruntime += sched_vslice(cfs_rq, se); + + /* sleeps up to a single latency don't count. + * 休眠进程 */ + if (!initial) + { + /* 一个调度周期 */ + unsigned long thresh = sysctl_sched_latency; + + /* + * Halve their sleep time's effect, to allow + * for a gentler effect of sleepers: + */ + /* 若设了GENTLE_FAIR_SLEEPERS */ + if (sched_feat(GENTLE_FAIR_SLEEPERS)) + thresh >>= 1; /* 补偿减为调度周期的一半 */ + + vruntime -= thresh; + } + + /* ensure we never gain time by being placed backwards. + * 如果是唤醒已经存在的进程,则单调附值 + */ + se->vruntime = max_vruntime(se->vruntime, vruntime); +} +``` + +我们可以看到enqueue_task_fair调用place_entity传递的initial参数为0 + +```c +place_entity(cfs_rq, se, 0); +``` + +所以会执行if (!initial)后的语句。因为进程睡眠后,vruntime就不会增加了,当它醒来后不知道过了多长时间,可能vruntime已经比 min_vruntime小了很多,如果只是简单的将其插入到就绪队列中,它将拼命追赶min_vruntime,因为它总是在红黑树的最左面。如果这 样,它将会占用大量的CPU时间,导致红黑树右边的进程被饿死。但是我们又必须及时响应醒来的进程,因为它们可能有一些工作需要立刻处理,所以系统采取了 一种折衷的办法,将当前cfs_rq->min_vruntime时间减去sysctl_sched_latency赋给vruntime,这时它 会被插入到就绪队列的最左边。这样刚唤醒的进程在当前执行进程时间耗尽时就会被调度上处理器执行。当然如果进程没有睡眠那么多时间,我们只需保留原来的时 间vruntime = max_vruntime(se->vruntime, vruntime)。这有什么好处的,我觉得它可以将所有唤醒的进程排个队,睡眠越久的越快得到响应。 + + +对于新进程创建时initial为1,所以它会执行`vruntime += sched_vslice(cfs_rq, se);`这句,而这里的vruntime就是当前CFS就绪队列的min_vruntime,新加进程应该在最近很快被调度,这样减少系统的响应时间,我们已经知道当前进程的vruntime越小,它在红黑树中就会越靠左,就会被很快调度到处理器上执行。但是,Linux内核需要根据新加入的进程的权重决策一下应该何时调度该进程,而不能任意进程都来抢占当前队列中靠左的进程,因为必须保证就绪队列中的所有进程尽量得到他们应得的时间响应, sched_vslice函数就将其负荷权重转换为等价的虚拟时间, 其定义在[kernel/sched/fair.c, line 626](http://lxr.free-electrons.com/source/kernel/sched/fair.c#L626) + + +函数就是根据initial的值来区分两种情况, 一般来说只有在新进程被加到系统中时,才会首次设置该参数, 但是这里的情况并非如此: + +由于内核已经承诺在当前的延迟周期内使所有活动进程都至少运行一次, 队列的min_vruntime用作基准虚拟时间, 通过减去sysctl_sched_latency, 则可以确保新唤醒新唤醒的进程只有在当前延迟周期结束后才能运行. + +但是如果进程在睡眠的过程中累积了比较大的不公平值(即se->vruntime值比较大), 则内核必须考虑这一点. 如果se->vruntime比先前的差值更大, 则将其作为进程的vruntime, 这会导致高进程在红黑树中处于靠左的位置, 而具有较小vruntime值得进程可以更早调度执行. + + + + +##2.6 __enqueue_entity完成红黑树的插入 +------- + +如果进程最近在运行, 其虚拟时间是有效的, 那么它可以直接通过__enqueue_entity加入到红黑树 + +```c +// enqueue_entity函数解析 + /* 将进程插入到红黑树中 */ + if (se != cfs_rq->curr) + __enqueue_entity(cfs_rq, se); + se->on_rq = 1; +``` + +__enqueue_entity函数定义在[kernel/sched/fair.c, line 486](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L486)中, 其实就是一个机械性地红黑树插入操作 + +```c +/* + * Enqueue an entity into the rb-tree: + */ +static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) +{ + struct rb_node **link = &cfs_rq->tasks_timeline.rb_node; + struct rb_node *parent = NULL; + struct sched_entity *entry; + int leftmost = 1; + + /* + * Find the right place in the rbtree: + * 从红黑树中找到se所应该在的位置 + * 同时leftmost标识其位置是不是最左结点 + * 如果在查找结点的过程中向右走了, 则置leftmost为0 + * 否则说明一直再相左走, 最终将走到最左节点, 此时leftmost恒为1 + */ + while (*link) { + parent = *link; + entry = rb_entry(parent, struct sched_entity, run_node); + /* + * We dont care about collisions. Nodes with + * the same key stay together. + * 以se->vruntime值为键值进行红黑树结点的比较 + */ + if (entity_before(se, entry)) { + link = &parent->rb_left; + } else { + link = &parent->rb_right; + leftmost = 0; + } + } + /* + * Maintain a cache of leftmost tree entries (it is frequently + * used): + * 如果leftmost为1, 说明se是红黑树当前的最左结点, 即vruntime最小 + * 那么把这个节点保存在cfs就绪队列的rb_leftmost域中 + */ + if (leftmost) + cfs_rq->rb_leftmost = &se->run_node; + + /* 将新进程的节点加入到红黑树中 */ + rb_link_node(&se->run_node, parent, link); + /* 为新插入的结点进行着色 */ + rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline); +} +``` + +#3 dequeue_task_fair出队操作 +------- + +dequeue_task_fair函数在完成睡眠等情况下调度, 将任务从就绪队列中移除 + +其执行的过程正好跟enqueue_task_fair的思路相同, 只是操作刚好相反 + + +dequeue_task_fair的执行流程如下 + +* 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做. + +* 否则, 具体的工作委托给dequeue_entity完成, 其中内核会借机用update_curr更新统计量 + 在enqueue_entity内部如果需要会调用__dequeue_entity将进程插入到CFS红黑树中合适的结点 + + +dequeue_task_fair定义在[/kernel/sched/fair.c, line 4155](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v4.6#L4155), 其大致框架流程如下 + + +##3.1 dequeue_task_fair函数 +------- + +```c +/* + * The dequeue_task method is called before nr_running is + * decreased. We remove the task from the rbtree and + * update the fair scheduling stats: + */ +static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags) +; + + struct cfs_rq *cfs_rq; + struct sched_entity *se = &p->se; + int task_sleep = flags & DEQUEUE_SLEEP; + + // 设置 + flags |= DEQUEUE_SLEEP; + + + for_each_sched_entity(se) { + cfs_rq = cfs_rq_of(se); + cfs_rq->h_nr_running--; + + if (cfs_rq_throttled(cfs_rq)) + break; + + update_load_avg(se, 1); + update_cfs_shares(cfs_rq); + } + + if (!se) + sub_nr_running(rq, 1); + + hrtick_update(rq); +} +``` + +##3.2 dequeue_entity将调度实体出队 +------- + +```c +static void +dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) +{ + /* + * Update run-time statistics of the 'current'. + */ + update_curr(cfs_rq); + dequeue_entity_load_avg(cfs_rq, se); + + if (schedstat_enabled()) + update_stats_dequeue(cfs_rq, se, flags); + + clear_buddies(cfs_rq, se); + + if (se != cfs_rq->curr) + __dequeue_entity(cfs_rq, se); + se->on_rq = 0; + account_entity_dequeue(cfs_rq, se); + + /* + * Normalize the entity after updating the min_vruntime because the + * update can refer to the ->curr item and we need to reflect this + * movement in our normalized position. + */ + if (!(flags & DEQUEUE_SLEEP)) + se->vruntime -= cfs_rq->min_vruntime; + + /* return excess runtime on last dequeue */ + return_cfs_rq_runtime(cfs_rq); + + update_min_vruntime(cfs_rq); + update_cfs_shares(cfs_rq); +} +``` + +##3.3 __dequeue_entity完成真正的出队操作 +------- + + +```c +static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) +{ + if (cfs_rq->rb_leftmost == &se->run_node) { + struct rb_node *next_node; + + next_node = rb_next(&se->run_node); + cfs_rq->rb_leftmost = next_node; + } + + rb_erase(&se->run_node, &cfs_rq->tasks_timeline); +} ``` \ No newline at end of file diff --git a/study/kernel/01-process/05-schedule/07-cfs/05-pick_next/README.md b/study/kernel/01-process/05-schedule/07-cfs/05-pick_next/README.md index 677de1c..e77080a 100644 --- a/study/kernel/01-process/05-schedule/07-cfs/05-pick_next/README.md +++ b/study/kernel/01-process/05-schedule/07-cfs/05-pick_next/README.md @@ -1,823 +1,827 @@ -Linux CFS调度器之pick_next_task_fair选择下一个被调度的进程 -======= - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-29 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/details/51456569) | - - -CFS负责处理普通非实时进程, 这类进程是我们linux中最普遍的进程 - - -#1 前景回顾 -------- - -##1.1 CFS调度算法 -------- - -**CFS调度算法的思想** - -理想状态下每个进程都能获得相同的时间片,并且同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。也就是说,当一个进程占用CPU时,其他进程就必须等待。CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度. - -##1.2 负荷权重和虚拟时钟 - -**虚拟时钟是红黑树排序的依据** - -具体实现时,CFS通过每个进程的**虚拟运行时间(vruntime)**来衡量哪个进程最值得被调度. CFS中的就绪队列是一棵以vruntime为键值的红黑树,虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的vruntime最小. - -**优先级计算负荷权重, 负荷权重和当前时间计算出虚拟运行时间** - -虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。而,CFS调度器中的权重在内核是对用户态进程的优先级nice值, 通过prio_to_weight数组进行nice值和权重的转换而计算出来的 - - -**虚拟时钟相关公式** - - linux内核采用了计算公式: - -| 属性 | 公式 | 描述 | -|:-------:|:-------:| -| ideal_time | sum_runtime *se.weight/cfs_rq.weight | 每个进程应该运行的时间 | -| sum_exec_runtime | | 运行队列中所有任务运行完一遍的时间 | -| se.weight | | 当前进程的权重 | -| cfs.weight | | 整个cfs_rq的总权重 | - -这里se.weight和cfs.weight根据上面讲解我们可以算出, sum_runtime是怎们计算的呢,linux内核中这是个经验值,其经验公式是 - -| 条件 | 公式 | -|:-------:|:-------:| -| 进程数 > sched_nr_latency | sum_runtime=sysctl_sched_min_granularity *nr_running | -| 进程数 <=sched_nr_latency | sum_runtime=sysctl_sched_latency = 20ms | - ->注:sysctl_sched_min_granularity =4ms -> ->sched_nr_latency是内核在一个延迟周期中处理的最大活动进程数目 - -linux内核代码中是通过一个叫vruntime的变量来实现上面的原理的,即: - -每一个进程拥有一个vruntime,每次需要调度的时候就选运行队列中拥有最小vruntime的那个进程来运行,vruntime在时钟中断里面被维护,每次时钟中断都要更新当前进程的vruntime,即vruntime以如下公式逐渐增长: - - -| 条件 | 公式 | -|:-------:|:-------:| -| curr.nice!=NICE_0_LOAD | vruntime += delta* NICE_0_LOAD/se.weight; | -| curr.nice=NICE_0_LOAD | vruntime += delta; | - - -##1.3 CFS进程入队和出队 -------- - -enqueue_task_fair和dequeue_task_fair分别用来向CFS就绪队列中添加或者删除进程 - -完全公平调度器CFS中有两个函数可用来增删队列的成员:enqueue_task_fair和dequeue_task_fair分别用来向CFS就绪队列中添加或者删除进程 - - -**enqueue_task_fair进程入队** - -向就绪队列中放置新进程的工作由函数enqueue_task_fair函数完成, 该函数定义在[kernel/sched/fair.c, line 5442](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5442), 其函数原型如下 - -该函数将task_struct *p所指向的进程插入到rq所在的就绪队列中, 除了指向所述的就绪队列rq和task_struct的指针外, 该函数还有另外一个参数wakeup. 这使得可以指定入队的进程是否最近才被唤醒并转换为运行状态(此时需指定wakeup = 1), 还是此前就是可运行的(那么wakeup = 0). - -```c -static void -enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags) -``` -enqueue_task_fair的执行流程如下 - -* 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做. - -* 否则, 具体的工作委托给enqueue_entity完成, 其中内核会借机用update_curr更新统计量 - 在enqueue_entity内部如果需要会调用__enqueue_entity将进程插入到CFS红黑树中合适的结点 - - -**dequeue_task_fair进程出队操作** - -dequeue_task_fair函数在完成睡眠等情况下调度, 将任务从就绪队列中移除 - -其执行的过程正好跟enqueue_task_fair的思路相同, 只是操作刚好相反 - - -enqueue_task_fair的执行流程如下 - -* 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做. - -* 否则, 具体的工作委托给dequeue_entity完成, 其中内核会借机用update_curr更新统计量 - 在enqueue_entity内部如果需要会调用__dequeue_entity将进程插入到CFS红黑树中合适的结点 - - -##1.4 今日看点(CFS如何选择最合适的进程) -------- - - -每个调度器类sched_class都必须提供一个pick_next_task函数用以在就绪队列中选择一个最优的进程来等待调度, 而我们的CFS调度器类中, 选择下一个将要运行的进程由pick_next_task_fair函数来完成 - - -之前我们在将主调度器的时候, 主调度器schedule函数在进程调度抢占时, 会通过__schedule函数调用全局pick_next_task选择一个最优的进程, 在pick_next_task中我们就按照优先级依次调用不同调度器类提供的pick_next_task方法 - -今天就让我们窥探一下完全公平调度器类CFS的pick_next_task方法pick_next_fair - - - - -**pick_next_task_fair** - -选择下一个将要运行的进程pick_next_task_fair执行. 其代码执行流程如下 - -对于pick_next_task_fair函数的讲解, 我们从simple标签开始, 这个是常规状态下pick_next的思路, 简单的来说pick_next_task_fair的函数框架如下 - -```c -again: - 控制循环来读取最优进程 - -#ifdef CONFIG_FAIR_GROUP_SCHED - 完成组调度下的pick_next选择 - 返回被选择的调度时实体的指针 -#endif - -simple: - 最基础的pick_next函数 - 返回被选择的调度时实体的指针 - -idle : - 如果系统中没有可运行的进行, 则需要调度idle进程 -``` - -可见我们会发现, - -* simple标签是CFS中最基础的pick_next操作 - -* idle则使得在没有进程被调度时, 调度idle进程 - -* again标签用于循环的进行pick_next操作 - -* CONFIG_FAIR_GROUP_SCHED宏指定了组调度情况下的pick_next操作, 如果不支持组调度, 则pick_next_task_fair将直接从simple开始执行 - - - -#2 simple无组调度最简单的pick_next_task_fair -------- - -在不支持组调度情况下(选项CONFIG_FAIR_GROUP_SCHED), CFS的pick_next_task_fair函数会直接执行simple标签, 优选下一个函数, 这个流程清晰而且简单, 但是已经足够我们理解cfs的pick_next了 - -##2.1 simple的基本流程 -------- - -pick_next_task_fair函数的simple标签定义在[kernel/sched/fair.c, line 5526)](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5526), 代码如下所示 - - -```c -simple: - cfs_rq = &rq->cfs; -#endif - /* 如果nr_running计数器为0, - * 当前队列上没有可运行进程, - * 则需要调度idle进程 */ - if (!cfs_rq->nr_running) - goto idle; - /* 将当前进程放入运行队列的合适位置 */ - put_prev_task(rq, prev); - - do - { - /* 选出下一个可执行调度实体(进程) */ - se = pick_next_entity(cfs_rq, NULL); - /* 把选中的进程从红黑树移除,更新红黑树 - * set_next_entity会调用__dequeue_entity完成此工作 */ - set_next_entity(cfs_rq, se); - /* group_cfs_rq return NULL when !CONFIG_FAIR_GROUP_SCHED - * 在非组调度情况下, group_cfs_rq返回了NULL */ - cfs_rq = group_cfs_rq(se); - } while (cfs_rq); /* 在没有配置组调度选项(CONFIG_FAIR_GROUP_SCHED)的情况下.group_cfs_rq()返回NULL.因此,上函数中的循环只会循环一次 */ - - - /* 获取到调度实体指代的进程信息 */ - p = task_of(se); - - if (hrtick_enabled(rq)) - hrtick_start_fair(rq, p); - - return p; -``` - -其基本流程如下 - -| 流程 | 描述 | -|:-------:|:-------:| -| !cfs_rq->nr_running -=> goto idle; | 如果nr_running计数器为0, 当前队列上没有可运行进程, 则需要调度idle进程 | -| put_prev_task(rq, prev); | 将当前进程放入运行队列的合适位置, 每次当进程被调度后都会使用set_next_entity从红黑树中移除, 因此被抢占时需要重新加如红黑树中等待被调度 | -| se = pick_next_entity(cfs_rq, NULL); | 选出下一个可执行调度实体 | -| set_next_entity(cfs_rq, se); | set_next_entity会调用__dequeue_entity把选中的进程从红黑树移除,并更新红黑树 | - - -##2.2 put_prev_task -------- - - -##2.2.1 **全局put_prev_task函数** - - -put_prev_task用来将前一个进程prev放回到就绪队列中, 这是一个全局的函数, 而每个调度器类也必须实现一个自己的put_prev_task函数(比如CFS的put_prev_task_fair), - -由于CFS调度的时候, prev进程不一定是一个CFS调度的进程, 因此必须调用全局的put_prev_task来调用prev进程所属调度器类sched_class的对应put_prev_task方法, 完成将进程放回到就绪队列中 - - -全局的put_prev_task函数定义在[kernel/sched/sched.h, line 1245](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1245), 代码如下所示 -```c -static inline void put_prev_task(struct rq *rq, struct task_struct *prev) -{ - prev->sched_class->put_prev_task(rq, prev); -} -``` - -##2.2.2 **CFS的put_prev_task_fair函数** - - -然后我们来分析一下CFS的put_prev_task_fair函数, 其定义在[kernel/sched/fair.c, line 5572](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5572) - -在选中了下一个将被调度执行的进程之后,回到pick_next_task_fair中,执行set_next_entity - -```c -/* - * Account for a descheduled task: - */ -static void put_prev_task_fair(struct rq *rq, struct task_struct *prev) -{ - struct sched_entity *se = &prev->se; - struct cfs_rq *cfs_rq; - - for_each_sched_entity(se) { - cfs_rq = cfs_rq_of(se); - put_prev_entity(cfs_rq, se); - } -} -``` - -前面我们说到过函数在组策略情况下, 调度实体之间存在父子的层次, for_each_sched_entity会从当前调度实体开始, 然后循环向其父调度实体进行更新, 非组调度情况下则只执行一次 - -而put_prev_task_fair函数最终会调用put_prev_entity函数将prev的调度时提se放回到就绪队列中等待下次调度 - - -##2.2.3 **put_prev_entity函数** - -[put_prev_entity](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3443)函数定义在[kernel/sched/fair.c, line 3443](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3443), 他在更新了虚拟运行时间等信息后, 最终通过__enqueue_entity函数将prev进程(即current进程)放回就绪队列rq上 - - - -##2.3 pick_next_entity -------- - -###2.3.1 **pick_next_entity函数完全注释** - -```c -/* - * Pick the next process, keeping these things in mind, in this order: - * 1) keep things fair between processes/task groups - * 2) pick the "next" process, since someone really wants that to run - * 3) pick the "last" process, for cache locality - * 4) do not run the "skip" process, if something else is available - * - * 1. 首先要确保任务组之间的公平, 这也是设置组的原因之一 - * 2. 其次, 挑选下一个合适的(优先级比较高的)进程 - * 因为它确实需要马上运行 - * 3. 如果没有找到条件2中的进程 - * 那么为了保持良好的局部性 - * 则选中上一次执行的进程 - * 4. 只要有任务存在, 就不要让CPU空转, - * 只有在没有进程的情况下才会让CPU运行idle进程 - */ -static struct sched_entity * -pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr) -{ - /* 摘取红黑树最左边的进程 */ - struct sched_entity *left = __pick_first_entity(cfs_rq); - struct sched_entity *se; - - /* - * If curr is set we have to see if its left of the leftmost entity - * still in the tree, provided there was anything in the tree at all. - * - * 如果 - * left == NULL 或者 - * curr != NULL curr进程比left进程更优(即curr的虚拟运行时间更小) - * 说明curr进程是自动放弃运行权利, 且其比最左进程更优 - * 因此将left指向了curr, 即curr是最优的进程 - */ - if (!left || (curr && entity_before(curr, left))) - { - left = curr; - } - - /* se = left存储了cfs_rq队列中最优的那个进程 - * 如果进程curr是一个自愿放弃CPU的进程(其比最左进程更优), 则取se = curr - * 否则进程se就取红黑树中最左的进程left, 它必然是当前就绪队列上最优的 - */ - se = left; /* ideally we run the leftmost entity */ - - /* - * Avoid running the skip buddy, if running something else can - * be done without getting too unfair. - * - * cfs_rq->skip存储了需要调过不参与调度的进程调度实体 - * 如果我们挑选出来的最优调度实体se正好是skip - * 那么我们需要选择次优的调度实体se来进行调度 - * 由于之前的se = left = (curr before left) curr left - * 则如果 se == curr == skip, 则选择left = __pick_first_entity进行即可 - * 否则则se == left == skip, 则选择次优的那个调度实体second - */ - if (cfs_rq->skip == se) - { - struct sched_entity *second; - - if (se == curr) /* se == curr == skip选择最左的那个调度实体left */ - { - second = __pick_first_entity(cfs_rq); - } - else /* 否则se == left == skip, 选择次优的调度实体second */ - { - /* 摘取红黑树上第二左的进程节点 */ - second = __pick_next_entity(se); - /* 同时与left进程一样, - * 如果 - * second == NULL 没有次优的进程 或者 - * curr != NULL curr进程比left进程更优(即curr的虚拟运行时间更小) - * 说明curr进程比最second进程更优 - * 因此将second指向了curr, 即curr是最优的进程*/ - if (!second || (curr && entity_before(curr, second))) - second = curr; - } - - /* 判断left和second的vruntime的差距是否小于sysctl_sched_wakeup_granularity - * 即如果second能抢占left */ - if (second && wakeup_preempt_entity(second, left) < 1) - se = second; - } - - /* - * Prefer last buddy, try to return the CPU to a preempted task. - * - * - */ - if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1) - se = cfs_rq->last; - - /* - * Someone really wants this to run. If it's not unfair, run it. - */ - if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1) - se = cfs_rq->next; - - /* 用过一次任何一个next或者last - * 都需要清除掉这个指针 - * 以免影响到下次pick next sched_entity */ - clear_buddies(cfs_rq, se); - - return se; -} -``` - -###2.3.2 **从left, second和curr进程中选择最优的进程** - - -pick_next_entity则从CFS的红黑树中摘取一个最优的进程, 这个进程往往在红黑树的最左端, 即vruntime最小, 但是也有例外, 但是不外乎这几个进程 - -| 调度实体 | 描述 | -|:-------:|:-------:| -| left = __pick_first_entity(cfs_rq) | **红黑树的最左节点**, 这个节点拥有当前队列中vruntime最小的特性, 即应该优先被调度 | -| second = __pick_first_entity(left) | **红黑树的次左节点**, 为什么这个节点也可能呢, 因为内核支持skip跳过某个进程的抢占权力的, 如果left被标记为skip(由cfs_rq->skip域指定), 那么可能就需要找到次优的那个进程 | -| curr结点 | curr节点的vruntime可能比left和second更小, 但是由于它正在运行, 因此它不在红黑树中(进程抢占物理机的时候对应节点同时会从红黑树中删除), 但是如果其vruntime足够小, 意味着cfs调度器应该尽可能的补偿curr进程, 让它再次被调度 | - -其中__pick_first_entity会返回cfs_rq红黑树中的最左节点rb_leftmost所属的调度实体信息, 该函数定义在[kernel/sched/fair.c, line 543](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3443) - -而__pick_next_entity(se)函数则返回se在红黑树中中序遍历的下一个节点信息, 该函数定义在[kernel/sched/fair.c, line 544](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3443), 获取下一个节点的工作可以通过内核红黑树的标准操作rb_next完成 - - - -###2.3.3 **cfs_rq的last和next指针域** - -在pick_next_entity的最后, 要把红黑树最左下角的进程和另外两个进程(即next和last)做比较, next是抢占失败的进程, 而last则是抢占成功后被抢占的进程, 这三个进程到底哪一个是最优的next进程呢? - -Linux CFS实现的判决条件是: - -1. 尽可能满足需要刚被唤醒的进程抢占其它进程的需求 - -2. 尽可能减少以上这种抢占带来的缓存刷新的影响 - - -**cfs_rq的last和next指针,last表示最后一个执行wakeup的sched_entity,next表示最后一个被wakeup的sched_entity。他们在进程wakeup的时候会赋值,在pick新sched_entity的时候,会优先选择这些last或者next指针的sched_entity,有利于提高缓存的命中率** ** - -因此我们优选出来的进程必须同last和next指针域进行对比, 其实就是检查就绪队列中的最优进程, 即红黑树中最左节点last是否可以抢占last和next指针域, 检查是否可以抢占是通过wakeup_preempt_entity函数来完成的. - - -###2.3.4 **wakeup_preempt_entity检查是否可以被抢占** - - -```c -// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5317 -/* - * Should 'se' preempt 'curr'. - * - * |s1 - * |s2 - * |s3 - * g - * |<--->|c - * - * w(c, s1) = -1 - * w(c, s2) = 0 - * w(c, s3) = 1 - * - */ -static int -wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se) -{ - /* vdiff为curr和se vruntime的差值*/ - s64 gran, vdiff = curr->vruntime - se->vruntime; - - /* cfs_rq的vruntime是单调递增的,也就是一个基准 - * 各个进程的vruntime追赶竞争cfsq的vruntime - * 如果curr的vruntime比较小, 说明curr更加需要补偿, - * 即se无法抢占curr */ - if (vdiff <= 0) - return -1; - - /* 计算curr的最小抢占期限粒度 */ - gran = wakeup_gran(curr, se); - /* 当差值大于这个最小粒度的时候才抢占,这可以避免频繁抢占 */ - if (vdiff > gran) - return 1; - - return 0; -} - - -// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5282 -static unsigned long -wakeup_gran(struct sched_entity *curr, struct sched_entity *se) -{ - /* NICE_0_LOAD的基准最小运行期限 */ - unsigned long gran = sysctl_sched_wakeup_granularity; - - /* - * Since its curr running now, convert the gran from real-time - * to virtual-time in his units. - * - * By using 'se' instead of 'curr' we penalize light tasks, so - * they get preempted easier. That is, if 'se' < 'curr' then - * the resulting gran will be larger, therefore penalizing the - * lighter, if otoh 'se' > 'curr' then the resulting gran will - * be smaller, again penalizing the lighter task. - * - * This is especially important for buddies when the leftmost - * task is higher priority than the buddy. - * - * 计算进程运行的期限,即抢占的粒度 - */ - return calc_delta_fair(gran, se); -} -``` - -到底能不能选择last和next两个进程, 则是wakeup_preempt_entity函数来决定的, 看下面的图解即可: - -![last进程next进程和left进程的比较](./images/last_next_left.png) - -* 如果S3是left,curr是next或者last,left的vruntime值小于curr和next, 函数wakeup_preempt_entity肯定返回1,那么就说明next和last指针的vruntime和left差距过大,这个时候没有必要选择这个last或者next指针,而是应该优先补偿left - -* 如果next或者last是S2,S1,那么vruntime和left差距并不大,并没有超过sysctl_sched_wakeup_granularity ,那么这个next或者last就可以被优先选择,而代替了left - -而清除last和next这两个指针的时机有这么几个: - -* sched_tick的时候, 如果一个进程的运行时间超过理论时间(这个时间是根据load和cfs_rq的load, 平均分割sysctl_sched_latency的时间), 那么如果next或者last指针指向这个正在运行的进程, 需要清除这个指针, 使得pick sched_entity不会因为next或者last指针再次选择到这个sched_entity - -* 当一个sched_entity调度实体dequeue出运行队列,那么如果有next或者last指针指向这个sched_entity, 那么需要删除这个next或者last指针。 - -* 刚才说的那种case,如果next,last指针在pick的时候被使用了一次,那么这次用完了指针,需要清除相应的指针,避免使用过的next,last指针影响到下次pick - -* 当进程yield操作的时候,进程主动放弃了调度机会,那么如果next,last指针指向了这个sched_entity,那么需要清除相应指针。 - - - -##2.4 set_next_entity -------- - - -现在我们已经通过pick_next_task_fair选择了进程, 但是还需要完成一些工作, 才能将其标记为运行进程. 这是通过set_next_entity来处理的. 该函数定义在[kernel/sched/fair.c, line 3348](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3348) - - -当前执行进程(我们选择出来的进程马上要抢占处理器开始执行)不应该再保存在就绪队列上, 因此set_next_entity()函数会调用__dequeue_entity(cfs_rq, se)把选中的下一个进程移出红黑树. 如果当前进程是最左节点, __dequeue_entity会将leftmost指针设置到次左进程 - -```c - /* 'current' is not kept within the tree. */ - if (se->on_rq) /* 如果se尚在rq队列上 */ - { - /* ...... */ - /* 将se从cfs_rq的红黑树中删除 */ - __dequeue_entity(cfs_rq, se); - /* ...... */ - } -``` - -尽管该进程不再包含在红黑树中, 但是进程和就绪队列之间的关联并没有丢失, 因为curr标记了当前进程cfs_rq->curr = se; - -```c - cfs_rq->curr = se; -``` - -然后接下来是一些统计信息的处理, 如果内核开启了调度统计CONFIG_SCHEDSTATS标识, 则会完成调度统计的计算和更新 - - - -```c -#ifdef CONFIG_SCHEDSTATS - /* - * Track our maximum slice length, if the CPU's load is at - * least twice that of our own weight (i.e. dont track it - * when there are only lesser-weight tasks around): - */ - if (schedstat_enabled() && rq_of(cfs_rq)->load.weight >= 2*se->load.weight) { - se->statistics.slice_max = max(se->statistics.slice_max, - se->sum_exec_runtime - se->prev_sum_exec_runtime); - } -#endif -``` - -在set_next_entity的最后, 将选择出的调度实体se的sum_exec_runtime保存在了prev_sum_exec_runtime中, 因为该调度实体指向的进程, 马上将抢占处理器成为当前活动进程, 在CPU上花费的实际时间将记入sum_exec_runtime, 因此内核会在prev_sum_exec_runtime保存此前的设置. 要注意进程中的sum_exec_runtime没有重置. 因此差值sum_exec_runtime - prev_sum_runtime确实标识了在CPU上执行花费的实际时间. - -最后我们附上set_next_entity函数的完整注释信息 - - -```c -static void -set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) -{ - /* 'current' is not kept within the tree. */ - if (se->on_rq) /* 如果se尚在rq队列上 */ - { - /* - * Any task has to be enqueued before it get to execute on - * a CPU. So account for the time it spent waiting on the - * runqueue. - */ - if (schedstat_enabled()) - update_stats_wait_end(cfs_rq, se); - /* 将se从cfs_rq的红黑树中删除 */ - __dequeue_entity(cfs_rq, se); - update_load_avg(se, 1); - } - /* 新sched_entity中的exec_start字段为当前clock_task */ - update_stats_curr_start(cfs_rq, se); - /* 将se设置为curr进程 */ - cfs_rq->curr = se; -#ifdef CONFIG_SCHEDSTATS - /* - * Track our maximum slice length, if the CPU's load is at - * least twice that of our own weight (i.e. dont track it - * when there are only lesser-weight tasks around): - */ - if (schedstat_enabled() && rq_of(cfs_rq)->load.weight >= 2*se->load.weight) { - se->statistics.slice_max = max(se->statistics.slice_max, - se->sum_exec_runtime - se->prev_sum_exec_runtime); - } -#endif - /* 更新task上一次投入运行的从时间 */ - se->prev_sum_exec_runtime = se->sum_exec_runtime; -} -``` - - - - -#3 idle进程的调度 -------- - -```c - /* 如果nr_running计数器为0, - * 当前队列上没有可运行进程, - * 则需要调度idle进程 */ - if (!cfs_rq->nr_running) - goto idle; -``` - -如果系统中当前运行队列上没有可调度的进程, 那么会调到idle标签去调度idle进程. - - -idle标签如下所示 - -```c -idle: - /* - * This is OK, because current is on_cpu, which avoids it being picked - * for load-balance and preemption/IRQs are still disabled avoiding - * further scheduler activity on it and we're being very careful to - * re-start the picking loop. - */ - lockdep_unpin_lock(&rq->lock); - new_tasks = idle_balance(rq); - lockdep_pin_lock(&rq->lock); - /* - * Because idle_balance() releases (and re-acquires) rq->lock, it is - * possible for any higher priority task to appear. In that case we - * must re-start the pick_next_entity() loop. - */ - if (new_tasks < 0) - return RETRY_TASK; - - if (new_tasks > 0) - goto again; - - return NULL; -``` - -其关键就是调用idle_balance进行任务的迁移 - - 每个cpu都有自己的运行队列, 如果当前cpu上运行的任务都已经dequeue出运行队列,而且idle_balance也没有移动到当前运行队列的任务,那么schedule函数中,按照stop > idle > rt > cfs > idle这三种调度方式顺序,寻找各自的运行任务,那么如果rt和cfs都未找到运行任务,那么最后会调用idle schedule的idle进程,作为schedule函数调度的下一个任务 - -如果某个cpu空闲, 而其他CPU不空闲, 即当前CPU运行队列为NULL, 而其他CPU运行队列有进程等待调度的时候, 则内核会对CPU尝试负载平衡, CPU负载均衡有两种方式: pull和push, 即空闲CPU从其他忙的CPU队列中pull拉一个进程复制到当前空闲CPU上, 或者忙的CPU队列将一个进程push推送到空闲的CPU队列中. - -idle_balance其实就是pull的工作. - - -#4 组调度策略的支持 -------- - -组调度的情形下, 调度实体之间存在明显的层次关系, 因此在跟新子调度实体的时候, 需要更新父调度实体的信息, 同时我们为了保证同一组内的进程不能长时间占用处理机, 必须补偿其他组内的进程, 保证公平性 - - -```c -#ifdef CONFIG_FAIR_GROUP_SCHED - /* 如果nr_running计数器为0, 即当前队列上没有可运行进程, - * 则需要调度idle进程 */ - if (!cfs_rq->nr_running) - goto idle; - /* 如果当前运行进程prev不是被fair调度的普通非实时进程 */ - if (prev->sched_class != &fair_sched_class) - goto simple; - - /* - * Because of the set_next_buddy() in dequeue_task_fair() it is rather - * likely that a next task is from the same cgroup as the current. - * - * Therefore attempt to avoid putting and setting the entire cgroup - * hierarchy, only change the part that actually changes. - */ - - do { - struct sched_entity *curr = cfs_rq->curr; - - /* - * Since we got here without doing put_prev_entity() we also - * have to consider cfs_rq->curr. If it is still a runnable - * entity, update_curr() will update its vruntime, otherwise - * forget we've ever seen it. - */ - if (curr) - { - /* 如果当前进程curr在队列上, - * 则需要更新起统计量和虚拟运行时间 - * 否则设置curr为空 */ - if (curr->on_rq) - update_curr(cfs_rq); - else - curr = NULL; - - /* - * This call to check_cfs_rq_runtime() will do the - * throttle and dequeue its entity in the parent(s). - * Therefore the 'simple' nr_running test will indeed - * be correct. - */ - if (unlikely(check_cfs_rq_runtime(cfs_rq))) - goto simple; - } - /* 选择一个最优的调度实体 */ - se = pick_next_entity(cfs_rq, curr); - cfs_rq = group_cfs_rq(se); - } while (cfs_rq); /* 如果被调度的进程仍属于当前组,那么选取下一个可能被调度的任务,以保证组间调度的公平性 */ - /* 获取调度实体se的进程实体信息 */ - p = task_of(se); - - /* - * Since we haven't yet done put_prev_entity and if the selected task - * is a different task than we started out with, try and touch the - * least amount of cfs_rqs. - */ - if (prev != p) - { - struct sched_entity *pse = &prev->se; - - while (!(cfs_rq = is_same_group(se, pse))) - { - int se_depth = se->depth; - int pse_depth = pse->depth; - - if (se_depth <= pse_depth) - { - put_prev_entity(cfs_rq_of(pse), pse); - pse = parent_entity(pse); - } - if (se_depth >= pse_depth) - { - set_next_entity(cfs_rq_of(se), se); - se = parent_entity(se); - } - } - - put_prev_entity(cfs_rq, pse); - set_next_entity(cfs_rq, se); - } - - if (hrtick_enabled(rq)) - hrtick_start_fair(rq, p); - - return p; -``` - - -#与主调度器schedule进行通信 -------- - -我们在之前讲解主调度器的时候就提到过, 主调度器函数schedule会调用__schedule来完成抢占, 而主调度器的主要功能就是选择一个新的进程来抢占到当前的处理器. 因此其中必然不能缺少pick_next_task工作 - ->参见主调度器schedule)中调用全局的pick_next_task选择抢占的进程一节的内容 -> ->[CSDN地址]() -> ->[github地址](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/03-design/03-main_scheduler) - - -__schedule调用全局的pick_next_task函数选择一个最优的进程, 内核代码参见[kernel/sched/core.c, line 3142](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L3142) - -```c -static void __sched notrace __schedule(bool preempt) -{ - /* ...... */ - next = pick_next_task(rq); - /* ...... */ -} -``` - -全局的pick_next_task函数会从按照优先级遍历所有调度器类的pick_next_task函数, 去查找最优的那个进程, 当然因为大多数情况下, 系统中全是CFS调度的非实时进程, 因而linux内核也有一些优化的策略 - -其执行流程如下 - -* 如果当前cpu上所有的进程都是cfs调度的普通非实时进程, 则直接用cfs调度, 如果无程序可调度则调度idle进程 - -* 否则从优先级最高的调度器类sched_class_highest(目前是stop_sched_class)开始依次遍历所有调度器类的pick_next_task函数, 选择最优的那个进程执行 - -其定义在[kernel/sched/core.c, line 3068](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L3064) - - -#5 总结 -------- - -pick_next_task_fair用于完全公平调度器在CFS的运行队列中优选出一个最优的进程, 为了适应组调度策略和基本的策略, pick_next_task_fair使用的不同的代码标签 - -* simple标签是CFS中最基础的pick_next操作 - -* idle则使得在没有进程被调度时, 调度idle进程 - -* again标签用于循环的进行pick_next操作 - -* CONFIG_FAIR_GROUP_SCHED宏指定了组调度情况下的pick_next操作, 如果不支持组调度, 则pick_next_task_fair将直接从simple开始执行, 同样组调度情况下, 如果之前运行的进程不是CFS调度的进程, 我们也无需保存其组信息, 可以直接执行simple标签进行调度 - - -pick_next_task_fair的基本流程如下 - - -其基本流程如下 - -| 流程 | 描述 | -|:-------:|:-------:| -| !cfs_rq->nr_running -=> goto idle; | 如果nr_running计数器为0, 当前队列上没有可运行进程, 则需要调度idle进程 | -| put_prev_task(rq, prev); | 将当前进程放入运行队列的合适位置, 每次当进程被调度后都会使用set_next_entity从红黑树中移除, 因此被抢占时需要重新加如红黑树中等待被调度 | -| se = pick_next_entity(cfs_rq, NULL); | 选出下一个可执行调度实体 | -| set_next_entity(cfs_rq, se); | set_next_entity会调用__dequeue_entity把选中的进程从红黑树移除,并更新红黑树 | - - -其中最关键的pick_next_entity函数选择出下一个最渴望被公平调度器调度的进程, 函数的执行流程其实很简单 - -1. 先从最左节点left和当前节点curr中选择出最渴望被调度(即虚拟运行vruntime最小)的那个调度实体色 - -2. 判断第一步优选出的调度实体se是不是cfs_rq中被跳过调度的那个进程skip, 如果是则可能需要继续优选红黑树次左节点 - - * 如果se == curr == skip则需要跳过curr选择最左的那个调度实体second = left = __pick_first_entity(cfs_rq); - - * 否则se == left == skip, 则从次优的调度实体second和curr中选择最优的那个进程 - -3. 检查left是否可以抢占last和next调度实体, 此项有助于提高缓存的命中率 - -* cfs_rq的last和next指针, last表示最后一个执行wakeup的sched_entity, next表示最后一个被wakeup的sched_entity, 在pick新sched_entity的时候,会优先选择这些last或者next指针的sched_entity,有利于提高缓存的命中率 - - - -于是我们会发现, 下一个将要被调度的调度实体或者进程, 总是下列几个调度实体之一 - -| 调度实体 | 描述 | -|:-------:|:-------:| -| left = __pick_first_entity(cfs_rq) | **红黑树的最左节点**, 这个节点拥有当前队列中vruntime最小的特性, 即应该优先被调度 | -| second = __pick_first_entity(left) | **红黑树的次左节点**, 为什么这个节点也可能呢, 因为内核支持skip跳过某个进程的抢占权力的, 如果left被标记为skip(由cfs_rq->skip域指定), 那么可能就需要找到次优的那个进程 | -| cfs_rq的curr结点 | curr节点的vruntime可能比left和second更小, 但是由于它正在运行, 因此它不在红黑树中(进程抢占物理机的时候对应节点同时会从红黑树中删除), 但是如果其vruntime足够小, 意味着cfs调度器应该尽可能的补偿curr进程, 让它再次被调度, 同样这种优化也有助于提高缓存的命中率 | -|cfs_rq的last或者next | last表示最后一个执行wakeup的sched_entity, next表示最后一个被wakeup的sched_entity, 在pick新sched_entity的时候,会优先选择这些last或者next指针的sched_entity,有利于提高缓存的命中率 | - -即红黑树中的最左结点left和次左结点second(检查两个节点是因为cfs_rq的skip指针域标识了内核需要跳过不调度的实体信息, 如果left被跳过, 则需要检查second) - -以及cfs_rq的调度实体curr, last和next, curr是当前正在运行的进程, 它虽然已经运行, 但是可能仍然很饥渴, 那么我们应该继续补偿它, 而last表示最后一个执行wakeup的sched_entity, next表示最后一个被wakeup的sched_entity, 刚被唤醒的进程可能更希望得到CPU, 因此在pick新sched_entity的时候,会优先选择这些last或者next指针的sched_entity,有利于提高缓存的命中率 +Linux CFS调度器之pick_next_task_fair选择下一个被调度的进程 +======= + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-29 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/details/51456569) | + + +CFS负责处理普通非实时进程, 这类进程是我们linux中最普遍的进程 + + +#1 前景回顾 +------- + +##1.1 CFS调度算法 +------- + +**CFS调度算法的思想** + +理想状态下每个进程都能获得相同的时间片,并且同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。也就是说,当一个进程占用CPU时,其他进程就必须等待。CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度. + +##1.2 负荷权重和虚拟时钟 + +**虚拟时钟是红黑树排序的依据** + +具体实现时,CFS通过每个进程的**虚拟运行时间(vruntime)**来衡量哪个进程最值得被调度. CFS中的就绪队列是一棵以vruntime为键值的红黑树,虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的vruntime最小. + +**优先级计算负荷权重, 负荷权重和当前时间计算出虚拟运行时间** + +虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。而,CFS调度器中的权重在内核是对用户态进程的优先级nice值, 通过prio_to_weight数组进行nice值和权重的转换而计算出来的 + + +**虚拟时钟相关公式** + + linux内核采用了计算公式: + +| 属性 | 公式 | 描述 | +|:-------:|:-------:| +| ideal_time | sum_runtime *se.weight/cfs_rq.weight | 每个进程应该运行的时间 | +| sum_exec_runtime | | 运行队列中所有任务运行完一遍的时间 | +| se.weight | | 当前进程的权重 | +| cfs.weight | | 整个cfs_rq的总权重 | + +这里se.weight和cfs.weight根据上面讲解我们可以算出, sum_runtime是怎们计算的呢,linux内核中这是个经验值,其经验公式是 + +| 条件 | 公式 | +|:-------:|:-------:| +| 进程数 > sched_nr_latency | sum_runtime=sysctl_sched_min_granularity *nr_running | +| 进程数 <=sched_nr_latency | sum_runtime=sysctl_sched_latency = 20ms | + +>注:sysctl_sched_min_granularity =4ms +> +>sched_nr_latency是内核在一个延迟周期中处理的最大活动进程数目 + +linux内核代码中是通过一个叫vruntime的变量来实现上面的原理的,即: + +每一个进程拥有一个vruntime,每次需要调度的时候就选运行队列中拥有最小vruntime的那个进程来运行,vruntime在时钟中断里面被维护,每次时钟中断都要更新当前进程的vruntime,即vruntime以如下公式逐渐增长: + + +| 条件 | 公式 | +|:-------:|:-------:| +| curr.nice!=NICE_0_LOAD | vruntime += delta* NICE_0_LOAD/se.weight; | +| curr.nice=NICE_0_LOAD | vruntime += delta; | + + +##1.3 CFS进程入队和出队 +------- + +enqueue_task_fair和dequeue_task_fair分别用来向CFS就绪队列中添加或者删除进程 + +完全公平调度器CFS中有两个函数可用来增删队列的成员:enqueue_task_fair和dequeue_task_fair分别用来向CFS就绪队列中添加或者删除进程 + + +**enqueue_task_fair进程入队** + +向就绪队列中放置新进程的工作由函数enqueue_task_fair函数完成, 该函数定义在[kernel/sched/fair.c, line 5442](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5442), 其函数原型如下 + +该函数将task_struct *p所指向的进程插入到rq所在的就绪队列中, 除了指向所述的就绪队列rq和task_struct的指针外, 该函数还有另外一个参数wakeup. 这使得可以指定入队的进程是否最近才被唤醒并转换为运行状态(此时需指定wakeup = 1), 还是此前就是可运行的(那么wakeup = 0). + +```c +static void +enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags) +``` +enqueue_task_fair的执行流程如下 + +* 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做. + +* 否则, 具体的工作委托给enqueue_entity完成, 其中内核会借机用update_curr更新统计量 + 在enqueue_entity内部如果需要会调用__enqueue_entity将进程插入到CFS红黑树中合适的结点 + + +**dequeue_task_fair进程出队操作** + +dequeue_task_fair函数在完成睡眠等情况下调度, 将任务从就绪队列中移除 + +其执行的过程正好跟enqueue_task_fair的思路相同, 只是操作刚好相反 + + +enqueue_task_fair的执行流程如下 + +* 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做. + +* 否则, 具体的工作委托给dequeue_entity完成, 其中内核会借机用update_curr更新统计量 + 在enqueue_entity内部如果需要会调用__dequeue_entity将进程插入到CFS红黑树中合适的结点 + + +##1.4 今日看点(CFS如何选择最合适的进程) +------- + + +每个调度器类sched_class都必须提供一个pick_next_task函数用以在就绪队列中选择一个最优的进程来等待调度, 而我们的CFS调度器类中, 选择下一个将要运行的进程由pick_next_task_fair函数来完成 + + +之前我们在将主调度器的时候, 主调度器schedule函数在进程调度抢占时, 会通过__schedule函数调用全局pick_next_task选择一个最优的进程, 在pick_next_task中我们就按照优先级依次调用不同调度器类提供的pick_next_task方法 + +今天就让我们窥探一下完全公平调度器类CFS的pick_next_task方法pick_next_fair + + + + +**pick_next_task_fair** + +选择下一个将要运行的进程pick_next_task_fair执行. 其代码执行流程如下 + +对于pick_next_task_fair函数的讲解, 我们从simple标签开始, 这个是常规状态下pick_next的思路, 简单的来说pick_next_task_fair的函数框架如下 + +```c +again: + 控制循环来读取最优进程 + +#ifdef CONFIG_FAIR_GROUP_SCHED + 完成组调度下的pick_next选择 + 返回被选择的调度时实体的指针 +#endif + +simple: + 最基础的pick_next函数 + 返回被选择的调度时实体的指针 + +idle : + 如果系统中没有可运行的进行, 则需要调度idle进程 +``` + +可见我们会发现, + +* simple标签是CFS中最基础的pick_next操作 + +* idle则使得在没有进程被调度时, 调度idle进程 + +* again标签用于循环的进行pick_next操作 + +* CONFIG_FAIR_GROUP_SCHED宏指定了组调度情况下的pick_next操作, 如果不支持组调度, 则pick_next_task_fair将直接从simple开始执行 + + + +#2 simple无组调度最简单的pick_next_task_fair +------- + +在不支持组调度情况下(选项CONFIG_FAIR_GROUP_SCHED), CFS的pick_next_task_fair函数会直接执行simple标签, 优选下一个函数, 这个流程清晰而且简单, 但是已经足够我们理解cfs的pick_next了 + +##2.1 simple的基本流程 +------- + +pick_next_task_fair函数的simple标签定义在[kernel/sched/fair.c, line 5526)](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5526), 代码如下所示 + + +```c +simple: + cfs_rq = &rq->cfs; +#endif + /* 如果nr_running计数器为0, + * 当前队列上没有可运行进程, + * 则需要调度idle进程 */ + if (!cfs_rq->nr_running) + goto idle; + /* 将当前进程放入运行队列的合适位置 */ + put_prev_task(rq, prev); + + do + { + /* 选出下一个可执行调度实体(进程) */ + se = pick_next_entity(cfs_rq, NULL); + /* 把选中的进程从红黑树移除,更新红黑树 + * set_next_entity会调用__dequeue_entity完成此工作 */ + set_next_entity(cfs_rq, se); + /* group_cfs_rq return NULL when !CONFIG_FAIR_GROUP_SCHED + * 在非组调度情况下, group_cfs_rq返回了NULL */ + cfs_rq = group_cfs_rq(se); + } while (cfs_rq); /* 在没有配置组调度选项(CONFIG_FAIR_GROUP_SCHED)的情况下.group_cfs_rq()返回NULL.因此,上函数中的循环只会循环一次 */ + + + /* 获取到调度实体指代的进程信息 */ + p = task_of(se); + + if (hrtick_enabled(rq)) + hrtick_start_fair(rq, p); + + return p; +``` + +其基本流程如下 + +| 流程 | 描述 | +|:-------:|:-------:| +| !cfs_rq->nr_running -=> goto idle; | 如果nr_running计数器为0, 当前队列上没有可运行进程, 则需要调度idle进程 | +| put_prev_task(rq, prev); | 将当前进程放入运行队列的合适位置, 每次当进程被调度后都会使用set_next_entity从红黑树中移除, 因此被抢占时需要重新加如红黑树中等待被调度 | +| se = pick_next_entity(cfs_rq, NULL); | 选出下一个可执行调度实体 | +| set_next_entity(cfs_rq, se); | set_next_entity会调用__dequeue_entity把选中的进程从红黑树移除,并更新红黑树 | + + +##2.2 put_prev_task +------- + + +###2.2.1 **全局put_prev_task函数** + + +put_prev_task用来将前一个进程prev放回到就绪队列中, 这是一个全局的函数, 而每个调度器类也必须实现一个自己的put_prev_task函数(比如CFS的put_prev_task_fair), + +由于CFS调度的时候, prev进程不一定是一个CFS调度的进程, 因此必须调用全局的put_prev_task来调用prev进程所属调度器类sched_class的对应put_prev_task方法, 完成将进程放回到就绪队列中 + + +全局的put_prev_task函数定义在[kernel/sched/sched.h, line 1245](http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1245), 代码如下所示 +```c +static inline void put_prev_task(struct rq *rq, struct task_struct *prev) +{ + prev->sched_class->put_prev_task(rq, prev); +} +``` + +###2.2.2 **CFS的put_prev_task_fair函数** + + +然后我们来分析一下CFS的put_prev_task_fair函数, 其定义在[kernel/sched/fair.c, line 5572](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5572) + +在选中了下一个将被调度执行的进程之后,回到pick_next_task_fair中,执行set_next_entity + +```c +/* + * Account for a descheduled task: + */ +static void put_prev_task_fair(struct rq *rq, struct task_struct *prev) +{ + struct sched_entity *se = &prev->se; + struct cfs_rq *cfs_rq; + + for_each_sched_entity(se) { + cfs_rq = cfs_rq_of(se); + put_prev_entity(cfs_rq, se); + } +} +``` + +前面我们说到过函数在组策略情况下, 调度实体之间存在父子的层次, for_each_sched_entity会从当前调度实体开始, 然后循环向其父调度实体进行更新, 非组调度情况下则只执行一次 + +而put_prev_task_fair函数最终会调用put_prev_entity函数将prev的调度时提se放回到就绪队列中等待下次调度 + + +###2.2.3 **put_prev_entity函数** + +[put_prev_entity](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3443)函数定义在[kernel/sched/fair.c, line 3443](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3443), 他在更新了虚拟运行时间等信息后, 最终通过__enqueue_entity函数将prev进程(即current进程)放回就绪队列rq上 + + + +##2.3 pick_next_entity +------- + +###2.3.1 **pick_next_entity函数完全注释** +------- + +```c +/* + * Pick the next process, keeping these things in mind, in this order: + * 1) keep things fair between processes/task groups + * 2) pick the "next" process, since someone really wants that to run + * 3) pick the "last" process, for cache locality + * 4) do not run the "skip" process, if something else is available + * + * 1. 首先要确保任务组之间的公平, 这也是设置组的原因之一 + * 2. 其次, 挑选下一个合适的(优先级比较高的)进程 + * 因为它确实需要马上运行 + * 3. 如果没有找到条件2中的进程 + * 那么为了保持良好的局部性 + * 则选中上一次执行的进程 + * 4. 只要有任务存在, 就不要让CPU空转, + * 只有在没有进程的情况下才会让CPU运行idle进程 + */ +static struct sched_entity * +pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr) +{ + /* 摘取红黑树最左边的进程 */ + struct sched_entity *left = __pick_first_entity(cfs_rq); + struct sched_entity *se; + + /* + * If curr is set we have to see if its left of the leftmost entity + * still in the tree, provided there was anything in the tree at all. + * + * 如果 + * left == NULL 或者 + * curr != NULL curr进程比left进程更优(即curr的虚拟运行时间更小) + * 说明curr进程是自动放弃运行权利, 且其比最左进程更优 + * 因此将left指向了curr, 即curr是最优的进程 + */ + if (!left || (curr && entity_before(curr, left))) + { + left = curr; + } + + /* se = left存储了cfs_rq队列中最优的那个进程 + * 如果进程curr是一个自愿放弃CPU的进程(其比最左进程更优), 则取se = curr + * 否则进程se就取红黑树中最左的进程left, 它必然是当前就绪队列上最优的 + */ + se = left; /* ideally we run the leftmost entity */ + + /* + * Avoid running the skip buddy, if running something else can + * be done without getting too unfair. + * + * cfs_rq->skip存储了需要调过不参与调度的进程调度实体 + * 如果我们挑选出来的最优调度实体se正好是skip + * 那么我们需要选择次优的调度实体se来进行调度 + * 由于之前的se = left = (curr before left) curr left + * 则如果 se == curr == skip, 则选择left = __pick_first_entity进行即可 + * 否则则se == left == skip, 则选择次优的那个调度实体second + */ + if (cfs_rq->skip == se) + { + struct sched_entity *second; + + if (se == curr) /* se == curr == skip选择最左的那个调度实体left */ + { + second = __pick_first_entity(cfs_rq); + } + else /* 否则se == left == skip, 选择次优的调度实体second */ + { + /* 摘取红黑树上第二左的进程节点 */ + second = __pick_next_entity(se); + /* 同时与left进程一样, + * 如果 + * second == NULL 没有次优的进程 或者 + * curr != NULL curr进程比left进程更优(即curr的虚拟运行时间更小) + * 说明curr进程比最second进程更优 + * 因此将second指向了curr, 即curr是最优的进程*/ + if (!second || (curr && entity_before(curr, second))) + second = curr; + } + + /* 判断left和second的vruntime的差距是否小于sysctl_sched_wakeup_granularity + * 即如果second能抢占left */ + if (second && wakeup_preempt_entity(second, left) < 1) + se = second; + } + + /* + * Prefer last buddy, try to return the CPU to a preempted task. + * + * + */ + if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1) + se = cfs_rq->last; + + /* + * Someone really wants this to run. If it's not unfair, run it. + */ + if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1) + se = cfs_rq->next; + + /* 用过一次任何一个next或者last + * 都需要清除掉这个指针 + * 以免影响到下次pick next sched_entity */ + clear_buddies(cfs_rq, se); + + return se; +} +``` + +###2.3.2 **从left, second和curr进程中选择最优的进程** +------- + + +pick_next_entity则从CFS的红黑树中摘取一个最优的进程, 这个进程往往在红黑树的最左端, 即vruntime最小, 但是也有例外, 但是不外乎这几个进程 + +| 调度实体 | 描述 | +|:-------:|:-------:| +| left = __pick_first_entity(cfs_rq) | **红黑树的最左节点**, 这个节点拥有当前队列中vruntime最小的特性, 即应该优先被调度 | +| second = __pick_first_entity(left) | **红黑树的次左节点**, 为什么这个节点也可能呢, 因为内核支持skip跳过某个进程的抢占权力的, 如果left被标记为skip(由cfs_rq->skip域指定), 那么可能就需要找到次优的那个进程 | +| curr结点 | curr节点的vruntime可能比left和second更小, 但是由于它正在运行, 因此它不在红黑树中(进程抢占物理机的时候对应节点同时会从红黑树中删除), 但是如果其vruntime足够小, 意味着cfs调度器应该尽可能的补偿curr进程, 让它再次被调度 | + +其中__pick_first_entity会返回cfs_rq红黑树中的最左节点rb_leftmost所属的调度实体信息, 该函数定义在[kernel/sched/fair.c, line 543](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3443) + +而__pick_next_entity(se)函数则返回se在红黑树中中序遍历的下一个节点信息, 该函数定义在[kernel/sched/fair.c, line 544](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3443), 获取下一个节点的工作可以通过内核红黑树的标准操作rb_next完成 + + + +###2.3.3 **cfs_rq的last和next指针域** +------- + +在pick_next_entity的最后, 要把红黑树最左下角的进程和另外两个进程(即next和last)做比较, next是抢占失败的进程, 而last则是抢占成功后被抢占的进程, 这三个进程到底哪一个是最优的next进程呢? + +Linux CFS实现的判决条件是: + +1. 尽可能满足需要刚被唤醒的进程抢占其它进程的需求 + +2. 尽可能减少以上这种抢占带来的缓存刷新的影响 + + +**cfs_rq的last和next指针,last表示最后一个执行wakeup的sched_entity,next表示最后一个被wakeup的sched_entity。他们在进程wakeup的时候会赋值,在pick新sched_entity的时候,会优先选择这些last或者next指针的sched_entity,有利于提高缓存的命中率** ** + +因此我们优选出来的进程必须同last和next指针域进行对比, 其实就是检查就绪队列中的最优进程, 即红黑树中最左节点last是否可以抢占last和next指针域, 检查是否可以抢占是通过wakeup_preempt_entity函数来完成的. + + +###2.3.4 **wakeup_preempt_entity检查是否可以被抢占** +------- + + +```c +// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5317 +/* + * Should 'se' preempt 'curr'. + * + * |s1 + * |s2 + * |s3 + * g + * |<--->|c + * + * w(c, s1) = -1 + * w(c, s2) = 0 + * w(c, s3) = 1 + * + */ +static int +wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se) +{ + /* vdiff为curr和se vruntime的差值*/ + s64 gran, vdiff = curr->vruntime - se->vruntime; + + /* cfs_rq的vruntime是单调递增的,也就是一个基准 + * 各个进程的vruntime追赶竞争cfsq的vruntime + * 如果curr的vruntime比较小, 说明curr更加需要补偿, + * 即se无法抢占curr */ + if (vdiff <= 0) + return -1; + + /* 计算curr的最小抢占期限粒度 */ + gran = wakeup_gran(curr, se); + /* 当差值大于这个最小粒度的时候才抢占,这可以避免频繁抢占 */ + if (vdiff > gran) + return 1; + + return 0; +} + + +// http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L5282 +static unsigned long +wakeup_gran(struct sched_entity *curr, struct sched_entity *se) +{ + /* NICE_0_LOAD的基准最小运行期限 */ + unsigned long gran = sysctl_sched_wakeup_granularity; + + /* + * Since its curr running now, convert the gran from real-time + * to virtual-time in his units. + * + * By using 'se' instead of 'curr' we penalize light tasks, so + * they get preempted easier. That is, if 'se' < 'curr' then + * the resulting gran will be larger, therefore penalizing the + * lighter, if otoh 'se' > 'curr' then the resulting gran will + * be smaller, again penalizing the lighter task. + * + * This is especially important for buddies when the leftmost + * task is higher priority than the buddy. + * + * 计算进程运行的期限,即抢占的粒度 + */ + return calc_delta_fair(gran, se); +} +``` + +到底能不能选择last和next两个进程, 则是wakeup_preempt_entity函数来决定的, 看下面的图解即可: + +![last进程next进程和left进程的比较](./images/last_next_left.png) + +* 如果S3是left,curr是next或者last,left的vruntime值小于curr和next, 函数wakeup_preempt_entity肯定返回1,那么就说明next和last指针的vruntime和left差距过大,这个时候没有必要选择这个last或者next指针,而是应该优先补偿left + +* 如果next或者last是S2,S1,那么vruntime和left差距并不大,并没有超过sysctl_sched_wakeup_granularity ,那么这个next或者last就可以被优先选择,而代替了left + +而清除last和next这两个指针的时机有这么几个: + +* sched_tick的时候, 如果一个进程的运行时间超过理论时间(这个时间是根据load和cfs_rq的load, 平均分割sysctl_sched_latency的时间), 那么如果next或者last指针指向这个正在运行的进程, 需要清除这个指针, 使得pick sched_entity不会因为next或者last指针再次选择到这个sched_entity + +* 当一个sched_entity调度实体dequeue出运行队列,那么如果有next或者last指针指向这个sched_entity, 那么需要删除这个next或者last指针。 + +* 刚才说的那种case,如果next,last指针在pick的时候被使用了一次,那么这次用完了指针,需要清除相应的指针,避免使用过的next,last指针影响到下次pick + +* 当进程yield操作的时候,进程主动放弃了调度机会,那么如果next,last指针指向了这个sched_entity,那么需要清除相应指针。 + + + +##2.4 set_next_entity +------- + + +现在我们已经通过pick_next_task_fair选择了进程, 但是还需要完成一些工作, 才能将其标记为运行进程. 这是通过set_next_entity来处理的. 该函数定义在[kernel/sched/fair.c, line 3348](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3348) + + +当前执行进程(我们选择出来的进程马上要抢占处理器开始执行)不应该再保存在就绪队列上, 因此set_next_entity()函数会调用__dequeue_entity(cfs_rq, se)把选中的下一个进程移出红黑树. 如果当前进程是最左节点, __dequeue_entity会将leftmost指针设置到次左进程 + +```c + /* 'current' is not kept within the tree. */ + if (se->on_rq) /* 如果se尚在rq队列上 */ + { + /* ...... */ + /* 将se从cfs_rq的红黑树中删除 */ + __dequeue_entity(cfs_rq, se); + /* ...... */ + } +``` + +尽管该进程不再包含在红黑树中, 但是进程和就绪队列之间的关联并没有丢失, 因为curr标记了当前进程cfs_rq->curr = se; + +```c + cfs_rq->curr = se; +``` + +然后接下来是一些统计信息的处理, 如果内核开启了调度统计CONFIG_SCHEDSTATS标识, 则会完成调度统计的计算和更新 + + + +```c +#ifdef CONFIG_SCHEDSTATS + /* + * Track our maximum slice length, if the CPU's load is at + * least twice that of our own weight (i.e. dont track it + * when there are only lesser-weight tasks around): + */ + if (schedstat_enabled() && rq_of(cfs_rq)->load.weight >= 2*se->load.weight) { + se->statistics.slice_max = max(se->statistics.slice_max, + se->sum_exec_runtime - se->prev_sum_exec_runtime); + } +#endif +``` + +在set_next_entity的最后, 将选择出的调度实体se的sum_exec_runtime保存在了prev_sum_exec_runtime中, 因为该调度实体指向的进程, 马上将抢占处理器成为当前活动进程, 在CPU上花费的实际时间将记入sum_exec_runtime, 因此内核会在prev_sum_exec_runtime保存此前的设置. 要注意进程中的sum_exec_runtime没有重置. 因此差值sum_exec_runtime - prev_sum_runtime确实标识了在CPU上执行花费的实际时间. + +最后我们附上set_next_entity函数的完整注释信息 + + +```c +static void +set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) +{ + /* 'current' is not kept within the tree. */ + if (se->on_rq) /* 如果se尚在rq队列上 */ + { + /* + * Any task has to be enqueued before it get to execute on + * a CPU. So account for the time it spent waiting on the + * runqueue. + */ + if (schedstat_enabled()) + update_stats_wait_end(cfs_rq, se); + /* 将se从cfs_rq的红黑树中删除 */ + __dequeue_entity(cfs_rq, se); + update_load_avg(se, 1); + } + /* 新sched_entity中的exec_start字段为当前clock_task */ + update_stats_curr_start(cfs_rq, se); + /* 将se设置为curr进程 */ + cfs_rq->curr = se; +#ifdef CONFIG_SCHEDSTATS + /* + * Track our maximum slice length, if the CPU's load is at + * least twice that of our own weight (i.e. dont track it + * when there are only lesser-weight tasks around): + */ + if (schedstat_enabled() && rq_of(cfs_rq)->load.weight >= 2*se->load.weight) { + se->statistics.slice_max = max(se->statistics.slice_max, + se->sum_exec_runtime - se->prev_sum_exec_runtime); + } +#endif + /* 更新task上一次投入运行的从时间 */ + se->prev_sum_exec_runtime = se->sum_exec_runtime; +} +``` + + + + +#3 idle进程的调度 +------- + +```c + /* 如果nr_running计数器为0, + * 当前队列上没有可运行进程, + * 则需要调度idle进程 */ + if (!cfs_rq->nr_running) + goto idle; +``` + +如果系统中当前运行队列上没有可调度的进程, 那么会调到idle标签去调度idle进程. + + +idle标签如下所示 + +```c +idle: + /* + * This is OK, because current is on_cpu, which avoids it being picked + * for load-balance and preemption/IRQs are still disabled avoiding + * further scheduler activity on it and we're being very careful to + * re-start the picking loop. + */ + lockdep_unpin_lock(&rq->lock); + new_tasks = idle_balance(rq); + lockdep_pin_lock(&rq->lock); + /* + * Because idle_balance() releases (and re-acquires) rq->lock, it is + * possible for any higher priority task to appear. In that case we + * must re-start the pick_next_entity() loop. + */ + if (new_tasks < 0) + return RETRY_TASK; + + if (new_tasks > 0) + goto again; + + return NULL; +``` + +其关键就是调用idle_balance进行任务的迁移 + + 每个cpu都有自己的运行队列, 如果当前cpu上运行的任务都已经dequeue出运行队列,而且idle_balance也没有移动到当前运行队列的任务,那么schedule函数中,按照stop > idle > rt > cfs > idle这三种调度方式顺序,寻找各自的运行任务,那么如果rt和cfs都未找到运行任务,那么最后会调用idle schedule的idle进程,作为schedule函数调度的下一个任务 + +如果某个cpu空闲, 而其他CPU不空闲, 即当前CPU运行队列为NULL, 而其他CPU运行队列有进程等待调度的时候, 则内核会对CPU尝试负载平衡, CPU负载均衡有两种方式: pull和push, 即空闲CPU从其他忙的CPU队列中pull拉一个进程复制到当前空闲CPU上, 或者忙的CPU队列将一个进程push推送到空闲的CPU队列中. + +idle_balance其实就是pull的工作. + + +#4 组调度策略的支持 +------- + +组调度的情形下, 调度实体之间存在明显的层次关系, 因此在跟新子调度实体的时候, 需要更新父调度实体的信息, 同时我们为了保证同一组内的进程不能长时间占用处理机, 必须补偿其他组内的进程, 保证公平性 + + +```c +#ifdef CONFIG_FAIR_GROUP_SCHED + /* 如果nr_running计数器为0, 即当前队列上没有可运行进程, + * 则需要调度idle进程 */ + if (!cfs_rq->nr_running) + goto idle; + /* 如果当前运行进程prev不是被fair调度的普通非实时进程 */ + if (prev->sched_class != &fair_sched_class) + goto simple; + + /* + * Because of the set_next_buddy() in dequeue_task_fair() it is rather + * likely that a next task is from the same cgroup as the current. + * + * Therefore attempt to avoid putting and setting the entire cgroup + * hierarchy, only change the part that actually changes. + */ + + do { + struct sched_entity *curr = cfs_rq->curr; + + /* + * Since we got here without doing put_prev_entity() we also + * have to consider cfs_rq->curr. If it is still a runnable + * entity, update_curr() will update its vruntime, otherwise + * forget we've ever seen it. + */ + if (curr) + { + /* 如果当前进程curr在队列上, + * 则需要更新起统计量和虚拟运行时间 + * 否则设置curr为空 */ + if (curr->on_rq) + update_curr(cfs_rq); + else + curr = NULL; + + /* + * This call to check_cfs_rq_runtime() will do the + * throttle and dequeue its entity in the parent(s). + * Therefore the 'simple' nr_running test will indeed + * be correct. + */ + if (unlikely(check_cfs_rq_runtime(cfs_rq))) + goto simple; + } + /* 选择一个最优的调度实体 */ + se = pick_next_entity(cfs_rq, curr); + cfs_rq = group_cfs_rq(se); + } while (cfs_rq); /* 如果被调度的进程仍属于当前组,那么选取下一个可能被调度的任务,以保证组间调度的公平性 */ + /* 获取调度实体se的进程实体信息 */ + p = task_of(se); + + /* + * Since we haven't yet done put_prev_entity and if the selected task + * is a different task than we started out with, try and touch the + * least amount of cfs_rqs. + */ + if (prev != p) + { + struct sched_entity *pse = &prev->se; + + while (!(cfs_rq = is_same_group(se, pse))) + { + int se_depth = se->depth; + int pse_depth = pse->depth; + + if (se_depth <= pse_depth) + { + put_prev_entity(cfs_rq_of(pse), pse); + pse = parent_entity(pse); + } + if (se_depth >= pse_depth) + { + set_next_entity(cfs_rq_of(se), se); + se = parent_entity(se); + } + } + + put_prev_entity(cfs_rq, pse); + set_next_entity(cfs_rq, se); + } + + if (hrtick_enabled(rq)) + hrtick_start_fair(rq, p); + + return p; +``` + + +#5 与主调度器schedule进行通信 +------- + +我们在之前讲解主调度器的时候就提到过, 主调度器函数schedule会调用__schedule来完成抢占, 而主调度器的主要功能就是选择一个新的进程来抢占到当前的处理器. 因此其中必然不能缺少pick_next_task工作 + +>参见主调度器schedule)中调用全局的pick_next_task选择抢占的进程一节的内容 +> +>[CSDN地址]() +> +>[github地址](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/03-design/03-main_scheduler) + + +__schedule调用全局的pick_next_task函数选择一个最优的进程, 内核代码参见[kernel/sched/core.c, line 3142](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L3142) + +```c +static void __sched notrace __schedule(bool preempt) +{ + /* ...... */ + next = pick_next_task(rq); + /* ...... */ +} +``` + +全局的pick_next_task函数会从按照优先级遍历所有调度器类的pick_next_task函数, 去查找最优的那个进程, 当然因为大多数情况下, 系统中全是CFS调度的非实时进程, 因而linux内核也有一些优化的策略 + +其执行流程如下 + +* 如果当前cpu上所有的进程都是cfs调度的普通非实时进程, 则直接用cfs调度, 如果无程序可调度则调度idle进程 + +* 否则从优先级最高的调度器类sched_class_highest(目前是stop_sched_class)开始依次遍历所有调度器类的pick_next_task函数, 选择最优的那个进程执行 + +其定义在[kernel/sched/core.c, line 3068](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L3064) + + +#6 总结 +------- + +pick_next_task_fair用于完全公平调度器在CFS的运行队列中优选出一个最优的进程, 为了适应组调度策略和基本的策略, pick_next_task_fair使用的不同的代码标签 + +* simple标签是CFS中最基础的pick_next操作 + +* idle则使得在没有进程被调度时, 调度idle进程 + +* again标签用于循环的进行pick_next操作 + +* CONFIG_FAIR_GROUP_SCHED宏指定了组调度情况下的pick_next操作, 如果不支持组调度, 则pick_next_task_fair将直接从simple开始执行, 同样组调度情况下, 如果之前运行的进程不是CFS调度的进程, 我们也无需保存其组信息, 可以直接执行simple标签进行调度 + + +pick_next_task_fair的基本流程如下 + + +其基本流程如下 + +| 流程 | 描述 | +|:-------:|:-------:| +| !cfs_rq->nr_running -=> goto idle; | 如果nr_running计数器为0, 当前队列上没有可运行进程, 则需要调度idle进程 | +| put_prev_task(rq, prev); | 将当前进程放入运行队列的合适位置, 每次当进程被调度后都会使用set_next_entity从红黑树中移除, 因此被抢占时需要重新加如红黑树中等待被调度 | +| se = pick_next_entity(cfs_rq, NULL); | 选出下一个可执行调度实体 | +| set_next_entity(cfs_rq, se); | set_next_entity会调用__dequeue_entity把选中的进程从红黑树移除,并更新红黑树 | + + +其中最关键的pick_next_entity函数选择出下一个最渴望被公平调度器调度的进程, 函数的执行流程其实很简单 + +1. 先从最左节点left和当前节点curr中选择出最渴望被调度(即虚拟运行vruntime最小)的那个调度实体色 + +2. 判断第一步优选出的调度实体se是不是cfs_rq中被跳过调度的那个进程skip, 如果是则可能需要继续优选红黑树次左节点 + + * 如果se == curr == skip则需要跳过curr选择最左的那个调度实体second = left = __pick_first_entity(cfs_rq); + + * 否则se == left == skip, 则从次优的调度实体second和curr中选择最优的那个进程 + +3. 检查left是否可以抢占last和next调度实体, 此项有助于提高缓存的命中率 + +* cfs_rq的last和next指针, last表示最后一个执行wakeup的sched_entity, next表示最后一个被wakeup的sched_entity, 在pick新sched_entity的时候,会优先选择这些last或者next指针的sched_entity,有利于提高缓存的命中率 + + + +于是我们会发现, 下一个将要被调度的调度实体或者进程, 总是下列几个调度实体之一 + +| 调度实体 | 描述 | +|:-------:|:-------:| +| left = __pick_first_entity(cfs_rq) | **红黑树的最左节点**, 这个节点拥有当前队列中vruntime最小的特性, 即应该优先被调度 | +| second = __pick_first_entity(left) | **红黑树的次左节点**, 为什么这个节点也可能呢, 因为内核支持skip跳过某个进程的抢占权力的, 如果left被标记为skip(由cfs_rq->skip域指定), 那么可能就需要找到次优的那个进程 | +| cfs_rq的curr结点 | curr节点的vruntime可能比left和second更小, 但是由于它正在运行, 因此它不在红黑树中(进程抢占物理机的时候对应节点同时会从红黑树中删除), 但是如果其vruntime足够小, 意味着cfs调度器应该尽可能的补偿curr进程, 让它再次被调度, 同样这种优化也有助于提高缓存的命中率 | +|cfs_rq的last或者next | last表示最后一个执行wakeup的sched_entity, next表示最后一个被wakeup的sched_entity, 在pick新sched_entity的时候,会优先选择这些last或者next指针的sched_entity,有利于提高缓存的命中率 | + +即红黑树中的最左结点left和次左结点second(检查两个节点是因为cfs_rq的skip指针域标识了内核需要跳过不调度的实体信息, 如果left被跳过, 则需要检查second) + +以及cfs_rq的调度实体curr, last和next, curr是当前正在运行的进程, 它虽然已经运行, 但是可能仍然很饥渴, 那么我们应该继续补偿它, 而last表示最后一个执行wakeup的sched_entity, next表示最后一个被wakeup的sched_entity, 刚被唤醒的进程可能更希望得到CPU, 因此在pick新sched_entity的时候,会优先选择这些last或者next指针的sched_entity,有利于提高缓存的命中率 diff --git a/study/kernel/01-process/README.md b/study/kernel/01-process/README.md index a7258b1..73da11e 100644 --- a/study/kernel/01-process/README.md +++ b/study/kernel/01-process/README.md @@ -3,7 +3,7 @@ | 2016-07-21 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.5) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | -#0. 项目链接 +#1 项目链接 ------- | 项目 | 描述 | @@ -13,8 +13,7 @@ | [LDD-LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process) | 与CSDN博客同步更新, 但是除了包含博客的内容, 还包含了一些以驱动方式实现的实验代码 | - -#1. 进程的描述 +#2 进程的描述 ------- | CSDN | GitHub | @@ -24,12 +23,12 @@ |[Linux进程ID号--Linux进程的管理与调度(三)](http://blog.csdn.net/gatieme/article/details/51383377) | [study/kernel/01-process/01-task/03-pid](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/01-task/03-pid)| -#2. 进程的创建 +#3 进程的创建 ------- | CSDN | GitHub | | ------------- |:-------------:| -| [Linux下的进程类别(内核线程、轻量级进程和用户进程)以及其创建方式--Linux进程的管理与调度(四) ](http://blog.csdn.net/gatieme/article/details/51482122) | [study/kernel/01-process/02-create/01-duplicate](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/02-create/01-duplicate)| +| [Linux下的进程类别(内核线程、轻量级进程和用户进程)以及其创建方式--Linux进程的管理与调度(四) ](http://blog.csdn.net/gatieme/article/details/51482122) | [study/kernel/01-process/02-create/01-duplicate](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/02-create/01-duplicate)| | [Linux下0号进程的前世(init_task进程)今生(idle进程)----Linux进程的管理与调度(五)](http://blog.csdn.net/gatieme/article/details/51484562) | [study/kernel/01-process/02-create/02-idel](http://blog.csdn.net/gatieme/article/details/51484562) | | [Linux下1号进程的前世(kernel_init)今生(init进程)----Linux进程的管理与调度(六)](http://blog.csdn.net/gatieme/article/details/51532804) | [study/kernel/01-process/02-create/03-init](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/02-create/03-init)| | [Linux下2号进程的kthreadd--Linux进程的管理与调度(七)](http://blog.csdn.net/gatieme/article/details/51566690) | [study/kernel/01-process/02-create/04-kthreadd](http://blog.csdn.net/gatieme/article/details/51566690) | @@ -37,7 +36,7 @@ | [Linux进程内核栈与thread_info结构详解--Linux进程的管理与调度(九)](http://blog.csdn.net/gatieme/article/details/51577479) | [study/kernel/01-process/02-create/06-thread_info](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/02-create/06-thread_info) | | [Linux内核线程kernel thread详解--Linux进程的管理与调度(十)](http://blog.csdn.net/gatieme/article/details/51589205) | [study/kernel/01-process/02-create/07-kernel_thead](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/02-create/07-kernel_thead)| -#3. 进程的加载与运行 +#4 进程的加载与运行 ------- | CSDN | GitHub | @@ -48,7 +47,7 @@ -#4. 进程的退出 +#5 进程的退出 ------- | CSDN | GitHub | @@ -57,7 +56,7 @@ -#5. 进程的调度 +#6 进程的调度 ------- @@ -73,3 +72,16 @@ | [Linux进程优先级的处理--Linux进程的管理与调度(二十二)](http://blog.csdn.net/gatieme/article/details/51719208) | [study/kernel/01-process/05-schedule/03-design/06-priority](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/03-design/06-priority) | | [Linux唤醒抢占----Linux进程的管理与调度(二十三)](http://blog.csdn.net/gatieme/article/details/51872831) | [study/kernel/01-process/05-schedule/03-design/07-wakeup](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/03-design/07-wakeup) | + +#7 调度普通进程-完全公平调度器CFS +------- + +| CSDN | GitHub | +| ------------- |:-------------:| +| [Linux进程调度之CFS调度器概述--Linux进程的管理与调度(二十四)](http://blog.csdn.net/gatieme/article/details/52067518) | [study/kernel/01-process/05-schedule/07-cfs/01-cfs/](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/07-cfs/01-cfs) | +| [Linux CFS调度器之负荷权重load_weight--Linux进程的管理与调度(二十五)](http://blog.csdn.net/gatieme/article/details/52067665) | [study/kernel/01-process/05-schedule/07-cfs/02-load_weight/](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/07-cfs/02-load_weight) | +| [Linux CFS调度器之虚拟时钟vruntime与调度延迟--Linux进程的管理与调度(二十六)](http://blog.csdn.net/gatieme/article/details/52067748) | [study/kernel/01-process/05-schedule/07-cfs/03-vruntime/](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/07-cfs/03-vruntime) | +| [Linux CFS调度器之队列操作--Linux进程的管理与调度(二十七)](http://blog.csdn.net/gatieme/article/details/52067898) | [study/kernel/01-process/05-schedule/07-cfs/04-queue/](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/07-cfs/04-queue) | +| [Linux CFS调度器之pick_next_task_fair选择下一个被调度的进程--Linux进程的管理与调度(二十八)](http://blog.csdn.net/gatieme/article/details/52068016) | [study/kernel/01-process/05-schedule/07-cfs/05-pick_next/](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/07-cfs/05-pick_next) | +| [Linux CFS调度器之task_tick_fair处理周期性调度器--Linux进程的管理与调度(二十九)](http://blog.csdn.net/gatieme/article/details/52068050) | [study/kernel/01-process/05-schedule/07-cfs/06-task_tick_fair/](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/07-cfs/06-task_tick_fair) | +| [Linux CFS调度器之唤醒抢占--Linux进程的管理与调度(三十)](http://blog.csdn.net/gatieme/article/details/52068061) | [study/kernel/01-process/05-schedule/07-cfs/07-task_new_fair/](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/01-process/05-schedule/07-cfs/07-task_new_fair) | \ No newline at end of file