XV6_2020实验报告

[同济大学]

Xv6_2020实验报告

[os暑期实践学习]

软件学院  2152814周成杰

[2023-07]

Xv6_2020实验报告

2152814 周成杰

github仓库:xv6_2020

目录

1.环境搭建. 1

1.1. 准备linux子系统:. 1

1.2.配置qemu,根据说明文档配置安装即可:. 1

1.3.安装xv6:. 2

1.4.在WSL中使用vscode, 3

1.5.配置WSL与github的SSH连接. 3

2.测试xv6系统的使用. 4

2.1.源码准备:. 4

2.2.编译配置:. 5

2.3.测试结果:. 5

3.Lab1-Lab11实验请况. 6

3.1. Lab1: Xv6 and Unix utilities:. 6

3.1.0. pre: book-rescv-rev1教材的第一章练习:. 6

3.1.1. sleep(easy) 8

3.1.2. pingpong(easy) 8

3.1.3. primes(moderate/hard) 8

3.1.4. find(moderate) 10

3.1.5. xargs(moderate) 11

3.1.6. Lab1总实验结果. 13

3.2. Lab2: System calls 13

3.2.0. pre: book-rescv-rev1教材的第二章练习——gdb使用练习:. 13

3.2.1. System call tracing(moderate) 14

3.2.2. Sysinfo(moderate) 15

3.2.3. Lab2总实验结果. 17

3.3. Lab3: Page tables 17

3.3.1. Print a page table(easy) 17

3.3.2. A kernel page table per process(hard) 18

3.3.3. Simplify copyin/copyinstr 20

3.4. Lab4: Traps 22

3.4.0. 前提认识. 22

3.4.1. RISC-V assembly(easy) 23

3.4.2. Backtrace(moderate) 25

3.4.3. Alarm(Hard) 26

3.4.4. Lab4总实验结果. 29

3.5. Lab5: Xv6 Lazy page allocation 29

3.5.0. 前言. 29

3.5.1. Eliminate allocation from sbrk()(easy) 29

3.5.2. Lazy allocation(moderate) 30

3.5.3. Lazytests and Usertests(moderate) 33

3.5.4. Lab5总实验结果. 35

3.6. Lab6: Copy-on-Write Fork for xv6 35

3.6.0. 前言. 35

3.6.1. Implement copy-on write(hard) 36

3.6.2. Lab6总实验结果. 38

3.7. Lab7: Multithreading 39

3.7.1. Uthread: switching between threads(moderate) 39

3.7.2. Using threads(moderate) 40

3.7.3. Barrier(moderate) 41

3.8. Lab8: Locks 42

3.8.1. Memory allocator(moderate) 42

3.8.2. Buffer cache(hard) 43

3.8.3. Lab8总实验结果. 45

3.9. Lab9: File system 46

3.9.1. Large files(moderate) 46

3.9.2. Symbolic links(moderate) 47

3.9.3. Lab9总实验结果. 49

3.10. Lab10: Mmap 49

3.10.0. 前言. 49

3.10.1. mmap(hard) 50

3.10.2. Lab10总实验结果. 53

3.11. Lab11: Network 53

3.11.0. 前言. 53

3.11.1. 需要完成的工作/任务(hard) 54

3.11.2. Lab11总实验结果. 56

4.实验心得. 56

1.环境搭建

环境配置选择WSL在Windows的linux子系统,Linux系统选择推荐的Ubuntu20.0版本,具体的搭建方法:

1.1. 准备linux子系统:

  1. 使用具有管理员权限的Window PowerShell,执行如下命令:

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

  1. 安装ubuntu 20.04在Microsoft store中搜索安装即可,安装后第一次启动设置好用户名和密码即可

1.2.配置qemu,根据说明文档配置安装即可:

sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu

1.3.安装xv6:

  1. 克隆源码和本地编译完成:    

进入到WSL中自己的用户名的目录中:

xv6完整源码:

git clone git://github.com/mit-pdos/xv6-riscv.git

xv6_2020实验所用的源码:

git clone git://g.csail.mit.edu/xv6-labs-2020

  1. 本地编译:

通过cd 命令进入到xv6-labs-2020目录,切换到实验分支:

git checkout util

编译执行如下进入xv6系统:

make qemu

  如图即为成功进入系统,要退出系统使用: Ctrl-a x, 退出进入qemu监视器:Ctrl-a c, 退出qemu: q

1.4.在WSL中使用vscode,

windowsvsode中安装Remote WSL拓展插件即可,在vscode中连接到linux子系统后就可以使用:

ps:要在wsl的一个目录下直接使用 code . 命令进入Vscode必须要在系统的环境变量的配置正确的情况下才能使用(一般安装vscode使用installer方式就是正确的);否则使用vscode直接连接WSL即可

1.5.配置WSL与github的SSH连接

进入WSL后依次:

ssh-keygen //生成ssh key pair
//
将公钥(~/.ssh/id_rsa.pub添加到github账户)
eval &(ssh-agent -s) //开启wsl中的ssh-agent
ssh-add ~/.ssh/id_rsa //
添加ssh key的私钥到ssh agent
ssh -T git@github.com //
验证连接,出现"Hi, ***"表示连接成功  

注:实验所有代码的github仓库:xv6_2020

2.测试xv6系统的使用

2.1.源码准备:

vscode中连接wsl后,进入到/xv6-labs-2020/users目录中,创建一个自己的测试文件copy.c

// copy.c: 将控制台输入的内容输出到控制台,用于熟悉操作

#include "./kernel/types.h"
#include "./user/user.h"

int main(){
   
char buf[64];

   
while(1){
     
//console读取输入,通过system callread函数实现
      int n = read(0,buf,sizeof(buf));
     
//无输入或字符串为quit就结束程序
      if(n <=0 || buf[0] == 'q'){
         
break;
      }
     
//console的输入输出到控制台,通过system callwrite函数实现
      write(1,buf,n);
  }
 
// exit退出程序
  exit(0);
}

2.2.编译配置:

Makefile中的如下位置配置copy程序:

2.3.测试结果:

回到wsl中的相应位置执行make qemuls查看和运行和都没有问题:

3.Lab1-Lab11实验请况

3.1. Lab1: Xv6 and Unix utilities

3.1.0. pre: book-rescv-rev1教材的第一章练习:

  • 题目:

写一个程序,使用unix system calls在两个进程间”ping-pong“一个字节,使用一对pipe,一个pipe对应一个方向,另外一个pipe对应另外一个方向。

  • 问题分析和解决:

创建两个管道设在一个父进程和对应的子进程之间,利用fork和pipe函数完成任务。

具体的核心代码:

ps:代码位置:xv6_2020仓库的util分支中,新建/user/pingpong.c;修改Makefile150左右添加编译配置

  • 测试结果:

3.1.1. sleep(easy)    

  1. 实验目的
      写一个用户程序,调用sleep system call实现,执行sleep 10,表示程序等待10个时钟周期。  
  2. 实验内容
      使用xv6系统的系统调用函数sleep即可,主要处理输入参数问题等错误处理,判断是否带有参数,参数是否符合要求
    ps:代码位置:xv6_2020仓库的util分支中,新建/user/sleep.c;修改Makefile的150左右添加编译配置
  3. 实验结果
      进行了错误测试:不带参、多参、一参非数字等情况符合要求,能够sleep 10成功,通过了grade测试:

3.1.2. pingpong(easy)
即book-rescv-rev1教材的第一章练习,已经在上面给出    

