从零写 OS 内核-第六篇:初探多任务 —— 从单核循环到进程切换
“一个内核只能干一件事?那叫裸机程序。 真正的操作系统,必须能‘同时’做多件事!”
在前五篇中,我们完成了内核的启动、进入保护模式、开启分页,构建了虚拟内存的基础。 但到目前为止,我们的内核仍然是 单任务 的:从 kernel_main 开始,一路执行到底,无法中断,无法切换。
而现代操作系统的核心能力之一就是 多任务(Multitasking) —— 让用户感觉多个程序在“同时”运行。
今天,我们就来实现 最简化的协作式多任务 ,并理解 进程上下文切换 的核心原理。 虽不能“真并发”,但已迈出多任务的第一步!
🤔 一、什么是多任务?为什么需要它?
多任务 是指操作系统 在多个任务(进程/线程)之间快速切换 , 让用户产生“同时运行”的错觉(即使 CPU 只有一个核心)。
多任务的两种类型:
| 类型 | 说明 | 实现难度 |
|---|---|---|
| 协作式(Cooperative) | 任务主动让出 CPU(如调用 yield() ) | ⭐ 简单 |
| 抢占式(Preemptive) | 由时钟中断强制切换(如 Linux) | ⭐⭐⭐ 复杂 |
💡 今天我们先实现协作式多任务 ——它逻辑清晰,是理解上下文切换的最佳起点。
🧱 二、核心概念:进程与上下文
什么是进程?
一个
运行中的程序实例
拥有
独立的虚拟地址空间
(我们已有分页!)
拥有
执行状态
:寄存器、栈、程序计数器等
什么是上下文(Context)?
CPU 寄存器的
完整快照
,包括:
- 通用寄存器(EAX, EBX, …)
- 指令指针(EIP)
- 栈指针(ESP)
- 标志寄存器(EFLAGS)
- 段寄存器(CS, DS, …)
✅ 上下文切换 = 保存当前任务状态 + 恢复下一个任务状态
📦 三、数据结构:进程控制块(PCB)
我们需要一个结构体来描述每个进程:
🔑 每个进程必须有独立的内核栈 ! 否则切换时栈会互相覆盖,导致崩溃。
⚙️ 四、上下文切换:汇编是唯一选择
C 语言无法直接操作 EIP (程序计数器), 必须用汇编实现上下文保存与恢复 。
- 保存当前上下文( switch_to 的前半部分)
-
加载下一个任务上下文
🌟 ret 指令会从栈顶弹出 EIP ——这正是我们保存在任务栈中的“下一条指令地址”!
🔄 五、协作式调度器:实现
yield()
创建新任务:
💡 关键技巧 :在新任务的栈顶预先压入 entry 地址, 这样当 switch_to 执行 ret 时,会直接跳转到 entry() !
🧪 六、测试:让两个任务交替运行
运行效果:
✅ 成功实现多任务切换!
⚠️ 七、尚未解决的问题(为抢占式做准备)
当前协作式多任务有明显缺陷:
- 一个任务死循环,系统卡死
(必须主动
yield
) -
无时间片概念
,无法公平调度
- 无进程创建/销毁 API
- 无用户态支持
🔜 下一步: 引入时钟中断(PIT/HPET) ,实现 抢占式调度 !
🌱 八、多任务的意义:不只是“看起来同时”
多任务机制为后续功能奠定基础:
进程隔离
:每个进程有自己的页目录(
cr3
)
系统调用
:用户程序通过
int 0x80
进入内核
信号与 IPC
:进程间通信
文件描述符表
:每个进程独立的打开文件列表
💡 进程是操作系统的“基本调度单位” , 掌握它,你就真正踏入了操作系统的核心。
💬 写在最后
从单任务到多任务, 不仅是代码结构的改变, 更是 思维方式的跃迁 —— 从“顺序执行”到“并发抽象”。
今天你写的 switch_to , 正是 Linux context_switch 、Windows 线程调度的雏形。
🌟 多任务不是魔法,而是精心设计的状态切换。
📬 动手试试 : 在你的内核中添加两个任务,一个打印 “A”,一个打印 “B”,观察交替输出。 欢迎在评论区晒出你的多任务日志!
👇 下一篇你想看: 时钟中断与抢占式调度 ,还是 系统调用接口设计 ?
#操作系统 #内核开发 #多任务 #进程管理 #上下文切换 #协作式调度 #x86 #从零开始
📢 彩蛋 :关注后回复关键词 “task” ,获取:
完整多任务实现代码(含汇编上下文切换)
PCB 结构体详细注释
* QEMU 调试多任务切换的 GDB 脚本
console.log(greet(“World”));
python
Python example
deffibonacci(n):
ifn <=1:
returnn
returnfibonacci(n-1) + fibonacci(n-2)
“`
评论