6.S081参考书笔记 —— 第3章页表

MIT的参考书的第三章内容的代码部分文本较多,本文做了它的流程图和划重点,结合MIT参考书看比较更容易。

先给CPU一个实际的 虚拟地址转换物理地址 获取数据 的过程(不含TLB)
在这里插入图片描述

第三章 页表

页表是操作系统为每个进程提供自己的私有地址空间和内存的机制。页表决定了内存地址的含义,以及物理内存的哪些部分可以访问。它们允许xv6隔离不同进程的地址空间,并将它们复用到单个物理内存上。页表还提供了一层抽象(a level of indirection),这允许xv6执行一些特殊操作:在若干个地址空间中映射相同的内存(a trampoline page),并用一个未映射的页面保护内核和用户栈区。本章的其余部分解释了RISC-V硬件提供的页表以及xv6如何使用它们。

页式硬件

RISC-V指令(用户和内核指令都包括)使用的是虚拟地址。而机器的RAM或物理内存是由物理地址索引的。RISC-V页表硬件通过将每个虚拟地址映射到物理地址来为这两种地址建立联系。
XV6基于Sv39 RISC-V运行,这意味着它只使用64位虚拟地址的低39位;而高25位不使用。

RISC-V页表上的页表条目(Page Table Entries/PTE)

  • 一共有2^27 个
  • 包含44位的物理页码和一些标志

页式硬件

  • 通过虚拟地址前27位索引页表找到PTE
  • 得到的56位物理地址 = 44位(PTE) + 12(虚拟地址后12位)
  • 页表通过逻辑到物理地址的转换给了操作系统控制权,转换的粒度是一个个对齐的物理块(一个物理块包含 2 12 = 4096 2^{12}=4096 212=4096字节),这样的块称为页面。

img

内核地址空间

如何将内核虚拟地址映射到物理地址

image-20210913002252323

QEMU模拟了一台计算机,它包括从物理地址0x80000000开始并至少到0x86400000结束的RAM(物理内存),xv6称结束地址为PHYSTOP

内核使用“直接映射”获取内存和内存映射设备寄存器;也就是说,将资源映射到等于物理地址的虚拟地址。

有几个内核虚拟地址不是直接映射:

  • 蹦床页面(trampoline page)。它映射在虚拟地址空间的顶部;用户页表具有相同的映射。第4章讨论了蹦床页面的作用,但我们在这里看到了一个有趣的页表用例;一个物理页面(持有蹦床代码)在内核的虚拟地址空间中映射了两次:一次在虚拟地址空间的顶部,一次直接映射。
  • 内核栈页面。每个进程都有自己的内核栈,它将映射到偏高一些的地址,这样xv6在它之下就可以留下一个未映射的保护页(guard page)。保护页的PTE是无效的(也就是说PTE_V没有设置),所以如果内核溢出内核栈就会引发一个异常,内核触发panic。如果没有保护页,栈溢出将会覆盖其他内核内存,引发错误操作。恐慌崩溃(panic crash)是更可取的方案。(注:Guard page不会浪费物理内存,它只是占据了虚拟地址空间的一段靠后的地址,但并不映射到物理地址空间。)

代码:创建一个地址空间

image-20210913011000589

main.c 的流程图

image-20210913014035277

物理内存分配

内核必须在运行时为页表、用户内存、内核栈和管道缓冲区分配和释放物理内存。xv6使用内核末尾到PHYSTOP之间的物理内存进行运行时分配。它一次分配和释放整个4096字节的页面。它使用链表的数据结构将空闲页面记录下来。分配时需要从链表中删除页面;释放时需要将释放的页面添加到链表中。

代码(物理内存分配)