3.1.3. primes(moderate/hard)

  1. 实验目的
      使用pipe和fork来设置管道。第一个进程将数字2到35输入管道。对于每个素数,您将安排创建一个进程,该进程通过一个管道从其左邻居读取数据,并通过另一个管道向其右邻居写入数据。由于xv6的文件描述符和进程数量有限,因此第一个进程可以在35处停止。
  2. 实验内容
      结合给出的算法提示:

 
  按照教材中的pipe章节中的使用方法,在该题中,子进程用于数据处理和筛选素数,父进程用于递归创建进程,整个进程的输入输出由一个管道控制文件描述符,父进程等待子进程对一个数据的筛选并写入后继续进行,不断地递归调用,最终筛选到管道中没有数据为止。
具体的核心代码为:

 
ps:代码位置:xv6_2020仓库的util分支中,新建/user/primes.c;修改Makefile的150左右添加编译配置

  1. 实验结果

3.1.4. find(moderate)

  1. 实验目的
      实现一个简单的文件系统搜索功能,在本系统的目录树中寻找出具有特定名称的具体文件。
  2. 实验内容
      核心思想就是使用递归,并且注意在递归之前判断裁剪一些分支,比如要求的不在.和..目录下递归。还要注意如何正确的处理字符串。
    核心代码:


ps:代码位置:xv6_2020仓库的util分支中,新建/user/find.c;修改Makefile的150左右添加编译配置

  1. 实验结果

3.1.5. xargs(moderate)

  1. 实验目的
      实现unix中的xargs命令的基本功能,将前一个命令的结果作为参数给xargs指定的命令执行
  2. 实验内容
      要实现xargs,主要思想是提取前一个作为参数的命令结果,然后构建一个子进程将读取的参数传给xargs指定的命令运行,使用wait等待子进程结束。
    核心代码:

 
ps:代码位置:xv6_2020仓库的util分支中,新建/user/xargs.c;修改Makefile的150左右添加编译配置

  1. 实验结果


     

3.1.6. Lab1总实验结果


ps:代码位置:xv6_2020仓库的util分支

  1. 实验体会:
        要很好的完成本实验必须,了解xv6系统的文件描述符和管道的概念,在xv6中文件描述符是一个很好的抽象,简化了文件的定位。结合管道的概念,我们可以将一个文件描述符指定的文件空间作为管道的缓冲区,这是上面几个实验中都有所体现的。
      尽管xv6系统的实验已经很接近底层代码了,但是仍然是C语言的编写,让我们体会一个经典操作系统的不断封装提升的过程,也让我思考xv6系统中使用的系统调用的具体实现又是怎样的?最终应当是回归到物理层面的电路等层面上吧。

3.2. Lab2: System calls

3.2.0. pre: book-rescv-rev1教材的第二章练习——gdb使用练习:

  1. 按序在xv6-labs-2020中执行下面的命令:

  1. 结果:

未能成功,提示Program not run,猜测可能是riscv64的版本问题,但是我尝试后又找不到正确的riscv64-linux-gnu-gdb,结果如下图,暂未找到解决办法:

3.2.1. System call tracing(moderate)  

  1. 实验目的
      添加trace系统调用实现跟踪功能。需要一个参数和一个整数的mask掩码,需要指定跟踪的系统调用。trace系统调用应该实现对调用它的进程和随后派生的任何子进程启用跟踪,但不应影响其他的进程。
  2. 实验内容
      - 按照提示一步步操作即可
      - 作为一个系统调用,需要在kernel/syscall.h中定义一个系统调用的序号,模仿其他定义为:#define SYS_trace 22
      - 在对应的文件中添加完相应的声明(包括fork代码的补全,和名称数组的构建),可以根据vscode的报错提示一步步完善,添加
      - 核心代码(syscall函数中打印追踪信息):

 
ps:代码位置:xv6_2020仓库的syscall分支——/kernel/syscall.h的25行;/user/user.h的27行;/user/usys.pl的41行;/kernel/syscall.c的103、130、135、153行;/kernel/sysproc.c的101行;/kernel/syscall.h的25行;/kernel/proc.c中的fork函数;Makefile的150左右

  1. 实验结果
     


      本实验主要是让我们理解xv6系统的一个系统调用执行时的具体过程,涉及到汇编和寄存器的相关内容。

3.2.2. Sysinfo(moderate)

  1. 实验目的
      在该xv6系统中添加一个sysinfo函数,手机正在运行的系统信息,该函数需要一个struct sysinfo结构体作为参数,我们要做的就是设计这个结构体作为参数,配置好相应的环境。
  2. 实验内容
      本实验和上一个trace一样,需要在多个文件中配置相应的依赖,根据提示完成函数的设计编写,本实验主要涉及到三个函数:一个nproc计算进程数,一个free_mem计算空闲空间字节数,一个sysinfo调用copyout函数输出信息。
      实验过程中注意文件查找,理解空闲链表结构;遇到一个问题是在user/user.h中声明函数sysinfo时,注意结构体sysinfo是一个定义为仅内部可见的结构体,必须要在同一文件中提前声明才能用于定义参数。
      核心代码:

 
ps:代码位置:xv6_2020仓库的syscall分支——/kernel/syscall.h的26行;/user/user.h的28行;/user/usys.pl的42行;/kernel/syscall.c的104、131、140;/kernel/sysproc.c的115行;/kernel/syscall.h的26行;/kernel/proc.c的726行;/kernel/defs.h的108行;新增/user/sysinfo.c;Makefile的150行左右

  1. 实验结果
     

       

3.2.3. Lab2总实验结果

 


ps:代码位置:xv6_2020仓库的syscall分支

  1. 实验体会
      本实验主要是将系统调用的内部实现过程展现出来,我们从目标的系统调用一层一层的往上找,直到汇编过程和寄存器设置等等。主要是让我们理解一个系统实现其功能的内部逻辑.

3.3. Lab3: Page tables

3.3.1. Print a page table(easy)

  1. 实验目的

了解RISC-V页表,编写一个打印页表内容的函数

  1. 实验内容
  • 首先根据提示在/kernel/exec.c的121行添加对应代码
  • 本实验只要仿照freewalk函数的方法完成递归遍历即可,我这里将递归函数提出为printwalk函数,由vmprint调用:

 ps:代码位置:xv6_2020仓库的pgtbl分支——在/kernel/vm.c的最后(458)增添函数vmprint;  /kernel/defs.h的181行; /kernel/exec.c的121行

  1. 实验结果

启动xv6时打印页表信息成功:

本实验较为简单,需要改的主体部分就是一个打印页表内容的函数仿写freewalk函数就行了。

3.3.2. A kernel page table per process(hard)

  1. 实验目的

实现允许内核直接解引用用户指针。

