操作系统第二讲:Boot, Process, Kernel
第二讲的核心不是背启动流程,而是理解操作系统为什么必须和硬件合作。让普通操作直接执行,保证性能;让危险操作受控进入内核,保证安全;让 OS 能随时抢回 CPU,保证控制权。这就是第二讲最重要的主线。
操作系统第二讲:Boot, Process, Kernel
复习目标:理解计算机从按下电源键到操作系统启动的过程;理解“程序”和“进程”的区别;理解进程的内存布局;理解为什么操作系统必须依赖用户态/内核态、特权指令、内存保护和时钟中断来实现隔离与控制。
参考课件:Operating Systems Lecture 2 — Boot, Process, Kernel, Prof
目录
- 操作系统第二讲:Boot, Process, Kernel
-
- @[TOC](目录)
- 0. 这一讲到底在讲什么?
- 1. 开机之后发生了什么?
-
- 2. Real Mode 和 Protected Mode
-
- 3. JOS 启动案例:代码到底在干什么?
-
- 4. UEFI:更现代的 BIOS
- 5. 进程:运行中的程序
-
- 6. PCB:操作系统给进程建立的档案袋
- 7. 进程的内存布局
-
- 8. CPU 虚拟化:如何让多个程序“同时”使用 CPU?
-
- 9. Dual Mode:用户态和内核态
-
- 10. 特权指令:普通程序不能直接执行的指令
-
- 11. 内存保护
-
- 12. Timer Interrupt:OS 如何抢回 CPU?
-
- 13. CPL:CPU 怎么知道现在是用户态还是内核态?
- 14. 第二讲整体知识地图
- 15. 小测题与答案
-
- 16. 常见 Q&A
-
- 17. 拓展理解
-
- 18. 复习清单
- 19. 最后总结
目录
- 操作系统第二讲:Boot, Process, Kernel
-
- @[TOC](目录)
- 0. 这一讲到底在讲什么?
- 1. 开机之后发生了什么?
- 2. Real Mode 和 Protected Mode
- 3. JOS 启动案例:代码到底在干什么?
- 4. UEFI:更现代的 BIOS
- 5. 进程:运行中的程序
- 6. PCB:操作系统给进程建立的档案袋
- 7. 进程的内存布局
- 8. CPU 虚拟化:如何让多个程序“同时”使用 CPU?
- 9. Dual Mode:用户态和内核态
- 10. 特权指令:普通程序不能直接执行的指令
- 11. 内存保护
- 12. Timer Interrupt:OS 如何抢回 CPU?
- 13. CPL:CPU 怎么知道现在是用户态还是内核态?
- 14. 第二讲整体知识地图
- 15. 小测题与答案
- 16. 常见 Q&A
- 17. 拓展理解
- 18. 复习清单
- 19. 最后总结
0. 这一讲到底在讲什么?
第一讲我们讲了操作系统的三个角色:
- Referee(裁判):分配资源,保证公平,防止程序互相破坏。
- Illusionist(魔术师):制造“每个程序好像独占 CPU、独占内存”的幻觉。
- Glue(胶水):屏蔽硬件差异,给应用提供统一接口。
第二讲就是把这些概念落地。它回答三个核心问题:
-
电脑是怎么启动起来的?
从 BIOS/UEFI 到 bootloader,再到 kernel。 -
程序是怎么变成进程的?
程序是磁盘上的静态文件,进程是正在运行的程序实例。 -
为什么普通程序不能随便控制硬件和内核?
因为 CPU 和 OS 共同提供了用户态/内核态、特权指令、内存保护、时钟中断等机制。
一句话概括本讲:
计算机启动时,BIOS/UEFI 先运行,bootloader 把 kernel 加载进内存;kernel 启动后管理进程;为了让进程既能高效运行又不能乱搞系统,CPU 和 OS 共同提供用户态/内核态、特权指令、内存保护和时钟中断。
1. 开机之后发生了什么?
1.1 BIOS:开机后第一个跑的软件
BIOS,全称 Basic Input Output System,可以理解为主板自带的固件。它不是你安装在硬盘里的普通软件,而是更靠近硬件的启动程序,通常存储在主板 ROM / Flash 里。
BIOS 主要做这些事:
-
POST 自检
检查 CPU、内存、显示设备、键盘等硬件是否大致正常。 -
初始化硬件状态
比如 VGA 显示、键盘、基础 I/O 设备。 -
构建硬件描述信息
比如 ACPI,用来告诉操作系统硬件长什么样、如何管理电源和设备。 -
从磁盘加载 bootloader
传统 BIOS 通常从启动盘的第一个扇区加载 bootloader。一个扇区通常是 512 字节。 -
把控制权交给 bootloader
也就是设置代码段和指令指针,让 CPU 跳到 bootloader 继续执行。
可以把 BIOS 想象成:
电脑刚醒来时的“第一位引导员”。它不负责真正管理整个系统,只负责把机器带到可以继续启动操作系统的状态。
启动流程大概是:
Power On
↓
CPU 从固定地址开始执行 BIOS
↓
BIOS 做硬件自检和初始化
↓
BIOS 找到启动设备
↓
BIOS 加载 bootloader 到内存
↓
CPU 跳转到 bootloader
1.2 Bootloader:真正把内核搬进内存的人
Bootloader 是启动加载器。它通常属于操作系统的一部分,或者和操作系统一起存放在磁盘、U 盘等介质上。
Bootloader 主要做四件事:
- 从 real mode 切换到 protected mode
- 检查 kernel image 是否正确
- 把 kernel 从磁盘加载到内存
- 把控制权转交给真正的 OS kernel
用一句话说:
BIOS 负责找到 bootloader;bootloader 负责找到并加载 kernel;kernel 才是真正开始管理系统的操作系统核心。
也可以这样理解:
BIOS:我知道怎么从硬件启动,并找到启动器。
Bootloader:我知道这个 OS 的内核文件在哪里、格式是什么、怎么加载。
Kernel:我负责管理 CPU、内存、磁盘、进程、文件系统等真正的操作系统功能。
1.3 为什么 BIOS 不直接加载 kernel?
这是一个很自然的问题:既然 BIOS 可以读磁盘,为什么不直接把内核加载起来?
原因是:不同操作系统的内核格式、启动方式、文件系统、启动参数都不一样。如果 BIOS 直接支持所有 OS,会变得极其复杂,也很难维护。
所以中间加一层 bootloader 更合理。
它带来的好处包括:
- 灵活性:不同 OS 可以使用不同 bootloader。
- 兼容性:BIOS 不需要理解所有内核格式。
- 启动设备选择:可以从硬盘、U 盘、网络等位置启动。
- 多系统管理:例如 GRUB 可以让你选择 Linux 或 Windows。
- 安全验证:可以检查内核镜像是否被篡改。
- 错误处理:启动失败时可以显示提示或进入恢复模式。
- 便于更新:bootloader 可以随 OS 更新,而不需要改主板固件。
所以启动链路不是:
BIOS → Kernel
而是:
BIOS → Bootloader → Kernel
2. Real Mode 和 Protected Mode
2.1 Real Mode 是什么?
Real mode 是 x86 早期为了兼容 8086 而保留的模式。它非常原始,能力有限。
典型特点:
- 地址空间很小,早期约 1MB。
- 没有现代意义上的内存保护。
- 没有完善的权限隔离。
- 更像“裸机直接跑代码”的状态。
电脑刚启动时,为了兼容历史,CPU 会先处在 real mode 或类似的早期启动状态。这个阶段只能做最基础的初始化。
2.2 Protected Mode 是什么?
Protected mode 是现代操作系统真正需要的模式。它支持:
- 更大的地址空间。
- 内存保护。
- 用户态/内核态权限隔离。
- 多任务。
- 分段、分页等地址管理机制。
为什么 bootloader 要从 real mode 切换到 protected mode?
因为 kernel 想实现:
- 进程隔离。
- 虚拟内存。
- 系统调用。
- 中断和异常处理。
- 用户态/内核态切换。
这些都需要 protected mode 这样的硬件支持。
可以把它理解为:
real mode:机器刚启动时的原始状态
protected mode:现代操作系统真正开始工作的状态
3. JOS 启动案例:代码到底在干什么?
课件里用了 JOS 作为启动案例。JOS 是 MIT 6.828 / 6.S081 里常用的教学操作系统,它的启动代码非常适合帮助我们理解“从硬件到 OS”的过程。
3.1 CPU 开机后的第一条指令
课件中给出了这条指令:
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
这表示:
- CPU 刚启动时,
CS = 0xf000 IP = 0xfff0- 所以物理地址是
0xffff0 - 第一条指令是一个 long jump / far jump
在 real mode 下,物理地址计算方式是:
physical address = CS × 16 + IP
代入:
CS = 0xf000
IP = 0xfff0
physical address
= 0xf000 × 16 + 0xfff0
= 0xf0000 + 0xfff0
= 0xffff0
所以 CPU 开机后不是随便执行,而是硬件规定它从固定地址开始执行。这个地址位于 BIOS ROM 区域的顶部附近。
ljmp $0xf000,$0xe05b 的意思是:
把 CS 设置为 0xf000
把 IP 设置为 0xe05b
跳到 BIOS 主体初始化代码继续执行
这条指令的本质是:
CPU 先落在一个固定入口点,然后跳到 BIOS 真正的初始化逻辑。
3.2 JOS bootloader 汇编代码主线
课件里的汇编代码看起来比较多,但主线其实很清晰:
.code16
cli
cld
这表示当前还是 16-bit real mode。
cli:关闭中断,避免启动过程被打断。cld:清除方向标志,保证字符串操作按正常方向进行。
接下来会初始化段寄存器:
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
这一步是为了建立一个干净的段环境。
然后代码会启用 A20。A20 是 x86 的历史包袱。早期为了兼容 8086,超过 1MB 的地址会发生回绕。启用 A20 之后,CPU 才能正常访问 1MB 以上的内存。
可以简单记:
开 A20 是为了突破早期 1MB 地址限制。
接下来 bootloader 会加载 GDT,并设置 CR0 的 PE bit:
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
这里:
lgdt gdtdesc:加载全局描述符表 GDT。cr0:CPU 控制寄存器。CR0_PE_ON:Protection Enable,打开后进入 protected mode。
然后还需要一次 far jump:
ljmp $PROT_MODE_CSEG, $protcseg
为什么要 jump?
因为 CPU 需要刷新 CS,开始使用 protected mode 下的代码段选择子。之后代码进入 .code32,也就是 32-bit protected mode。
后面会设置各个段寄存器和栈:
movw $PROT_MODE_DSEG, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss
movl $start, %esp
call bootmain
这一段的意思是:
- 设置数据段、栈段等段寄存器。
- 设置栈指针
esp。 - 调用 C 函数
bootmain()。
所以 bootloader 汇编部分的完整主线是:
16-bit real mode
↓
关中断,初始化段寄存器
↓
启用 A20
↓
设置 GDT
↓
打开 CR0 的 PE 位
↓
far jump 切换到 protected mode
↓
进入 32-bit 代码
↓
设置栈
↓
调用 bootmain()
这段代码最重要的意义是:
它把机器从“刚开机的原始状态”带到“可以运行 C 代码加载内核”的状态。
3.3 bootmain:加载 ELF 内核
课件里的 C 代码大意是读取磁盘上的 ELF 内核文件。
ELF 是 Linux / Unix 系统常见的可执行文件格式。一个 ELF 文件里会有:
- ELF Header:描述整个文件。
- Program Headers:告诉 loader 哪些段要加载到内存。
- Segments:真正的代码和数据。
核心流程可以简化为:
void bootmain(void) {
// 1. 读取磁盘第一页,拿到 ELF header
readseg((uint32_t) ELFHDR, SECTSIZE * 8, 0);
// 2. 检查 ELF magic number,确认它确实是 ELF 文件
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;
// 3. 遍历 program header,把每个 segment 加载到指定物理地址
for (...) {
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
}
// 4. 跳转到内核入口地址
((void (*)(void)) (ELFHDR->e_entry))();
}
这段代码的本质是:
bootloader 不需要理解整个操作系统,它只需要看懂 kernel image 的格式,把代码段和数据段搬进内存,然后跳到 kernel 的入口点。
其中 readseg() 做的是按扇区读磁盘。磁盘通常不是按字节读,而是按 sector 这样的块来读。
所以启动加载的流程是:
读取 ELF header
↓
检查 ELF magic
↓
读取 program headers
↓
把每个 segment 加载到目标物理地址
↓
跳转到 e_entry,也就是内核入口
4. UEFI:更现代的 BIOS
UEFI,全称 Unified Extensible Firmware Interface,可以理解为 BIOS 的继任者。
它相比传统 BIOS 有很多优势:
- 启动更快。
- 支持文件系统。
- 可以放在主板 flash、硬盘甚至网络位置。
- 支持鼠标等更丰富输入。
- 支持 Secure Boot。
- UI 更友好。
- 某种意义上更像一个 mini OS。
简单对比:
| 项目 | BIOS | UEFI |
|---|---|---|
| 历史 | 更传统 | 更现代 |
| 文件系统支持 | 较弱 | 更强 |
| UI | 文本/简陋界面 | 可图形化 |
| 安全启动 | 传统 BIOS 不强调 | 支持 Secure Boot |
| 能力 | 更像启动固件 | 更像 mini OS |
复习时不用深挖 UEFI 的内部实现,重点知道:
BIOS/UEFI 是固件层,bootloader 是 OS 启动链的一部分,kernel 是真正的 OS 核心。
5. 进程:运行中的程序
5.1 程序和进程的区别
课件定义:
Process is the execution of an application program with restricted rights.
也就是:
进程是一个正在执行的程序实例,并且它拥有受限制的权限。
程序和进程的区别可以类比面向对象:
程序 program:类 class
进程 process:对象 object / instance
例如:
/bin/ls
这是一个程序文件。
如果你在两个终端里同时运行:
ls
ls
就会产生两个进程。它们执行的是同一个程序,但它们是两个不同的运行实例。
所以:
程序:静态的,放在磁盘上
进程:动态的,正在运行,有自己的执行状态和资源上下文
一个程序可以有:
- 0 个进程:程序存在,但现在没人运行它。
- 1 个进程:正在运行一次。
- 多个进程:同一个程序被运行多次。
5.2 进程拥有哪些东西?
一个进程通常拥有:
- 自己的地址空间。
- 文件描述符上下文。
- 文件系统上下文。
- 一个或多个线程。
- 当前执行状态。
- 寄存器和程序计数器。
- I/O 状态。
这里“restricted rights” 很关键。进程不是想干什么就干什么,它不能随便访问其他进程的内存,也不能随便执行特权指令。
这体现了 OS 的裁判角色:
每个进程都能运行,但必须在规则内运行。
6. PCB:操作系统给进程建立的档案袋
PCB,全称 Process Control Block,中文叫进程控制块。
它是 OS 用来记录进程状态的数据结构。可以理解成 OS 给每个进程建立的“档案袋”。
PCB 里通常包括:
- Process ID,PID:进程编号。
- Process state:运行、就绪、等待等状态。
- Process priority:调度优先级。
- Program counter:下一条要执行的指令地址。
- Memory-related information:地址空间、页表等内存信息。
- Register information:寄存器值。
- I/O status information:打开的文件、I/O 设备等。
- Accounting information:运行时间、资源使用情况等统计信息。
PCB 最典型的使用场景是:上下文切换。
假设 CPU 正在运行进程 A,现在 OS 想切到进程 B:
进程 A 正在运行
↓
OS 保存 A 的寄存器、PC、栈指针等状态到 A 的 PCB
↓
OS 从 B 的 PCB 恢复 B 的寄存器、PC、栈指针
↓
CPU 开始运行 B
如果没有 PCB,进程被暂停后就无法恢复。因为 OS 不知道它执行到哪里、寄存器里是什么、栈在哪里。
所以可以记:
PCB 是“进程暂停后还能继续运行”的关键。
7. 进程的内存布局
7.1 经典内存区域
一个进程的虚拟地址空间通常可以分成这些区域:
高地址
↓
Stack
Heap
Uninitialized data (.bss)
Code (.text)
Initialized data (.data)
↓
低地址
每个区域的含义是:
| 区域 | 作用 |
|---|---|
.text |
代码区,存放机器指令 |
.data |
已初始化的全局变量/静态变量 |
.bss |
未初始化或零初始化的全局变量/静态变量 |
heap |
动态分配内存,例如 malloc / new |
stack |
函数调用栈,存放局部变量、返回地址、调用上下文 |
重点是:这个地址布局是虚拟地址布局,不是物理地址布局。
也就是说,进程以为自己有一片连续的内存,但实际物理内存可能完全不连续。后面地址翻译和分页会详细讲这个。
7.2 代码例子:a、b、c、d、e 分别在哪里?
课件给了这个例子:
int a = 0;
int b;
void hello() {
static int c;
int d;
int *e = malloc(...);
}
对应关系是:
int a = 0; // .data 或某些实现中可能放 .bss
int b; // .bss
void hello() {
static int c; // .bss
int d; // stack
int *e = malloc(...); // e 本身在 stack,malloc 出来的对象在 heap
}
这里要特别注意 e:
int *e = malloc(...);
这行代码里有两个东西:
-
e这个指针变量本身
它是局部变量,所以在 stack。 -
malloc(...)分配出来的那块内存
它在 heap。
所以更精确的说法是:
e 本身在 stack
e 指向的对象在 heap
这类题考试很容易考,因为它能检查你是否真的理解不同内存区域的生命周期。
7.3 生命周期角度理解内存区域
还可以从生命周期角度理解:
| 变量类型 | 生命周期 | 通常位置 |
|---|---|---|
| 函数代码 | 程序整个运行期间 | .text |
| 全局初始化变量 | 程序整个运行期间 | .data |
| 全局未初始化变量 | 程序整个运行期间 | .bss |
| static 局部变量 | 程序整个运行期间 | .data / .bss |
| 普通局部变量 | 函数调用期间 | stack |
| malloc/new 分配对象 | 手动释放前一直存在 | heap |
7.4 readelf:观察可执行文件结构
课件提到可以使用 readelf 查看可执行文件里面有什么。
例如:
gcc -g test.c -o test
readelf -S test
-S 表示查看 section headers。你可能会看到:
.text
.data
.bss
.rodata
.symtab
.strtab
这说明我们在图里看到的 .text、.data、.bss 不是凭空想象,而是真实存在于可执行文件和运行时内存布局中的概念。
不过课件也提醒:具体输出依赖编译器、优化等级、是否开启 debug 信息等。
8. CPU 虚拟化:如何让多个程序“同时”使用 CPU?
8.1 基本模型
CPU 虚拟化的基本模型是:
运行进程 A 一小会儿
↓
运行进程 B 一小会儿
↓
运行进程 C 一小会儿
↓
再切回进程 A
这样用户感觉多个程序在同时运行。实际上,如果只有一个 CPU core,同一时刻只能运行一个执行流,只是切换得很快。
这就是 OS 的魔术师角色:
每个进程都好像拥有自己的 CPU,但实际上 OS 在不断切换它们。
8.2 CPU 虚拟化的两个挑战
课件提出两个挑战:
挑战一:Performance
虚拟化不能太慢。
如果每条指令都交给 OS 检查,安全是安全了,但性能会非常差。
挑战二:Control / Isolation
OS 必须能在需要时拿回 CPU。
否则程序可以这样:
while (1) {
// do nothing
}
如果 OS 没有办法抢回 CPU,这个程序就会永远霸占 CPU,整个系统就卡住了。
所以 OS 要同时满足:
大多数时候让程序直接跑,保证性能
关键时候必须接管控制,保证隔离和安全
8.3 三种虚拟化方案
方案一:Direct Run
App instructions → CPU
优点:
- 性能好。
缺点:
- 没有隔离。
- OS 不参与。
- 程序可能乱访问硬件和内存。
方案二:Always Go Through OS
App instructions → OS → CPU
优点:
- 隔离好。
- OS 可以检查每一步。
缺点:
- 性能极差。
- 每条指令都经过 OS,开销太大。
方案三:Limited Direct Execution
普通指令 → 直接在 CPU 上运行
敏感指令 → 进入 OS,由 OS 决定是否允许
这是现代 OS 的核心思路。
它兼顾了:
- 性能:大多数普通指令直接运行。
- 隔离:敏感操作必须经过 OS。
- 控制:OS 可以通过中断和异常重新获得 CPU。
8.4 Limited Direct Execution 的两个特征
课件总结了两个特征。
Feature 1:Restricted Operations
也就是特权指令。
敏感操作不能让用户程序直接执行,必须交给 OS 判断是否合法。
例如:
- I/O 读写。
- 修改权限级别。
- 上下文切换。
- 设置系统时间。
- 修改关键硬件状态。
Feature 2:Inter-process Switching
进程之间要能切换。
切换分两类:
-
Voluntary switching,自愿切换
例如进程调用系统调用、等待 I/O、调用wait()。 -
Involuntary switching,非自愿切换
例如 timer interrupt 到来,OS 强制打断当前进程,切换到另一个进程。
这就是抢占式多任务的基础。
9. Dual Mode:用户态和内核态
9.1 为什么需要双模式?
如果所有程序都拥有最高权限,系统会非常危险。
一个普通程序可能:
- 修改别的进程内存。
- 直接操作磁盘。
- 修改系统时间。
- 关闭中断。
- 永远霸占 CPU。
- 修改页表。
- 破坏内核数据结构。
所以 CPU 提供了不同权限级别。操作系统利用这些权限级别,把程序分成:
User mode:用户态,普通应用程序运行
Kernel mode:内核态,操作系统内核运行
可以用课堂里的比喻:
学生:用户态
老师/TA:内核态
学生不能随便改成绩系统,老师/TA 才有管理权限。普通程序也不能随便改系统状态,内核才有最高权限。
9.2 硬件需要提供什么?
为了支持双模式,硬件需要提供:
- Privileged instructions,特权指令
- Memory protection,内存保护
- Timer interrupts,时钟中断
- Safe mode transfer,安全模式切换
为什么必须靠硬件?
因为恶意程序不会自觉遵守规则。如果只靠软件约定,程序完全可以绕开。必须由 CPU 在执行指令时检查当前权限级别。
10. 特权指令:普通程序不能直接执行的指令
课件把指令分为两类:
| 特权指令 | 非特权指令 |
|---|---|
| I/O read/write | 算术运算 |
| Context switch | 调用函数 |
| Changing privilege level | 读取 CPU 状态 |
| Set system time | 读取系统时间 |
一个简单判断原则是:
任何可能影响其他进程或整个系统的指令,大概率都是特权指令。
例如,设置系统时间看起来很简单,但如果普通程序可以随便改时间,会影响:
- 日志顺序。
- 文件时间戳。
- 定时任务。
- 证书有效期。
- 分布式系统一致性。
所以它必须是受控操作。
10.1 如果应用需要特权操作怎么办?
答案是:通过 系统调用。
用户程序不能直接操作磁盘,但可以调用:
open(path, flags);
read(fd, buf, size);
write(fd, buf, size);
close(fd);
这些函数最终会进入内核。内核检查权限,确认合法后,替用户程序完成操作。
所以系统调用的本质是:
用户程序向内核提出请求,请内核代替自己执行特权操作。
它不是普通函数调用,而是用户态到内核态的一次受控切换。
10.2 用户态强行执行特权指令会怎样?
课件中展示了一个用 assembly 执行特权指令的例子。大意是:用户程序尝试执行只有内核态才能执行的指令,结果 CPU 检测到权限不足,于是抛出异常,OS 接管,程序被终止。
流程是:
用户程序执行特权指令
↓
CPU 检查当前权限级别
↓
发现当前是 user mode
↓
该指令需要 kernel mode
↓
CPU 抛出异常
↓
OS 接管
↓
进程被终止或收到信号
这个例子说明:
权限不是靠编译器保证的,而是 CPU 在运行时硬件检查的。
编译器可能能把这段代码编译出来,但 CPU 执行时会拦住。
11. 内存保护
11.1 为什么需要内存保护?
如果没有内存保护,一个进程可以随便访问另一个进程的内存。
比如恶意程序可以:
读取浏览器密码
修改别的进程变量
破坏内核代码
覆盖系统数据结构
所以 OS 必须保证:
进程 A 只能访问进程 A 自己允许访问的地址
进程 B 只能访问进程 B 自己允许访问的地址
用户程序不能随便访问 kernel code/data
内存保护是进程隔离的基础。
11.2 Segmentation:base + bounds
课件先讲了分段方式。
基本思路是:
每个进程有 base 和 bounds
每次内存访问都检查地址是否在合法范围内
合法范围是:
[base, base + bounds)
如果访问地址超出范围,就抛出异常。
示意:
virtual address
↓
检查是否小于 bounds
↓
加上 base 得到 physical address
优点:
- 思路简单。
- 可以实现基本隔离。
缺点:
- heap 和 stack 不容易灵活扩展。
- 内存共享不方便。
- 容易产生内存碎片。
- 对复杂地址空间支持不好。
所以现代系统主要依赖分页。
11.3 Paging:现代 OS 的核心机制之一
Paging,分页,是 OS 中非常重要的概念。
它的核心思想是:
进程看到的每个虚拟地址,会被映射到某个物理地址;这个映射可以是不连续的。
也就是说,进程看到:
虚拟地址 0x1000
虚拟地址 0x2000
虚拟地址 0x3000
物理内存中可能对应:
物理地址 0x9a000
物理地址 0x31000
物理地址 0xf2000
它们不需要连续。
这里有两方合作:
-
OS 决定映射关系
OS 在内核态设置页表,决定虚拟页映射到哪个物理页,以及权限是什么。 -
CPU/MMU 执行地址翻译和权限检查
程序每次访问内存时,硬件负责查页表或 TLB,检查是否允许访问。
这就是硬件和操作系统合作的典型例子。
11.4 kernel 为什么放在高地址?
很多系统会把用户进程放在低地址,把 kernel code/data 映射到高地址。
示意:
高地址
↓
Kernel code/data
Stack
Heap
.bss
.text
.data
↓
低地址
为什么要这样设计?
原因一:方便内核处理系统调用和中断
当用户进程通过系统调用进入内核时,内核需要马上访问自己的代码和数据。如果每个进程地址空间里都在固定高地址映射内核,切换会更方便。
原因二:保持用户空间和内核空间分离
低地址主要给用户程序,高地址留给内核,结构清晰。
原因三:用户看得到地址范围,但不能访问
这点最关键。
虽然 kernel 映射在进程地址空间的高地址区域,但页表权限会标记为:
user mode:不可访问
kernel mode:可以访问
所以用户程序随机访问内核区域时:
用户态访问 kernel address
↓
CPU/MMU 检查权限
↓
发现 user mode 不允许访问
↓
抛出异常
这就是虚拟内存和权限位共同实现隔离。
12. Timer Interrupt:OS 如何抢回 CPU?
12.1 为什么需要 timer interrupt?
假设一个用户程序写了:
while (1) {
}
如果 OS 没有办法打断它,这个程序就会永远霸占 CPU。
所以 OS 需要一种机制:
即使进程不主动让出 CPU,OS 也能定期拿回控制权。
这就是 timer interrupt。
12.2 timer interrupt 的执行流程
大致流程是:
用户进程正在运行
↓
硬件计时器到点
↓
触发 timer interrupt
↓
CPU 进入内核态
↓
OS 保存当前进程状态
↓
调度器选择下一个进程
↓
恢复某个进程运行
这个“下一个进程”可能是别的进程,也可能还是原来的进程。
Timer interrupt 的重要意义是:
它让抢占式调度成为可能。
没有 timer interrupt,OS 只能依赖进程自愿让出 CPU,比如主动调用 yield() 或阻塞在 I/O 上。但恶意程序或死循环程序不会主动让出 CPU。
13. CPL:CPU 怎么知道现在是用户态还是内核态?
在 x86 中,当前特权级叫 Current Privilege Level,CPL。
x86 使用 CS 段寄存器的低 2 位表示 CPL。
常用的是:
CPL = 0:kernel mode
CPL = 3:user mode
虽然 x86 有 4 个 ring:
Ring 0
Ring 1
Ring 2
Ring 3
但大多数 OS 主要只用:
Ring 0:内核态
Ring 3:用户态
课件中问了一个有迷惑性的问题:如何切换用户态和内核态?
比如:
CPL &= 0x0
CPL |= 0x3
...
这里要注意:
CPL 不是普通 C 变量,用户程序不能通过
&=或|=随便修改它。
CPL 由 CPU 的受控机制修改,比如:
- 系统调用。
- 中断。
- 异常。
- 从内核返回用户态的特殊指令。
所以正确理解是:
用户态 → 内核态:必须通过 syscall / interrupt / exception 等受控入口
内核态 → 用户态:必须通过受控返回机制
这就是下一讲 context switch 和 mode transfer 要详细展开的内容。
14. 第二讲整体知识地图
可以把本讲串成下面这条主线:
按下电源
↓
BIOS / UEFI 运行
↓
硬件自检和初始化
↓
加载 bootloader
↓
bootloader 切换 CPU 模式
↓
bootloader 加载 kernel
↓
kernel 初始化并开始管理系统
↓
程序被加载并运行成进程
↓
进程拥有自己的地址空间和 PCB
↓
OS 用 limited direct execution 让程序高效运行
↓
CPU/OS 用 user mode 和 kernel mode 保证隔离
↓
特权指令、内存保护、时钟中断共同防止程序失控
本讲最重要的几个关键词:
BIOS
Bootloader
Kernel
Real Mode
Protected Mode
Process
PCB
Address Space
.text / .data / .bss / heap / stack
Limited Direct Execution
User Mode
Kernel Mode
Privileged Instruction
Memory Protection
Paging
Timer Interrupt
CPL
15. 小测题与答案
题 1:BIOS 和 bootloader 的区别是什么?
答案:
BIOS 是主板固件,开机后最先运行,负责硬件自检、初始化设备、寻找启动设备,并加载 bootloader。
Bootloader 是启动加载器,通常随 OS 存放在磁盘或启动介质中,负责切换 CPU 模式、检查并加载 kernel image,最后跳转到 kernel。
题 2:为什么 BIOS 不直接加载 kernel?
答案:
因为不同操作系统的内核格式、文件系统、启动方式、启动参数、安全验证方式都不同。如果 BIOS 直接支持所有 OS,会非常复杂。Bootloader 提供了灵活性、兼容性、启动设备管理、安全验证、错误处理和更新能力。
题 3:real mode 和 protected mode 的核心区别是什么?
答案:
real mode 是早期兼容模式,能力有限,没有现代意义上的权限隔离和内存保护。protected mode 支持更大的地址空间、内存保护、权限级别和多任务,是现代 OS 真正运行所需的模式。
题 4:程序和进程有什么区别?
答案:
程序是静态的可执行文件,通常存放在磁盘上。进程是程序的一次运行实例,拥有执行状态、地址空间、寄存器上下文、文件描述符等资源。一个程序可以对应多个进程。
题 5:PCB 里保存哪些信息?什么时候用?
答案:
PCB 保存 PID、进程状态、优先级、程序计数器、寄存器、内存信息、I/O 状态、记账信息等。上下文切换时,OS 会把当前进程状态保存到 PCB,并从另一个进程的 PCB 恢复状态。
题 6:下面变量分别在哪里?
int a = 0;
int b;
void hello() {
static int c;
int d;
int *e = malloc(...);
}
答案:
a:已初始化全局变量,通常在.data,某些实现中零初始化也可能放.bss。b:未初始化全局变量,在.bss。c:static 局部变量,生命周期贯穿整个程序,未初始化时在.bss。d:普通局部变量,在 stack。e:指针变量本身在 stack,malloc分配出的对象在 heap。
题 7:为什么不能让普通程序直接执行 I/O 指令?
答案:
因为 I/O 操作可能影响整个系统和其他进程。如果普通程序可以随便操作设备,就会破坏隔离和安全。因此 I/O 指令通常是特权指令,只能在内核态执行。用户程序需要通过系统调用请求内核代为执行。
题 8:limited direct execution 是什么?
答案:
limited direct execution 是现代 OS 虚拟化 CPU 的基本思路:大多数普通指令让用户程序直接在 CPU 上运行,以保证性能;敏感操作必须进入 OS,由内核检查和执行,以保证隔离和控制。
题 9:timer interrupt 的作用是什么?
答案:
timer interrupt 让 OS 能定期抢回 CPU 控制权,防止某个进程无限占用 CPU,并支持抢占式调度。时钟中断发生后,OS 可以保存当前进程状态并调度另一个进程运行。
题 10:CPL 是普通程序可以直接修改的吗?
答案:
不是。CPL 是 CPU 当前特权级,x86 中由 CS 段寄存器低 2 位表示。用户程序不能通过普通赋值或位运算修改 CPL。用户态进入内核态必须通过系统调用、中断、异常等受控机制。
16. 常见 Q&A
Q1:BIOS、bootloader、kernel 三者谁更“底层”?
从启动顺序看:
BIOS/UEFI → bootloader → kernel
BIOS/UEFI 最先运行,最靠近硬件;bootloader 是 OS 启动链的一部分;kernel 是真正长期运行并管理系统资源的核心。
Q2:Bootloader 是操作系统的一部分吗?
通常可以认为是 OS 启动体系的一部分。它不负责进程管理、内存管理、文件系统运行时管理等完整 OS 功能,但它负责把 kernel 加载起来,是 OS 能运行的前置步骤。
Q3:Kernel 和完整操作系统有什么区别?
Kernel 是操作系统的核心,负责 CPU 调度、内存管理、进程管理、文件系统核心逻辑、设备驱动、中断处理等。
完整操作系统除了 kernel,还包括:
- shell
- 图形界面
- 系统服务
- 标准库
- 工具程序
- 包管理器
- 用户应用环境
所以:
Kernel ⊂ Operating System
Q4:为什么说进程的地址不是物理地址?
因为进程看到的是虚拟地址。虚拟地址需要经过页表和 MMU 翻译,才能变成物理地址。这样 OS 才能实现隔离、共享、灵活分配和按需加载。
Q5:为什么 heap 往上长,stack 往下长?
经典布局里,heap 从低地址向高地址增长,stack 从高地址向低地址增长。这样两者可以共享中间的空闲空间,程序运行时动态扩展更方便。
不过这只是经典模型,具体系统和架构实现可能有所不同。
Q6:为什么普通函数调用不能进入内核态?
普通函数调用只是修改用户态程序自己的栈和 PC。它不会改变 CPU 特权级。
进入内核态需要硬件支持的受控入口,例如 syscall、trap、interrupt、exception。这样 CPU 和 OS 才能检查入口是否合法,避免用户程序跳到内核任意位置执行。
Q7:用户程序调用 read() 是不是直接读磁盘?
不是。用户程序调用的 read() 通常是 libc 提供的接口。它最终通过系统调用进入内核,内核检查权限和参数,再通过文件系统和设备驱动完成真正的读操作。
调用链大概是:
用户程序 read()
↓
libc wrapper
↓
syscall / trap
↓
kernel syscall handler
↓
文件系统 / 设备驱动
↓
返回用户态
Q8:内核映射在每个进程地址空间里,会不会不安全?
不会,只要权限设置正确。
内核区域可以映射在进程地址空间中,但页表项会标记为 user mode 不可访问。用户程序访问这部分地址会触发异常。只有 CPU 已经进入 kernel mode 后,内核才能访问这些地址。
Q9:为什么异常、系统调用、中断都能进入内核?
因为它们都是硬件和 OS 预先设计好的受控入口。
它们不会让用户程序随便跳到内核任意位置,而是通过固定的入口表和权限检查进入内核 handler。这保证了安全性。
Q10:第二讲和第三讲有什么关系?
第二讲提出问题:
用户态和内核态为什么存在?
用户程序如何进入内核?
OS 如何抢回 CPU?
第三讲会进一步讲:
- exception
- interrupt
- system call
- interrupt vector table / IDT
- interrupt stack
- x86 mode transfer
- context switch
也就是具体解释“用户态和内核态到底怎么切换”。
17. 拓展理解
17.1 OS 为什么需要硬件支持?
操作系统不是纯软件就能实现隔离的。
如果 CPU 不提供用户态/内核态,普通程序就可以执行任意指令。
如果 MMU 不提供地址翻译和权限检查,普通程序就可以访问任意内存。
如果没有 timer interrupt,OS 就无法强制抢回 CPU。
所以现代 OS 的很多功能,本质上都是:
OS 设计策略,硬件负责强制执行。
这也是系统课很重要的一条主线:hardware-software cooperation。
17.2 “受限制的直接执行”为什么优雅?
因为它抓住了一个平衡:
所有指令都模拟:太慢
所有指令都直接跑:不安全
大多数直接跑,少数敏感操作受控进入 OS:兼顾性能和安全
这就是 limited direct execution 的精髓。
它也是很多系统设计的共同思想:
快路径尽量直接,慢路径/危险路径交给内核处理。
17.3 进程隔离和虚拟内存的关系
进程隔离依赖虚拟内存。
每个进程都有自己的虚拟地址空间。即使两个进程都访问 0x400000,它们最终映射到的物理页也可以完全不同。
这让每个程序都产生一种幻觉:
我拥有一整片属于自己的内存。
但实际上:
OS 和 MMU 在背后管理虚拟地址到物理地址的映射。
17.4 为什么本讲是后续课程的地基?
本讲后面很多知识都会继续展开:
| 本讲概念 | 后续对应内容 |
|---|---|
| 用户态/内核态 | 系统调用、中断、异常 |
| PCB | 上下文切换、调度 |
| 虚拟地址 | 地址翻译、分页、TLB |
| heap / stack | malloc、虚拟内存、缺页 |
| timer interrupt | 抢占式调度 |
| 特权指令 | syscall、设备 I/O |
| kernel high address | 内核地址空间、页表权限 |
所以这一讲不是孤立知识点,而是后面一半课程的基础。
18. 复习清单
复习第二讲时,可以按下面的问题自测:
- 我能说清楚 BIOS 做了哪些事。
- 我能说清楚 bootloader 做了哪些事。
- 我知道为什么 BIOS 不直接加载 kernel。
- 我能解释 real mode 和 protected mode 的区别。
- 我能看懂
0xf000:0xfff0 → 0xffff0的地址计算。 - 我知道 bootloader 为什么要设置 GDT、打开 CR0 的 PE 位。
- 我知道程序和进程的区别。
- 我能解释 PCB 是什么,以及上下文切换为什么需要 PCB。
- 我能判断
.text、.data、.bss、heap、stack 分别放什么。 - 我知道
int *e = malloc(...)中e和*e的位置不同。 - 我能解释 limited direct execution。
- 我知道为什么需要 user mode 和 kernel mode。
- 我能举例说明什么是特权指令。
- 我知道用户程序执行特权指令会触发异常。
- 我能解释 segmentation 的 base + bounds 思想。
- 我能解释 paging 为什么能实现内存保护。
- 我知道为什么 kernel 常放在高地址。
- 我知道 timer interrupt 如何帮助 OS 抢回 CPU。
- 我知道 CPL 是什么,以及为什么用户不能直接修改它。
19. 最后总结
第二讲的核心不是背启动流程,而是理解操作系统为什么必须和硬件合作。
从启动角度看:
BIOS/UEFI → bootloader → kernel
从运行角度看:
program → process → PCB → address space
从保护角度看:
user mode / kernel mode
privileged instructions
memory protection
timer interrupt
从设计思想看:
让普通操作直接执行,保证性能;
让危险操作受控进入内核,保证安全;
让 OS 能随时抢回 CPU,保证控制权。
这就是第二讲最重要的主线。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)