分配器(allocator)位于***kalloc.c***(kernel/kalloc.c:1)中。分配器的数据结构是可供分配的物理内存页的空闲列表。每个空闲页的列表元素是一个struct run(kernel/kalloc.c:17)。分配器从哪里获得内存来填充该数据结构呢?它将每个空闲页的run结构存储在空闲页本身,因为在那里没有存储其他东西。空闲列表受到自旋锁(spin lock)的保护(kernel/kalloc.c:21-24)。列表和锁被封装在一个结构体中,以明确锁在结构体中保护的字段。现在,忽略锁以及对acquirerelease的调用;第6章将详细查看有关锁的细节。

image-20210913092145651

分配器有时将地址视为整数,以便对其执行算术运算(例如,在freerange中遍历所有页面),有时将地址用作读写内存的指针(例如,操纵存储在每个页面中的run结构);这种地址的双重用途是分配器代码充满C类型转换的主要原因。另一个原因是释放和分配从本质上改变了内存的类型。

函数kfree (kernel/kalloc.c:47)首先将内存中的每一个字节设置为1。这将导致使用释放后的内存的代码(使用“悬空引用”)读取到垃圾信息而不是旧的有效内容,从而希望这样的代码更快崩溃。然后kfree将页面前置(头插法)到空闲列表中:它将pa转换为一个指向struct run的指针r,在r->next中记录空闲列表的旧开始,并将空闲列表设置为等于r

kalloc删除并返回空闲列表中的第一个元素。

进程地址空间

每个进程都有一个单独的页表,当xv6在进程之间切换时,也会更改页表。如图2.3所示,一个进程的用户内存从虚拟地址零开始,可以增长到MAXVA (kernel/riscv.h:348),原则上允许一个进程内存寻址空间为256G。

当进程向xv6请求更多的用户内存时,xv6首先使用kalloc来分配物理页面。然后,它将PTE添加到进程的页表中,指向新的物理页面。Xv6在这些PTE中设置PTE_WPTE_XPTE_RPTE_UPTE_V标志。大多数进程不使用整个用户地址空间;xv6在未使用的PTE中留空PTE_V

图3.4更详细地显示了xv6中执行态进程的用户内存布局。栈是单独一个页面,显示的是由exec创建后的初始内容。包含命令行参数的字符串以及指向它们的指针数组位于栈的最顶部。再往下是允许程序在main处开始启动的值(即main的地址、argcargv),这些值产生的效果就像刚刚调用了main(argc, argv)一样。

image-20210913211252960

为了检测用户栈是否溢出了所分配栈内存,xv6在栈正下方放置了一个无效的保护页(guard page)。如果用户栈溢出并且进程试图使用栈下方的地址,那么由于映射无效(PTE_V为0)硬件将生成一个页面故障异常。当用户栈溢出时,实际的操作系统可能会自动为其分配更多内存。

代码:sbrk

image-20210913094513856

XV6使用进程的页表,不仅是告诉硬件如何映射用户虚拟地址,也是明晰哪一个物理页面已经被分配给该进程的唯一记录。这就是为什么释放用户内存(在uvmunmap中)需要检查用户页表的原因。

代码exec

exec是创建地址空间的用户部分的系统调用。它使用一个存储在文件系统中的文件初始化地址空间的用户部分。

image-20210913184150031

exec将ELF文件中的字节加载到ELF文件指定地址的内存中。用户或进程可以将他们想要的任何地址放入ELF文件中。因此exec是有风险的,因为ELF文件中的地址可能会意外或故意的引用内核。对一个设计拙劣的内核来说,后果可能是一次崩溃,甚至是内核的隔离机制被恶意破坏(即安全漏洞)。xv6执行许多检查来避免这些风险。例如,if(ph.vaddr + ph.memsz < ph.vaddr)检查总和是否溢出64位整数,危险在于用户可能会构造一个ELF二进制文件,其中的php. vaddr指向用户选择的地址,而php. memsz足够大,使总和溢出到0x1000,这看起来像是一个有效的值。在xv6的旧版本中,用户地址空间也包含内核(但在用户模式下不可读写),用户可以选择一个与内核内存相对应的地址,从而将ELF二进制文件中的数据复制到内核中。在xv6的RISC-V版本中,这是不可能的,因为内核有自己独立的页表;loadseg加载到进程的页表中,而不是内核的页表中。

