多任务 — 交换一叠寄存器,变出两个世界
多任务 — 交换一叠寄存器,变出两个世界
这一章讲什么: 到此为止,MikanOS 只有一个「世界」在跑:主循环转圈, 鼠标、键盘、闪烁光标全是它扮演。本章把它变成两个、然后任意多个世界: 上下文切换——把 CPU 的全部寄存器存进一块内存(上下文结构), 再从另一块内存把另一套寄存器装回去。任务自己愿意换叫协作式; 定时器中断按头换叫抢占式——操作系统的心脏从这一章开始跳。
1. 先看现象:两个窗口各数各的数
主线例子:
TaskB 窗口:死循环,循环次数++,显示
主任务窗口:照常处理消息(鼠标/键盘/定时器)
协作版:两边自觉轮流调 SwitchContext(&对方, &自己) —— 一边不自愿,全系统冻结
抢占版:每 20ms(50 次/秒)任务切换定时器超时 → 中断处理程序里 SwitchTask()
→ 「当前任务」的寄存器入栈保存,下一任务的寄存器装回 → 谁也没察觉被夺了 CPU
验证(13.4):把切换周期调大到 1 秒 —— 计数器只在你眨眼的间隔里跳:
99、299、499、699、899…… 每次恰好 +100 后停住 —— 每秒真切一次的铁证上下文的定义(全书最重要的定义之一):完成任务所需的数据集合—— 可执行文件、命令行参数、环境变量、栈内存、各寄存器的值。 同一个程序开两个实例,前三个相同,后两个不同,所以是两个上下文1。
2. 顶层全景
13.1 概念:并行(多核)vs 并发(分时单核);本书只做单核并发
13.2 手工切换:TaskContext 结构体 + SwitchContext()(汇编,iret 收尾)
13.3 抢占:任务切换定时器 20ms;Tick() 返回「该切了吗」;中断里切,EOI 要早调
13.4 验证:周期放大到 1s,看 99/299/499 阶梯
13.5 抽象:Task 类(栈+上下文)、TaskManager(数组轮转);任务数量任意
图说:从「能切」到「强制切」到「随便切」,三步走完多任务的整条路。3. 核心原理
3.1 为什么必须是「一整叠」寄存器
CPU 的行为只有一句:「从 RIP 指的内存读机器语言,执行,更新寄存器,重复。」 所以交换寄存器的值 = 交换执行环境,这就是上下文切换的精髓2。 要保存的不止 RIP/RSP:RDI、RSI(参数)、RAX–R15 全部通用寄存器、RFLAGS、 还有浮点世界——fxsave/fxrstor 指令把 XMM 等一组浮点寄存器集体存进 fxsave_area3。 有些寄存器在两个上下文里相同(CR3、段寄存器、RBP、RBX、R12–R15),暂时不存—— 「稍后做抢占式时,保存它们就有意义了」4。
SwitchContext(下一个上下文的地址, 保存当前上下文的地址)全用汇编写, 三个拿不到的值有专门技巧:RIP = 从栈里读 call 压入的返回地址;RSP = 当前栈指针 + 8; RFLAGS 不能 mov,要用 pushfq 压栈再读5。 收尾不用 ret 用 iret:普通 ret 只弹 RIP,而切换要同时换 CS 和 RSP, iret 一口气弹出 RIP、CS、RFLAGS(以及 RSP、SS)6。 恢复顺序有个死亡细节:RDI 必须最后恢复——它是上下文结构的地址,先恢复就找不到结构了7。
3.2 栈对齐:一个 8 像素的执念
System V ABI 规定:call 之前 RSP 必须 16 字节对齐;call 压入 8 字节返回地址后,
函数体内的 RSP 低四位必须是 8——编译器按这个假设输出 movaps 之类的指令,
不满足就 GP 异常8。所以新任务的 RSP 初值要算成
(栈顶 & ~0xf) - 8:先对齐 16,再故意偏 8,让任务函数「看起来像被 call 进来的一样」9。
另一个坑:即使你的程序不算浮点,编译器也可能插入浮点指令,
所以任务上下文里还要留 MXCSR 的位置(初始化为屏蔽所有异常)10。
3.3 协作 vs 抢占,以及 EOI 的位置
协作式:任务自己挑时候调 SwitchContext。够用在 OS 内部,但对应用不行—— 一个 buggy 应用不调用切换,全系统冻结11。 抢占式:任务切换定时器(20ms,kTimerFreq × 0.02)到点后,在中断处理程序里强制切12。 两个工程细节:
- 任务切换定时器不能走消息队列——消息要等主任务被切回来才处理, 那就永远切不回去了。Tick() 改为返回 bool:任务切换定时器超时时直接返回 true、 就地重新注册,中断处理程序见到 true 就 SwitchTask()13。
- EOI 要在 SwitchTask() 之前调——切换一走,这个任务里的代码就不再执行, 拖在后面的 NotifyEndOfInterrupt() 永远等不到;不销案,定时器中断从此哑火, 任务再也切不动14。
3.4 Task / TaskManager:任务系统的产品化
两个上下文写死还行,N 个就得有数据结构。Task 类 = ID + 栈数组 + 上下文结构; InitContext() 把「造上下文」的杂活(清零、填 RIP=函数入口、RDI/RSI=参数、 CR3=当前页表、RFLAGS=0x202(IF=1)、CS/SS、栈指针对齐)全收进来15。 TaskManager 像 LayerManager 管图层一样管任务:tasks_ 数组 + current_task_index_,SwitchTask() = index++ 后切到下一个任务; NewTask().InitContext(...) 方法链,任务随处可造16。 至此,「启动多个应用」的技术地基完工——缺的只是应用(第 18 章)和休眠(第 14 章)。
4. 作者的判断与证据
判断:抢占式是底线,协作式只是教学台阶。 理由写得很直白:恶意或出错的应用不会自觉让出 CPU, 「整个操作系统就会被冻结」17。但教学顺序上,先协作后抢占是对的—— 亲手写过「轮流调用」再看「被夺走 CPU」,才知道中断在这里干了什么。
判断:验证要靠「放慢到可见」。 切得太快,协作和抢占看起来一模一样; 把周期放大到 1 秒,99→299→499 的阶梯就是「每秒切换一次」的直接证据18。 这个「把频率调低看阶梯」的验证手法,值得抄走。
证据: 图 13.6(两个上下文并行计数)、图 13.7(正常 20ms 切换)、图 13.8(1 秒阶梯), 外加 RFLAGS/CS/RSP 恢复后系统不崩这一「整体可用性」证据。
5. 边界与局限
- 没有锁、没有临界区:两个任务共享全局(layer_manager、printk)而不自知, 第 14 章马上会撞上这个坑;真正的同步原语本书不教。
- 轮转调度一视同仁:没有优先级、没有休眠(第 14 章)、没有公平性策略; 调度理论请读 OSTEP。
- 单核假设:多核启动本书不做,上下文切换全靠「中断进来时只有一个在跑」。
- fxsave_area 是定长数组:SIMD 寄存器组的保存区,不在 ABI 保护范围内,自己存。
6. 可带走的
本章主线例子,一行写完: 存全部寄存器入 TaskContext → 装回另一套 → iret 起飞;20ms 定时器中断强制换叠;1 秒周期下计数器按 +100 阶梯跳 = 抢占实证。
- 上下文 = 一套寄存器 + 栈;CPU 只看寄存器,所以换寄存器就是换世界;
- SwitchContext 三技巧:RIP 从栈里读返回地址、RSP+8、RFLAGS 用 pushfq;RDI 最后恢复;
- 换栈换特权要用 iret(ret 只弹 RIP);
- 新任务 RSP 初值 = (栈顶 & ~0xf) − 8,满足「函数体内低四位是 8」的 ABI 执念;
- 协作式一个任务不守规矩全系统死;抢占式靠定时器中断强制切,这是底线;
- 切换定时器不能走消息队列(死锁);EOI 必须在切走之前调(不然中断哑火);
- Task/TaskManager 的抽象让任务数量任意、随处可造——OS 内部功能从此都「任务化」;
- 验证看不见的快速行为:把频率放慢到肉眼可见。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 多任务与事件的对比、上下文定义 | 13.1 多任务和上下文 | text/90-ch13-01-13-1.txt:9(搜「鼠标移动、键盘输入和光标闪烁」) · text/90-ch13-01-13-1.txt:31(搜「数据或变量的集合」) |
| 并行与并发、单核取舍 | 13.1 多任务和上下文 | text/91-ch13-02-13-2-osbook-day13a.txt:12(搜「8个内核」) · text/91-ch13-02-13-2-osbook-day13a.txt:18(搜「分时方式」) · text/91-ch13-02-13-2-osbook-day13a.txt:25(搜「只实现使用一个CPU内核」) |
| 交换寄存器是切换精髓 | 13.2 处理上下文切换(osbook_day13a) | text/91-ch13-02-13-2-osbook-day13a.txt:48(搜「巧妙改变寄存器的值」) |
| TaskContext 与保存范围 | 13.2 处理上下文切换(osbook_day13a) | text/91-ch13-02-13-2-osbook-day13a.txt:131(搜「CR3、段寄存器」) |
| RIP/RSP/RFLAGS 保存技巧 | 13.2 处理上下文切换(osbook_day13a) | text/91-ch13-02-13-2-osbook-day13a.txt:161(搜「返回地址」) · text/91-ch13-02-13-2-osbook-day13a.txt:165(搜「pushfq」) |
| iret 与 RDI 最后恢复 | 13.2 处理上下文切换(osbook_day13a) | text/91-ch13-02-13-2-osbook-day13a.txt:170(搜「iret」) · text/91-ch13-02-13-2-osbook-day13a.txt:175(搜「必须最后恢复RDI」) |
| 栈对齐专栏 | 13.3 自动上下文切换(osbook_day13b) | text/92-ch13-03-13-3-osbook-day13b.txt:4(搜「16字节」) · text/92-ch13-03-13-3-osbook-day13b.txt:11(搜「低四位」) |
| MXCSR 初始化 | 13.2 处理上下文切换(osbook_day13a) | text/91-ch13-02-13-2-osbook-day13a.txt:115(搜「MXCSR」) |
| 协作式的死穴 | 13.3 自动上下文切换(osbook_day13b) | text/92-ch13-03-13-3-osbook-day13b.txt:26(搜「合作式多任务处理」) · text/92-ch13-03-13-3-osbook-day13b.txt:30(搜「整个操作系统就会被冻结」) |
| 20ms 切换定时器 | 13.3 自动上下文切换(osbook_day13b) | text/92-ch13-03-13-3-osbook-day13b.txt:46(搜「0.02s」) |
| Tick 返回 bool 的理由 | 13.3 自动上下文切换(osbook_day13b) | text/92-ch13-03-13-3-osbook-day13b.txt:69(搜「无法正常工作」) |
| EOI 要提前 | 13.3 自动上下文切换(osbook_day13b) | text/92-ch13-03-13-3-osbook-day13b.txt:90(搜「尽可能靠后」;:95,搜「不再执行」) |
| 1 秒阶梯验证 | 13.4 验证多任务处理(osbook_day13c) | text/94-ch13-05-13-5-osbook-day13d.txt:3(搜「1s」) · text/94-ch13-05-13-5-osbook-day13d.txt:4(搜「899」) |
| Task 类与 TaskManager | 13.5 更多任务(osbook_day13d) | text/94-ch13-05-13-5-osbook-day13d.txt:36(搜「唯一的ID」) · text/94-ch13-05-13-5-osbook-day13d.txt:77(搜「TaskManager的作用」) |
Footnotes
-
出处:「13.1 多任务和上下文」(
text/90-ch13-01-13-1.txt:9(搜「鼠标移动、键盘输入和光标闪烁」);text/90-ch13-01-13-1.txt:31(搜「数据或变量的集合」))。 ↩ -
出处:「13.1 多任务和上下文」(
text/91-ch13-02-13-2-osbook-day13a.txt:12(搜「8个内核」);text/91-ch13-02-13-2-osbook-day13a.txt:18(搜「分时方式」);text/91-ch13-02-13-2-osbook-day13a.txt:25(搜「只实现使用一个CPU内核」))。 ↩ -
出处:「13.2 处理上下文切换(osbook_day13a)」(
text/91-ch13-02-13-2-osbook-day13a.txt:48(搜「巧妙改变寄存器的值」))。 ↩ -
出处:「13.2 处理上下文切换(osbook_day13a)」(
text/91-ch13-02-13-2-osbook-day13a.txt:131(搜「CR3、段寄存器」))。 ↩ -
出处:「13.2 处理上下文切换(osbook_day13a)」(
text/91-ch13-02-13-2-osbook-day13a.txt:161(搜「返回地址」);text/91-ch13-02-13-2-osbook-day13a.txt:165(搜「pushfq」))。 ↩ -
出处:「13.2 处理上下文切换(osbook_day13a)」(
text/91-ch13-02-13-2-osbook-day13a.txt:170(搜「iret」);text/91-ch13-02-13-2-osbook-day13a.txt:175(搜「必须最后恢复RDI」))。 ↩ -
出处:「13.3 自动上下文切换(osbook_day13b)」(
text/92-ch13-03-13-3-osbook-day13b.txt:4(搜「16字节」);text/92-ch13-03-13-3-osbook-day13b.txt:11(搜「低四位」))。 ↩ -
出处:「13.2 处理上下文切换(osbook_day13a)」(
text/91-ch13-02-13-2-osbook-day13a.txt:115(搜「MXCSR」))。 ↩ -
出处:「13.3 自动上下文切换(osbook_day13b)」(
text/92-ch13-03-13-3-osbook-day13b.txt:26(搜「合作式多任务处理」);text/92-ch13-03-13-3-osbook-day13b.txt:30(搜「整个操作系统就会被冻结」))。 ↩ -
出处:「13.3 自动上下文切换(osbook_day13b)」(
text/92-ch13-03-13-3-osbook-day13b.txt:46(搜「0.02s」))。 ↩ -
出处:「13.3 自动上下文切换(osbook_day13b)」(
text/92-ch13-03-13-3-osbook-day13b.txt:69(搜「无法正常工作」))。 ↩ -
出处:「13.3 自动上下文切换(osbook_day13b)」()。 ↩
-
出处:「13.4 验证多任务处理(osbook_day13c)」(
text/94-ch13-05-13-5-osbook-day13d.txt:3(搜「1s」);text/94-ch13-05-13-5-osbook-day13d.txt:4(搜「899」))。 ↩ -
出处:「13.5 更多任务(osbook_day13d)」(
text/94-ch13-05-13-5-osbook-day13d.txt:36(搜「唯一的ID」);text/94-ch13-05-13-5-osbook-day13d.txt:77(搜「TaskManager的作用」))。 ↩ -
出处:「13.5 更多任务(osbook_day13d)」(
text/94-ch13-05-13-5-osbook-day13d.txt:36(搜「唯一的ID」);text/94-ch13-05-13-5-osbook-day13d.txt:77(搜「TaskManager的作用」))。 ↩ -
出处:「13.4 验证多任务处理(osbook_day13c)」(
text/94-ch13-05-13-5-osbook-day13d.txt:3(搜「1s」);text/94-ch13-05-13-5-osbook-day13d.txt:4(搜「899」))。 ↩ -
出处:「13.3 自动上下文切换(osbook_day13b)」(
text/92-ch13-03-13-3-osbook-day13b.txt:26(搜「合作式多任务处理」);text/92-ch13-03-13-3-osbook-day13b.txt:30(搜「整个操作系统就会被冻结」))。 ↩ -
出处:「13.2 处理上下文切换(osbook_day13a)」(
text/91-ch13-02-13-2-osbook-day13a.txt:48(搜「巧妙改变寄存器的值」))。 ↩