Xv6有一个单独的用于在内核中执行程序时的内核页表。内核页表直接映射(恒等映射)到物理地址,也就是说内核虚拟地址x映射到物理地址仍然是xXv6还为每个进程的用户地址空间提供了一个单独的页表,只包含该进程用户内存的映射,从虚拟地址0开始。因为内核页表不包含这些映射,所以用户地址在内核中无效。因此,当内核需要使用在系统调用中传递的用户指针(例如,传递给write()的缓冲区指针)时,内核必须首先将指针转换为物理地址。

  1. 实验内容
  • 根据第一条提示在/kernel/proc.h的proc结构中增加一个字段kernel_pagetable
  • 根据第二条提示参考kvminit函数增加一个kvm_map_pagetable函数并提供一个uvmmp函数用于映射kernel_pagetable,这将解决陷入内核的问题
  • 第三条提示表示每个进程独立的内核页表需要有独立的内核栈,在原来的xv6设计中,所有处于内核态的进程都共享了同一个页表,就意味着共享同一个地址空间。提示说明修改 procinit中的内容,然后,将部分内容实现到分配进程时的函数allocproc
  • 根据第四条提示,实现在进程进入内核时切换到内核页表
  • 最后,进程从内核态退出后,需要释放独立的内核页表,这里模仿freewalk函数实现清空页表。
  • 核心代码:
  • ps:代码位置:xv6_2020仓库的pgtbl分支——/kernel/proc.h的118行;在/kernel/vm.c的文件末尾;  /kernel/proc.c的131、150、182、757行;/kernel/defs.h的183行
  1. 实验结果

通过本实验可以(1)了解虚拟内存机制:在xv6中,内存被划分为固定大小的页,并使用虚拟内存机制将物理内存映射到每个进程的虚拟地址空间中;(2)了解页表和地址翻译:页表是一种数据结构,用于将虚拟地址转换为物理地址。在xv6中,每个进程都有一个自己的页表,用于将其虚拟地址映射到物理地址。(3)实现内核页表:在xv6中,内核使用共享的页表来管理整个系统的内存,这种设计可能会导致安全问题。通过在每个进程中维护一个自己的内核页表,可以提高内存安全性和灵活性。(4)理解内存保护机制:通过在内核页表中标记只读页,可以防止进程意外修改内核数据。类似地,通过在页表中标记只执行页,可以防止恶意代码执行。(5)熟悉操作系统内核的实现:通过完成这个实验,你将更深入地了解操作系统内核的实现,包括虚拟内存、页表、地址翻译、内存保护等方面。

3.3.3. Simplify copyin/copyinstr

  1. 实验目的

在内核页表中维护一个用户态页表的副本,使内核态可以对用户态传进来的指针进行解引用。原来的copyin是通过软件模拟访问页表的过程获取物理地址的,在内核页表映射副本后,可以利用CPU的硬件寻址功能进行寻址。

  1. 实验内容
  • 根据提示,首先添加复制函数。需要注意在内核模式下,无法访问设置了PTE_U的页面,所以我们要将其移除。

核心复制函数:

  • 接着需要在内核更改进程的用户映射的每一处包括forkexecsbrk都复制一份到进程的内核页表
  • ps:代码位置:xv6_2020仓库的pgtbl分支——/kernel/proc.h的118行;在/kernel/vm.c的458行,382行;  /kernel/proc.c的383、150、182、757行;/kernel/defs.h的160行

  1. 实验结果

Lab3实验针对页表的问题给出几个问题,由浅入深的向我们展示了用户态和内核态页表的区别和具体的工作形式。

3.4. Lab4: Traps

3.4.0. 前提认识

  1. 基础
  • 系统调用、异常和中断

有三种事件会导致CPU搁置普通指令的执行,强制将控制权转移给处理该事件的特殊代码。一种情况是系统调用,当用户程序执行ecall指令要求内核为其做某事时。另一种情况是异常:一条指令(用户或内核)做了一些非法的事情,如除以零或使用无效的虚拟地址。第三种情况是设备中断,当一个设备发出需要注意的信号时,例如当磁盘硬件完成一个读写请求时。

  • xv6 book使用trap作为这些情况的通用术语。通常,代码在执行时发生trap,之后都会被恢复,而且不需要意识到发生了什么特殊的事情。也就是说,我们通常希望trap是透明的;这一点对于中断来说尤其重要,被中断的代码通常不会意识到会发生trap。通常的顺序是:trap迫使控制权转移到内核;内核保存寄存器和其他状态,以便恢复执行;内核执行适当的处理程序代码(例如,系统调用实现或设备驱动程序);内核恢复保存的状态,并从trap中返回;代码从原来的地方恢复执行。
  • Xv6 trap 处理分为四个阶段:RISC-V CPU采取的硬件行为,为内核C代码准备的汇编入口,处理trap的C 处理程序,以及系统调用或设备驱动服务。
  1. RISC-V trap machinery

每个RISC-V CPU都有一组控制寄存器,内核写入这些寄存器来告诉CPU如何处理trap,内核可以通过读取这些寄存器来发现已经发生的trap。RISC-V文档包含了完整的叙述[1]。riscv.h(kernel/riscv.h:1)包含了xv6使用的定义。

这里是最重要的寄存器的概述:

  • stvec:内核在这里写下trap处理程序的地址;RISC-V跳转到这里来处理trap。
  • sepc:当trap发生时,RISC-V会将程序计数器保存在这里(因为PC会被stvec覆盖)。sret(从trap中返回)指令将sepc复制到pc中。内核可以写sepc来控制sret的返回到哪里。
  • scause:RISC -V在这里放了一个数字,描述了trap的原因。
  • sscratch:内核在这里放置了一个值,在trap处理程序开始时可以方便地使用。
  • sstatus:sstatus中的SIE位控制设备中断是否被启用,如果内核清除SIE,RISC-V将推迟设备中断,直到内核设置SIE。SPP位表示trap是来自用户模式还是supervisor模式,并控制sret返回到什么模式。

当需要执行trap时,RISC-V硬件对所有的trap类型(除定时器中断外)进行以下操作:

  1. 如果该trap是设备中断,且sstatus SIE位为0,则不执行以下任何操作。
  2. 通过清除SIE来禁用中断。
  3. 复制pc到sepc。
  4. 将当前模式(用户态或特权态)保存在sstatus的SPP位。
  5. 在scause设置该次trap的原因。
  6. 将模式转换为特权态。
  7. 将stvec复制到pc。
  8. 从新的pc开始执行。

注意,CPU不会切换到内核页表,不会切换到内核中的栈,也不会保存pc以外的任何寄存器。内核软件必须执行这些任务。

  1. Traps from user space

在用户空间执行时,如果用户程序进行了系统调用(ecall指令),或者做了一些非法的事情,或者设备中断,都可能发生trap。来自用户空间的trap的处理路径是uservec(kernel/trampoline.S:16),然后是usertrap(kernel/trap.c:37);返回时是usertrapret(kernel/trap.c:90),然后是userret(kernel/trampoline.S:16)。

RISC-V硬件在trap过程中不切换页表,所以用户页表必须包含uservec的映射,即stvec指向的trap处理程序地址。uservec必须切换satp,使其指向内核页表;为了在切换后继续执行指令,uservec必须被映射到内核页表与用户页表相同的地址。

我认为这一节就是在说用户态和内核态共享了两个页,分别是trapolinetrapfram,前者实现trap时对指令的访问,后者实现trap时保存相关参数,实现保护作用。

3.4.1. RISC-V assembly(easy)

  1. 实验目的

本实验不用进行代码的编写,需要我们用自己对RISC-V指令的理解回答一些相关汇编指令、寄存器等的问题。

  1. 实验内容

回答问题:

  1. 哪些寄存器保存函数的参数?例如,在mainprintf的调用中,哪个寄存器保存13?

Aa0-a7存放函数的参数;a2

  1. main的汇编代码中对函数f的调用在哪里?对g的调用在哪里(提示:编译器可能会将函数内联)

A:没有题给所说的调用代码;g(x)被内链到f(x)中,然后f(x)又被进一步内链到main()

  1. printf函数位于哪个地址?

