- 多任务
- 抢占式和非抢占式
- Linux的进程调度
- 2.5前的O(n)调度
- 2.5的O(1)调度
- 2.6的 完全公平调度算法(CFS)
- 策略
- 进程分类:I/O 和 CPU消耗型进程区别
- 进程优先级:nice和priority
- 实时进程和普通进程
- Linux调度类
- 调度器:主调度器+周期调度器
- 调度策略:实时(FIFO + RR)/ 普通(NORMAL)
- CFS 完全公平调度算法
- 分配规则
- vruntime体现公平性
- 数据结构
- 抢占和上下文切换
- 上下文切换
- 用户抢占和内核抢占的区别
4.1 多任务
多任务操作系统:多进程并发执行,可以划分为两类
- 非抢占式多任务(cooperative multitasking):由调度程序来决定何时停止一个进程的运行,通常与时间片(timeslice)搭配使用
- 抢占式多任务(preemptive multitasking):进程一直执行,直到它完成或主动让步,然后处理器切换到另一个进程。绝大多数操作系统基本不用这个
4.2 Linux的进程调度
(1)历史演变
- 2.5之前:复杂度为O(n)的调度算法,从1991年Linux的第1版到后来的2.4,Linux调度程序都很简陋
- 2.5开始:引入O(1)调度程序,特点是在拥有数以十计的多CPU环境下,性能完美。缺点是对于交互程序体验不佳
- 2.6开始,Linux的“完全公平调度算法”(简称CFS)代替了O(1)算法,该算法基于“反转楼梯最后期限调度算法”理论(Rotating Staircase Deadline scheduler)
(2)调度算法:
- O(n)算法
- pick next算法,遍历runqueue中所有进程,选出优先级最高的进程调用
- 缺点很明显,进程越来越多时,处理器消耗大量时间
- O(1)算法
- 进程优先级范围是0到139,共140个。因此该算法为每个优先级都设置了一个runqueue,即包含140个可运行状态的进程链表
- 用一个bitmap记录这140个进程链表,因此需要5个32位int来表示,每个进程占一个bit。
- 初始bitmap中的所有位都被置0,当某个优先级的进程处于可运行状态时,该优先级所对应的位就被置1
- 选取过程可以理解为:遍历bitmap,找到对应位为1 且 优先级最高的那个
- CFS算法
- 楼梯调度算法 staircase scheduler
- RSDL旋转楼梯(Rotating Staircase Deadline Schelduler)
- CFS 完全公平调度器
- 该算法吸取了上面两个的思想,最终被内核采纳
- 详细见下面4.4
- BFS调度器
- 又称“脑残调度器”,没啥用,见Reference
(3)Reference
- Linux进程调度策略的发展和演变--Linux进程的管理与调度(十六):https://blog.csdn.net/gatieme/article/details/51701149
4.3 策略
(1)进程的分类
- 第一种分类方式:对于这种进程,调度器应该尽量降低它们的调度频率,延长运行时间
类型 | 描述 | 示例 | 评价 |
I/O消耗型,更注重响应时间 | 进程大多数时间用来提交或等待I/O请求 | 数据库服务器, 文本编辑器 | 真正运行时间短,多数时间用于阻塞 |
CPU消耗型,更注重吞吐量 | 大多数时间用于执行代码上 | 深度学习训练模型 | 对于这种进程,调度器应该尽量降低它们的调度频率,延长运行时间 |
- 第二种分类方式
类型 | 描述 | 示例 |
交互式进程(interactive process) | 此类进程经常与用户进行交互, 因此需要花费很多时间等待键盘和鼠标操作. 当接受了用户的输入后, 进程必须很快被唤醒, 否则用户会感觉系统反应迟钝 | shell, 文本编辑程序和图形应用程序 |
批处理进程(batch process) | 此类进程不必与用户交互, 因此经常在后台运行. 因为这样的进程不必很快相应, 因此常受到调度程序的怠慢 | 程序语言的编译程序, 数据库搜索引擎以及科学计算 |
实时进程(real-time process) | 这些进程由很强的调度需要, 这样的进程绝不会被低优先级的进程阻塞. 并且他们的响应时间要尽可能的短 | 视频音频应用程序, 机器人控制程序以及从物理传感器上收集数据的程序 |
(2)进程优先级的概念
- 传统操作系统:优先级高的进程先运行,低的后运行,相同优先级的进程按轮询挨个调度
- Linux:优先级决定了该进程在所有可用进程中的时间占比,优先级高的进程获得时间片长
(3)Linux进程优先级属性:nice(NI值)和Priority(PR值)
- nice值
- 范围是 -20 到 +19,默认值为0
- nice值越大,优先级越低,获得的处理器时间越少
- 也称静态优先级,即当nice值设定好了之后,除非我们用renice去改它,否则它是不变的
- nice值是所有Unix系统的标准化的概念,不同的Unix系统nice值运用方式有所差异:在Mac OS中,nice值代表分配给进程时间片的绝对值,是绝对的。而Linux中,nice代表时间片的比例,是相对的
- priority
- 实时优先级,也称动态优先级,因为priority的值在之前内核的O1调度器上表现是会变化的
- 默认情况下范围从0到99(包括0和99)
- priority值越大,优先级越高
- 重要Reference:https://blog.51cto.com/frankch/1773621
(4)实时进程和非实时进程(普通进程)
- 在内核中,进程优先级的取值范围是通过一个宏定义的,这个宏的名称是MAX_PRIO,它的值为140。而这个值又是由另外两个值相加组成的,一个是代表nice值取值范围的NICE_WIDTH宏(-20到+19,共40),另一个是代表实时进程(realtime)优先级范围的MAX_RT_PRIO宏(0到99,共100)
- 说白了就是,Linux实际上实现了140个优先级范围,取值范围是从0-139,这个值越小,优先级越高
- 实时进程:优先级值在0-99范围内的,都是实时进程,可选择的调度策略:SCHED_FIFO、SCHED_RR(Round Robin)
- 非实时进程 / 普通进程:100-139范围内的是非实时进程,对应策略有:SCHED_NORMAL、SCHED_OTHER、SCHED_IDLE
- 重要Reference:https://blog.51cto.com/frankch/1773621
(5)时间片
- 时间片太长,影响交互;时间片太短,容易造成上下文切换的消耗
- 对于Linux来说,nice值影响每个进程的时间片
(6)一个例子
- 假设存在两个进程:文字编辑进程(I/O消耗型进程) 和 视频编码进程(CPU消耗进程)
- 对于文字编辑(I/O)进程:希望有更高的处理器时间(在Linux中即优先级更高),这并非因为它需要更多的处理器时间,而是我们希望在它需要时总能优先得到处理器
- 理想的打算:分配给 IO进程 和 CPU进程 相同的nice值,因此它们时间片各占50%。每次使用时,IO进程使用时间不到50%会提前释放时间片,CPU进程用满50%。对于IO进程来说,它拥有更高的优先级;对于CPU进程来说,它拥有更长的使用时间。显然,这是公平的
4.4 Linux调度相关
(1)调度器:Linux有两个调度器一起工作
- 主调度器 scheduler:一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU
- 周期性调度器 scheduler_tick:另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要
(2)调度策略(算法):六种
- 实时进程:SCHED_FIFO,SCHED_RR,SCHED_DEADLINE(新支持的策略,基于EDF算法)
- 普通进程:SCHED_NORMAL,SCHED_BATCH(也是基于CFS的,分化版本)
- IDLE进程(可作了解,不重要):SCHED_IDLE(系统空闲时才跑这个调度算法)
(3)调度器类:五个
调度器类 | 所属进程优先级 | 描述 | 对应调度策略 |
---|---|---|---|
stop_sched_class | 最高 | 优先级最高的线程,会中断所有其他线程,且不会被其他任务打断作用 | 无, 不需要调度普通进程 |
dl_sched_class | 较高 | 采用EDF最早截至时间优先算法调度实时进程 | SCHED_DEADLINE |
rt_sched_class | 高 | 采用提供 Roound-Robin算法或者FIFO算法调度实时进程 | SCHED_FIFO, SCHED_RR |
fair_sched_clas | 中 | 采用CFS算法调度普通的非实时进程 | SCHED_NORMAL, SCHED_BATCH |
idle_sched_class | 低 | 采用CFS算法调度idle进程, 每个cup的第一个pid=0线程:swapper,是一个静态线程 | SCHED_IDLE |
(4)总结
调度器类 | 调度策略 | 调度策略对应的调度算法 | 调度实体 | 调度实体对应的调度对象 |
---|---|---|---|---|
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 |
(5)Reference
Linux进程调度器概述--Linux进程的管理与调度(十五):https://blog.csdn.net/gatieme/article/details/51699889
4.5 CFS 完全公平调度算法
(1)CFS公平调度设计思路
- 根据进程优先级权重(nice)分配运行时间
- 分配给进程的运行时间 = 调度周期 * 进程权重 / 所有进程权重之和
- 调度周期很好理解,就是将所有处于TASK_RUNNING态进程都调度一遍的时间,差不多相当于O(1)调度算法中运行队列和过期队列切换一次的时间
- 举个例子,比如只有两个进程A, B,权重分别为1和2,调度周期设为30ms,那么分配给A的CPU时间为:30ms * (1/(1+2)) = 10ms;而B的CPU时间为:30ms * (2/(1+2)) = 20ms。那么在这30ms中A将运行10ms,B将运行20ms
(2)公平体现在哪——vruntime
- 其实公平是体现在另外一个量上面,叫做virtual runtime(vruntime),它记录着进程已经运行的时间,但是并不是直接记录,而是要根据进程的权重将运行时间放大或者缩小一个比例
- vruntime = 实际运行时间 * 1024 / 进程权重
- 为了不把大家搞晕,这里我直接写1024,实际上它等于nice为0的进程的权重,代码中是NICE_0_LOAD。也就是说,所有进程都以nice为0的进程的权重1024作为基准,计算自己的vruntime增加速度。
- 还以上面AB两个进程为例,B的权重是A的2倍,那么B的vruntime增加速度只有A的一半
(3)CFS的内部实现,主要由四个部分组成,位于 kernel/sched_fair.c 中
- 时间记账
- CFS不再有时间片的概念,由vruntime变量来记录该进程的运行时间
- 进程选择
- 当CFS需要选择下一个运行进程时,它会挑一个具有最小vruntime的进程
- CFS使用红黑树来组织可运行进程队列,迅速找到具有最小vruntime的进程(即最左侧的叶子节点)
- 调度器入口
- 在文件 kernel/sched.c 中,进程调度的主要入口点函数 schedule():在可运行队列中,选择哪个进程可以运行,何时投入运行
- schedule()通常都需要和一个调度类相关联:找到优先级最高的调度类,询问该调度类谁是下一个该运行的进程
- 睡眠和唤醒
- 休眠(被阻塞)的进程处于一个不可执行状态,常见原因比如read()等。
- 进入休眠时,内核把该进程标记成休眠状态,从可执行红黑树中移除。唤醒进程则相反
- 第三章曾讨论过,休眠对应两种状态:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE,区别在于
- TASK_INTERRUPTIBLE:如果接收到一个信号,会被提前唤醒并响应该信号
- TASK_UNINTERRUPTIBLE:如果接收到一个信号,会忽略
- 休眠的进程在内核中用等待队列wake_queue_head_t来表示,需要休眠的进程进入该队列,被唤醒后再出来
- 唤醒操作通过函数wake_up()进行,负责唤醒指定的等待队列上的所有进程
(4)Reference
- https://www.cnblogs.com/tianguiyu/articles/6091378.html
- https://blog.csdn.net/weixin_42092278/article/details/83959440
- https://blog.csdn.net/gatieme/article/details/51699889
4.6 抢占和上下文切换
(1)上下文切换
- 概念:从一个可执行进程切换到另一个可执行进程
- 对应函数:kernel/sched.c 中的 context_switch() 函数
- 1. 调用switch_mm(), 把虚拟内存从一个进程映射切换到新进程中
- 2. 调用switch_to(),从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息
(2)抢占:分为用户抢占和内核抢占
- 用户抢占
- 内核即将返回用户空间时(无论是【系统调用返回】还是【中断处理返回】),都会检查need_resched标志,如果它被设置了,内核会选择一个(更适合的)进程投入运行
- 发生时机有两种
- 从【系统调用】返回【用户空间】
- 从【中断处理程序】返回【用户空间】
- 内核抢占
- 只要重新调度是安全的(没有锁),内核就能进行抢占
- 每个进程的thread_info中有个preempt_count计数器,使用锁时+1,释放锁时-1。只有为0时才能安全抢占
- 发生时机
- 当从【中断处理程序】正在执行,且返回内核空间之前。当一个【中断处理例程】退出,在返回到内核态时(kernel-space)。这是隐式的调用schedule()函数,当前任务没有主动放弃CPU使用权,而是被剥夺了CPU使用权
- 当内核代码再一次具有可抢占性的时候,如解锁(spin_unlock_bh)及使能软中断(local_bh_enable)等, 此时当kernel code从不可抢占状态变为可抢占状态时(preemptible again)。也就是preempt_count从正整数变为0时。这也是隐式的调用schedule()函数
- 如果内核中的任务显式的调用schedule(), 任务主动放弃CPU使用权
- 如果内核中的任务阻塞(这同样也会导致调用schedule()), 导致需要调用schedule()函数。任务主动放弃CPU使用权
- Reference
- Linux进程调度器概述--Linux进程的管理与调度(十五):https://blog.csdn.net/gatieme/article/details/51699889
4.7 实时调度策略
见4.4的调度策略
Reference
- Linux进程调度器概述--Linux进程的管理与调度(十五):https://blog.csdn.net/gatieme/article/details/51699889
- Linux进程调度策略的发展和演变--Linux进程的管理与调度(十六):https://blog.csdn.net/gatieme/article/details/51701149