Lecture 7 Labs
Pagetable
内存中, 0x80000000 以下是外部设备和用户程序的内存,往上是 Kernel 使用的内存。 当 CPU 需要执行指令时,会通过 MMU 将 VA(virtual address) 转换为 PA(physical address) ,然后索引到内存的真实物理地址。 MMU 中有一个 satp register ,它是用来存储当前正在使用的 pagetable ,如果它里面的值是 0 ,说明机器处于刚启动的状态, MMU 不会对进行翻译, VA 会直接指向 PA 。
在 Kernel 的高地址再向上,是一段不被使用的空内存,这是为了防止内核的内存溢出,也能保证内核不崩溃。而再 Kernel 的高地址部分,有一段是 root pagetable ,当 satp 加载这一段地址后, MMU 就会开始进行 VA 到 PA 的转换。
当 xv6 初始化程序时。
- 第一个初始化的页表是 Guard page ,它设置了 V 标志却没设置 U 标志,这样会导致用户程序运行超过这个地址时出现异常。
- 第二个初始化是 stack ,它拥有所有的权限。
- 倒数第二个初始化是 trapframe ,它拥有读写权限。
- 最后初始化是 trampoline ,因为只有它设置了 X 权限。
通常较低层次的页表是共享的,这是标准的做法。
XV6 的内核堆是放在 Page 的高位,这是因为 Risk-V 中是向下增长的,同时在 kernel heap 下面是 Guard page ,它没有实际的物理映射,因此即使内核堆溢出也不会崩溃,如果 kernel heap 过大,连 Guard page 都溢出了,则会出现页面错误,实际上大部分架构都是这样设计的。

Process
内核启动 (Kernel Boot)
启动电脑时,当按下电源键,计算机经过 BIOS/UEFI、Bootloader 后,加载操作系统内核到内存并执行。此时,还没有任何“进程”的概念,只有内核代码在 CPU 上运行。
内核会初始化硬件、内存管理(页表)、中断控制器等。创建 0 号进程 (Idle Process)
内核初始化完成后,会创建0 号进程(通常称为 idle 进程或 swapper)。这是一个特殊的内核线程,主要作用是当系统没有其他任务可运行时,让 CPU 进入低功耗状态或执行空循环。
创建 1 号进程 (Init Process)
0 号进程通过 fork 和 exec 创建1 号进程,通常称为 init。职责: init 是用户空间所有进程的祖先。它的任务之一是启动登录界面或直接启动 Shell。 Linux 中: 现代 Linux 通常是 systemd 或 init。
Init 启动 Shell
init 进程会读取配置文件,找到 Shell 程序的路径(例如 /bin/sh),然后执行:- fork():创建一个子进程。
- exec():在子进程中加载并运行 Shell 程序的二进制代码。
- wait():init 会等待 Shell 进程退出(通常 Shell 会一直运行,直到系统关机)。
所有的程序都是通过 init, fork, exec 创建的。
- init
- 内核直接调用 uvmcreate() 创建页表,手动映射用户代码。
- fork
- 分配新 PCB(进程控制块)。
- 调用 uvmcopy() 复制父进程的用户页表(分配新物理页,拷贝数据)。
- 复制父进程的内核页表指针(共享内核)。
- 子进程开始运行,代码和父进程一样(继续执行 fork() 返回处)。
- exec
- 创建全新页表:调用 uvmcreate()(从内核全局页表复制内核部分)。
- 重建用户空间:解析 ELF 文件,把新程序的代码/数据映射到 Entry 0。
- 销毁旧用户空间:释放原进程的用户内存。
- 进程 PID 不变,但代码变成了新程序。
其中 init 和 exec 的创建过程几乎一样,区别只是 init 需要先创建页表。
启动一个进程,通常是先通过 fork 创建一个子进程,然后使用 exec 夺舍子进程成为新的进程。
fork 后,子进程的用户态页表是复制自父进程的用户页表,而内核态页表需要单独创建,这是为了防止父进程在子进程结束之前退出。
如果内核态页表不单独创建,同时父进程在子进程结束前退出了,父进程的内核态页表会被销毁,子进程会出现缺页。