A0x0000000000000628, main 中使用 pc 相对寻址来计算得到这个地址。

  1. mainprintfjalr之后的寄存器ra中有什么值?

A0x0000000000000038, jalr 指令的下一条汇编指令的地址。

  1. 运行代码,回答问题

1)运行:

unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

程序的输出是什么?这是将字节映射到字符的ASCII码表。输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i设置成什么?是否需要将57616更改为其他值?

A “He110 World”; 0x726c6400; 不需要,57616 的十六进制是 110,无论端序(十六进制和内存中的表示不是同个概念)

2在下面的代码中,“y=”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?

printf("x=%d y=%d", 3);

A输出的是一个受调用前的代码影响的“随机”的值。因为 printf 尝试读的参数数量比提供的参数数量多。 第二个参数 3 通过 a1 传递,而第三个参数对应的寄存器 a2 在调用前不会被设置为任何具体的值,而是会 包含调用发生前的任何已经在里面的值。

  1. 实验结果

本实验就是回答RISC-V相关的问题,执行给出的代码观察分析即可,让读者熟悉RISC-V指令。

3.4.2. Backtrace(moderate)

  1. 实验目的

这个实验的目的是让我们了解程序调用的过程,对进程的栈的结构有所了解。

  1. 实验内容
  • 根据提示

在这里插入图片描述

fp指向当前栈帧的开始地址,sp指向当前栈帧的结束地址,栈从高地址向低地址增长。

栈帧中从高地址到低地址第一个8字节fp-8是return address, 当前调用层的返回地址

栈帧中从高地址到低地址第二个8字节fp-16是previous adress, 即调用当前函数的上一层栈帧的fp开始地址。(这也是第三条提示的内容)

剩下的为保存的寄存器、局部变量等。一个栈帧的大小不固定,但是至少 16 字节。

在 xv6 中,使用一个页来存储栈,如果 fp 已经到达栈页的上界,则说明已经到达栈底

因此这个实验的目的就是追踪当前执行栈的上一层。打印对应的返回地址。

第二条提示说明了如何获取当前栈帧的fp, 当前执行程序栈帧地址在寄存器s0中。

  • 根据提示把题给函数添加到kernel/riscv.h
  • 最后一条提示说明了每个栈会分配一个页面,可以通过 PGROUNDDOWN(fp) 和 PGROUNDUP(fp)获取fp页面的最高和最低地址,只有当fp在这个范围内时才是合法的。以此可以实现循环返回调用的上一层。最后添加自己的backtrace函数:

  • Ps:代码位置:xv6_2020 仓库的traps分支——/kernel/printf.c的138行;/kernel/defs.h的83行;/kernel/risc.h的405行;/kernel/sysproc.c的76行
  1. 实验结果

可以看到,尽管具体的地址略有不同,但针对几个文件的路径查找都成功了,可以认定我们的backtrace成功。本实验主要是通过对文件路径查询的过程让我们了解进程的栈结构。

3.4.3. Alarm(Hard)

  1. 实验目的

利用时钟中断计时,根据sigalarm中的参数,每隔一定时间输出alarm

PS:程序计数器的过程是这样的:

  1. ecall指令中将PC保存到SEPC
  2. usertrap中将SEPC保存到p->trapframe->epc
  3. p->trapframe->epc加4指向下一条指令
  4. 执行系统调用
  5. usertrapret中将SEPC改写为p->trapframe->epc中的值
  6. sret中将PC设置为SEPC的值
  1. 实验内容
  • 根据提示struct proc中增加字段,同时记得在allocproc中将它们初始化为0,并在freeproc中也设为0
  • 然后在sys_sigalarm中读取参数,修改/kernel/trap.c 的usertrap函数,至此通过test0
  • 接下来要通过test1test2,要解决的主要问题是寄存器保存恢复和防止重复执行的问题。考虑一下没有alarm时运行的大致过程:
  1. 进入内核空间,保存用户寄存器到进程陷阱帧
  2. 陷阱处理过程
  3. 恢复用户寄存器,返回用户空间

而当添加了alarm后,变成了以下过程

  1. 进入内核空间,保存用户寄存器到进程陷阱帧
  2. 陷阱处理过程
  3. 恢复用户寄存器,返回用户空间,但此时返回的并不是进入陷阱时的程序地址,而是处理函数handler的地址,而handler可能会改变用户寄存器

因此我们要在usertrap中再次保存用户寄存器,当handler调用sigreturn时将其恢复,并且要防止在handler执行过程中重复调用

  • 在通过test0的基础上按照类似地步骤进一步修改。
  • 核心代码:
  • Ps:代码位置:xv6_2020 仓库的traps分支——/kernel/proc.h的118行;/kernel/proc.c的116、173行;/kernel/sysproc.c的105行;/kernel/trap.c的81行;/user/user.h的43行;按需修改/user/usys.pl、/kernel/syscall.c、/kernel/syscall.h
  1. 实验结果

本实验复习回顾了前面的系统调用部分的内容,实现时钟中断和输出alarm.

3.4.4. Lab4总实验结果

本实验探索如何使用陷阱实现系统调用。首先使用栈做了一个热身练习,然后实现一个用户级陷阱处理的示例。关于核心需要阅读的代码:kernel/trampoline.S:涉及从用户空间到内核空间再到内核空间的转换的程序集,kernel/trap.c:处理所有中断的代码。

3.5. Lab5: Xv6 Lazy page allocation

3.5.0. 前言

这次实验主要实现Lazy allocation的功能,即进程在动态分配内存的时候先不分配,等到要用到发生缺页中断的时候再实际分配,核心是实现缺页中断的处理。xv6的文档介绍了三种缺页中断的应用,第一为Copy on write,即fork的时候先不复制内存,等到要用到发生缺页中断的时候再实际分配;第二为硬盘虚拟内存,就是当内存不够大的时候将一部分硬盘区域当作内存交换区,虚拟地址只映射到一个无效位置,当访问该虚拟地址发生缺页中断时再把一个页的内容保存进磁盘,然后从磁盘中加载当前这个虚拟地址指向的实际内容;第三就是本实验的内容。

3.5.1. Eliminate allocation from sbrk()(easy)

  1. 实验目的

首项任务是删除sbrk(n)系统调用中的页面分配代码(位于sysproc.c中的函数sys_sbrk())。sbrk(n)系统调用将进程的内存大小增加n个字节,然后返回新分配区域的开始部分(即旧的大小)。新的sbrk(n)应该只将进程的大小(myproc()->sz)增加n,然后返回旧的大小。它不应该分配内存——因此您应该删除对growproc()的调用(但是您仍然需要增加进程的大小!)。

  1. 实验内容

本实验根据提示我们仅仅需要更改/kernel/sysproc.c中的sys_sbrk函数即可,完成题目要求的删除页面分配代码,具体的实现是:(可以看到仅仅是将进程大小增加了n(ps:考虑n小于0的情况是后面的人物的内容))

  • Ps:代码位置:xv6_2020 仓库的lazy分支——/kernel/sysproc.c的50行左右
  1. 实验结果

上面usertrap(): …这条消息来自trap.c中的用户陷阱处理程序;它捕获了一个不知道如何处理的异常。stval=0x0..04008表示导致页面错误的虚拟地址是0x4008

3.5.2. Lazy allocation(moderate)

  1. 实验目的

