Thread Priority
1 前景回顾
1.1 进程调度
内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来.
调度器面对的情形就是这样, 其任务是在程序之间共享 CPU 时间, 创造并行执行的错觉, 该任务分为两个不同的部分, 其中一个涉及调度策略, 另外一个涉及上下文切换.
内核必须提供一种方法, 在各个进程之间尽可能公平地共享 CPU 时间, 而同时又要考虑不同的任务优先级.
调度器的一个重要目标是有效地分配 CPU 时间片,同时提供很好的用户体验。调度器还需要面对一些互相冲突的目标,例如既要为关键实时任务最小化响应时间, 又要最大限度地提高 CPU 的总体利用率.
调度器的一般原理是, 按所需分配的计算能力, 向系统中每个进程提供最大的公正性, 或者从另外一个角度上说, 他试图确保没有进程被亏待.
1.2 进程的分类
linux 把进程区分为实时进程和非实时进程, 其中非实时进程进一步划分为交互式进程和批处理进程
| 类型 | 描述 | 示例 |
|---|---|---|
| 交互式进程(interactive process) | 此类进程经常与用户进行交互, 因此需要花费很多时间等待键盘和鼠标操作. 当接受了用户的输入后, 进程必须很快被唤醒, 否则用户会感觉系统反应迟钝 | shell, 文本编辑程序和图形应用程序 |
| 批处理进程(batch process) | 此类进程不必与用户交互, 因此经常在后台运行. 因为这样的进程不必很快相应, 因此常受到调度程序的怠慢 | 程序语言的编译程序, 数据库搜索引擎以及科学计算 |
| 实时进程(real-time process) | 这些进程由很强的调度需要, 这样的进程绝不会被低优先级的进程阻塞. 并且他们的响应时间要尽可能的短 | 视频音频应用程序, 机器人控制程序以及从物理传感器上收集数据的程序 |
在 linux 中, 调度算法可以明确的确认所有实时进程的身份, 但是没办法区分交互式程序和批处理程序, linux2.6 的调度程序实现了基于进程过去行为的启发式算法, 以确定进程应该被当做交互式进程还是批处理进程. 当然与批处理进程相比, 调度程序有偏爱交互式进程的倾向
1.3 不同进程采用不同的调度策略
根据进程的不同分类 Linux 采用不同的调度策略.
对于实时进程,采用 FIFO, Round Robin 或者 Earliest Deadline First (EDF)最早截止期限优先调度算法|的调度策略.
对于普通进程,则需要区分交互式和批处理式的不同。传统 Linux 调度器提高交互式应用的优先级,使得它们能更快地被调度。而 CFS 和 RSDL 等新的调度器的核心思想是”完全公平”。这个设计理念不仅大大简化了调度器的代码复杂度,还对各种调度需求的提供了更完美的支持.
注意 Linux 通过将进程和线程调度视为一个,同时包含二者。进程可以看做是单个线程,但是进程可以包含共享一定资源(代码和/或数据)的多个线程。因此进程调度也包含了线程调度的功能.
目前非实时进程的调度策略比较简单, 因为实时进程值只要求尽可能快的被响应, 基于优先级, 每个进程根据它重要程度的不同被赋予不同的优先级,调度器在每次调度时, 总选择优先级最高的进程开始执行. 低优先级不可能抢占高优先级, 因此 FIFO 或者 Round Robin 的调度策略即可满足实时进程调度的需求.
但是普通进程的调度策略就比较麻烦了, 因为普通进程不能简单的只看优先级, 必须公平的占有 CPU, 否则很容易出现进程饥饿, 这种情况下用户会感觉操作系统很卡, 响应总是很慢,因此在 linux 调度器的发展历程中经过了多次重大变动, linux 总是希望寻找一个最接近于完美的调度策略来公平快速的调度进程.
1.4 linux 调度器的演变
一开始的调度器是复杂度为O(n)O(n)的始调度算法(实际上每次会遍历所有任务,所以复杂度为 O(n)), 这个算法的缺点是当内核中有很多任务时,调度器本身就会耗费不少时间,所以,从 linux2.5 开始引入赫赫有名的O(1)O(1)调度器
然而,linux 是集全球很多程序员的聪明才智而发展起来的超级内核,没有最好,只有更好,在 O(1)O(1)调度器风光了没几天就又被另一个更优秀的调度器取代了,它就是CFS 调度器 Completely Fair Scheduler. 这个也是在 2.6 内核中引入的,具体为 2.6.23,即从此版本开始,内核使用 CFS 作为它的默认调度器,O(1)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.5 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 个调度器类, 一个调度器类可以用一种种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程.
其所属进程的优先级顺序为
|
|
3 个调度实体
调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度.
这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实体,即 seched_entity 结构, 其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组.
linux 中针对当前可调度的实时和非实时进程, 定义了类型为 seched_entity 的 3 个调度实体
- sched_dl_entity 采用 EDF 算法调度的实时调度实体
- sched_rt_entity 采用 Roound-Robin 或者 FIFO 算法调度的实时调度实体 rt_sched_class
- 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 |
2 linux 优先级的表示
2.1 优先级的内核表示
linux 优先级概述
在用户空间通过 nice 命令设置进程的静态优先级, 这在内部会调用 nice 系统调用, 进程的 nice 值在-20~+19 之间. 值越低优先级越高.
setpriority 系统调用也可以用来设置进程的优先级. 它不仅能够修改单个线程的优先级, 还能修改进程组中所有进程的优先级, 或者通过制定 UID 来修改特定用户的所有进程的优先级
内核使用一些简单的数值范围 0~139 表示内部优先级, 数值越低, 优先级越高。
从 0~99 的范围专供实时进程使用, nice 的值[-20,19]则映射到范围 100~139
linux2.6 内核将任务优先级进行了一个划分, 实时优先级范围是 0 到 MAX_RT_PRIO-1(即 99),而普通进程的静态优先级范围是从 MAX_RT_PRIO 到 MAX_PRIO-1(即 100 到 139).
| 优先级范围 | 描述 |
|---|---|
| 0——99 | 实时进程 |
| 100——139 | 非实时进程 |
内核的优先级表示
内核表示优先级的所有信息基本都放在include/linux/sched/prio.h中, 其中定义了一些表示优先级的宏和函数.
优先级数值通过宏来定义, 如下所示,
其中 MAX_NICE 和 MIN_NICE 定义了 nice 的最大最小值
而 MAX_RT_PRIO 指定了实时进程的最大优先级, 而 MAX_PRIO 则是普通进程的最大优先级数值
|
|
| 宏 | 值 | 描述 |
|---|---|---|
| MIN_NICE | -20 | 对应于优先级 100, 可以使用 NICE_TO_PRIO 和 PRIO_TO_NICE 转换 |
| MAX_NICE | 19 | 对应于优先级 139, 可以使用 NICE_TO_PRIO 和 PRIO_TO_NICE 转换 |
| NICE_WIDTH | 40 | nice 值得范围宽度, 即[-20, 19]共 40 个数字的宽度 |
| MAX_RT_PRIO, MAX_USER_RT_PRIO | 100 | 实时进程的最大优先级 |
| MAX_PRIO | 140 | 普通进程的最大优先级 |
| DEFAULT_PRIO | 120 | 进程的默认优先级, 对应于 nice=0 |
| MAX_DL_PRIO | 0 | 使用 EDF 最早截止时间优先调度算法的实时进程最大的优先级 |
而内核提供了一组宏将优先级在各种不同的表示形之间转移
|
|
还有一些 nice 值和 rlimit 值之间相互转换的函数 nice_to_rlimit 和 rlimit_to_nice, 这在 nice 系统调用进行检查的时候很有用, 他们定义在include/linux/sched/prio.h, L47中, 如下所示
|
|
DEF 最早截至时间优先实时调度算法的优先级描述
此外新版本的内核还引入了 EDF 实时调度算法, 它的优先级比 RT 进程和 NORMAL/BATCH 进程的优先级都要高, 关于 EDF 的优先级的设置信息都早内核头文件include/linux/sched/deadline.h
因此内核将 MAX_DL_PRIO 设置为 0, 可以参见内核文件include/linux/sched/deadline.h
|
|
此外也提供了一些 EDF 优先级处理所需的函数, 如下所示, 可以参见内核文件include/linux/sched/deadline.h
|
|
2.2 进程的优先级表示
|
|
动态优先级 静态优先级 实时优先级
其中 task_struct 采用了三个成员表示进程的优先级:prio 和 normal_prio 表示动态优先级, static_prio 表示进程的静态优先级.
为什么表示动态优先级需要两个值 prio 和 normal_prio
调度器会考虑的优先级则保存在 prio. 由于在某些情况下内核需要暂时提高进程的优先级, 因此需要用 prio 表示. 由于这些改变不是持久的, 因此静态优先级 static_prio 和普通优先级 normal_prio 不受影响.
此外还用了一个字段 rt_priority 保存了实时进程的优先级
| 字段 | 描述 |
|---|---|
| static_prio | 用于保存静态优先级, 是进程启动时分配的优先级, ,可以通过 nice 和 sched_setscheduler 系统调用来进行修改, 否则在进程运行期间会一直保持恒定 |
| rt_priority | 用于保存实时优先级 |
| normal_prio | 表示基于进程的静态优先级 static_prio 和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级 |
| prio | 保存进程的动态优先级 |
实时进程的优先级用实时优先级 rt_priority 来表示
3 进程优先级的计算
前面说了 task_struct 中的几个优先级的字段
| 静态优先级 | 实时优先级 | 普通优先级 | 动态优先级 |
|---|---|---|---|
| static_prio | rt_priority | normal_prio | prio |
但是这些优先级是如何关联的呢, 动态优先级 prio 又是如何计算的呢?
3.1 normal_prio 函数设置普通优先级 normal_prio
静态优先级 static_prio(普通进程)和实时优先级 rt_priority(实时进程)是计算的起点
因此他们也是进程创建的时候设定好的, 我们通过 nice 修改的就是普通进程的静态优先级 static_prio
首先通过静态优先级 static_prio 计算出普通优先级 normal_prio, 该工作可以由 nromal_prio 来完成, 该函数定义在kernel/sched/core.c#L861
|
|
| 进程类型 | 调度器 | 普通优先级 normal_prio |
|---|---|---|
| EDF 实时进程 | EDF | MAX_DL_PRIO-1 = -1 |
| 普通实时进程 | RT | MAX_RT_PRIO-1 - p->rt_priority = 99 - rt_priority |
| 普通进程 | CFS | __normal_prio(p) = static_prio |
普通优先级 normal_prio 需要根据普通进程和实时进程进行不同的计算, 其中__normal_prio 适用于普通进程, 直接将普通优先级 normal_prio 设置为静态优先级 static_prio. 而实时进程的普通优先级计算依据其实时优先级 rt_priority.
3.1.1 辅助函数 task_has_dl_policy 和 task_has_rt_policy
定义在kernel/sched/sched.h#L117 中
其本质其实就是传入 task->policy 调度策略字段看其值等于 SCHED_NORMAL, SCHED_BATCH, SCHED_IDLE, SCHED_FIFO, SCHED_RR, SCHED_DEADLINE 中的哪个, 从而确定其所属的调度类, 进一步就确定了其进程类型
|
|
3.1.2 关于 rt_priority 数值越大, 实时进程优先级越高的问题
我们前面提到了数值越小, 优先级越高, 但是此处我们会发现 rt_priority 的值越大, 其普通优先级越小, 从而优先级越高.
因此网上出现了一种说法, 优先级越高?这又是怎么回事?难道有一种说法错了吗?
实际的原因是这样的,对于一个实时进程,他有两个参数来表明优先级——prio 和 rt_priority,
prio 才是调度所用的最终优先级数值,这个值越小,优先级越高;
而 rt_priority 被称作实时进程优先级,他要经过转化——prio=MAX_RT_PRIO - 1- p->rt_priority;
MAX_RT_PRIO = 100, ;这样意味着 rt_priority 值越大,优先级越高;
而内核提供的修改优先级的函数,是修改 rt_priority 的值,所以越大,优先级越高。
所以用户在使用实时进程或线程,在修改优先级时,就会有“优先级值越大,优先级越高的说法”,也是对的。
3.1.3 为什么需要__normal_prio 函数
我们肯定会奇怪, 为什么增加了一个__normal_prio 函数做了这么简单的工作, 这个其实是有历史原因的: 在早期的 O(1)O(1)调度器中, 普通优先级的计算涉及相当多技巧性地工作, 必须检测交互式进程并提高其优先级, 而必须”惩罚”非交互进程, 以便是得系统获得更好的交互体验. 这需要很多启发式的计算, 他们可能完成的很好, 也可能不工作
3.2 effective_prio 函数设置动态优先级 prio
可以通过函数 effective_prio 用静态优先级 static_prio 计算动态优先级 prio, 即·
|
|
该函数定义在kernel/sched/core.c, line 861
|
|
我们会发现函数首先 effective_prio 设置了普通优先级, 显然我们用 effective_prio 同时设置了两个优先级(普通优先级 normal_prio 和动态优先级 prio)
因此计算动态优先级的流程如下
- 设置进程的普通优先级(实时进程 99-rt_priority, 普通进程为 static_priority)
- 计算进程的动态优先级(实时进程则维持动态优先级的 prio 不变, 普通进程的动态优先级即为其普通优先级)
最后, 我们综述一下在针对不同类型进程的计算结果
| 进程类型 | 实时优先级 rt_priority | 静态优先级 static_prio | 普通优先级 normal_prio | 动态优先级 prio |
|---|---|---|---|---|
| EDF 调度的实时进程 | rt_priority | 不使用 | MAX_DL_PRIO-1 | 维持原 prio 不变 |
| RT 算法调度的实时进程 | rt_priority | 不使用 | MAX_RT_PRIO-1-rt_priority | 维持原 prio 不变 |
| 普通进程 | 不使用 | static_prio | static_prio | static_prio |
| 优先级提高的普通进程 | 不使用 | static_prio(改变) | static_prio | 维持原 prio 不变 |
3.2.1 为什么 effective_prio 使用优先级数值检测实时进程
t_prio 会检测普通优先级是否在实时范围内, 即是否小于 MAX_RT_PRIO.参见include/linux/sched/rt.h#L6
|
|
而前面我们在 normal_prio 的时候, 则通过 task_has_rt_policy 来判断其 policy 属性来确定
policy == SCHED_FIFO || policy == SCHED_RR;1
那么为什么 effective_prio 重检测实时进程是 rt_prio 基于优先级数值, 而非 task_has_rt_policy 或者 rt_policy?
对于临时提高至实时优先级的非实时进程来说, 这个是必要的, 这种情况可能发生在是哦那个实时互斥量(RT-Mutex)时.
3.3 设置 prio 的时机
- 在新进程用 wake_up_new_task 唤醒时, 或者使用 nice 系统调用改变其静态优先级时, 则会通过 effective_prio 的方法设置 p->prio
wake_up_new_task(), 计算此进程的优先级和其他调度参数,将新的进程加入到进程调度队列并设此进程为可被调度的,以后这个进程可以被进程调度模块调度执行。
- 进程创建时 copy_process 通过调用 sched_fork 来初始化和设置调度器的过程中会设置子进程的优先级
3.4 nice 系统调用的实现
nice 系统调用是的内核实现是 sys_nice, 其定义在kernel/sched/core.c#L7498,
它在通过一系列检测后, 通过set_user_nice 函数, 其定义在kernel/sched/core.c#L3497
关于其具体实现我们会在另外一篇博客里面详细讲
3.5 fork 时优先级的继承
在进程分叉处子进程时, 子进程的静态优先级继承自父进程. 子进程的动态优先级 p->prio 则被设置为父进程的普通优先级, 这确保了实时互斥量引起的优先级提高不会传递到子进程.
可以参照 sched_fork 函数, 在进程复制的过程中 copy_process 通过调用 sched_fork 来设置子进程优先级, 参见sched_fork 函数
|
|
4 总结
task_struct 采用了四个成员表示进程的优先级:prio 和 normal_prio 表示动态优先级, static_prio 表示进程的静态优先级. 同时还用了 rt_priority 表示实时进程的优先级
| 字段 | 描述 |
|---|---|
| static_prio | 用于保存静态优先级, 是进程启动时分配的优先级, ,可以通过 nice 和 sched_setscheduler 系统调用来进行修改, 否则在进程运行期间会一直保持恒定 |
| prio | 进程的动态优先级, 这个有显示才是调度器重点考虑的进程优先级 |
| normal_prio | 普通进程的静态优先级 static_prio 和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级, 可以通过 normal_prio 来计算(非实时进程用 static_prIo 计算, 实时进程用 rt_priority 计算) |
| rt_priority | 实时进程的静态优先级 |
调度器会考虑的优先级则保存在 prio. 由于在某些情况下内核需要暂时提高进程的优先级, 因此需要用 prio 表示. 由于这些改变不是持久的, 因此静态优先级 static_prio 和普通优先级 normal_prio 不受影响. 此外还用了一个字段 rt_priority 保存了实时进程的优先级静态优先级 static_prio(普通进程)和实时优先级 rt_priority(实时进程)是计算的起点, 通过他们计算进程的普通优先级 normal_prio 和动态优先级 prio.
内核通过 normal_prIo 函数计算普通优先级 normal_prio 通过 effective_prio 函数计算动态优先级 prio
参考
-
No backlinks found.