分页硬件的开启
概要:介绍 xv6 进程地址空间的布局(用户低地址、内核高地址)、进程双栈机制(用户栈与内核栈),以及分页硬件开启后如何维护进程隔离与共享。
1. 进程地址空间布局:隔离与共享
xv6 的核心设计思想是:每个进程都认为自己拥有完整的地址空间,但实际上这个空间由内核通过“页表”小心地维护。
- 用户部分(低地址): 从虚拟地址
0开始。- 布局顺序: 指令(Code) -> 全局变量(Data) -> 栈(Stack) -> 堆(Heap,通过 malloc 增长)。
- 内核部分(高地址): 从
0x80100000开始。- 巧妙之处: 每个进程的页表里都映射了同一份内核代码。
- 优点: 当用户程序发起系统调用进入内核时,硬件不需要切换页表,内核可以直接利用当前的页表访问用户内存(因为用户内存就在低地址处)。
2. 进程的双栈机制 (Dual Stacks)
这是理解操作系统安全性的关键。xv6 为每个进程准备了两套栈:
| 栈类型 | 运行位置 | 作用 |
|---|---|---|
| 用户栈 (User Stack) | 用户空间 | 执行普通的 C 程序指令、存放局部变量。 |
| 内核栈 (Kernel Stack) | 内核空间 (p->kstack) |
当进程陷入内核(系统调用、中断)时使用。 |
为什么要两个栈? 为了安全性。如果内核直接在用户栈上运行,恶意用户可能会故意破坏栈指针(SP),导致内核崩溃。通过独立内核栈,即便用户程序的栈溢出了,内核依然能稳定运行并处理错误。
3. 启动时的“内存魔术” (Boot & Entry)
这里有一个经典的“先有鸡还是先有蛋”的问题:内核想在高地址运行,但刚开机时没有分页机制,只能访问低物理地址。
关键步骤:
- 加载: Boot loader 把内核加载到物理地址
0x100000(1MB 处,避开了 I/O 设备区)。 - 临时页表 (
entrypgdir): 此时分页还没开启。内核在main.c中定义了一个简单的临时页表。- 它将虚拟地址
0~4MB映射到物理地址0~4MB(保证当前运行的代码不崩)。 - 它将虚拟地址
KERNBASE ~ KERNBASE+4MB也映射到物理地址0~4MB。
- 它将虚拟地址
- 开启分页: 设置
%cr3寄存器指向这个页表,并开启%cr0的分页位。 - 长跳转: 此时硬件已经支持虚拟地址了。内核执行一个跳转指令,直接跳到高地址处的
main函数。
两个页表项的作用
这里建立了两个页表项:
- A: 虚拟地址
0~4MB映射到物理地址0~4MB - B:虚拟地址
KERNBASE ~ KERNBASE+4MB映射到物理地址0~4MB
- 刚开始,CPU 正在执行 entry.S 里的指令。此时 CPU 的程序计数器(EIP)指在低地址,比如 0x00100050
- 此时开启分页
- 开启分页的一瞬间,CPU 寻找下一条指令 0x00100052 时,必须经过页表翻译。也就是说,程序计数器EIP上一秒还在低地址,开启分页硬件后,EIP+1需要通过分页硬件进行翻译,此时如果没有A这个页表项,那么就找不到EIP+1对应的物理地址。此时A页表项发挥作用。
- 而链接器将main函数这部分的地址,贴上了 0x80100000 开头的高地址标签,CPU执行到这部分时,如果没有B这个页表项,就找不到这些虚拟的高地址对应的低的物理地址。此时页表项B发挥作用。
内核的位置
内核物理上在加载到内存后,位置从来没有动过。
4. 开启MMU前后对比

留下评论