要求实现对缺页中断的处理,具体的任务:

  1. 实验内容
  • 因为在sbrk的时候仅仅指扩大了进程的虚拟地址区域,所以在访问这些虚拟地址时会发生缺页中断,这里就需要在发生缺页中断的时候分配物理内存然后映射,中断处理函数usertrap()对缺页中断进行处理:

Ps:代码位置:xv6_2020 仓库的lazy分支——/kernel/trap.c的73行左右

  • 这里把缺页中断的实际处理过程抽象成了一个函数,实际上仅从任务2考虑是没有必要的,但是任务3中还需要对copyincopyout这些函数中发生缺页的情况进行处理,所以抽象成一个函数方便各处调用。

handle_page函数在proc.c里,因为这里已经包含了所需要的头文件:

Ps:代码位置:xv6_2020 仓库的lazy分支——/kernel/proc.c的725行左右

  • 这些return -1的情况也是任务3的内容,任务2可以忽略,主要都是借鉴函数uvmalloc。然后修改一下uvmunmap(),即把一些因为缺页导致的panic跳掉了,因为这些页从来就没分配过,也就不用释放:

Ps:代码位置:xv6_2020 仓库的lazy分支——/kernel/vm.c的190行左右

  1. 实验结果

    和上一个任务对比,运行echo hi已经能够正常运行。表示本次任务应该正确。

3.5.3. Lazytests and Usertests(moderate)

  1. 实验目的

这个任务主要是把上面两个任务遗留的一些不合法情况进行处理。

  1. 实验内容
  • 第一是sbrk的参数为负数的问题,根据growproc函数的内容,对参数为负数的情况就是释放参数绝对值大小的内存,仿造growproc()就行了,见上面的代码。uvmdealloc本身不用修改,因为内部就是调用uvmunmap的。
  • 第二是缺页中断中当虚拟地址不合法时应该直接返回并杀掉进程,不合法包含两种情况,一是虚拟地址太大,大出了进程所申请的内存(不管实际有没有分配),因为进程虚拟地址从0开始,所以只要保证虚拟地址小于p->sz即可;而是虚拟地址太小,比进程的栈顶还低(注意栈是从高往低增长的),这就需要知道栈顶的位置,查看测试程序usertests,发现它获取栈顶的方法就是读sp寄存器,但是缺页中断的处理是在内核态,sp指向的也是内核栈的栈顶,想要获得用户栈的栈顶,可以借助进程的中断帧来实现,即读取p->trapframe->sp,需要保证虚拟地址大于等于这个值。杀掉进程可以观察usertrap函数的其他位置,发现只要令p->killed=1即可,见上面的代码。
  • 第三是如果申请物理内存失败时也要杀掉进程,加上映射失败,照着uvmalloc里写就行了。
  • 第四是fork的时候复制到缺页的虚拟地址时的处理,注意到fork的这部分是调用的uvmcopy,所以改uvmcopy,和uvmunmap一样,缺页导致的panic跳掉:

Ps:代码位置:xv6_2020 仓库的lazy分支——/kernel/vm.c的333行左右

  • 第五是readwrite文件的时候如果传入了一个缺页的虚拟地址(在将文件读入内存和将内存写入文件时需要传入地址),追踪这两个函数的过程可以发现最终处理地址调用的是copyincopyinstrcopyout函数,注意到这几个函数会先walk一下传入的虚拟地址,如果得不到物理地址就直接返回失败,而不会经过缺页中断的过程,所以直接加入代码让其在判断得不到物理地址的情况下调用handle_page函数即可:

Ps:代码位置:xv6_2020 仓库的lazy分支——/kernel/vm.c的440行左右几个函数copyin、copyout、copyinstr的同一位置

  1. 实验结果

        

可以看到Lazytests和usertests都输出ALL TESTS PASSED,证明解决方案正确。

3.5.4. Lab5总实验结果

总结缺页中断的发生时刻应该是在MMU访问到一个PTE_V位为0PTE时,在xv6中这个PTE的其他位是没有意义的,而在riscv-pk(用在spike模拟器上的代理内核)则让PTE的其他位指向一个标记结构体,里面包含了这个缺页的信息,比如该缺页是否是因为内存被置换到硬盘上了,置换到了哪个位置等信息,这样就使得该系统可以处理多种原因导致的缺页中断,而xv6应该是不支持硬盘虚拟内存的。

总的make grade如下:

3.6. Lab6: Copy-on-Write Fork for xv6

3.6.0. 前言

虚拟内存提供了一定程度的间接寻址:内核可以通过将PTE标记为无效或只读来拦截内存引用,从而导致页面错误,还可以通过修改PTE来更改地址的含义。在计算机系统中有一种说法,任何系统问题都可以用某种程度的抽象方法来解决。Lazy allocation实验中提供了一个例子。本实验探索另一个例子:写时复制分支(copy-on write fork)。

问题:xv6中的fork()系统调用将父进程的所有用户空间内存复制到子进程中。如果父进程较大,则复制可能需要很长时间。更糟糕的是,这项工作经常造成大量浪费;例如,子进程中的fork()后跟exec()将导致子进程丢弃复制的内存,而其中的大部分可能都从未使用过。另一方面,如果父子进程都使用一个页面,并且其中一个或两个对该页面有写操作,则确实需要复制。

为解决上述问题:copy-on-write (COW) fork()的目标是推迟到子进程实际需要物理内存拷贝时再进行分配和复制物理内存页面。COW fork()只为子进程创建一个页表,用户内存的PTE指向父进程的物理页。COW fork()将父进程和子进程中的所有用户PTE标记为不可写。当任一进程试图写入其中一个COW页时,CPU将强制产生页面错误。内核页面错误处理程序检测到这种情况将为出错进程分配一页物理内存,将原始页复制到新页中,并修改出错进程中的相关PTE指向新的页面,将PTE标记为可写。当页面错误处理程序返回时,用户进程将能够写入其页面副本。COW fork()将使得释放用户内存的物理页面变得更加棘手。给定的物理页可能会被多个进程的页表引用,并且只有在最后一个引用消失时才应该被释放。

3.6.1. Implement copy-on write(hard)

  1. 实验目的
  2. 实验内容
  • 首先是uvmcopy的部分,原来的操作是从老页表中获得虚拟地址对应的物理地址,创建一个新物理页,然后将老物理地址的内容复制到新物理页,再把新物理页通过新页表映射到虚拟地址,现在就要改成直接将老物理地址通过新页表映射到虚拟地址,同时需要将老页表和新页表对应底层pte抹去PTE_W为并添加PTE_C位,这里的PTE_C是我自己定义的一个标志位。根据Riscv的标准,pte的低10位作为标志位,其中的0-7位是包括PTE_V、PTE_W之类已经被用掉的标志位,8-9位是可供用户自定义使用的标志位,这里我选取第8位,即PTE_C = 1L << 8。0表示该pte没有用在copy on write中,1表示有,这样在处理缺页中断的时候就比较方便了,只要该pte的PTE_C位为0,说明这次缺页中断的原因不是copy on write,而是真的缺页,就可以直接返回错
  • 在/kernel/kalloc.c中进行如下修改

定义引用计数的全局变量ref,其中包含了一个自旋锁和一个引用计数数组,由于ref是全局变量,会被自动初始化为全0。

使用自旋锁是考虑到:进程P1和P2共用内存M,M引用计数为2,此时CPU1要执行fork产生P1的子进程,CPU2要终止P2,那么假设两个CPU同时读取引用计数为2,执行完成后CPU1中保存的引用计数为3,CPU2保存的计数为1,那么后赋值的语句会覆盖掉先赋值的语句,从而产生错误

