ucore step by step
tutorial
tutorial
  • 欢迎来到ucore step-by-step的世界
  • lab0: 预备起
    • 溯源: ucore的历史
    • 概览: 指导书的结构
    • 开搞: 实验环境搭建
  • lab0.5: 比麻雀更小的麻雀(最小可执行内核)
    • 内存布局,OpenSBI,elf和bin
    • 链接脚本和入口点
    • "真正的"入口点
    • 从SBI到stdio
    • Just make it
    • 项目组成和执行流
  • lab1: 断, 都可以断
    • 掉进兔子洞(中断入口点)
    • 中断处理程序
    • 滴答滴答(时钟中断)
    • 项目组成和执行流
  • lab2: 物理内存和页表
    • 内核初始映射
    • 物理内存管理
    • 页面分配算法
    • 项目组成和执行流
  • lab3: 缺页异常和页面置换
    • 扫清外围
    • 使用多级页表
    • 页面置换机制
    • FIFO置换算法
    • 项目组成和执行流
  • lab4: 进程管理
    • 进程与线程
    • 相关数据结构
    • 进程模块初始化
    • 进程切换
    • 项目组成和执行流
  • lab5: 用户程序
    • 用户程序
    • system call!
    • 中断处理
    • 项目组成和执行流
  • lab6: 进程调度
    • 再次认识进程切换
    • 调度算法框架
    • 项目组成和执行流
  • lab7: 同步互斥
    • 同步互斥的基本概念
    • 信号量
    • 条件变量与管程
    • 项目组成和执行流
  • lab8: 文件系统
    • 文件系统抽象层VFS
    • 硬盘文件系统SFS
    • 设备即文件
    • 从zhong duan 到 zhong duan
    • 项目组成和执行流
  • 练习题
    • lab1
    • lab2
    • lab3
    • lab4
    • lab5
    • lab6
    • lab7
    • lab8
  • 附录
    • makefile
由 GitBook 提供支持
在本页

这有帮助吗?

  1. lab2: 物理内存和页表

内核初始映射

上一页lab2: 物理内存和页表下一页物理内存管理

最后更新于5年前

这有帮助吗?

改写自

之前的内核实现并未使能页表机制,实际上内核是直接在物理地址空间上运行的。这样虽然比较简单,但是为了后续能够支持多个用户进程能够在内核中并发运行,满足隔离等性质,我们要先运用学过的页表知识,把内核的运行环境从物理地址空间转移到虚拟地址空间,为之后的功能打好铺垫。

更具体的,我们现在想将内核代码放在虚拟地址空间中以 0xffffffffc0200000 开头的一段高地址空间中。因此,我们将下面的参数修改一下:

// tools/kernel.ld
BASE_ADDRESS = 0xFFFFFFFFC0200000;
//之前这里是 0x80200000

我们修改了链接脚本中的起始地址。但是这样做的话,就能从物理地址空间转移到虚拟地址空间了吗?让我们回顾一下在相当于 bootloader 的 OpenSBI 结束后,我们要面对的是怎样一种局面:

  • 物理内存状态:OpenSBI 代码放在 [0x80000000,0x80200000) 中,内核代码放在以 0x80200000 开头的一块连续物理内存中。

  • CPU 状态:处于 S Mode ,寄存器 satp 的 MODE\text{MODE}MODE 被设置为 Bare ,即无论取指还是访存我们都通过物理地址直接访问物理内存。 PC=0x80200000\text{PC}=0\text{x}80200000PC=0x80200000 指向内核的第一条指令。栈顶地址 SP\text{SP}SP 处在 OpenSBI 代码内。

  • 内核代码:由于改动了链接脚本的起始地址,认为自己处在以虚拟地址 0xffffffffc0200000 开头的一段连续虚拟地址空间中,以此为依据确定代码里每个部分的地址(每一段都是从BASE_ADDRESS往后依次摆开的,所以代码里各段都会认为自己在0xffffffffc0200000之后的某个地址上,或者说编译器和链接器会把里面的符号/变量地址都对应到0xffffffffc0200000之后的某个地址上)

接下来,我们在入口点 entry.S 中所要做的事情是:将 SP\text{SP}SP 寄存器从原先指向OpenSBI 某处的栈空间,改为指向我们自己在内核的内存空间里分配的栈;同时需要跳转到函数 kern_init 中。

在之前的实现中,我们已经在 entry.S 自己分配了一块 16KiB16\text{KiB}16KiB 的内存用来做启动栈:

#include <mmu.h>
#include <memlayout.h>

    .section .text,"ax",%progbits
    .globl kern_entry
kern_entry:
    la sp, bootstacktop

    tail kern_init

.section .data
    # .align 2^12
    .align PGSHIFT
    .global bootstack
bootstack:
    .space KSTACKSIZE
    .global bootstacktop
bootstacktop:

符号 bootstacktop 就是我们需要的栈顶地址, 符号 kern_init 代表了我们要跳转到的地址。之前我们直接将 bootstacktop 的值给到 SP\text{SP}SP, 再跳转到 rust_main 就行了。看起来原来的代码仍然能用啊?

