## 实验三：虚实地址转换

### 实验之前

- 阅读实验指导三，可以结合 `lab-3` 分支的代码来理解。
- 本次的实验题将使用 `lab-3+` 分支，它包含了后面章节的内容，而我们不需要管那些部分。

### 实验题

1.  原理：在 `os/src/entry.asm` 中，`boot_page_table` 的意义是什么？当跳转执行 `rust_main` 时，不考虑缓存，硬件通过哪些地址找到了 `rust_main` 的第一条指令？

    {% reveal %}
> 1.  `boot_page_table` 是一个用二进制表示的根页表，其中包含两个 1GB 大页，分别是将虚拟地址 `0x8000_0000` 至 `0xc000_0000` 映射到物理地址 `0x8000_0000` 至 `0xc000_0000`，以及将虚拟地址 `0xffff_ffff_8000_0000` 至 `0xffff_ffff_c000_0000` 映射到物理地址 `0x8000_0000` 至 `0xc000_0000`。
>
>     由于我们在 `linker.ld` 中指定了起始地址为 `0xffff_ffff_8020_0000`，操作系统执行文件会认为所有的符号都是在这个高地址上的。但是我们在硬件上只能将内核加载到 `0x8020_0000` 开始的内存空间上，此时的 `pc` 也会调转到这里。
>
>     为了让程序能够正确跳转至高地址的 `rust_main`，我们需要在 `entry.asm` 中先应用内核重映射，即将高地址映射到低地址。但我们不可能在替换页表的同时修改 `pc`，此时 `pc` 仍然处于低地址。所以，页表中的另一项（低地址的恒等映射）则保证程序替换页表后的短暂时间内，`pc` 仍然可以顺着低地址去执行内存中的指令。
>
>     *注：如果 `boot_page_table` 中不包含低地址恒等映射，程序可能仍然可以正常运行。这可能和硬件的缓存设计有关。但保险起见，应当保留这两个映射。*
>
> 2.  执行 `jal rust_main` 时，硬件需要加载 `rust_main` 对应的地址，大概是 `0xffff_ffff_802x_xxxx`。
>     - 页表已经启用，硬件先从 `satp` 高位置读取内存映射模式，再从 `satp` 低位置读取根页表页号，即 `boot_page_table` 的物理页号
>     - 对于 Sv39 模式，页号有三级共 27 位。对于 `rust_main` 而言，一级页号是其 [30:38] 位，即 510。硬件此时定位到根页表的第 510 项
>     - 这一项的标志为 XWR，说明它指向一个大页而不是指向下一级页表；目标的页号为 `0x8_0000`，即物理地址 `0x8000_0000` 开始的区间；这一项的 V 位为 1，说明目标在内存中。因此，硬件寻址到页基址 + 页内偏移，即 `0x8000_0000 + 0x2x_xxxx`，找到 `rust_main`
    {% endreveal %}

    <br>
2.  分析：为什么 `Mapping` 中的 `page_tables` 和 `mapped_pairs` 都保存了一些 `FrameTracker`？二者有何不同？

    {% reveal %}
> 页表也是需要我们去分配页面来存储的。因此，`page_tables` 存放了所有页表所用到的页面，而 `mapped_pairs` 则存放了进程所用到的页面。 
    {% endreveal %}

    <br>
3.  分析：假设某进程需要虚拟地址 A 到物理地址 B 的映射，这需要操作系统来完成。那么操作系统在建立映射时有没有访问 B？如果有，它是怎么在还没有映射的情况下访问 B 的呢？

    {% reveal %}
> 建立映射不需要访问 B，而只需要操作页表即可。不过，通常程序都会需要操作系统建立映射的同时向页面中加载一些数据。此时，尽管 A→B 的映射尚不存在，因为我们将整个可用物理内存都建立了内核映射，所以操作系统仍然可以通过线性偏移量来访问到 B。
    {% endreveal %}

    <br>
4.  实验：了解并实现时钟页面置换算法（或任何你感兴趣的算法），可以自行设计样例来比较性能

    - 置换算法只需要修改 `os/src/memory/mapping/swapper.rs`

    - 在 `main.rs` 中调用 `create_kernel_thread` 来创建线程，你可以任意修改其中运行的函数，以达到测试效果