在kinit中初始化ref的自旋锁

修改kalloc和kfree函数,在kalloc中初始化内存引用计数为1,在kfree函数中对内存引用计数减1,如果引用计数为0时才真正删除

添加四个函数,这些函数中用到了walk,记得在defs.h中添加声明,最后也需要将这些函数的声明添加到defs.h,在cowalloc中,读取内存引用计数,如果为1,说明只有当前进程引用了该物理内存(其他进程此前已经被分配到了其他物理页面),就只需要改变PTE使能PTE_W;否则就分配物理页面,并将原来的内存引用计数减1。该函数需要返回物理地址,这将在copyout中使用到。

修改freerange

核心的一些代码:

  

  • 修改uvmcopy,不为子进程分配内存,而是使父子进程共享内存,但禁用PTE_W,同时标记PTE_F,记得调用kaddrefcnt增加引用计数;修改usertrap

  

  • copyout中处理相同的情况,如果是COW页面,需要更换pa0指向的物理地址
  • Ps:代码位置:xv6_2020 仓库的cow分支——/kernel/risc.c的383行;/kernel/kalloc.c;/kernel/trap.c的115;/kernel/vm.c的323

  1. 实验结果

3.6.2. Lab6总实验结果

本实验旨在通过使用COW技术来减少进程之间的内存复制,从而提高操作系统的性能和效率。通过实现COW机制,我们能够更好地理解现代操作系统中内存管理的实现方式。COW机制是一种基于写时复制的技术,它可以通过共享物理页来减少内存复制,从而提高系统的性能和效率。在实现COW机制时,我们需要理解内存映射、虚拟内存、页表等概念,以及如何使用这些技术来实现COW机制。更深入地了解了操作系统的性能优化技术,COW机制是一种常用的性能优化技术,它可以在一定程度上减少内存复制和内存分配的次数,从而提高操作系统的性能和效率。在实现COW机制时,我们需要考虑如何平衡内存使用和性能优化的需求,并根据具体的场景选择合适的技术来实现COW机制。提高了自己的操作系统内核编程能力,在实现COW机制时,我们需要深入了解操作系统的内核实现,并掌握一些常用的内核编程技术,例如内存管理、进程管理、文件系统等。这将有助于我们更好地理解操作系统的实现和设计,并为我们未来从事操作系统相关开发工作打下坚实的基础。

3.7. Lab7: Multithreading

3.7.1. Uthread: switching between threads(moderate)

  1. 实验目的

为用户级线程系统设计上下文切换机制,然后实现它。为了让您开始,您的xv6有两个文件:user/uthread.cuser/uthread_switch.S,以及一个规则:运行在Makefile中以构建uthread程序。uthread.c包含大多数用户级线程包,以及三个简单测试线程的代码。线程包缺少一些用于创建线程和在线程之间切换的代码。

  1. 实验内容

这个任务要求对一个程序填空,这个程序在用户层面实现了多线程的调度。但实际上这个调度和xv6内核的进程调度非常相似。首先是对thread_schedule函数进行填空,只需要调用切换函数就行了,根据文档的说明,这个切换函数是保存当前线程用到的所有callee-saved寄存器,然后将这些寄存器全部赋值为下一个线程里保存的值,联想到xv6内核里的context结构体就是用来保存这些寄存器的,所以直接复制过来并作为thread结构体的一个属性。

  • 首先定义thread的上下文:

Ps:代码位置:xv6_2020 仓库的thread分支——/user/uthread.c的14行;

  • 此时我们需要修改thread的结构体,增添上tcontext上下文这个字段。
  • 根据提示,现在我们需要模仿/kernel/switch.S在/kernel/uthread_swtch.S写入:

   Ps:代码:xv6_2020 ——/user/uthread_switch.S

  • 修改thread_scheduler,添加线程切换语句;在thread_create中对thread结构体做一些初始化

Ps:代码位置:xv6_2020 仓库的thread分支——/user/uthread.c的95、115行

  1. 实验结果

3.7.2. Using threads(moderate)

  1. 实验目的

这个任务要求解决一个程序中的竞争问题。手册上的具体要求:

  1. 实验内容
  • 根据提示,需要构建散列桶。通常两个线程的竞争出现在同时写的过程中,所以把put函数用锁包起来,同时为了速度,一个桶弄一个锁:

Ps:代码位置:xv6_2020 仓库的thread分支——/notxv6/ph.c的50行左右的put函数

  1. 实验结果

根据手册,我们make grade后的ph_safe和ph_fast通过,说明本任务测试成功。

3.7.3. Barrier(moderate)

  1. 实验目的

这个任务要求解决一个程序中出现的同步问题。

  1. 实验内容

核心是理解pthread_cond_wait这个函数的功能。pthread_cond_wait这个函数在调用时会释放锁,隐含的意思就是在执行这个函数前必须先锁上;函数在阻塞结束被唤醒时会获取锁,隐含的意思就是在这个函数调用结束后需要释放锁:

Ps:代码位置:xv6_2020 仓库的thread分支——/notxv6/barrier.c的对应位置

  1. 实验结果

本次实验基本没涉及操作系统的理论,重点是并行程序的应用。对应我们操作系统课在进程(线程)这一章节的竞争、同步、死锁这些应用上的问题,是操作系统中的重点部分。

3.8. Lab8: Locks

3.8.1. Memory allocator(moderate)

  1. 实验目的

任务要求给物理内存分配程序重新设计锁,使得等待锁时的阻塞尽量少。

  1. 实验内容
  • kmem定义为一个数组,包含NCPU个元素,即每个CPU对应一个
  • CPU的数量将空闲内存分组,分配内存的时候优先从当前所用CPU所管理的空闲内存中分配,如果没有则从其他CPU的空闲内存中获取,这样就可以把原来的锁拆开,每个CPU各自处理自己的空闲内存时只要锁上自己的锁就行了:

 

Ps:代码位置:xv6_2020 仓库的lock分支——/kernel/kalloc.c

  1. 实验结果

    Usertests的测试全部通过,说明该阶段的任务的解决方案正确。

3.8.2. Buffer cache(hard)

  1. 实验目的

这个任务要求给硬盘缓存分配程序重新设计锁,使得等待锁时的阻塞尽量少。

  1. 实验内容
  • 因为硬盘缓存包含遍历查找操作,即查找当前硬盘块是否已被缓存,显然这时就不能把缓存也按CPU进行分配,加上这个任务的操作也比较复杂,因此比上个任务多了很多问题。
  • 根据提示定义哈希桶结构
  • 接着在binit中:初始化散列桶的锁;将所有散列桶的head->prevhead->next都指向自身表示为空;将所有的缓冲区挂载到bucket[0]桶上,代码如下:

Ps:代码位置:xv6_2020 仓库的lock分支——/kernel/bio.c的相应位置

  • 现在在buf.h中增加新字段timestamp:在原始方案中,每次brelse都将被释放的缓冲区挂载到链表头,禀明这个缓冲区最近刚刚被使用过,在bget中分配时从链表尾向前查找,这样符合条件的第一个就是最久未使用的。而在提示中建议使用时间戳作为LRU判定的法则,这样我们就无需在brelse中进行头插法更改结点位置
  • 然后更改brelse,不再获取全局锁:

  • 更改bget,当没有找到指定的缓冲区时进行分配,分配方式是优先从当前列表遍历,找到一个没有引用且timestamp最小的缓冲区,如果没有就申请下一个桶的锁,并遍历该桶,找到后将该缓冲区从原来的桶移动到当前桶中,最多将所有桶都遍历完。在代码中要注意锁的释放:

