当前位置: 首页 > news >正文

网站售后服务内容寮步做网站公司

网站售后服务内容,寮步做网站公司,wordpress post页幻灯片,门户网站程序文章目录 【 0. 引言 】背景本章任务 【 1. C 中的动态内存分配 】1.1 C语言的内存分配1.2 kalloc 中的动态内存分配 【 2. 地址空间 】2.1 虚拟地址和地址空间2.1.1 地址虚拟化出现之前2.1.2 加一层抽象加强内存管理2.1.3 增加硬件加速虚实地址转换 2.2 分段内存管理2.2.1 等量… 文章目录 【 0. 引言 】背景本章任务 【 1. C 中的动态内存分配 】1.1 C语言的内存分配1.2 kalloc 中的动态内存分配 【 2. 地址空间 】2.1 虚拟地址和地址空间2.1.1 地址虚拟化出现之前2.1.2 加一层抽象加强内存管理2.1.3 增加硬件加速虚实地址转换 2.2 分段内存管理2.2.1 等量分配2.2.2 按需分配 2.3 分页内存管理2.4 C的内存布局 【 3. SV39多级页表机制内容介绍 】3.1 内存控制相关的CSR寄存器3.2 地址格式与组成3.3 多级页表原理3.3.1 线性表实现3.3.2 字典树实现 【 4. SV39多级页表机制OS 实现 】4.1 地址相关的数据结构抽象4.1.1 页表实现va–pa的转换过程 4.2 页表的建立过程4.3 启用页表后的跨页表操作4.4 内核页表4.5 用户页表的加载 【 0. 引言 】 背景 上一章我们分别实现了多道程序和分时多任务系统它们的核心机制都是任务切换。由于多道程序和分时多任务系统的设计初衷不同它们在任务切换的时机和策略也不同。有趣的一点是任务切换机制对于应用是完全 透明 (Transparent) 的应用可以不对内核实现该机制的策略做任何假定除非要进行某些针对性优化甚至可以完全不知道这机制的存在。在大多数应用也就是应用开发者的视角中它们会独占一整个 CPU 和特定连续或不连续的内存空间。当然通过上一章的学习我们知道在现代操作系统中出于公平性的考虑我们极少会让独占CPU这种情况发生。所以应用自认为的独占CPU只是内核想让应用看到的一种 幻象 (Illusion) 而 CPU 计算资源被 时分复用 (TDM, Time-Division Multiplexing) 的实质被内核通过恰当的抽象隐藏了起来对应用不可见。与之相对我们目前还没有对内存管理功能进行有效的管理仅仅是把程序放到某处的物理内存中。在内存访问方面所有的应用都直接通过物理地址访问物理内存这使得应用开发者需要了解繁琐的物理地址空间布局访问内存也很不方便。在上一章中出于任务切换的需要所有的应用都在初始化阶段被加载到内存中并同时驻留下去直到它们全部运行结束。而且所有的应用都 直接通过物理地址访问物理内存会带来以下问题 首先 内核提供给应用的内存访问接口不够透明也不好用。由于应用直接访问物理内存这需要它在构建的时候就需要规划自己需要被加载到哪个地址运行。为了避免冲突可能还需要应用的开发者们对此进行协商这显然是一件在今天看来不可理喻且极端麻烦的事情。其次 内核并没有对应用的访存行为进行任何保护措施每个应用都有整块物理内存的读写权力。即使应用被限制在 U 特权级下运行它还是能够造成很多麻烦比如它可以读写其他应用的数据来窃取信息或者破坏它的正常运行甚至它还可以修改内核的代码段来替换掉原本的 trap_handler 来挟持内核执行恶意代码。总之这造成系统既不安全、也不稳定。再次目前应用的内存使用空间在其运行前已经限定死了 内核不能灵活地给应用程序提供的运行时动态可用内存空间 。比如一个应用结束后这个应用所占的空间就被释放了但这块空间无法动态地给其它还在运行的应用使用。 因此为了防止应用胡作非为本章将更好的管理物理内存并提供给应用一个抽象出来的更加透明易用、也更加安全的访存接口这就是基于分页机制的虚拟内存。站在应用程序运行的角度看就是存在一个从“0”地址开始的非常大的可读/可写/可执行的地址空间Address Space。实现地址空间的第一步就是实现分页机制建立好虚拟内存和物理内存的页映射关系。此过程涉及硬件细节不同的地址映射关系组合相对比较复杂。总体而言我们需要思考如下问题 硬件中物理内存的范围是什么哪些物理内存空间需要建立页映射关系如何建立页表使能分页机制如何确保OS能够在分页机制使能前后的不同时间段中都能正常寻址和执行代码页目录表一级的起始地址设置在哪里二级/三级等页表的起始地址设置在哪里需要多大空间如何设置页目录表项的内容如何设置其它页表项的内容如果要让每个任务有自己的地址空间那每个任务是否要有自己的页表代表应用程序的任务和操作系统需要有各自的页表吗在有了页表之后任务和操作系统之间应该如何传递数据 如果能解决上述问题我们就能更好地理解地址空间虚拟地址等操作系统的抽象概念与操作系统的虚存具体实现之间的联系。 本章任务 本章展现了操作系统一系列功能 通过 动态内存分配提高了应用程序对内存的动态使用效率。通过页表的 虚实内存映射机制简化了编译器对应用的地址空间设置。通过页表的虚实内存映射机制加强了应用之间应用与内核之间的内存隔离增强了系统安全。 【 1. C 中的动态内存分配 】 到目前为止如果将我们的内核也看成一个应用那么其中所有的变量都是被静态分配在内存中的这样在对空闲内存的使用方面缺少灵活性。我们希望能在操作系统中提供 动态申请和释放内存 的能力这样就可以 加强操作系统对各种以内存为基础的资源分配与管理。在应用程序的视角中 动态内存分配中的内存其实就是操作系统管理的 “堆 Heap”。但现在要实现操作系统那么就需要操作系统自身能提供动态内存分配的能力。如果要实现动态内存分配的能力需要操作系统需要有如下功能 初始时能提供一块大内存空间作为初始的“堆”。在 没有分页机制情况下这块空间是物理内存空间否则就是虚拟内存空间。提供在堆上 分配一块内存的函数接口。这样函数调用方就能够得到一块地址连续的空闲内存块进行读写。提供 释放内存的函数接口。能够回收内存以备后续的内存分配请求。提供 空闲空间管理的连续内存分配算法。能够有效地管理空闲快这样就能够动态地维护一系列空闲和已分配的内存块。可选提供 建立在堆上的数据结构和操作。有了上述基本的内存分配与释放函数接口就可以实现类似动态数组动态字典等空间灵活可变的堆数据结构提高编程的灵活性。 1.1 C语言的内存分配 在使用C语言的过程中大家其实对new/delete的使用方法已经烂熟于心了。在C中对动态内存的申请是采用如下的函数实现的 void* malloc (size_t size); void free (void* ptr);其中malloc函数 的作用是 从堆中分配一块大小为 size 字节的空间并返回一个指向它的指针。而后续不用的时候将这个 指针传给 free 即可在堆中回收这块空间。我们通过返回的指针变量来间接访问堆上的空间而无法直接进行 访问。事实上我们在程序中能够 直接 看到的变量都是被静态分配在栈或者全局数据段上的它们大小在编译期已知比如这里 一个指针类型的大小就可以等于计算机可寻址空间的位宽。这样的它们却可以作为背后一块大小在编译期无法确定的空间的代表这是一件非常有趣的 事情。 1.2 kalloc 中的动态内存分配 同一个页的地址而言它对应的物理内存是连续的。但是 连续的虚拟地址空间不一定对应着连续的物理地址空间因此我们需要一个数据结构来存储哪些物理内存是可用的。对于这种给不连续的情况我们采用链表的数据结构将空闲的每个PAGE大小的物理内存空间作为listnode来进行内存的管理。这些新增的代码在kalloc.c之中。我们采用链表结构记录空闲的物理地址。因此当应用程序申请一段动态内存的时候只需要把链表头所指向地址拿出即可。 // os/kalloc.c struct linklist {struct linklist *next; };struct {struct linklist *freelist; } kmem;注意我们的管理仅仅在页这个粒度进行所以所有的地址必须是 PAGE_SIZE 对齐的。 // os/kalloc.c: 页面分配 void * kalloc(void) {struct linklist *l;l kmem.freelist;kmem.freelist l-next;return (void*)l; }// os/kalloc.c: 页面释放 void * kfree(void *pa) {struct linklist *l;l (struct linklist*)pa;l-next kmem.freelist;kmem.freelist l; }那么我们的内核有那些空闲内存需要管理呢 事实上qemu 已经规定了内核需要管理的内存范围可以参考这里具体来说需要软件管理的内存为 [0x80000000, 0x88000000)其中rustsbi 使用了 [0x80000000, 0x80200000) 的范围其余都是内核使用。来看看 kmem 的初始化 我们在main函数中会执行kinit它会初始化从ekernel到PHYSTOP的所有物理地址作为空闲的物理地址。freerange中调用的kfree函数以页为单位向对应内存中填入垃圾数据全1并把初始化好的一个页作为新的空闲listnode插入到链表首部。 // os/kalloc.c// ekernel 为链接脚本定义的内核代码结束地址PHYSTOP 0x88000000 void kinit() {freerange(ekernel, (void*)PHYSTOP); }// kfree [pa_start, pa_end) void freerange(void *pa_start, void *pa_end) {char *p;p (char*)PGROUNDUP((uint64)pa_start);for(; p PGSIZE (char*)pa_end; p PGSIZE)kfree(p); }注意C语言之中要求进行内存回收也就是malloc以及free要成对出现。但是我们的OS中不强制要求这一点也就是如果测例本身未在申请动态内存后显式地调用free来释放内存OS无需帮助它释放内存。 【 2. 地址空间 】 直到现在我们的操作系统给应用看到的是一个非常原始的物理内存空间可以简单地理解为一个可以随便访问的大数组。为了限制应用访问内存空间的范围并给操作系统提供内存管理的灵活性计算机硬件引入了各种 内存保护/映射硬件机制如 RISC-V的基址-边界翻译和保护机制、x86的分段机制、RISC-V/x86/ARM都有的 分页机制它们的共同之处在于 CPU访问的数据和指令内存地址是虚地址需要进行转换形成合法的物理地址或产生非法的异常 。为了用好这种硬件机制操作系统需要升级自己的能力。操作系统为了更好地管理这两种形式的内存并给应用程序提供统一的访问接口即应用程序不需要了解虚拟内存和物理内存的区别的操作系统提出了 地址空间 Address Space 抽象并在内核中建立虚实地址空间的映射机制给应用程序提供一个虚拟的内存环境。本节将结合操作系统的发展历程回顾来介绍 地址空间 Address Space 抽象的实现策略 是如何变化的。 2.1 虚拟地址和地址空间 2.1.1 地址虚拟化出现之前 我们之前介绍过在最早整套硬件资源只用来执行单个裸机应用的时候并不存在真正意义上的操作系统而只能算是一种应用 函数库。那个时候物理内存的一部分用来保存函数库的代码和数据余下的部分都交给应用来使用。从功能上可以将应用 占据的内存分成几个段代码段、全局数据段、堆和栈等。当然由于就只有这一个应用它想如何调整布局都是它自己的 事情。从内存使用的角度来看批处理系统和裸机应用很相似批处理系统的每个应用也都是独占内核之外的全部内存空间 只不过当一个应用出错或退出之后它所占据的内存区域会被清空而序列中的下一个应用将自己的代码和数据放置进来。 这个时期内核提供给应用的访存视角是一致的因为它们确实会在运行过程中始终独占一块固定的内存区域每个应用开发者 都基于这一认知来规划程序的内存布局。后来为了降低等待 I/O 带来的无意义的 CPU 资源损耗多道程序出现了。而为了提升用户的交互式体验提高生产力分时 多任务系统诞生了。它们的特点在于应用开始多出了一种“暂停”状态这可能来源于它主动 yield 交出 CPU 资源或是在 执行了足够长时间之后被内核强制性换出。当应用处于暂停状态的时候它驻留在内存中的代码、数据该何去何从呢 曾经有一种 做法是每个应用仍然和在批处理系统中一样独占内核之外的整块内存当暂停的时候内核负责 将它的代码、数据保存在磁盘或 硬盘中然后把即将换入的应用保存在磁盘上的代码、数据恢复到内存这些都做完之后才能开始执行新的应用。不过由于这种做法需要大量读写内存和外部存储设备而它们的 速度都比 CPU 慢上几个数量级这导致任务切换的开销过大 甚至完全不能接受。既然如此就只能像我们在第三章中的做法一样限制每个应用的最大可用内存空间小于物理内存的容量这样 就可以同时把多个应用的数据驻留在内存中。在任务切换的时候只需完成任务上下文保存与恢复即可这只是在内存的帮助下保存、 恢复少量通用寄存器甚至无需访问外存这从很大程度上降低了任务切换的开销。在本章的引言中介绍过第三章中操作系统的做法对应用程序开发带了一定的困难。从应用开发的角度看需要应用程序决定自己会被加载到哪个物理地址运行需要直接访问真实的 物理内存。这就要求应用开发者对于硬件的特性和使用方法有更多了解产生额外的学习成本也会为应用的开发和调试带来不便。从 内核的角度来看将直接访问物理内存的权力下放到应用会使得它 难以对应用程序的访存行为进行有效管理已有的特权级机制亦无法 阻止很多来自应用程序的恶意行为。 2.1.2 加一层抽象加强内存管理 为了解决这种困境抽象仍然是最重要的指导思想。在这里抽象意味着内核要负责将物理内存管理起来并为上面的应用提供 一层抽象接口从之前的失败经验学习这层抽象需要达成下面的设计目标 透明 应用开发者可以不必了解底层真实物理内存的硬件细节且在非必要时也不必关心内核的实现策略 最小化他们的心智负担高效 这层抽象至少在大多数情况下不应带来过大的额外开销安全 这层抽象应该有效检测并阻止应用读写其他应用或内核的代码、数据等一系列恶意行为。 最终到目前为止仍被操作系统内核广泛使用的抽象被称为 地址空间 (Address Space) 。某种程度上讲可以将它看成 一块巨大但并不一定真实存在的内存。在每个应用程序的视角里操作系统分配给应用程序一个范围有限但其实很大独占的连续地址空间其中有些地方被操作系统限制不能访问如内核本身占用的虚地址空间等因此应用程序可以在划分给它的地址空间中随意规划内存布局它的 各个段也就可以分别放置在地址空间中它希望的位置当然是操作系统允许应用访问的地址。 应用同样可以使用一个地址作为索引来读写自己地址空间的数据就像用物理地址作为索引来读写物理内存上的数据一样这种地址被称为 虚拟地址 (Virtual Address) 。当然操作系统要达到 地址空间 抽象的设计目标需要有计算机硬件的支持这就是计算机组成原理课上讲到的 MMU 和 TLB 等硬件机制。 从此 应用能够直接看到并访问的内存就只有操作系统提供的地址空间且它的任何一次访存使用的地址都是虚拟地址无论取指令来执行还是读写 栈、堆或是全局数据段都是如此。事实上特权级机制被拓展使得应用不再具有通过物理地址直接访问物理内存的能力。应用所处的执行环境在安全方面被进一步强化形成了用户态特权级和地址空间的二维安全措施。 由于每个应用独占一个地址空间里面只含有自己的各个段于是它可以随意规划 各个段的分布而无需考虑和其他应用冲突同时它完全无法窃取或者破坏其他应用的数据毕竟那些段在其他应用的地址空间 内鉴于 应用只能通过虚拟地址读写它自己的地址空间 这是它没有能力去访问的。这是 地址空间 抽象对应用程序执行的安全性和稳定性的一种保障。 我们知道应用的数据终归还是存在物理内存中的那么虚拟地址如何形成地址空间虚拟地址空间如何转换为物理内存呢操作系统可以设计巧妙的数据结构来表示地址空间。但如果完全由操作系统来完成转换每次处理器地址访问所需的虚实地址转换那开销就太大了。这就需要 扩展硬件功能来加速地址转换过程回忆 计算机组成原理 课上讲的 MMU 和 TLB 。 2.1.3 增加硬件加速虚实地址转换 开始回顾一下 计算机组成原理 课。如上图所示当应用取指或者执行 一条访存指令的时候它都是在以虚拟地址为索引读写自己的地址空间。此时 CPU 中的 内存管理单元 (MMU, Memory Management Unit) 自动将这个虚拟地址进行 地址转换 (Address Translation) 变为一个物理地址 也就是物理内存上这个应用的数据真实被存放的位置。也就是说在 MMU 的帮助下应用对自己地址空间的读写才能被实际转化为 对于物理内存的访问。事实上 每个应用的地址空间都可以看成一个从虚拟地址到物理地址的映射。可以想象对于不同的应用来说该映射可能是不同的 即 MMU 可能会将来自不同两个应用地址空间的相同虚拟地址翻译成不同的物理地址。要做到这一点就需要 硬件提供一些寄存器 软件可以对它进行设置来控制 MMU 按照哪个应用的地址空间进行地址转换。于是将应用的数据放到物理内存并进行管理而 在任务切换的时候需要将控制 MMU 选用哪个应用的地址空间进行映射的那些寄存器也一并进行切换则是作为软件部分的内核需 要完成的工作。回过头来在介绍内核对于 CPU 资源的抽象——时分复用的时候我们曾经提到它为应用制造了一种每个应用独占整个 CPU 的 幻象而隐藏了多个应用分时共享 CPU 的实质。而地址空间也是如此 应用也只能看到它独占整个地址空间的幻象而 藏在背后的实质仍然是多个应用共享物理内存它们的数据分别存放在内存的不同位置。地址空间只是一层抽象接口它有很多种具体的实现策略。对于不同的实现策略来说操作系统内核如何规划应用数据放在物理内存的位置 而 MMU 又如何进行地址转换也都是不同的。下面我们简要介绍几种曾经被使用的策略并探讨它们的优劣。 2.2 分段内存管理 2.2.1 等量分配 曾经的一种做法如上图所示每个应用的地址空间大小限制为一个固定的常数 bound 也即 每个应用的可用虚拟地址区间 均为 [0,bound) 。随后以这个大小为单位将物理内存除了内核预留空间之外的部分划分为若干 个大小相同的 插槽 (Slot) 每个应用的所有数据都被内核放置在其中一个插槽中对应于物理内存上的一段连续物理地址 区间假设其起始物理地址为 base 则由于二者大小相同这个区间实际为 [base,basebound) 。因此地址转换很容易完成只需检查一下虚拟地址不超过地址空间 的大小限制此时需要借助特权级机制通过异常来进行处理然后做一个线性映射将虚拟地址加上 base 就得到了数据实际所在的物理地址。可以看出这种实现极其简单MMU 只需要 base,bound 两个寄存器在地址转换进行比较或加法运算即可而内核只需要在任务切换的同时切换 base 寄存器由于 bound是一个常数内存 管理方面它只需考虑一组插槽的占用状态可以用一个 位图 (Bitmap) 来表示随着应用的新增和退出对应置位或清空。然而它的问题在于浪费的内存资源过多。注意到应用地址空间预留了一部分它是用来让栈得以向低地址增长同时允许堆 往高地址增长支持应用运行时进行动态内存分配。每个应用的情况都不同内核只能按照在它能力范围之内的消耗内存最多 的应用的情况来统一指定地址空间的大小而其他内存需求较低的应用根本无法充分利用内核给他们分配的这部分空间但这部分空间又是一个完整的插槽的一部分也不能再交给其他应用使用这种在地址空间内部无法被充分利用的空间被称为 内碎片 (Internal Fragment) 它限制了系统同时共存的应用数目。如果应用的需求足够多样化那么内核无论如何设置 应用地址空间的大小限制也不能得到满意的结果。这就是固定参数的弊端虽然实现简单但不够灵活。 2.2.2 按需分配 为了解决这个问题一种分段管理的策略开始被使用如下图所示 注意到 内核开始以更细的粒度也就是应用地址空间中的一个逻辑段作为单位来安排应用的数据在物理内存中的布局。对于每个段来说从它在某个应用地址空间中的虚拟地址到它被实际存放在内存中的物理地址中间都要经过一个不同的线性映射于是 MMU 需要用一对不同的 base/bound 进行区分。这里由于每个段的大小都是不同的我们也不再能仅仅 使用一个 bound 进行简化。当任务切换的时候这些对寄存器也需要被切换。简单起见我们这里忽略一些不必要的细节。比如应用在以虚拟地址为索引访问地址空间的时候它如何知道该地址属于哪个段从而硬件可以使用正确的一对 base/bound 寄存器进行合法性检查和完成实际的地址转换。这里只关注 分段管理是否解决了内碎片带来的内存浪费问题。注意到每个段都只会在内存中占据一块与它实际所用到的大小相等的空间。堆 的情况可能比较特殊它的大小可能会在运行时增长但是那需要应用通过系统调用向内核请求。也就是说这是一种 按需分配而 不再是内核在开始时就给每个应用分配一大块很可能用不完的内存。由此不再有内碎片了。尽管内碎片被消除了但内存浪费问题并没有完全解决。这是因为每个段的大小都是不同的它们可能来自不同的应用功能 也不同内核就需要使用更加通用、也更加复杂的连续内存分配算法来进行内存管理而不能像之前的插槽那样以一个比特 为单位。顾名思义连续内存分配算法就是每次需要分配一块连续内存来存放一个段的数据。 随着一段时间的分配和回收物理内存还剩下一些相互不连续的较小的可用连续块其中有一些只是两个已分配内存块之间的很小的间隙它们自己可能由于空间较小已经无法被 用于分配被称为 外碎片 (External Fragment) 。 举个例子在内存上分配三个操作系统分配的用于装载进程的内存区域A、B和C。假设三个内存区域都是相连的故而三个内存区域不会产生外部碎片。现在假设B对应的进程执行完毕了操作系统随即收回了B这个时候A和C中间就有一块空闲区域了。 如果这时再想分配一个比较大的块 就需要将这些不连续的外碎片“拼起来”形成一个大的连续块。然而这是一件开销很大的事情涉及到极大的内存读写开销。具体而言这需要移动和调整一些已分配内存块在物理内存上的位置才能让那些小的外碎片能够合在一起形成一个大的空闲块。如果连续内存分配算法 选取得当可以尽可能减少这种操作。课上所讲到的那些算法包括 first-fit/worst-fit/best-fit 或是 buddy system其具体表现取决于实际的应用需求各有优劣。那么分段内存管理带来的外碎片和连续内存分配算法比较复杂的 问题可否被解决呢 2.3 分页内存管理 问题背景内碎片和外碎片 仔细分析一下可以发现段的大小不一是外碎片产生的根本原因。之前我们把应用的整个地址空间连续放置在物理内存中在 每个应用的地址空间大小均相同的情况下只需利用类似位图的数据结构维护一组插槽的占用状态从逻辑上 分配和回收都是以一个固定的比特为单位自然也就不会存在外碎片了。但是这样粒度过大不够灵活又在地址空间内部产生了内碎片。解决方法分页内存管理 若要结合二者的优点的话就需要 内核始终以一个同样大小的单位来在物理内存上放置应用地址空间中的数据这样内核就可以 使用简单的插槽式内存管理使得内存分配算法比较简单且不会产生外碎片同时这个 单位的大小要足够小从而其内部没有 被用到的内碎片的大小也足够小尽可能提高内存利用率这便是我们将要介绍的 分页内存管理。 如上图所示 内核以页为单位进行物理内存管理每个应用的地址空间可以被分成若干个虚拟页面 (Page) 可用的物理内存也同样可以被分成若干个物理页帧 (Frame) 。 虚拟页面和物理页帧的大小相同每个虚拟页面中的数据实际上都存储在某个物理页帧上。分段内存管理和分页内存管理的区别 相比分段内存管理分页内存管理的粒度更小应用地址空间中的每个逻辑段都由多个虚拟页面组成而 每个虚拟页面在地址转换的过程中都使用一个不同的线性映射而不是在分段内存管理中每个逻辑段都使用一个相同的线性映射。页号、页表 为了方便实现虚拟页面到物理页帧的地址转换我们给每个虚拟页面和物理页帧一个编号分别称为 虚拟页号 (VPN, Virtual Page Number) 和 物理页号 (PPN, Physical Page Number) 。 每个应用都有一个不同的 页表 (Page Table) 里面记录了该应用地址空间中的每个虚拟页面映射到物理内存中的哪个物理页帧即数据实际 被内核放在哪里。我们可以用页号来代表二者因此如果 将页表看成一个键值对其键的类型为虚拟页号值的类型则为物理 页号。流程当 MMU 进行地址转换的时候 MMU首先找到给定的虚拟地址所在的虚拟页面的页号然后查当前应用的页表根据虚拟页号找到物理页号最后按照虚拟地址在它所在的虚拟页面中的相对位置相应给物理页号对应的物理页帧的起始地址加上一个偏移量这就得到了实际访问的物理地址。 物理地址的权限 在页表中通过虚拟页号不仅能查到物理页号还能得到一组 rwx 保护位它限制了应用对转换得到的物理地址对应的内存的使用方式。 最典型的如 rwx r 表示当前应用可以读该内存 w 表示当前应用可以写该内存 x 则表示当前应用 可以从该内存取指令用来执行。一旦违反了这种限制则会触发异常被内核捕获到。通过适当的设置可以检查一些应用明显的 错误比如应用修改自己本应该只读的代码段或者从数据段取指令来执行。当一个应用的地址空间比较大的时候页表里面的项数会很多事实上每个虚拟页面都应该对应页表中的一项上图中我们已经 省略掉了那些未被使用的虚拟页面导致它的容量极速膨胀已经不再是像之前那样数个寄存器便可存下来的了CPU 内也没有 足够的硬件资源能够将它存下来。因此它 只能作为一种被内核管理的数据结构放在内存中但是 CPU 也会直接访问它来查页表 这也就需要内核和硬件之间关于页表的内存布局达成一致。由于分页内存管理既简单又灵活它逐渐成为了主流RISC-V 架构也使用了这种策略。后面我们会基于这种机制自己来动手从物理内存抽象出应用的地址空间来。 2.4 C的内存布局 在memory_layout.h之中我们展示了内存的布局。 其中前两项在上一节已经介绍过了。下面的MAXVA其实并不是SV39中最大的虚拟地址(39位全为1)。我们设置它为(138)是因为VA的高位填充是根据39位来进行的。为了方便全部填充0就不考虑大于(138)的虚拟地址。 在我们的框架之中va不可能大于MAXVA。大家可以看到我们指定TRAMPOLINE和TRAPFRAME在va的最高位这是为什么呢大家可以自行思考一下我们将在下面解释。 // the kernel expects there to be RAM // for use by the kernel and user pages // from physical address 0x80000000 to PHYSTOP. #define KERNBASE 0x80200000L #define PHYSTOP (0x80000000 128*1024*1024) // 128M// map the trampoline page to the highest address, // in both user and kernel space.// one beyond the highest possible virtual address. // MAXVA is actually one bit less than the max allowed by // Sv39, to avoid having to sign-extend virtual addresses // that have the high bit set. #define MAXVA (1L (9 9 9 12 - 1))#define USER_TOP (MAXVA) #define TRAMPOLINE (USER_TOP - PGSIZE) #define TRAPFRAME (TRAMPOLINE - PGSIZE)#define USTACK_BOTTOM (0x0)【 3. SV39多级页表机制内容介绍 】 在上一小节中我们已经简单介绍了分页的内存管理策略现在我们尝试在 RV64 架构提供的 SV39 分页机制的基础上完成内核中的软件对应实现。由于内容过多我们将分成两个小节进行讲解。本节主要讲解在RV64架构下的虚拟地址与物理地址的访问属性可读可写可执行等组成结构页号帧号偏移量等访问的空间范围等以及我们在OS中如何进行页表的处理。 3.1 内存控制相关的CSR寄存器 默认情况下 MMU 未被使能此时无论 CPU 位于哪个特权级访存的地址都会作为一个物理地址交给对应的内存控制单元来直接 访问物理内存。 我们可以 通过修改 S 特权级的一个名为 satp 的 CSR 来启用分页模式在这之后 S 和 U 特权级的访存 地址会被视为一个虚拟地址它需要经过 MMU 的地址转换变为一个物理地址再通过它来访问物理内存而 M 特权级的访存地址我们可设定是内存的物理地址。注解M 特权级的访存地址被视为一个物理地址还是一个需要经历和 S/U 特权级相同的地址转换的虚拟地址取决于硬件配置在这里我们不会进一步探讨。 上图是 RV64 架构下 satp 的字段分布。当 MODE 设置为 0 的时候代表所有访存都被视为物理地址而设置为 8 的时候SV39 分页机制被启用 所有 S/U 特权级的访存被视为一个 39 位的虚拟地址它们需要先经过 MMU 的地址转换流程 如果顺利的话则会 变成一个 56 位的物理地址来访问物理内存否则则会触发异常这体现了该机制的内存保护能力。虚拟地址和物理地址都是字节地址39 位的虚拟地址可以用来访问理论上最大 512GiB 2 39 2 9 ⋅ 2 30 512 G 2^{39}2^9\cdot 2^{30}512G 23929⋅230512G 的地址空间 而 56 位的物理地址在理论上甚至可以访问一块大小比这个地址空间的还高出几个数量级的物理内存。但是实际上 无论是 虚拟地址还是物理地址真正有意义、能够通过 MMU 的地址转换或是 CPU 内存控制单元的检查的地址仅占其中的很小 一部分 因此它们的理论容量上限在目前都没有实际意义。 3.2 地址格式与组成 我们采用分页管理单个页面的大小设置为 4KiB 即 2 12 2^{12} 212 个字节每个虚拟页面和物理页帧都对齐到这个页面大小也就是说 虚拟/物理地址区间 [0,4KiB) 为第0个虚拟页面/物理页帧而 [4KiB,8KiB) 为第1个以此类推。 4KiB 需要用 12 位字节地址来表示因此虚拟地址和物理地址都被分成两部分 它们的低 12 位即 [11:0] 被称为 页内偏移 (Page Offset) 。 页内偏移描述一个地址指向的字节在它所在页面中的相对位置。虚拟地址的高 27 位即 [38:12] 为它的 虚拟页号 VPN 物理地址的高 44 位即 [55:12] 为它的 物理页号 PPN。 页号可以用来定位一个虚拟/物理地址 属于哪一个虚拟页面/物理页帧。 地址转换流程 地址转换是以页为单位进行的在地址转换的前后地址的页内偏移部分不变。可以认为 MMU 只是从虚拟地址中取出 27 位虚拟页号 在页表中查到其对应的物理页号如果存在的话最后将得到的44位的物理页号与虚拟地址的12位页内偏移依序拼接到一起就变成了56位的物理地址。 注解RV64 架构中虚拟地址为何只有 39 位 在 64 位架构上虚拟地址长度确实应该和位宽一致为 64 位但是在启用 SV39 分页模式下只有低 39 位是真正有意义的。 SV39 分页模式规定 64 位虚拟地址的 [63:39] 这高 25 位必须和第 38 位相同否则 MMU 会直接认定它是一个 不合法的虚拟地址。通过这个检查之后 MMU 再取出低 39 位尝试将其转化为一个 56 位的物理地址。也就是说所有 2 64 2^{64} 264 个虚拟地址中只有最低的 256GiB 当第 38 位为0时以及最高的 256GiB 当第 38 位为 1 时是可能通过 MMU 检查的。当我们写软件代码的时候一个 地址的位宽毋庸置疑就是 64 位我们要清楚可用的只有最高和最低这两部分尽管它们已经巨大的超乎想象了而本节中 我们专注于介绍 MMU 的机制强调MMU 看到的真正用来地址转换的虚拟地址只有 39 位。 3.3 多级页表原理 3.3.1 线性表实现 页表的一种最简单的实现是线性表也就是按照地址从低到高、输入的虚拟页号从 0 开始递增的顺序依次在内存中放置每个虚拟页号对应的页表项 我们之前提到过页表的容量过大无法保存在 CPU 中。由于 每个页表项的大小是 8 字节我们只要知道第一个页表项对应虚拟页号 0 被放在的物理地址 base_addr 就能 直接计算出每个输入的虚拟页号对应的页表项所在的位置。如下图所示 事实上对于虚拟页号 i如果页表每个应用都有一个页表这里指其中某一个的起始地址为 base_addr则这个虚拟页号对应的页表项可以在物理地址 base_addr8i 处找到 这使得 MMU 的实现和内核的软件控制都变得非常简单。然而遗憾的是这远远超出了我们的物理内存限制由于虚拟页号有 2 27 2^{27} 227 种每个虚拟页号对应一个 8 字节的页表项则每个页表都需要消耗掉 1GiB 2 27 ⋅ 2 3 2 30 1 G 2^{27}\cdot 2^32^{30}1G 227⋅232301G 内存应用的数据还需要保存在内存的其他位置这就使得 每个应用要吃掉 1GiB 以上的内存。作为对比K210 开发板目前只有 8MiB 的内存因此从空间占用角度来说这种线性表实现是完全不可行的。线性表的问题在于线性表保存了所有虚拟页号对应的页表项但是高达 512GiB 的地址空间中真正会被应用 使用到的只是其中极小的一个子集本教程中的应用内存使用量约在数十~数百 KiB 量级也就导致有意义并能在页表中查到实际的物理页号的虚拟页号在 2 27 2^{27} 227 中也只是很小的一部分。由此线性表的绝大部分空间 其实都是被浪费掉的。 3.3.2 字典树实现 背景 那么如何进行优化呢核心思想就在于 按需分配 也就是说有多少合法的虚拟页号我们就维护一个多大的映射并为此使用多大的内存用来保存映射。这是因为每个应用的地址空间最开始都是空的或者说所有的虚拟页号均不合法那么这样的页表自然不需要占用任何内存 MMU 在地址转换的时候无需关心页表的内容而是将所有的虚拟页号均判为不合法即可。而在后面内核已经决定好了一个应用的各逻辑段存放位置之后 内核负责从零开始以虚拟页面为单位来让该应用的地址空间的某些部分变得合法反映在该应用的页表上也就是一对对映射顺次被插入进来自然页表所占据的内存大小也就逐渐增加。这种思想在计算机科学中得到了广泛应用为了方便接下来的说明我们可以举一道数据结构的题目作为例子 设想我们要维护 一个字符串的多重集集合中所有的字符串的字符集均为 α { a , b , c } \alpha\{a,b,c\} α{a,b,c}长度均为一个给定的常数 n该字符串集合一开始为空集。我们要支持两种操作第一种是将一个字符串插入集合第二种是查询一个字符串在当前 的集合中出现了多少次。简单起见假设 n 3 n3 n3 。那么我们可能会建立这样一棵 字典树 (Trie) 字典树由若干个节点图中用椭圆形来表示组成从逻辑上而言每个节点代表一个可能的字符串前缀。每个节点的存储内容 都只有三个指针对于蓝色的非叶节点来说它的三个指针各自指向一个子节点而对于绿色的叶子节点来说它的三个指针不再指向 任何节点而是具体保存一种可能的长度为 n 的字符串的计数。这样对于题目要求的两种操作我们只需根据输入的字符串中的每个字符在字典树上自上而下对应走出一步最终就能够找到字典树中维护的它的计数。之后我们可以将其直接返回或者加一。 注意到如果某些字符串自始至终没有被插入那么一些节点没有存在的必要。反过来说一些节点是由于我们插入了一个以它对应的字符串为前缀的字符串才被分配出来的。如下图所示 一开始仅存在一个根节点 root。在我们插入字符串 acb 的过程中我们只需要分配 a 和 ac 两个节点。 注意 ac 是一个叶节点它的 b 指针不再指向另外一个节点而是保存字符串 acb 的计数。 此时我们无法访问到其他未分配的节点如根节点的 b/c 或是 a 节点的 a/b 均为空指针。 如果后续再插入一个字符串那么 至多分配两个新节点 因为如果走的路径上有节点已经存在就无需重复分配了。 这可以说明 字典树中节点的数目或者说字典树消耗的内存是随着插入字符串的数目逐渐线性增加的。 读者可能很好奇为何在这里要用相当一部分篇幅来介绍字典树呢事实上 SV39 分页机制等价于一颗字典树。 27 位的虚拟页号可以看成一个长度 n 3 n3 n3 的字符串字符集为 α { 0 , 1 , 2 , . . . , 511 } \alpha \{0,1,2,...,511\} α{0,1,2,...,511} 因为每一位字符都由 9 个比特 2 9 512 2^9512 29512组成。而我们也不再维护所谓字符串的计数而是要找到字符串虚拟页号对应的页表项。 因此每个叶节点都需要保存 2 9 512 2^9512 29512 个 8 字节的页表项一共正好 2 9 ⋅ 2 3 2 11 4 K i B 2^9\cdot 2^32^{11}4KiB 29⋅232114KiB 可以直接放在一个物理页帧内。而对于非叶节点来说从功能上它只需要保存 512 个指向下级节点的指针即可不过我们就像叶节点那样也保存 512 个页表项这样所有的节点都可以被放在一个物理页帧内它们的位置可以用一个 物理页号来代替。当想从一个非叶节点向下走时只需找到当前字符对应的页表项的物理页号字段它就指向了下一级节点的位置 这样非叶节点中转的功能也就实现了。每个节点的内部是一个线性表也就是将这个节点起始物理地址加上字符对应的偏移量就找到了 指向下一级节点的页表项对于非叶节点或是能够直接用来地址转换的页表项对于叶节点。这种页表实现被称为 多级页表 (Multi-Level Page-Table) 。由于 SV39 中虚拟页号被分为三级页索引 (Page Index) 因此这是一种三级页表。 非叶节点的页表项标志位含义和叶节点相比有一些不同 当 V 为 0 的时候代表当前指针是一个空指针无法走向下一级节点即该页表项对应的虚拟地址范围是无效的只有当V 为1 且 R/W/X 均为 0 时表示是一个合法的页目录表项其包含的指针会指向下一级的页表。注意 当V 为1 且 R/W/X 不全为 0 时表示是一个合法的页表项其包含了虚地址对应的物理页号。在这里我们给出 SV39 中的 R/W/X 组合的含义 拓展大页 (Huge Page) 本教程中并没有用到大页的知识这里只是作为拓展不感兴趣的读者可以跳过。 事实上正确的说法应该是只要 R/W/X 不全为 0 就会停下来直接从当前的页表项中取出物理页号进行最终的地址转换。 如果这一过程并没有发生在多级页表的最深层那么在地址转换的时候并不是直接将物理页号和虚拟地址中的页内偏移接 在一起得到物理地址这样做会有问题由于有若干级页索引并没有被使用到即使两个虚拟地址的这些级页索引不同 还是会最终得到一个相同的物理地址导致冲突。我们需要重新理解将物理页号和页内偏移“接起来”这一行为它的本质是将物理页号对应的物理页帧的起始物理地址和 页内偏移进行求和前者是将物理页号左移上页内偏移的位数得到因此看上去恰好就是将物理页号和页内偏移接在一起。 但是如果在从多级页表往下走的中途停止未用到的页索引会和虚拟地址的 12 位页内偏移一起形成一个 位数更多的页内偏移也就对应于一个大页在转换物理地址的时候其算法仍是上述二者求和但那时便不再是简单的 拼接操作。在 SV39 中如果使用了一级页索引就停下来则它可以涵盖虚拟页号的前 9 位为某一固定值的所有虚拟地址 对应于一个 1GiB 的大页如果使用了二级页索引就停下来则它可以涵盖虚拟页号的前 18 位为某一固定值的所有虚拟地址对应于一个 2MiB 的大页。以同样的视角如果使用了 所有三级页索引才停下来它可以涵盖虚拟页号为某一个固定值的所有虚拟地址自然也就对应于一个大小为 4KiB 的虚拟页面。使用大页的优点在于当地址空间的大块连续区域的访问权限均相同的时候可以直接映射一个大页从时间上避免了大量 页表项的索引和修改从空间上降低了所需节点的数目。但是从内存分配算法的角度这需要内核支持从物理内存上分配 三种不同大小的连续区域 4KiB 或是另外两种大页便不能使用更为简单的插槽式管理。权衡利弊 之后本书全程只会以 4KiB 为单位进行页表映射而不会使用大页特性。 那么 SV39 多级页表相比线性表到底能节省多少内存呢这里直接给出结论设某个应用地址空间实际用到的区域总大小为 S 字节则地址空间对应的多级页表消耗内存为 S 512 \frac{S}{512} 512S​ 左右。下面给出了详细分析对此 不感兴趣的读者可以直接跳过。 分析 SV39 多级页表的内存占用 我们知道多级页表的总内存消耗取决于节点的数目每个节点则需要一个大小为 4KiB 物理页帧存放。不妨设某个应用地址空间中的实际用到的总空间大小为 S 字节则多级页表所需的内存至少有这样两个上界 每映射一个 4KiB 的虚拟页面最多需要新分配两个物理页帧来保存新的节点加上初始就有一个根节点 因此消耗内存不超过 4 K i B × ( 1 2 S 4 K i B 4 K i B 2 S ) 4KiB\times (12\frac{S}{4KiB}4KiB2S) 4KiB×(124KiBS​4KiB2S) 考虑已经映射了很多虚拟页面使得根节点的 512 个孩子节点都已经被分配的情况此时最坏的情况是每次映射 都需要分配一个不同的最深层节点加上根节点的所有孩子节点并不一定都被分配从这个角度来讲消耗内存不超过 4 K i B × ( 1 512 S 4 K i B 4 K i B 2 M i B S ) 4KiB\times (1512\frac{S}{4KiB}4KiB2MiBS) 4KiB×(15124KiBS​4KiB2MiBS)。 虽然这两个上限都可以通过刻意构造一种地址空间的使用来达到但是它们看起来很不合理因为它们均大于 S也就是 元数据比数据还大。其实真实环境中一般不会有如此极端的使用方式更加贴近 实际的是下面一种上限即除了根节点的一个物理页帧之外地址空间中的每个实际用到的大小为 T 字节的 连续 区间 会让多级页表额外消耗不超过 4 K i B × ( [ T 2 M i B ] [ T 1 G i B ] ) 4KiB\times([\frac{T}{2MiB}][\frac{T}{1GiB}]) 4KiB×([2MiBT​][1GiBT​]) 的内存。这是因为括号中的两项分别对应为了映射这段连续区间所需要新分配的最深层和次深层节点的数目前者每连续映射 2MiB 才会新分配一个而后者每连续映射 1GiB 才会新分配一个。由于后者远小于前者 可以将后者忽略最后得到的结果近似于 T 512 \frac{T}{512} 512T​。而一般情况下我们对于地址空间的使用方法都是在其中 放置少数几个连续的逻辑段因此当一个地址空间实际使用的区域大小总和为 S 字节的时候我们可以认为为此多级页表 消耗的内存在 S 512 \frac{S}{512} 512S​ 左右。相比线性表固定消耗 1GiB 的内存这已经相当可以 接受了。 上面主要是对一个固定应用的多级页表进行了介绍。在一个多任务系统中可能同时存在多个任务处于运行/就绪状态它们的多级页表在内存中共存那么 MMU 应该如何知道当前做地址转换的时候要查哪一个页表呢? 回到 satp CSR 的布局 CSR寄存器 的 PPN 字段指的就是多级页表根节点所在的物理页号。因此 每个应用的地址空间就可以用包含了它多级页表根节点所在物理页号 的 satp CSR 代表。在我们切换任务的时候 satp 也必须被同时切换。最后的最后我们给出 SV39 地址转换的全过程图示来结束多级页表原理的介绍 【 4. SV39多级页表机制OS 实现 】 本节我们将讲述OS是如何实现页表的支持的。在深入本章的内容之前大家一定要牢记完成虚拟地址查询页表或TLB转换成物理地址的过程是由硬件也就是CPU来完成的MMU。我们在框架之中实现的地址转换函数是为了我们在某些函数中自己计算虚拟地址到物理地址转换使用的。OS负责对页表进行建立、更改等处理真正在程序运行时CPU对指令、数据虚拟地址会十分机械地按照下面讲述的方法使用os创建好的页表进行地址转换。 4.1 地址相关的数据结构抽象 正如本章第一小节所说在分页内存管理中地址转换的核心任务在于如何维护虚拟页号到物理页号的映射——也就是页表。我们对页表的操作集中在vm.c文件之中。首先是为了实现页表我们新增的类型的定义 // os/types.htypedef uint64 pte_t; typedef uint64 pde_t; typedef uint64 *pagetable_t;// 512 PTEs第一小节中我们提到在页表中以虚拟页号作为索引不仅能够查到物理页号还能查到一组保护位它控制了应用对地址空间每个 虚拟页面的访问权限。但实际上还有更多的标志位物理页号和全部的标志位以某种固定的格式保存在一个结构体中它被称为 页表项 (PTE, Page Table Entry) 是利用虚拟页号在页表中查到的结果。 第一小节中我们提到在页表中以虚拟页号作为索引不仅能够查到物理页号还能查到一组保护位它控制了应用对地址空间每个 虚拟页面的访问权限。但实际上还有更多的标志位物理页号和全部的标志位以某种固定的格式保存在一个结构体中它被称为 页表项 (PTE, Page Table Entry) 是利用虚拟页号在页表中查到的结果。 上图为 SV39 分页模式下的页表项其中 这 位是物理页号最低的 位 则是标志位它们的含义如下请注意为方便说明下文我们用 页表项的对应虚拟页面 来表示索引到 一个页表项的虚拟页号对应的虚拟页面 仅当 V(Valid) 位为 1 时页表项才是合法的R/W/X 分别控制索引到这个页表项的对应虚拟页面是否允许读/写/取指U 控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问G 我们暂且不理会A(Accessed) 记录自从页表项上的这一位被清零之后页表项的对应虚拟页面是否被访问过D(Dirty) 则记录自从页表项上的这一位被清零之后页表项的对应虚拟页表是否被修改过。 由于pte只有54位每一个页表项我们用一个8字节的无符号整型来记录就已经足够。 4.1.1 页表实现va–pa的转换过程 下面我们通过来解读我们OS的walk函数了解SV39的页表读取机制。 :linenos:// os/vm.cpte_t * walk(pagetable_t pagetable, uint64 va, int alloc) {if (va MAXVA)panic(walk);for (int level 2; level 0; level--) {pte_t *pte pagetable[PX(level, va)];if (*pte PTE_V) {pagetable (pagetable_t) PTE2PA(*pte);} else {if (!alloc || (pagetable (pde_t *) kalloc()) 0)return 0;memset(pagetable, 0, PGSIZE);*pte PA2PTE(pagetable) | PTE_V;}}return pagetable[PX(0, va)]; }walk函数模拟了CPU进行MMU的过程。它的参数分别是页表待转换的虚拟地址va以及如果没有对应的物理地址时是否分配物理地址。 SV39的转换是由3级页表结构完成。在riscv.h之中定义的宏函数PX完成了每一级从va转换到pte的过程: :linenos:#define PXMASK 0x1FF// 9 bits #define PGSHIFT 12// bits of offset within a page #define PXSHIFT(level) (PGSHIFT (9 * (level))) #define PX(level, va) ((((uint64)(va)) PXSHIFT(level)) PXMASK)可以看到每一次我们只需要截取队va高27位中对应级别的9位即可。一开始截取最高9位接着是中间的9位和低9位。这9位我们如何使用呢SV39中要求我们把页表的44位和虚拟地址对应的9位*8直接拼接在一起做为pte的地址。页表的高44位也就是页号拼接上12位的0实际上就是pagetable指向的物理地址。我们可以计算得到一个4096大小的页表之中有4096/8512个页表项。因此我们得到的这9位实际上就是pte在这一页之中的偏移也就是其下标了。得到了页表项之后我们使用PTE2PA函数将该页表项的高44位也就是下一个页表的页号取出和12个0拼接通过左移和右移可以轻松实现就得到了下一级页表的起始物理地址了。接着重复这样的操作直到最后一个pte解析出来就可以返回最后一个pte了循环并没有处理最后一级。最后一个pte中记录了物理地址的物理页号PPN将它直接和虚拟地址的12位offset拼接就得到了对应的物理地址pa。整个过程中要注意随时通过PTE的标志位判断每一级的pte是否是有效的V位。如果无效则需要kalloc分配一个新的页表并初始化该pte在其中的位置。如果alloc参数0或者已经没有空闲的内存了这个情况在lab8之前不会遇到那么遇到中途V0的pte整个walk过程就会直接退出。当然这是OS的写法如果CPU在MMU的时候遇到这种情况就会直接报异常了。walk函数是我们比较底层的一个函数但也是所有遍历页表进行地址转换函数的基础。我们还实现了两个转换函数: :linenos:// Look up a virtual address, return the physical page, // or 0 if not mapped. // Can only be used to look up user pages. // Use walk uint64 walkaddr(pagetable_t pagetable, uint64 va);// Look up a virtual address, return the physical address. // Use walkaddr uint64 useraddr(pagetable_t pagetable, uint64 va);大家可以自行阅读。注意walkaddr函数没有考虑偏移量因此在使用的时候请首先考虑useraddr函数。 4.2 页表的建立过程 无论是CPU进行MMU还是我们自己walk实现va到pa的转换需要的页表都是需要OS来生成的。相关函数也是本章练习涉及到的主要函数。 // Create PTEs for virtual addresses starting at va that refer to // physical addresses starting at pa. va and size might not // be page-aligned. Returns 0 on success, -1 if walk() couldnt // allocate a needed page-table page. int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm);// Remove npages of mappings starting from va. va must be // page-aligned. The mappings must exist. // Optionally free the physical memory. void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free);以上是建立新映射和取消映射的函数mappages 在 pagetable 中建立 [va, va size) 到 [pa, pa size) 的映射页表属性为permuvmunmap 则取消一段映射do_free 控制是否 kfree 对应的物理内存比如这是一个共享内存那么第一次 unmap 就不 free最后一个 unmap 肯定要 free。mappages的perm是用于控制页表项的flags的。请注意它具体指向哪几位这将极大地影响页表的可用性。因为CPU进行MMU的时候一旦权限出错比如CPU在U态访问了flag之中U0的页表项是会直接报异常的。 4.3 启用页表后的跨页表操作 一旦启用了页表之后U态的测例程序就开始全部使用虚拟地址了。这就意味着它传给OS的指针参数也是虚拟地址我们无法直接去读虚拟地址而是要将它使用对应进程的页表转换成物理地址才能读取。为了方便大家我们预先准备了几个跨页表进行字符串数据交换的函数。 :linenos:// Copy from kernel to user. // Copy len bytes from src to virtual address dstva in a given page table. // Return 0 on success, -1 on error. int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len);// Copy from user to kernel. // Copy len bytes to dst from virtual address srcva in a given page table. // Return 0 on success, -1 on error. int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len);// Copy a null-terminated string from user to kernel. // Copy bytes to dst from virtual address srcva in a given page table, // until a \0, or max. // Return 0 on success, -1 on error. int copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max);用于与指定页表进行数据交换copyout 可以向页表中写东西后续用于 sys_read也就是给用户传数据copyin 用户接受用户的 buffer也就是从用户哪里读数据。 注意用户在启用了虚拟内存之后用户 syscall 给出的指针是不能直接用的因为与内核的映射不一样会读取错误的物理地址使用指针必须通过 useraddr 转化当然更加推荐的是 copyin/out 接口否则很可能损坏内存数据同时copyin/out 接口处理了虚存跨页的情况useraddr 则需要手动判断并处理。跨页会在测例文件bin比较大的时候出现。如果你的程序出现了完全De不出来的BUG可能就是跨页使用了错误的接口导致的。 4.4 内核页表 开启页表之后内核也需要进行映射处理。但是我们这里可以直接进行一一映射也就是va经过MMU转换得到的pa就是va本身但是转换过程还是会执行。内核需要能访问到所有的物理内存以处理频繁的操作不同进程内存的需求。内核页表建立过程在main函数之中调用。 :linenos:#define PTE_V (1L 0) // valid #define PTE_R (1L 1) #define PTE_W (1L 2) #define PTE_X (1L 3) #define PTE_U (1L 4) // 1 - user can access#define KERNBASE (0x80200000) extern char e_text[]; // kernel.ld sets this to end of kernel code. extern char trampoline[];pagetable_t kvmmake(void) {pagetable_t kpgtbl;kpgtbl (pagetable_t) kalloc();memset(kpgtbl, 0, PGSIZE);kvmmap(kpgtbl, KERNBASE, KERNBASE, (uint64) e_text - KERNBASE, PTE_R | PTE_X);kvmmap(kpgtbl, (uint64) e_text, (uint64) e_text, PHYSTOP - (uint64) e_text, PTE_R | PTE_W);kvmmap(kpgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);return kpgtbl; }void kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm) {if (mappages(kpgtbl, va, sz, pa, perm) ! 0)panic(kvmmap); }4.5 用户页表的加载 用户的加载逻辑在 loader.c 中其中唯一逻辑变化较大的就是 bin_loader 函数请结合注释理解这个函数 :linenos:pagetable_t bin_loader(uint64 start, uint64 end, struct proc *p) {// pg 代表根页表地址pagetable_t pg;// 根页表大小恰好为 1 个页pg (pagetable_t)kalloc();if (pg 0) {errorf(uvmcreate: kalloc error);return 0;}// 注意 kalloc() 分配的页为脏页这里需要先清空。memset(pagetable, 0, PGSIZE);// 映射 trapoline也就是 uservec 和 userret 的代码注意这里的权限!if (mappages(pagetable, TRAMPOLINE, PAGE_SIZE, (uint64)trampoline,PTE_R | PTE_X) 0) {kfree(pagetable);errorf(uvmcreate: mappages error);return 0;}// 映射 trapframe中断帧注意这里的权限!if (mappages(pg, TRAPFRAME, PGSIZE, (uint64)p-trapframe,PTE_R | PTE_W) 0) {panic(mappages fail);}// 接下来映射用户实际地址空间也就是把 physics address [start, end)// 映射到虚拟地址 [BASE_ADDRESS, BASE_ADDRESS length)// riscv 指令有对齐要求同时,如果不对齐直接映射的话会把部分内核地址映射到用户态很不安全// ch5我们就不需要这个限制了。if (!PGALIGNED(start)) {// Fix in ch5panic(user program not aligned, start %p, start);}end PGROUNDUP(end);// 实际的 map 指令。uint64 length end - start;if (mappages(pg, BASE_ADDRESS, length, start,PTE_U | PTE_R | PTE_W | PTE_X) ! 0) {panic(mappages fail);}p-pagetable pg;// 接下来 map user stack 注意这里的虚存选择想想为何要这样uint64 ustack_bottom_vaddr BASE_ADDRESS length PAGE_SIZE;mappages(pg, ustack_bottom_vaddr, USTACK_SIZE, (uint64)kalloc(),PTE_U | PTE_R | PTE_W | PTE_X);p-ustack ustack_bottom_vaddr;// 设置 trapframep-trapframe-epc BASE_ADDRESS;p-trapframe-sp p-ustack USTACK_SIZE;// exit 的时候会 unmap 页表中 [BASE_ADDRESS, max_page * PAGE_SIZE) 的页p-max_page PGROUNDUP(p-ustack USTACK_SIZE - 1) / PAGE_SIZE;return pg; }这里大家也要注意每一个测例进程都有一套自己的页表。因此在进程切换或者异常中断处理返回U态的时候需要设置satp的值为其对应的值才能使用正确的页表。具体的实现其实之前几章已经先做好了。我们需要重点关注一下trapframe 和 trampoline 代码的位置。在前面两节我们看到了memory_layout文件。这两块内存用户特权级切换必须用户态和内核态都能访问。所以它们在内核和用户页表中都有 map注意所有 kalloc() 分配的内存内核都能访问这是因为我们已经预先设置好页表了。 #define USER_TOP (MAXVA) #define TRAMPOLINE (USER_TOP - PGSIZE) #define TRAPFRAME (TRAMPOLINE - PGSIZE)
http://www.pierceye.com/news/249587/

