注: 翻译自 MIT xv6 rev11 book, 为了方便阅读,会附上相关的源码;本文中专有名词统一不做翻译
运行第一个进程
为了更详细的描述 xv6 的组织结构,让我们来看一下 kernel 是如何为自己创建第一个地址空间的,以及如何创建并启动第一个进程和执行第一个系统调用的。通过跟踪这些过程,我们可以清楚的了解到 xv6 是如何提供进程间强隔离的。
一台 PC 通电启动后,BIOS 会执行一些初始化工作,接着从磁盘(主引导分区)加载一个 boot loader 到(物理)内存中执行。 xv6 的 boot loader 会从磁盘上加载 kernel ,然后从 entry (entry.S 1044) 开始执行。 kernel 在最初启动阶段,x86 的分页硬件是关闭的,分页机制尚未开启,此时虚拟地址会直接对应物理地址。
代码语言:python代码运行次数:0复制entry.S
1042 # Entering xv6 on boot processor, with paging off.
1043 .globl entry
1044 entry:
1045 # Turn on page size extension for 4Mbyte pages
1046 movl %cr4, �x
1047 orl $(CR4_PSE), �x
1048 movl �x, %cr4
boot loader 会将 kernel 加载到物理内存 0x10 0000 地址处,之所以不是 0x8000 0000 是考虑一些小机器上并没有这么多的物理内存。另外不把 kernel 加载到 0x0 的原因是地址段 0xA 0000 : 0x10 0000 预留给了 I/O 设备(DMA)。
为了使 kernel 的剩余代码可以正常运行,entry 会设置一个 page table (entrypgdir) 把从 0x8000 0000 开始的虚拟地址映射到 0 开始的物理地址。将二段虚拟地址映射到同一段物理地址是页表的一种常见使用技巧,我们在后面会看到更多的例子。
代码语言:c复制 main.c
// The boot page table used in entry.S and entryother.S.
// PTE_PS in a page directory entry enables 4Mbyte pages.
1306 pde_t entrypgdir[NPDENTRIES] = {
1307 // Map VA’s [0, 4MB) to PA’s [0, 4MB)
1308 [0] = (0) | PTE_P | PTE_W | PTE_PS,
1309 // Map VA’s [KERNBASE, KERNBASE 4MB) to PA’s [0, 4MB)
1310 [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
1311 };
entry 的页表定义在 main.c 1306 我们会在第二章了解更多细节,这里我们只需要知道第 0 项会将虚拟地址 0 : 0x40 0000 映射到物理地址 0 : 0x40 0000,这个映射之所以需要是因为 entry 是执行在低内存地址处的,最终我们会去掉这项映射。
第 512 项(KERNBASE>>PDXSHIFT = 0x8000 0000 >> 22 = 512) 将虚拟地址 KERNBASE : KERNBASE 0x40 0000 映射到物理地址 0 : 0x40 0000,这项将在 entry 执行结束后由 kernel 使用,kernel 将从这个高内存地址处开始寻找指令和数据,同时这个映射也将 kernel 指令和数据大小限制在 4M 字节。
附录 B 提供了更多细节:kernel 是一个 ELF 格式的二进制文件, linker 链接器将起始地址设置成 0x80100000 (虚拟地址),见 kernel.ld 文件,KERNLINK 表示这个地址
回到 entry ,它会将 entrypgdir 的物理地址加载到控制寄存器 %cr3 , %cr3 的值必须是物理地址,存放虚拟地址是没有意义的,因为分页硬件并不知道如何翻译虚拟地址。符号 entrypgdir 会引用到一个高位地址,C 语言中的宏 V2P_WO 会减去 KERNBASE 来找到对应的物理地址。xv6 会通过设置控制寄存器 %cr0 的 CR0_PG 标记位来开启分页硬件。
代码语言:python代码运行次数:0复制entry.S
1049 # Set page directory
1050 movl $(V2P_WO(entrypgdir)), �x
1051 movl �x, %cr3
1052 # Turn on paging.
1053 movl %cr0, �x
1054 orl $(CR0_PG|CR0_WP), �x
1055 movl �x, %cr0
在开启分页后,CPU 处理器仍然在低地址处执行指令,之所以可以这样是因为 entrypgdir 的第 0 项映射,如果把第 0 项去掉,那么当计算机执行开启分页后的指令就会崩溃。
现在 entry 需要转移到 kernel 在高内存地址处执行,首先将栈指针 %esp 指向用作栈的内存区域 (1057)。所有的符号 symbol 都有高位地址,包括 stack , 所以栈在低位地址映射被去掉后将依然有效。最终,entry 将跳转到 main, 也是一个高位地址。1065 行的间接跳转是必要的,不然的话,汇编器就会生成 PC-relative 直接跳转指令, 这将会导致执行低位地址版本的 main. 注意 Main 是不会返回的,因为栈中没有返回地址 (return PC)。现在 kernel 开始在高内存地址处执行函数 main 了。
代码语言:python代码运行次数:0复制entry.S
1057 # Set up the stack pointer.
1058 movl $(stack KSTACKSIZE), %esp
1059
1060 # Jump to main(), and switch to executing at
1061 # high addresses. The indirect call is needed because
1062 # the assembler produces a PC−relative instruction
1063 # for a direct jump.
1064 mov $main, �x
1065 jmp *�x
1066
1067 .comm stack, KSTACKSIZE
链接器将 kernel 的 C 代码和 entry.S 链接在一起,使得 entry.S 里可以引用 kernel 里的符号,比如 main
注:在汇编语言中,.comm 是一个伪指令,用于定义一个全局可见,未初始化的共享内存区域(或者称为共享块)。它的主要用途是为程序中的全局变量或共享数据分配内存空间,但不会为这些变量提供初始值