少于 1 分钟阅读 次阅读

概要:介绍 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)

这里有一个经典的“先有鸡还是先有蛋”的问题:内核想在高地址运行,但刚开机时没有分页机制,只能访问低物理地址。

关键步骤:

  1. 加载: Boot loader 把内核加载到物理地址 0x100000(1MB 处,避开了 I/O 设备区)。
  2. 临时页表 (entrypgdir): 此时分页还没开启。内核在 main.c 中定义了一个简单的临时页表。
    • 它将虚拟地址 0~4MB 映射到物理地址 0~4MB(保证当前运行的代码不崩)。
    • 它将虚拟地址 KERNBASE ~ KERNBASE+4MB 也映射到物理地址 0~4MB
  3. 开启分页: 设置 %cr3 寄存器指向这个页表,并开启 %cr0 的分页位。
  4. 长跳转: 此时硬件已经支持虚拟地址了。内核执行一个跳转指令,直接跳到高地址处的 main 函数。

两个页表项的作用

这里建立了两个页表项:

  • A: 虚拟地址 0~4MB 映射到物理地址 0~4MB
  • B:虚拟地址 KERNBASE ~ KERNBASE+4MB 映射到物理地址 0~4MB
  1. 刚开始,CPU 正在执行 entry.S 里的指令。此时 CPU 的程序计数器(EIP)指在低地址,比如 0x00100050
  2. 此时开启分页
  3. 开启分页的一瞬间,CPU 寻找下一条指令 0x00100052 时,必须经过页表翻译。也就是说,程序计数器EIP上一秒还在低地址,开启分页硬件后,EIP+1需要通过分页硬件进行翻译,此时如果没有A这个页表项,那么就找不到EIP+1对应的物理地址。此时A页表项发挥作用。
  4. 而链接器将main函数这部分的地址,贴上了 0x80100000 开头的高地址标签,CPU执行到这部分时,如果没有B这个页表项,就找不到这些虚拟的高地址对应的低的物理地址。此时页表项B发挥作用。

内核的位置

内核物理上在加载到内存后,位置从来没有动过。

4. 开启MMU前后对比

alt text

留下评论