相关文章:

  • 最新电子产品网站模板网站建设公司 腾佳
  • 跟公司产品做网站用什么程序做网站最好优化
  • 在线代理网页浏览网站山东省城乡住房建设厅网站
  • 网站建设需准备什么彩页模板图片
  • 怎么用网站源码建站网站换空间步骤
  • 酒店网站开发回扣商丘企业网站建设服务
  • 网站建设策划解决方案河北自助建站系统平台
  • 有没有做高仿手表的网站设计师的职责
  • struts2 做的网站seo公司怎样找客户
  • 帮别人做网站赚钱吗中山快速建站合作
  • 保靖网站建设做网站要运用到代码吗
  • 我用织梦5.7做个网站应该把淘宝客店铺链接放到哪frontpage可以制作网页吗
  • 潍坊优化网站排名在线网页设计培训机构
  • c做的网站ps做 网站标准尺寸
  • 老虎淘客系统可以做网站吗wordpress po mo
  • 网站的建设与维护那个网站做图片好
  • 昆山网站建设详细方案建设企业网站初始必备的六大功能
  • 做网站是前端还是后端网站规划 设计 制作 发布与管理过程
  • 黄山网站开发威县做网站哪里便宜
  • 网站怎么分类视频聚合网站怎么做不侵权
  • 有没有做问卷还能赚钱的网站套别人的网站模板吗
  • 东莞做汽车有没有买票的网站做谷歌推广一个月赚10万
  • 抚州城乡建设厅网站建设局官网查询
  • 汉中微信网站建设装修3d效果图怎么制作
  • wordpress 主题放哪站内关键词自然排名优化
  • 网站备案后经营做网站实例教程
  • 软件网站怎么做的python下载安装教程
  • 旅游网站开发分析报告网站建设教程搭建芽嘱湖南岚鸿信赖
  • 网站的配色方案高校网站建设意义
  • 滇中引水工程建设管理局网站网站开发怎样验收