问题在于,由于我们修改了链接脚本的起始地址,编译器和链接器认为内核开头地址为 0xffffffffc0200000,因此这两个符号会被翻译成比这个开头地址还要高的某个虚拟地址。而我们的 CPU 目前还处于 Bare 模式,会将地址都当成物理地址处理。这样,我们跳转到 rust_main 就会跳转到比0xffffffffc0200000还大 的一个物理地址,物理地址都没有这么多位!这显然是会出问题的。

于是,我们需要想办法利用刚学的页表知识,帮内核将需要的虚拟地址空间构造出来。也就是:构建一个合适的页表,让satp指向这个页表,然后使用地址的时候都要经过这个页表的翻译,使得虚拟地址0xFFFFFFFFC0200000经过页表的翻译恰好变成0x80200000,就不会出错了。

我们实现一个最简单的页表,所有的虚拟地址有一个固定的偏移量。比如内核的第一条指令,虚拟地址为 0xffffffffc0200000 ,物理地址为 0x80200000 ,因此,我们只要将虚拟地址减去 0xffffffff40000000 ,就得到了物理地址。

使用上一节页表的知识,我们只需要做到当访问内核里面的一个虚拟地址 va\text{va}va 时,我们知道 va\text{va}va 处的代码或数据放在物理地址为 pa = va - 0xffffffff40000000 处的物理内存中,我们真正所要做的是要让 CPU 去访问 pa\text{pa}pa。因此,我们要通过恰当构造页表,来对于内核所属的虚拟地址,实现这种 va→pa\text{va}\rightarrow\text{pa}va→pa 的映射。

还记得上一节中所讲的大页吗?那时我们提到,将一个三级页表项的标志位 R,W,X\text{R,W,X}R,W,X 不设为全 000 ,可以将它变为一个叶子,从而获得大小为 1GiB1\text{GiB}1GiB 的一个大页。

我们假定内核大小不超过 1GiB1\text{GiB}1GiB,通过一个大页将虚拟地址区间[0xffffffffc0000000,0xffffffffffffffff] 映射到物理地址区间 [0x80000000,0xc0000000),而我们只需要分配一页内存用来存放三级页表,并将其最后一个页表项(也就是对应我们使用的虚拟地址区间的页表项)进行适当设置即可。

#include <mmu.h>
#include <memlayout.h>

    .section .text,"ax",%progbits
    .globl kern_entry
kern_entry:
    # t0 := 三级页表的虚拟地址
    lui     t0, %hi(boot_page_table_sv39)
    # t1 := 0xffffffff40000000 即虚实映射偏移量
    li      t1, 0xffffffffc0000000 - 0x80000000
    # t0 减去虚实映射偏移量 0xffffffff40000000,变为三级页表的物理地址
    sub     t0, t0, t1
    # t0 >>= 12,变为三级页表的物理页号
    srli    t0, t0, 12

    # t1 := 8 << 60,设置 satp 的 MODE 字段为 Sv39
    li      t1, 8 << 60
    # 将刚才计算出的预设三级页表物理页号附加到 satp 中
    or      t0, t0, t1
    # 将算出的 t0(即新的MODE|页表基址物理页号) 覆盖到 satp 中
    csrw    satp, t0
    # 使用 sfence.vma 指令刷新 TLB
    sfence.vma
    # 从此,我们给内核搭建出了一个完美的虚拟内存空间!
    #nop # 可能映射的位置有些bug。。插入一个nop

    # 我们在虚拟内存空间中:随意将 sp 设置为虚拟地址!
    lui sp, %hi(bootstacktop)

    # 我们在虚拟内存空间中:随意跳转到虚拟地址!
    # 跳转到 kern_init
    lui t0, %hi(kern_init)
    addi t0, t0, %lo(kern_init)
    jr t0

.section .data
    # .align 2^12
    .align PGSHIFT
    .global bootstack
bootstack:
    .space KSTACKSIZE
    .global bootstacktop
bootstacktop:

.section .data
    # 由于我们要把这个页表放到一个页里面,因此必须 12 位对齐
    .align PGSHIFT
    .global boot_page_table_sv39
# 分配 4KiB 内存给预设的三级页表
boot_page_table_sv39:
    # 0xffffffff_c0000000 map to 0x80000000 (1G)
    # 前 511 个页表项均设置为 0 ,因此 V=0 ,意味着是空的(unmapped)
    .zero 8 * 511
    # 设置最后一个页表项,PPN=0x80000,标志位 VRWXAD 均为 1
    .quad (0x80000 << 10) | 0xcf # VRWXAD

总结一下,要进入虚拟内存访问方式,需要如下步骤:

  1. 分配页表所在内存空间并初始化页表;

  2. 设置好页基址寄存器(指向页表起始地址);

  3. 刷新 TLB。

到现在为止我们终于理解了自己是如何做起白日梦——进入那看似虚无缥缈的虚拟内存空间的。

rcore tutorial