From 4e49bb21304b3efa01ebe5c61ec5470704ab50e4 Mon Sep 17 00:00:00 2001 From: YongkangLi Date: Mon, 26 Oct 2020 04:17:27 -0700 Subject: [PATCH 1/3] code normal form --- kernel/main.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/kernel/main.c b/kernel/main.c index 47fc29a..4ffa8ac 100644 --- a/kernel/main.c +++ b/kernel/main.c @@ -33,18 +33,18 @@ main(unsigned long hartid, unsigned long dtb_pa) // virtio_disk_init(); // emulated hard disk // userinit(); // first user process - test_kalloc(); // test kalloc - test_vm(hartid); // test kernel pagetable - test_getchar(); // test sbi_console_getchar + test_kalloc(); // test kalloc + test_vm(hartid); // test kernel pagetable + //test_getchar(); // test sbi_console_getchar for(int i = 1; i < NCPU; i++) { - unsigned long mask = 1 << i; - sbi_send_ipi(&mask); - } + unsigned long mask = 1 << i; + sbi_send_ipi(&mask); + } } while (1); - // scheduler(); + // scheduler(); /* if(cpuid() == 0){ consoleinit(); From 28142a51a1b21481acc1172a755bb22fce03597d Mon Sep 17 00:00:00 2001 From: YongkangLi Date: Fri, 25 Dec 2020 05:32:36 -0800 Subject: [PATCH 2/3] document about memory --- doc/memory-code.md | 52 ++++++++++++++++++++++++++++++++++ doc/memory.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 doc/memory-code.md create mode 100644 doc/memory.md diff --git a/doc/memory-code.md b/doc/memory-code.md new file mode 100644 index 0000000..02279d7 --- /dev/null +++ b/doc/memory-code.md @@ -0,0 +1,52 @@ +# 页表相关代码 + +## 创建地址空间 +大多数用于操纵地址空间和页表的xv6代码位于vm.c[(kernel/vm.c:1)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L1)中。中心数据结构是pagetable_t ,它实际上是一个指向RISC-V根页表页的指针;一个pagetable_t可以是内核页表,也可以是一个进程页表。主要功能是:walk(查找虚拟地址对应的页表项(PTE))和mappages (将映射关系填入页表中)。以kvm开头的函数用于操纵内核页表;以uvm开头的函数用于操纵用户页表;两者都使用其他功能。copyout和copyin将数据从用户虚拟地址复制,或复制到用户虚拟地址,这里的用户虚拟地址是作为系统调用参数提供的;它们位于vm.c中,因为它们需要显式转换这些地址才能找到相应的物理内存。 + +在刚进入启动阶段时,main调用kvminit[(kernel/vm.c:22)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L22)以创建内核的页表。该调用发生在xv6在RISC-V上启用分页之前,因此地址直接引用物理内存。kvminit首先分配一页物理内存来保存根页表页面。然后,它调用kvmmap来将转换关系填入页表中。这些转换包括内核的指令和数据、高达PHYSTOP的物理内存、以及实际上是设备的内存范围。 + +kvmmap (kernel / vm.c:118)调用mappages[(kernel/vm.c:149)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L22),这会将映射关系填写到页表中,以将一系列虚拟地址映射到相应的物理地址范围。它以页面间隔的距离对范围内的每个虚拟地址分别执行此操作。对于要映射的每个虚拟地址,mappages调用walk来查找该地址的PTE地址。然后初始化PTE来保存相关的物理页号(PPN)、所需的权限(PTE\_W ,PTE\_X和/或PTE\_R ),并初始化PTE\_V以将PTE标记为有效(置1)[(kernel/vm.c:161)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L161)。 + +walk[(kernel/vm.c:72)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L72)模拟RISC-V分页硬件,因为它在PTE中查找虚拟地址。它使用每个级别的9位虚拟地址来查找下一级页表或最终页面[(kernel/vm.c:78)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L78)的PTE 。如果PTE无效,则尚未分配所需的页面。如果设置了alloc参数,walk将分配一个新的页表页并将其物理地址写入PTE中。它返回树中最底层(叶子节点)中的PTE地址[(kernel/vm.c:88)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L88)。 + +上面的代码取决于将物理内存直接映射到内核虚拟地址空间中。例如,当walk下降页表的级别时,它将从PTE[(kernel/vm.c:80)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L80)中提取下一级页表的(物理)地址,然后将该地址作为虚拟地址以便在提取下一级[(kernel/vm.c:78)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L78)PTE 。 + +main调用kvminithart[(kernel/vm.c:53)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L53)填写内核页表的映射关系。它将根页表页的物理地址写入寄存器satp 。此后,CPU将使用内核页表转换地址。由于内核使用直接映射,因此下一条指令的当前虚拟地址将映射到正确的物理内存地址。 + +procinit[(kernel/proc.c:26)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/proc.c#L26)被main调用,为每个进程分配一个内核堆栈。它将每个堆栈映射到KSTACK生成的虚拟地址,从而为无效的堆栈保护页留出空间。kvmmap将实现映射关系的页表项(PTE)添加到内核页表中,调用kvminithart时将内核页表重新加载到satp中,以便硬件得知新的PTE。 + +每个RISC-V CPU都将页表项缓存在转换后备缓冲区(TLB)中,并且当xv6更改页表时,它必须告诉CPU使相应的缓存的TLB条目无效。如果没有,那么在某个时候TLB可能会使用旧的缓存映射,指向同时已分配给另一个进程的物理页面,结果导致某个进程可能会在某些页面上乱写其他进程的内存。RISC-V具有一条指令sfence.vma ,该指令刷新当前CPU的TLB。SATP寄存器重新加载后,XV6在kvminithart中执行sfence.vma,并在返回用户空间之前切换到用户页表的trampoline代码中[(kernel/trampoline.S:79)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/trampoline.S#L79) 。 + +## 物理内存分配器 +分配器代码详见在kalloc.c[(kernel/kalloc.c:1)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/kalloc.c#L1)。分配器的数据结构是一个空闲链表,这个链表保存了可用于分配的物理内存页。每个空闲页面的list元素都是一个结构体run[(kernel/kalloc.c:17)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/kalloc.c#L17)。那么问题来了——分配器从哪里获取内存来保存该数据结构?它将每个自由页面的结构体run存储在自由页面本身中,因为那里没有其他存储。空闲链表受自旋锁(spin lock)[(kernel/kalloc.c:21-24)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/kalloc.c#L21-L24)保护。列表和锁包装在一个结构体中,以说明锁保护结构体中的字段。现在可以忽略锁以及关于锁的获取和释放的调用。 + +函数main调用kinit初始化分配器[(kernel/kalloc.c:27)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/kalloc.c#L27)。kinit初始化空闲链表,以容纳内核末尾与PHYSTOP之间的每个页面。xv6应该通过解析硬件提供的配置信息来确定有多少物理内存可用。相反,xv6假定计算机具有128MB的RAM。kinit调用freerange通过对kfree的每页调用将内存添加到空闲列表。PTE只能引用在4096字节边界上对齐的物理地址(是4096的倍数),因此freerange使用PGROUNDUP来确保它仅释放对齐的物理地址。分配器开始时没有内存。这些对kfree的调用使它有所管理。 + +分配器有时将地址视为整数以便对其执行算术运算(比如遍历freerange中的所有页时),有时使用地址作为读写内存的指针(比如操纵存储在每个页中的运行结构时);地址的这种双重使用是分配器代码充满C语言中强制类型转换的主要原因。另一个原因是释放和分配固有地改变了内存的类型。 + +函数kfree[(kernel/kalloc.c:47)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/kalloc.c#L47)首先将内存中释放的每个字节设置为值1。这将导致释放内存(dangling use)之后使用内存的代码读取垃圾,而不是旧的有效内容。希望这将导致此类代码更快地破解。然后kfree将页面添加到空闲列表中:将物理地址(PA)强制转换为指向run结构体的指针,在r->next中记录空闲列表的原来开始的地方,并将空闲列表设置为r 。kalloc删除并返回空闲列表中的第一个元素。 + +## sbrk +sbrk是一个系统调用,用于进程缩小或增加其内存。系统调用由函数growproc[(kernel/proc.c:239)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/proc.c#L239)实现。growproc调用uvmalloc或uvmdealloc,这取决于n是正的还是负的。uvmalloc[(kernel/vm.c:229)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L229)使用kalloc分配物理内存,并使用mappages将PTE添加到用户页表中 。uvmdealloc调用uvmunmap (kernel / vm.c:174),它使用walk来查找PTE ,然后使用kfree释放它们引用的物理内存。 + +xv6通过使用进程的页表不仅告诉硬件如何映射用户虚拟地址,而且还是向该进程分配了哪些物理内存页的唯一记录。这就是释放用户内存(在uvmunmap中)需要检查用户页表的原因。 + +## exec +exec是创建地址空间的用户部分的系统调用。它用存储在文件系统中的文件初始化地址空间的用户部分。exec[(kernel/exec.c:13)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/exec.c#L13)使用namei[(kernel/exec.c:26)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/exec.c#L26)打开指定的二进制路径。然后,它读取ELF格式的头部(header)。xv6应用程序以广泛使用的ELF格式描述,在[(kernel/elf.h)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/elf.h)中定义。一个ELF二进制由一个ELF头部、结构体elfhdr[(kernel/elf.h:6)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/elf.h#L6)、一系列程序节头和结构体proghdr[(kernel/elf.h:25)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/elf.h#L25)组成。每个程序都描述了必须加载到内存中的应用程序的一个部分。xv6程序只有一个程序节头,但是其他系统可能有单独的节用于存储指令和数据。 + +第一步是快速检查文件是否可能包含ELF二进制文件。ELF二进制文件以四字节的魔数0x7F,字符'E',字符'L',字符'F'或ELF\_MAGIC[(kernel/elf.h:3)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/elf.h#L3)开头。如果ELF头部具有正确的魔数,则exec认为二进制文件格式正确。 + +exec调用proc\_pagetable[(kernel/exec.c:38)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/exec.c#L38)分配一个没有用户映射的新页表,调用uvmalloc[(kernel/exec.c:52)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/exec.c#L52)为每个ELF段分配内存,并用loadseg[(kernel/exec.c:10)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/exec.c#L10)将每个段加载到内存中。loadseg调用walkaddr查找分配内存的物理地址,在该地址上写入ELF段的每一页,调用readi从文件中读。 + +程序段头文件的filez可能小于memsz ,这表明它们之间的间隙应该用零填充(对于C全局变量),而不是从文件中读取。 +对于/init ,filesz为2112字节,而memsz为2136字节,因此uvmalloc分配了足够的物理内存来容纳2136字节,但仅从file/init读取2112字节。 + +现在exec分配并初始化用户堆栈。它仅分配一个堆栈页面。exec一次将一个字符串参数复制到堆栈的顶部,并在ustack中记录指向它们的指针。它将空指针放在传递给main的argv列表的末尾。ustack中的前三个条目是伪造的返回程序计数器,argc和argv指针。 + +exec在堆栈页面的下方放置了一个无法访问的页面,因此尝试使用多个页面的程序将出错。这个无法访问的页面还允许exec处理过大的参数。在这种情况下,exec调用copyout[(kernel/vm.c:355)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/vm.c#L355)函数以将参数复制到堆栈时会发现目标页面不可访问,从而返回-1。 + +在准备新内存映像的过程中,如果exec检测到诸如无效程序段之类的错误,它将跳转到bad标签处,释放新映像,并返回-1。exec必须等待释放旧映像,直到确定系统调用成功为止:如果旧映像不存在,则系统调用无法向其返回-1。exec只有在创建映像时才有可能会发生错误。映像完成后,exec将开始使用新页表[(kernel/exec.c:113)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/exec.c#L113)并释放旧页表[(kernel/exec.c:117)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/exec.c#L117)。 + +exec将ELF文件中的字节加载到ELF文件指定地址处的内存中。用户或进程可以将所需的任何地址放入ELF文件中。因此exec是有风险的,因为ELF文件中的地址可能会偶然或有目的地引用内核。粗心的内核可能导致崩溃,甚至会恶意破坏内核的隔离机制(即,安全漏洞攻击)。xv6执行了许多检查以避免这些风险。例如,if(ph.vaddr + ph.memsz < ph.vaddr )检查总和是否溢出64位整数。危险在于,用户可能用ph.vaddr构造一个ELF二进制文件,该ph.vaddr指向用户选择的地址,而ph.memsz足够大,使得总和溢出到0x1000,这看起来像是一个有效值。在旧版本的xv6中,用户地址空间也包含内核(但在用户模式下不可读/可写),用户可以选择与内核内存相对应的地址,从而将数据从ELF二进制文件复制到内核中。在xv6的RISC-V版本中,不会发生这种情况,因为内核具有自己的单独的页表。loadeg加载到进程的页表中,而不是内核的页表中。 + + diff --git a/doc/memory.md b/doc/memory.md new file mode 100644 index 0000000..eb93db4 --- /dev/null +++ b/doc/memory.md @@ -0,0 +1,70 @@ +# 页表 +页表是操作系统用来为每个进程提供专有地址空间和内存的一种机制。页表不仅决定了虚拟地址的真实含义(物理地址),还决定了一个进程能够访问内存的哪些地方。页表机制的存在使得xv6能够隔离不同进程的地址空间以及将一块物理内存复用为多个虚拟地址空间。同时,页表所提供的这种映射机制也使得xv6的内存管理十分巧妙。比如,将同一片物理内存映射到多个虚拟地址空间、不映射内核和用户进程的堆栈等等。本章接下来的部分将解释RISC-V硬件所提供的页表机制以及xv6是如何实现页表机制的。 + +## 分页硬件 +需要提到的是,RISC-V提供了操纵虚拟地址的指令(内核态和用户态都有)。机器的RAM,或者说物理内存,是通过物理地址来索引的。RISC-V的与页表相关的硬件通过将虚拟地址映射回物理地址起到了虚拟地址和物理地址的桥梁作用。 + +xv6跑在Sv39 RISC-V上,这意味着64位的虚拟地址实际上只有低39位被用到了,而高25位并没有被使用。根据Sv39规范,一个RISC-V页表在逻辑上拥有 $2^{27}=134217728$ 个页表项(page table entries, PTEs)。每个PTE包含一个44位的物理页号(physical page number, PPN)和一些标志位。分页硬件通过被用到的那39位的高27位在页表中索引PTE,而剩下的那低12位则为页内偏移(offset)。由于Sv39的物理地址为56位,且这56位中的低12直接由虚拟地址中的低12位(页内偏移)得到,则物理页号有 $56-12=44$ 位,这44位物理页号(PPN)由被索引的PTE所指向的物理页号(PPN)得到。一个页表可以简单理解为一个存放了很多页表项(PTEs)的数组,下图描述了这种逻辑关系。页表为操作系统提供了虚实地址转换的控制,这种控制以 $4096(2^{12})$ 字节的对齐块为粒度。这样的块就是一个页面。 +![图1](https://github.com/mit-pdos/xv6-riscv-book/raw/xv6-riscv/fig/riscv_address.svg) + +在Sv39标准的RISC-V中,不使用虚拟地址的高25位进行转换。将来,RISC-V可能会使用这些位来定义更多级别的转换。物理地址也有增长的空间。在PTE格式中,物理页号(PPN)字段可以再增长10位。 + +如图2所示,实际的虚实地址转化分为三个步骤。页表以高度为3的树的形式存储在物理内存中。根节点是一个4096字节的页表页,其中包含512个页表项(PTE),这些页表项(PTE)包含树的下一级页表页的物理地址。这些页面中的每一页面都包含512个页表项(PTE),这些页表项(PTE)是树的叶子节点。分页硬件使用这27位中的前9位在根页面表页面中选择一个页表项(PTE),中间9位在树的下一级的页表页中选择一个页表项(PTE),底部9位选择最后一个页表项(PTE)。综上所述,RISC-V采取三级页表机制。 + +如果不存在转换地址所需的三个PTE中的任何一个,则分页硬件会引发页面错误异常(page-fault exception),并将这个异常交给内核来处理(请参见第4章)。一种比较常见的情况是,在页表页中大量虚拟地址没有映射,这种三层树状页表机制将忽略整个页表页。 + +每个页表项(PTE)都包含标志位,这些标志位的存在使得分页硬件知道如何使用这些被关联的虚拟地址。标志位PTE\_V(VALID)指示该页表项(PTE)是否有效:如果该位被置0,则对该页面的引用会引发异常(即不允许引用)。PTE\_R(READ)控制是否允许指令读取该页面。PTE\_W(WRITE)控制是否允许指令写入该页面。PTE\_X(EXECUTE)控制CPU是否可以将该页面的内容解释为指令并执行它们。PTE\_U(USER)控制是否允许用户模式下的指令访问页面:如果该位被置0 ,则只能在超级用户模式下使用该页面项所指向的页面。下图展示了它们如何工作。标志和所有其他与页面硬件相关的结构都在[(kernel / riscv.h)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/riscv.h)中定义。 +![图2](https://github.com/mit-pdos/xv6-riscv-book/raw/xv6-riscv/fig/riscv_pagetable.svg) + +为了告诉硬件使用页表映射机制,内核必须将根页表页的物理地址写入satp寄存器。每个CPU都有自己的satp 。CPU将使用其自己的satp指向的页表来转换后续指令生成的所有地址。由于每个CPU都有自己的satp,因而不同的CPU可以运行不同的进程,每个进程都有一个由其自己的页表描述的专用地址空间。 + +关于术语的一些注释:物理内存是指DRAM中的存储单元。物理内存中每一个字节都有一个地址(通常情况下按字节编址),称为物理地址。指令仅使用虚拟地址,分页硬件负责将其转换为物理地址,然后将其发送至DRAM硬件以读取或写入额你存。与物理内存和虚拟地址不同,虚拟内存不是物理对象,而是内核提供的抽象和机制的集合,用于管理物理内存和虚拟地址。 + +## 内核地址空间 +xv6为每个进程维护一个页表,用于描述每个进程的用户地址空间。除此之外,还有单独一个页表用于描述内核的地址空间。内核配置其地址空间的布局,以使其能够以可预测的虚拟地址访问物理内存和各种硬件资源。下图显示了此布局如何将内核虚拟地址映射到物理地址。文件[(kernel / memlayout.h)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/memlayout.h)声明了xv6内核内存布局的常量。 +![图3](https://github.com/mit-pdos/xv6-riscv-book/raw/xv6-riscv/fig/xv6_layout.svg) + +QEMU可以模拟一个包含RAM(物理内存)的机器,该RAM从物理地址0x80000000开始,一直延伸到至少0x86400000,xv6将其称为PHYSTOP。QEMU模拟还包括I/O设备,例如磁盘接口。QEMU以物理地址空间中0x80000000以下的内存映射控制寄存器的形式为软件提供设备接口。内核可以通过读/写这些特殊的物理地址与设备进行交互;这样的读写实际上是在与设备硬件进行通信,而不是RAM。第4章介绍了xv6如何与设备交互。 + +内核使用“直接映射”来获取RAM和内存映射的设备寄存器。也就是说,将资源映射到与物理地址相等的虚拟地址。例如,内核本身在虚拟地址空间和物理内存中都位于KERNBASE = 0x80000000 。直接映射简化了读取或写入物理内存的内核代码。例如,当fork为子进程分配用户内存时,分配器返回该内存的物理地址;fork将父进程的用户内存复制到子进程时,fork直接将该地址用作虚拟地址。 + +有几个内核虚拟地址不是直接映射的: + + +- trampoline页面。它映射在虚拟地址空间的顶部。用户页表具有与此相同的映射。注意到,一个物理页面(包括trampoline代码)在内核的虚拟地址空间中映射了两次:一次在虚拟地址空间的顶部,一次是直接映射。 + +- 内核堆栈页面。每个进程都有自己的内核堆栈,该堆栈被映射到较高的位置,因此xv6在其下方可以留下未映射的保护页。保护页的PTE无效(即,PTE\_V置0),因此,如果内核溢出内核堆栈,则很可能会导致异常,并且内核会出现紧急情况。如果没有保护页,溢出的堆栈将覆盖其他内核内存,从而导致错误的操作,系统很可能会崩溃。 + +内核通过高内存映射使用其堆栈时,内核也可以通过直接映射的地址访问它们。替代设计可能只具有直接映射,并在直接映射的地址处使用堆栈。然而,在这种安排下,提供保护页将涉及取消映射虚拟地址,否则这些虚拟地址将引用物理内存,因此将很难使用。 + +内核通过置标志位PTE\_R(READ)和PTE\_\_X(EXECUTE)映射trampoline的页面和内核文本。内核从这些页面读取并执行指令。内核通过置标志位PTE\_R(RREAD)和PTE\_W(WRITE)映射其他页面,以便它可以在那些页面中读取和写入内存。保护页面的映射无效。 + +## 物理内存分配 +内核必须在运行时为页表,用户内存,内核堆栈和管道缓冲区分配和释放物理内存。 + +xv6使用内核末尾与PHYSTOP之间的物理内存进行运行时分配。它以一个页面(4096个字节)为粒度进行分配和释放。它通过一个链表来记录那些空闲的页面。分配即将被分配的页面从链表中删除。释放即将被释放的页面添加到列表中。 + +## 进程地址空间 +每个进程都有一个单独的页表,并且xv6在进程之间切换时,它也会切换对应的页表。如下图所示,进程的用户内存从虚拟地址0开始,可以增长到MAXVA[(kernel / riscv.h:348)](https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/riscv.h#L348),从而允许进程在原则上寻址256GB的内存。 +![图4](https://github.com/mit-pdos/xv6-riscv-book/raw/xv6-riscv/fig/processlayout.svg) + +当进程向xv6请求更多的用户内存时,xv6首先使用kalloc分配物理页面。然后,它将页表项(PTE)添加到流程的页表中,该页表指向新的物理页。xv6在这些页表项(PTE)中设置PTE\_W ,PTE\_X ,PTE\_R ,PTE\_U和PTE\_V标志位。大多数进程不会使用整个用户地址空间。对于未使用的页表项(PTE),xv6将PTE\_V(VALID)标志位置0。 + +我们在这里看到一些使用页表的带来的好处。首先,不同进程的页表将用户地址转换为物理内存的不同页面,以便每个进程都有专用的用户内存。其次,每个进程将其内存视为具有从零开始的连续虚拟地址,而进程的实际物理内存可能是非连续的。第三,内核在用户地址空间的顶部映射一个带有trampoline代码的页面,因此有一页物理内存会出现在在所有的用户地址空间。 + +图3.4更详细地显示了xv6中正在执行的进程的用户内存的布局。堆栈是单独的一个页面,包含了由exec创建时的初始内容。包含命令行参数以及它们的指针数组的字符串位于堆栈的最顶端。在该值之下是允许程序从main开始的值,就像函数main(argc,argv)刚刚被调用一样。 + +为了检测用户堆栈是否溢出已分配的堆栈内存,xv6在堆栈的正下方放置了一个无效的保护页。如果用户堆栈溢出,并且该进程尝试使用堆栈下面的地址,则硬件将引发页面错误异常(page fault exception),因为该映射无效。实际的操作系统可能会在用户堆栈溢出时自动为其分配更多的内存。 + +## 实际情况 +与大多数操作系统一样,xv6使用分页硬件来保护和映射内存。大多数操作系统在分页上有更精妙的用法,比如通过结合分页和页面错误异常,xv6并没有这样做。 + +xv6的内核使用虚拟地址和物理地址之间的直接映射,以及它假定在地址0x8000000处存在物理RAM(内核期望加载的位置),这些使得xv6得以简化。这适用于QEMU,但是在真正的硬件上,这样做并不好。实际的硬件将RAM和设备放置在不可预测的物理地址上,因此(例如)在0x8000000处可能并没有RAM,而xv6却希望可以在这里存储内核。更厉害的内核设计充分利用页表,使得可以将任意硬件物理内存布局转换为可预测的内核虚拟地址布局。 + +RISC-V支持物理地址级别的保护,但是xv6没有使用这个特性。 + +在具有大量内存的机器上,可以使用RISC-V对“超级页面”的支持。当物理内存较小时,小页面是更有意义的,它允许以精细的粒度分配分页到磁盘。例如,如果一个程序仅使用8KB的内存,那么为它提供整个4MB的物理内存超级页面就会造成浪费。而在具有大量RAM的计算机上,较大的页面会更有意义,这样可以减少页表操作的开销。 + +xv6内核缺少类似malloc这样的可以为小对象提供内存的分配器,这使得内核无法使用需要动态分配的复杂数据结构。 + +内存分配是一个长期的热门话题,其基本问题是如何有效地利用有限的内存,以及如何为将来未知的请求做准备。今天,人们更关心速度而非空间效率。此外,一个更精细的内核可能会分配许多不同大小的小块,而不是分配固定大小的块(如xv6中是4096个字节)。一个真正的内核分配器既要处理大的分配。也要处理小的分配。 From 96247e9e73ec0b9a952bd82615752e950024b2db Mon Sep 17 00:00:00 2001 From: YongkangLi Date: Fri, 25 Dec 2020 05:42:11 -0800 Subject: [PATCH 3/3] Revert "code normal form" This reverts commit 4e49bb21304b3efa01ebe5c61ec5470704ab50e4. --- kernel/main.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/kernel/main.c b/kernel/main.c index 4ffa8ac..47fc29a 100644 --- a/kernel/main.c +++ b/kernel/main.c @@ -33,18 +33,18 @@ main(unsigned long hartid, unsigned long dtb_pa) // virtio_disk_init(); // emulated hard disk // userinit(); // first user process - test_kalloc(); // test kalloc - test_vm(hartid); // test kernel pagetable - //test_getchar(); // test sbi_console_getchar + test_kalloc(); // test kalloc + test_vm(hartid); // test kernel pagetable + test_getchar(); // test sbi_console_getchar for(int i = 1; i < NCPU; i++) { - unsigned long mask = 1 << i; - sbi_send_ipi(&mask); - } + unsigned long mask = 1 << i; + sbi_send_ipi(&mask); + } } while (1); - // scheduler(); + // scheduler(); /* if(cpuid() == 0){ consoleinit();