一开始我寻找可更新缓存的办法是直接遍历整个数组,为了防止竞争,需要在遍历前把所有的桶锁起来,然而这样会发生死锁,即假设处理0号桶的进程运行到这里,把0号桶锁了,准备获取1号桶的锁,与此同时处理1号桶的进程运行到这里,把1号桶锁了,准备获取0号桶的锁,这样就死锁了。仔细分析原因,发现只要锁桶的顺序是乱序的,都可能发生死锁,这里的解决方法是使用“资源有序分配法”,就如上面的循环,从当前桶的下一个桶往上遍历到当前桶的前一个桶(循环遍历),保证了顺序,就不会死锁了。另外一个需要小心的是链表的操作,双向链表确实很容易写错,需要谨慎。

  • 修改剩下的相关函数:(bpin、bunpin)

  1. 实验结果

在Makefile中添加好bcachetest的编译选项后,使用该命令检查该阶段任务解决方案正误结果如上,成功。

3.8.3. Lab8总实验结果

本实验主要相关锁的实现、内存分配器和缓冲区高速缓存的实现。这些内容是操作系统中非常重要的基础。实验首先要求了解锁的概念和实现,在实验中,使用不同的锁:spinlock、sleeplock等。spinlock是一种忙等待锁,当锁被占用时,线程会一直尝试获取锁,直到成功为止。而sleeplock则是一种睡眠锁,当线程尝试获取锁失败时,它会释放CPU并睡眠,等待锁释放。这两种锁的实现都需要考虑并发执行时的竞争条件和原子操作的实现。

然后针对内存分配器的实现,实验要求实现了一个基于伙伴系统的内存分配器。伙伴系统可以避免内存碎片化,并能够快速地分配和释放内存。实现上面的要求涉及内存分配的算法。最后一部分是缓冲区高速缓存的实现,要求实现缓存磁盘块并在需要时快速访问它们,这和缓冲区的替换策略有关,并需要了解如何在操作系统中优化磁盘访问。

3.9. Lab9: File system

3.9.1. Large files(moderate)

  1. 实验目的

本阶段任务是希望文件系统实现对大文件的支持。

  1. 实验内容
  • 在fs.h中添加宏定义
  • 由于NDIRECT定义改变,其中一个直接块变为了二级间接块,需要修改inode结构体中addrs元素数量
  • 这个任务主要目的是支持更大的文件。和内存映射类似,文件系统中也有一个类似页表的结构,每个文件(inode)都有自己的一个页表,维护自己文件占用的文件块。和内存不同的是,这个页表的级别是自定义的,原始的xv6的表有13项,前12项直接包含文件块的地址,第13项是二级表的地址,二级表包含256项,每项都是文件块的地址,所以单个文件最大为12+256个文件块,为了支持更大的文件,需要将直接包含文件块地址的表项中取一项支持三级表,这样单个文件就可以扩大到11+256+256*256块。因此在bmap函数添加:

  • 相应的修改itrunc函数:

  • Ps:代码位置:xv6_2020 仓库的fs分支——/kernel/fs.h中的30行更改宏定义;/kernel/fs.c的400行修改bmap函数、470行修改itrunc函数
  1. 实验结果

3.9.2. Symbolic links(moderate)

  1. 实验目的

在本练习中,您将向xv6添加符号链接。符号链接(或软链接)是指按路径名链接的文件;当一个符号链接打开时,内核跟随该链接指向引用的文件。符号链接类似于硬链接,但硬链接仅限于指向同一磁盘上的文件,而符号链接可以跨磁盘设备。尽管xv6不支持多个设备,但实现此系统调用是了解路径名查找工作原理的一个很好的练习。

  1. 实验内容
  •  配置系统调用的常规操作,如在user/usys.pluser/user.h中添加一个条目,在kernel/syscall.ckernel/syscall.h中添加相关内容
  • 添加提示中的相关定义,T_SYMLINK以及O_NOFOLLOW
  • kernel/sysfile.c中实现sys_symlink,这里需要注意的是create返回已加锁的inode,此外iunlockput既对inode解锁,还将其引用计数减1,计数为0时回收此inode。

首先需要注意获得传入系统调用的字符串需要用argstr,不能用argaddr获得地址后自己复制,因为那个地址是用户内存的虚拟地址,现在在内核态,页表已经被换掉了。这里调用了create函数来创建符号文件,因为create函数里没有对inode的target属性赋值,所以需要在这里处理。另外就是create函数返回的有效inode是已经经过iget和ilock的了,所以这里create完就直接赋值target属性并更新到对应的dinode,然后iunlock解锁再iput释放inode指针(合起来是iunlockput函数)

Ps:代码位置:xv6_2020 仓库的fs分支——/kernel/sysfile.c的537行

  • 修改sys_open支持打开符号链接:

namei得到的inode是经过iget但没ilock的,所以取属性和修改属性需先ilock。另外就是memmove参数的最后一个参数需要是MAXPATH而不是sizeof(ip->target),因为inode的target属性我跟随dinode的target属性都设成192字节的字符串,大小大于MAXPATH即128,所以如果按sizeof来复制会溢出。

Ps:代码位置:xv6_2020 仓库的fs分支——/kernel/sysfile.c的365行

  1. 实验结果

3.9.3. Lab9总实验结果

本实验主要是文件系统中包括大文件和符号链接的实现。实验要求掌握文件系统基本的概念和实现。实验中使用inode,inode是文件系统中的一种数据结构,用于描述文件的元数据。实验要求学生最终学会如何创建、读取和写入文件,以及如何实现文件系统的目录结构。第一个任务针对支持大文件的问题展开探讨,第二个任务针对文件系统中符号链接的实现与使用。符号链接是文件系统中的一种特殊文件类型,它可以指向另一个文件或目录。

3.10. Lab10: Mmap

3.10.0. 前言

mmap和munmap系统调用允许UNIX程序对其地址空间进行详细控制。它们可用于在进程之间共享内存,将文件映射到进程地址空间,并作为用户级页面错误方案的一部分,如课程中讨论的垃圾收集算法。

mmap的声明:

void *mmap(void *addr, size_t length, int prot, int flags,

           int fd, off_t offset);

可以通过多种方式调用mmap,但本实验只需要与内存映射文件相关的功能子集。假设addr始终为零,这意味着内核应该决定映射文件的虚拟地址。mmap返回该地址,如果失败则返回0xffffffffffffffff。length是要映射的字节数;它可能与文件的长度不同。prot指示内存是否应映射为可读、可写,以及/或者可执行的;可以认为prot是PROT_READ或PROT_WRITE或两者兼有。flags要么是MAP_SHARED(映射内存的修改应写回文件),要么是MAP_PRIVATE(映射内存的修改不应写回文件)。不必在flags中实现任何其他位。fd是要映射的文件的打开文件描述符。可以假定offset为零(它是要映射的文件的起点)。

允许进程映射同一个MAP_SHARED文件而不共享物理页面。

munmap(addr, length)应删除指定地址范围内的mmap映射。如果进程修改了内存并将其映射为MAP_SHARED,则应首先将修改写入文件。munmap调用可能只覆盖mmap区域的一部分,可以认为它取消映射的位置要么在区域起始位置,要么在区域结束位置,要么就是整个区域(但不会在区域中间“打洞”)。

