[go: up one dir, main page]

Skip to content

DUTRB/XV6-NewLab

Repository files navigation

xv6 is a re-implementation of Dennis Ritchie's and Ken Thompson's Unix Version 6 (v6). xv6 loosely follows the structure and style of v6, but is implemented for a modern RISC-V multiprocessor using ANSI C.

xv6本身已经是一个可以运行的简易的操作系统了. 6.S081这门课并不是从零开始空手造一个xv6操作系统, 而是针对操作系统的不同角度, 在每个lab里让我们根据已有的功能, 对xv6进行优化.

1. 为什么要有虚拟内存?

  • 隔离

    虚拟内存允许每个进程之间都有独立的虚拟地址空间,不同进程间的内存不会互相干扰。实现了进程间的内存隔离,防止一个进程的错误导致其他程序的崩溃。

  • 简化 & 管理

    虚拟地址空间允许程序使用连续的虚拟地址空间,无需关心实际的物理地址中存在的内存碎片问题,简化了内存分配和管理问题。

    此外允许操作系统对物理内存分配进行优化,将不活跃的内存数据页转移到硬盘中,释放物理内存供其他程序使用。

  • 扩大

    虚拟内存可以将物理内存和磁盘上的存储空间组合起来使用,使每个进程看起来都有巨大的地址空间提供使用,允许CPU运行内存占用较大的程序。

  • 文件操作

    虚拟内存允许将文件映射到进程的地址空间中,可以轻松的读取和写入文件,减少繁琐的文件操作。

2. 为什么虚拟内存之和能超过物理内存?

操作系统使用 缺页故障,实现了fork写时复制,内存懒分配和磁盘交换策略,为进程提供了许多虚假的虚拟内存。这些虚假的虚拟内存可能并没有实际对应的物理内存,也可能对应的物理内存被交换到了磁盘当中。在进程的视角来看,似乎有远超过物理内存的虚拟内存量。

能使用磁盘交换的前提是:具有缺页故障机制。

3. XV6 完整的系统调用流程

  1. 在用户模式,调用C函数库中的系统调用的封装,如write
  2. 该封装中会将具体的系统调用号加载到a7中,然后调用ecall指令
  3. ecall指令会做三件事
    1. 切换UMode到SMode
    2. 将用户pc保存到sepc
    3. 跳转到stvec指定的位置,也就是trampoline
  4. 在trampoline中(切换用户页表带内核页表中)— uservec 所做的事情
    1. 将用户寄存器保存到进程的trapframe中
    2. 从trapframe中读取内核栈、内核页表、中断处理程序usertrap的位置,当前CPU核心id
    3. 加载内核栈到sp、切换satp位内核页表,跳转到陷阱处理程序
  5. 陷阱处理程序将trapframe中的epc写成ecall的下一条指令,调用syscall执行系统调用
  6. syscall调用具体的在内核中的系统调用代码,然后将返回值写到a0
  7. usertrap执行usertrapret,为返回用户空间做准备
    1. 比如设置进程trapframe中的内核相关的信息,内核栈、内核页表、中断处理程序位置等
    2. 设置sret指令的控制寄存器,以在sret执行时顺利恢复到用户模式
    3. 设置sepc为trapframe的epc
    4. 使用函数指针,调用trampoline代码中的userret,并将trapframe作为a0、用户页表位置作为a1
  8. trampoline中的userret做的就很简单了,切换回用户页表,从trapframe恢复用户寄存器
  9. 执行sret返回到UMode

4. 操作系统内存管理模块

  • 内存的申请与释放

    管理系统内存池,实现为为进程分配和释放内存。

  • 虚拟内存管理

    允许进程使用虚拟内存,并将虚拟内存映射到物理内存,包含地址转换,分页机制,页表管理等。

  • 共享内存

    允许多个进程共享相同的内存空间,以便轻松的进程进程通信和数据共享。

  • 内存监控与性能优化

    负责跟踪系统内存使用情况,以及性能分析和优化内存分配策略。

5. XV6 如何分配内存?

用户态程序通过malloc申请内存,malloc维护一个内存池。当malloc无法提供合适内存时,将使用sbrk系统调用向操作系统申请新的堆内存,sbrk会在页表中做好标记,提供足量的虚拟内存空间,并通过kalloc申请足量的物理内存映射到新的虚拟内存中,以便返回这些虚拟内存供malloc使用。

实际的物理内存中的空闲页面通过链表连接管理,kalloc在该空闲链表中获取物理内存。

