(2021) 20 [虚拟化] 进程调度
南京大学操作系统课蒋炎岩老师网络课程笔记。
视频:https://www.bilibili.com/video/BV1HN41197Ko?p=20
讲义:http://jyywiki.cn/OS/2021/slides/11.slides#/
背景 — 机制与策略分离
- 机制:一个通用的、可定制的抽象
- 策略:在机制上实现的具体
- 例子:
- 分页和存储保护(机制) VS. 进程实现(策略)
- 操作系统API(机制)VS. 应用程序(策略)
中断和虚拟存储为我们提供了进程抽象(机制),操作系统的实现者在中断时获得了 ”选择一个程序执行“ 的权利,但是到底应该选哪个呢?(策略)
本次课内容与目标
理解常见的处理器调度策略
- 轮转调度(round - robin)
- 优先级 / 反馈调度
- 公平调度
了解调度是操作系统领域重要的、未解决的问题。
虚假的(课本上的)处理器调度
一组基本的假设
我们接下来的讨论,基于这样一组基本的假设,这使得我们能够更好地聚焦与调度问题。
- 系统中有一个处理器(1970s)
- 多个进程共享CPU
- 包括系统调用(进程的一部分代码在syscall中运行)
- 偶尔会等待I/O返回,不适用CPU(通常时间较长)
- 处理器以固定的频率被中断
- 随时有可能有新的进程被创建 / 旧的进程退出
中断机制:在中断 / 系统调用中可以切换到其他进程执行
策略:Round-Robin 轮转
假设档期 TiT_iTi 运行,中断之后会试图切换到下一个线程 T(i+1)modnT_{(i+1)\ mod\ n}T(i+1) mod n ,如果下一个线程正在等待 I / O返回,就继续尝试下一个,如果所有的线程都不需要 CPU,就调度idle进程执行。
中断之间进程的执行称为时间片 (time-slicing)
没有优先级的处理。
策略:引入优先级
UNIX niceness
- -20 … 19
- 越 nice,越被不 nice 的人抢占
- -20: 极坏; most favorable to the process
- 19: 极好; least favorable to the process
- 基于优先级的各种策略
- 有坏人,永远轮不到好人 (RTOS;
好人流下了悔恨的泪水),除非高优先级的进程在等待IO,否则永远是高优先级先执行 - nice 相差 10, CPU 获得相差 10 倍 (Linux)(linux实测应为1:9)
- 有坏人,永远轮不到好人 (RTOS;
- 不妨试一试: nice/renice
taskset -c 0 nice -n 19 ./a.out &
taskset -c 0 nice -n 9 ./a.out &
top
命令除了查看进程的CPU和内存的占用情况之外,也可以查看进程的NICE值,即NI
。
真实的处理器调度
策略:动态优先级,多级反馈队列调度(MLFQ)
不会设置优先级?能不能让系统自动设定?
- 交互进程 (vi, vscode, …),大部分时候在等待外界输入,这时这种进程会主动让出CPU
- 优先调度它们能提升用户体验,减少卡顿 (试想 Round-Robin)
- 计算进程 (gcc, ld, …),拼命使用 CPU
调度策略
设置若干个 Round-Robin 队列,每个队列对应一个优先级。
如果将时间片用完了,则判定该进程执行大量运算,就下调其的优先级
- 优先调度高优先级队列
- 用完时间片 → 坏人,请你变得更好
- 让出 CPU I/O → 好人,可以变得更坏
详见教科书OSTEP
问题
这种方式在早年间可以很好地工作,但是在现代操作系统中,大量的进程之间会有通信和互动。这时,如果两个进程之间彼此频繁通信,我们的多级反馈调度也会把它们识别成互动进程,提高其优先级。
策略:Complete Fair Scheduling (CFS)
基本策略
随着现代操作系统越来越复杂,操作系统的设计者已经放弃了设计一个完美的调度算法,而是转而使用一种按执行时间完全公平的调度算法。注意其中还有NICE机制来调节优先级,但其他情况下完全公平。
所有复杂系统的调度都是拙劣的 Workaround
试图去模拟一个 “ideal multi-task CPU”
“让系统里的所用进程尽可能公平地分享处理器”。具体来说,它会为每个进程记录精确的运行时间,中断 / 异常发生之后,就切换到运行时间最少的进程来执行,而当下次中断 / 异常之后,当前进程可能就不是运行时间最少的了,这时再选择当前最少运行时间的进程来执行。
CFS实现优先级
让好人的时间变得快一些,坏人的时间变得慢一些……
- 不再是运行时间,而是 “vruntime” (virtual runtime)
- vrt[i]/vrt[j]vrt[i]\ /\ vrt[j]vrt[i] / vrt[j] 的增加比例 =wt[j]/wt[i]= wt[j]\ /\ wt[i]=wt[j] / wt[i]
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,
};
CFS 的复杂性
-
新进程 / 线程:子进程继承符进程的vruntime
-
I/O (例如 1 分钟) 以后回来 vruntime 严重落后。为了赶上,CPU 会全部归它所有。
Linux 的实现:被唤醒的进程获得 “最小” 的 vruntime (可以立即被执行)。
-
vruntime 有优先级的 “倍数”,如果溢出了 64-bit 整数怎么办?
假设:系统中最近、最远的时刻差不超过数轴的一半。我们可以比较它们的相对大小,
a < b
不再代表 “小于” !bool less(u64 a, u64 b) {return (i64)(a - b) < 0; }
实现CFS的数据结构
用什么数据结构维护所有进程的 vruntime?考虑:我们需要什么操作?
为每个进程维护映射 t↦vt(t)t↦v_t(t)t↦vt(t)
- 维护进程的 vruntimevt(t)←vt(t)+Δt/wvruntime v_t(t)←v_t(t)+Δt/wvruntimevt(t)←vt(t)+Δt/w
- 找到 ttt 满足 vt(t)v_t(t)vt(t) 最小
- 进程创建/退出/睡眠/唤醒时插入/删除 ttt
Linux内核中的实现:红黑树。
调度与互斥锁
处理器调度:不仅是计算
线程不是 while (1)
的循环,还可能等待互斥锁/信号量/设备 (比一个时间片短很多)。在此情形下,会发生什么?
- round-robin?
- 考虑三个进程/线程: producer, consumer, while (1)
- 主要是因为没有精确的时间统计
- CFS?
- (似乎没问题?) 线程有精确的 accounting 信息
优先级反转(priority inversion)
void bad_guy() { // 高优先级mutex_lock(&lk);...mutex_unlock(&lk);
}void nice_guy() { // 中优先级while (1) ;
}void very_nice_guy() { // 最低优先级mutex_lock(&lk);...mutex_unlock(&lk);
}
very nice guy 在持有锁的时候让出了处理器……
- bad guy 顺便也无法运行了 (nice guy 抢在了它前面 )
解决优先级反转问题
Linux: CFS 凑合用吧;实时系统(RTOS):火星车在 CPU Reset 啊喂??
- 优先级继承 (Priority Inheritance)/优先级提升 (Priority Ceiling)
- 持有 mutex 的线程/进程会继承 block 在该 mutex 上的最高优先级
- 不总是能 work (例如条件变量唤醒)
- 在系统中动态维护资源依赖关系
- 优先级继承是它的特例
- 似乎更困难了……
- 避免高/低优先级的任务争抢资源
- 对潜在的优先级反转进行预警 (lockdep)
- TX-based: 冲突的 TX 发生时,总是低优先级的 abort
由于在现代操作系统中进程 / 线程之间的资源依赖(如互斥锁等)过于复杂,Linux选择躺平。
实际情况下的一些问题
多处理器调度:多用户、多任务
还没完:我们的 CPU 里有多个共享内存的处理器啊!
- 不能简单地每个处理器上执行 CFS
- 出现 “一核出力,七核围观”
- 也不能简单地一个全局 CFS 维护队列
- 在处理器之间迁移会导致 L1 cache/TLB 全都白给
- 迁移?可能过一会儿还得移回来
- 不迁移?造成处理器的浪费
- 在处理器之间迁移会导致 L1 cache/TLB 全都白给
注意L1、L2缓存是每个CPU独立的,L3缓存共享的。
实际情况(1)
- A 要跑一个任务,因为要调用一个库,只能单线程跑
- B 跑并行的任务,创建 1000 个线程跑
- CFS 会发生什么?
- 提示: CFS 公平地在线程之间共享 CPU
更糟糕的是,优先级解决不了这个问题……
- B 不能随便提高自己进程的优先级
- “An unprivileged user can only increase the nice value and such changes are irreversible…”
Linux Control Groups (cgroups)
可以去读一下手册,man 7 cgroups
。
顺便提一下:cgroups也是docker实现所依赖的机制之一。
实际情况(2):Big.LITTLE/能耗
处理器的计算能力不同
- 均分 workloads 会让小核上的任务饥饿
- Linux Kernel EAS (Energy Aware Scheduler)
- 移动平台的考虑 (能耗 vs. 速度 vs. 吞吐量)。频率越低,IPC (Instruction per Cycle) 和能效都更好
如骁龙888中:Snapdragon 888
- 1X Prime Cortex-X1 (2.84GHz)
- 3X Performance Cortex-A78 (2.4GHz)
- 4X Efficiency Cortex-A55 (1.8GHz)
实际情况(3):Non-Uniform Memory Access
线程看起来在 “共享内存”,但共享内存却是 memory hierarchy 造就的假象,roducer/consumer 位于同一个/不同 module 性能差距可能很大。
实际情况(4):CPU Hot-plug
指CPU的热插拔的情况,就像弹出U盘一样弹出CPU。
总结
本次课内容与目标
- 理解常见的处理器的调度策略
- 了解调度是操作系统领域重要的、未解决的问题
Takeaway messages
- 机制和策略分离
- “做系统” 的矛盾
- 必须把问题简单化,才能做 “第一个” 东西出来
- 但随着需求的增长,复杂系统会失控
- 所有复杂系统的调度都是拙劣的 Workaround