3.10.1. mmap(hard)

  1. 实验目的

本任务就是要求实现Linux中的mmap函数的一个子集,相当于在第五次实验Lazy Allocation中加上了文件的操作。子集的定义比较模糊,但若是仅仅只针对测试程序,做出一些简化性的假设,本实验就能有个大体的思路。

单谈实验的要求,就是要实现一个内存映射文件的功能,将文件映射到内存中,从而减少与文件交互时的磁盘操作。

  1. 实验内容
  • 根据提示1,首先是配置mmapmunmap系统调用,此前已进行过多次类似流程,不再赘述。在kernel/fcntl.h中定义了宏,只有在定义了LAB_MMAP时这些宏才生效,而LAB_MMAP是在编译时在命令行通过gcc的-D参数定义的
  •  根据提示3,定义VMA结构体,并添加到进程结构体中:

Ps:代码位置:xv6_2020 仓库的mmap分支——/kernel/proc.h的85行

  •  在allocproc中将vma数组初始化为全0
  • 根据提示2、3、4,参考lazy实验中的分配方法(将当前p->sz作为分配的虚拟起始地址,但不实际分配物理页面),此函数写在sysfile.c中就可以使用静态函数argfd同时解析文件描述符和struct file

Ps:代码位置:xv6_2020 仓库的mmap分支——/kernel/sysfile.c的556行

  • 根据提示5,此时访问对应的页面就会产生页面错误,需要在usertrap中进行处理,主要完成三项工作:分配物理页面,读取文件内容,添加映射关系

Ps:代码位置:xv6_2020 仓库的mmap分支——/kernel/sysfile.c的500行

  •  根据提示6实现munmap,且提示7中说明无需查看脏位就可写回
  •  回忆lazy实验中,如果对惰性分配的页面调用了uvmunmap,或者子进程在fork中调用uvmcopy复制了父进程惰性分配的页面都会导致panic,因此需要修改uvmunmapuvmcopy检查PTE_V后不再panic
  • 根据提示8修改exit,将进程的已映射区域取消映射
  • Ps:代码位置:xv6_2020 仓库的mmap分支——/kernel/proc.c的367行
  • 根据提示9,修改fork,复制父进程的VMA并增加文件引用计数
  1. 实验结果

3.10.2. Lab10总实验结果

本实验针对内存映射文件(mmap)这个重要操作系统功能的实现。mmap可以将文件映射到进程的虚拟地址空间中,使得进程可以像访问内存一样访问文件,这样可以加快文件的访问速度,提高操作系统的效率。在实验中,学习如何实现内存映射文件,了解内存映射文件的原理和使用方式。使用mmap系统调用来将文件映射到进程的虚拟地址空间中,使用munmap系统调用来解除映射。学习在内核中实现页表和虚拟内存管理,以及处理页面映射和解除映射时的异常。

通过实验深入理解操作系统中内存管理的概念和实现方式,学习处理虚拟地址和物理地址之间的映射,以及管理页面的分配和释放。学习实现文件访问和虚拟内存管理之间的联系,了解操作系统中的文件系统和内存管理之间的交互。

3.11. Lab11: Network

3.11.0. 前言

实验中使用名为E1000的网络设备来处理网络通信。对于xv6,E1000看起来像是连接到真正以太网局域网(LAN)的真正硬件。事实上,用于与xv6驱动程序对话的E1000是qemu提供的模拟,连接到的LAN也由qemu模拟。在这个模拟LAN上,xv6(“来宾”)的IP地址为10.0.2.15。Qemu还安排运行Qemu的计算机出现在IP地址为10.0.2.2的LAN上。当xv6使用E1000将数据包发送到10.0.2.2时,qemu会将数据包发送到运行qemu的(真实)计算机上的相应应用程序(“主机”)。

实验中使用QEMU的“用户模式网络栈(user-mode network stack)”。QEMU手册中有更多关于用户模式栈的内容。

Makefile将QEMU配置为将所有传入和传出数据包记录到实验目录中的packets.pcap文件中。查看这些记录有助于确认xv6正在发送和接收期望的数据包。要显示记录的数据包,执行以下操作:

tcpdump -XXnr packets.pcap

一些文件已经被添加到实验的xv6存储库中。kernel/e1000.c文件包含E1000的初始化代码以及用于发送和接收数据包的空函数,填写这些函数。kernel/e1000_dev.h包含E1000定义的寄存器和标志位的定义。kernel/net.c和kernel/net.h包含一个实现IP、UDP和ARP协议的简单网络栈。这些文件还包含用于保存数据包的灵活数据结构(称为mbuf)的代码。最后,kernel/pci.c包含在xv6引导时在PCI总线上搜索E1000卡的代码。

3.11.1. 需要完成的工作/任务(hard)

  1. 实验目的

阅读文档,实现网卡驱动的功能。

  1. 实验内容
  • 根据提示一步步走即可
  • 针对发送函数有两个问题,第一是可不可以只保存一个mbuf,每次调用函数的时候就释放掉上一个mbuf,答案是不能,因为多进程的影响,可能有多个进程都调用了这个函数,那么就会有很多“上一个mbuf”,自然需要一个数组来存,那么怎么知道上一个mbuf的位置呢,这就是寄存器E1000_TDT的作用,猜想在进程切换的时候寄存器的值是包含在进程状态里面的,所以相当于每个进程都有各自保存上一个mbuf的位置。第二是tx_desc结构体里的cmd应该填什么,按照实验文档给出的参考材料,这里有8个位,只有一个位明确是必须填0,其他位都是可以或者必须填成1的,查看e1000_dev.h,发现里面刚好有且仅有两个常量E1000_TXD_CMD_RSE1000_TXD_CMD_EOP,这提示我们就填这两个位即可。

Ps:代码位置:xv6_2020 仓库的net分支——/kernel/e1000.c的100行

  • 关于接收函数,没有使用锁,因为这个函数不会并行。这个函数的作用是一次性读取收到的所有rx_ring,然后由net_rx发给上一层,这里没有区分收到的包是哪个进程的,因为这个函数实际上处于OSI里非常底层的数据链路层,net_rx函数收到包后会调用net_rx_ip或net_rx_arp,进入网络层,net_rx_ip函数会调用net_rx_udp,进入传输层,net_rx_udp函数会调用sockrecvudp,再由socket机制统一根据端口把包分给各个进程。也就是说,在物理层之上,传输层及以下的层都是不考虑并行的。

接收函数e1000_recv:

Ps:代码位置:xv6_2020 仓库的net分支——/kernel/e1000.c的130行

  • 测试程序中用的dns是谷歌的域名服务器8.8.8.8,由于谷歌不能用,不调整就会卡死。找到dns函数,修改有48的那一行为国内的dns服务器,这里填的是114.114.114.114

Ps:代码位置:xv6_2020 仓库的net分支——/kernel/nettests.c的222行

  1. 实验结果

这里直接用nettests测试还是失败了,换了很多个域名都卡死,但是make grade成功。

3.11.2. Lab11总实验结果

本实验是xv6系统中的网络编程,包括套接字编程和网络协议的实现。实验要求掌握一些套接字编程的概念和实现。实验通过填空的形式实现一个简单的网络应用程序,实现通过dns域名进行通信。查阅资料了解网络协议的实现,学习处理网络数据包和通信请求。尝试使用操作系统中的调试工具来调试网络应用程序和网络协议。使用gdb调试器和Wireshark网络协议分析器这些工具。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值