// Allocate one 4096-byte page of physical memory. 申请的是4096字节的实际物理地址
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
  struct run *r;
  acquire(&kmem.lock);
  // 申请内存操作
  r = kmem.freelist;        // 指向空闲内存块
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);
  // 将分配的内存块填充标记
  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;          // 返回空闲内存指针
}

内存链表的初始大小是0,并且它在第一次调用malloc且没有可用的空闲块时被初始化。当需要更多内存时,malloc会调用morecore来扩展内存。

7. 页表的作用 && 三级页表的优势和劣势


页表作用:

  • 地址映射:将虚拟地址映射到物理地址上。每个虚拟页对应了一个物理页框。

  • 内存隔离:页表允许多个应用程序同时运行,每个程序拥有自己的页表,使得内存空间互相隔离。

  • 内存保护:页表允许操作系统为不同的地址设置权限和保护位,限制对内存的操作,提升系统的安全性。

  • 优势

    1. 三级结构更加节省内存:由于PTE也需要在存放在内存中,一级页表可以表示2^27个PTE,如果在大范围的虚拟地址没有被映射的情况下,这对于内存时极大地浪费。而三级页表分为三段,每一部分仅有2^9个PTE表,极大节省内存占用。
    2. 可以更加灵活的管理物理地址空间:三级页表将地址划分为三个层次,每个层次按需分配。
  • 缺点:

    1. 实际转换的增加了额外的内存开销:每次进行地址映射CPU都需要从内存中加载三个PTE才可以实现地址的转换。
    2. 增加了内存管理复杂性:需要编写更加复杂的地址转换和操作代码,增加维护难度。

7. xv6是如何将虚拟地址和物理地址进行映射的?

通过mappages函数,需要给定页表、虚拟地址、映射内存大小和物理地址,使用walk函数返回对应的最底级页表项的地址,然后将需要映射的物理地址填入该页表项,设置相应的标志位。

8. 你提到可以将用户态页表塞到内核态里面,为什么要这样做,你是怎么实现的?

有两个好处,一是起到了一定的隔离作用,另一个是进程在内核态下可以使用mmu解析用户态的虚拟地址,而不是使用软件模拟翻译地址,减小时间开销。具体实现:首先在进程初始化时,建立一个独立的内核页表,并在fork时完成用户态和新建立内核页表的映射。并在用户申请内存时(sbrk)也进行映射。

9. 如何实现写时复制?

在内核初始化数组保存每个页表的引用计数,kfree时如果当前没有进程进行引用了再释放掉。fork时,只新建页表进行内存的映射,并标记父子进程的内存不可写,标记cow位。当父子进程中有进程需要写时,进入写时复制流程,分配一个新的内存块对原有内容进行拷贝,并更新页表映射的关系。

🔥 fork()底层执行流程,获取当前进程,然后调用allocproc() 创建新进程。最后使用 **uvmcopy()** 将父进程的页表复制给子进程,此外设置其他寄存器和返回值。

在xv6中,fork() 系统调用将父进程的所有地址空间内存复制到子进程中。如果父进程内存占用较大,而子进程执行exec() 系统调用转移到其他程序的执行,将会造成巨大的内存浪费。

fork写时复制涉及在父子进程fork() 操作时,并不会立即复制物理地址,而是让父进程和子进程的虚拟地址映射到同一块物理页面。只有当进程需要对这一页执行“写操作时”,才会将只读的物理页面复制一份进行修改。

  1. 首先是修改 uvcopy() 函数,在fork() 时会调用此函数进行父进程和子进程的页表复制。修改后,不再为子进程分配实际物理内存,只是将父进程的物理地址映射到子进程的虚拟地址空间中,同时设置“只读”标记,以及cow标记。
  2. 然后在子进程中,因为父子进程的共享内存页面设置为只读,当任何进程需要写这个页面时,便会触发15号缺页异常中断,在此中断处理逻辑中,为该进程复制一个新的物理内存提供写操作。
  3. 具体的内存申请我们编写一个cowalloc() 函数,传入页表和虚拟地址空间,返回分配的物理地址。cowallow执行的操作就很简单,找到子进程的虚拟地址空间对应的PTE,使用kalloc申请物理内存,然后使用mappages进行虚拟地址和物理地址的映射,返回虚拟内存地址。
  4. 进程在释放内存时需要考虑一个问题,此物理地址是否被其他进程的虚拟地址空间映射。因此还需要添加引用计数器,在 kfree() 时加入判断,只有该页面的引用计数为0后,才会实际释放该页面。