内核开发人员很容易省略关键的检查,而现实世界中的内核有很长一段丢失检查的历史,用户程序可以利用这些检查的缺失来获得内核特权。xv6可能没有完成验证提供给内核的用户级数据的全部工作,恶意用户程序可以利用这些数据来绕过xv6的隔离。

真实世界

像大多数操作系统一样,xv6使用分页硬件进行内存保护和映射。大多数操作系统通过结合分页和页面故障异常使用分页,比xv6复杂得多,我们将在第4章讨论这一点。

内核通过使用虚拟地址和物理地址之间的直接映射,以及假设在地址0x8000000处有物理RAM (内核期望加载的位置) ,Xv6得到了简化。这在QEMU中很有效,但在实际硬件上却是个坏主意;实际硬件将RAM和设备置于不可预测的物理地址,因此(例如)在xv6期望能够存储内核的0x8000000地址处可能没有RAM。更严肃的内核设计利用页表将任意硬件物理内存布局转换为可预测的内核虚拟地址布局。

RISC-V支持物理地址级别的保护,但xv6没有使用这个特性。

在有大量内存的机器上,使用RISC-V对“超级页面”的支持可能很有意义。而当物理内存较小时,小页面更有用,这样可以以精细的粒度向磁盘分配和输出页面。例如,如果一个程序只使用8KB内存,给它一个4MB的物理内存超级页面是浪费。在有大量内存的机器上,较大的页面是有意义的,并且可以减少页表操作的开销。

xv6内核缺少一个类似malloc可以为小对象提供内存的分配器,这使得内核无法使用需要动态分配的复杂数据结构。

内存分配是一个长期的热门话题,基本问题是有效使用有限的内存并为将来的未知请求做好准备。今天,人们更关心速度而不是空间效率。此外,一个更复杂的内核可能会分配许多不同大小的小块,而不是(如xv6中)只有4096字节的块;一个真正的内核分配器需要处理小分配和大分配。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
s081 lab2是一门实验课,旨在深入理解操作系统的基本概念和内部工作原理。这门课的主要内容是通过编写一个简化版操作系统,学习操作系统的底层实现。 在实验过程中,我们需要完成以下几个任务: 1. 实现一个简单的系统调用接口:在操作系统中,系统调用是用户程序与底层操作系统交互的接口。通过实现系统调用接口,我们可以向操作系统请求一些底层资源,如文件读写、进程管理等。 2. 构建虚拟内存系统:虚拟内存是操作系统对物理内存的虚拟化技术,将程序的内存空间划分为多个称为页的单元,并将这些页映射到物理内存的页框上。我们需要实现虚拟内存系统,包括页表管理、地址转换等。 3. 实现中断和异常处理:中断是指外部事件打断了程序的正常执行流程,例如硬件设备的输入输出完成等。我们需要编写中断处理程序,以便在发生中断时能够正确处理并返回原来的程序状态。 4. 解决并发与同步问题:并发是指多个任务同时执行的情况。在操作系统中,我们需要解决多个任务之间的并发和同步问题,以确保资源的正确访问和互斥。 通过完成这些任务,我们可以对操作系统的内部工作原理有更深入的了解。在实验过程中,我们需要仔细阅读文档、参考文献和实验官方提供的代码,并根据实验要求进行设计与实现。除了编程技术,还需要动手实践和调试,不断尝试和改进。 总而言之,s081 lab2是一门非常重要的实验课,通过编写操作系统的核心功能,我们可以更好地理解操作系统的概念和内部机制。通过这门课程的学习,我们可以提升我们的编程能力和对操作系统的理解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值