一个本硕双非的小菜鸡,备战24年秋招。打算尝试6.S081,将它的Lab逐一实现,并记录期间心酸历程。
代码下载
官方网站:6.S081官方网站
安装方式:
通过 APT 安装 (Debian/Ubuntu)
确保你的 debian 版本运行的是 “bullseye” 或 “sid”(在 ubuntu 上,这可以通过运行 cat /etc/debian_version 来检查),然后运行:
sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
(“buster”上的 QEMU 版本太旧了,所以你必须单独获取。
qemu-system-misc 修复
此时此刻,似乎软件包 qemu-system-misc 收到了一个更新,该更新破坏了它与我们内核的兼容性。如果运行 make qemu 并且脚本在 qemu-system-riscv64 -machine virt -bios none -kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 之后出现挂起
,
则需要卸载该软件包并安装旧版本:
$ sudo apt-get remove qemu-system-misc
$ sudo apt-get install qemu-system-misc=1:4.2-3ubuntu6
在 Arch 上安装
sudo pacman -S riscv64-linux-gnu-binutils riscv64-linux-gnu-gcc riscv64-linux-gnu-gdb qemu-arch-extra
测试您的安装
若要测试安装,应能够检查以下内容:
$ riscv64-unknown-elf-gcc --version
riscv64-unknown-elf-gcc (GCC) 10.1.0
...
$ qemu-system-riscv64 --version
QEMU emulator version 5.1.0
您还应该能够编译并运行 xv6: 要退出 qemu,请键入:Ctrl-a x。
# in the xv6 directory
$ make qemu
# ... lots of output ...
init: starting sh
$
在本实验中,您将获得重新设计代码以提高并行性的经验。多核机器上并行性差的一个常见症状是频繁的锁争用。提高并行性通常涉及更改数据结构和锁定策略以减少争用。您将对xv6内存分配器和块缓存执行此操作。
切换分支执行操作
git stash
git fetch
git checkout mmap
make clean
mmap和munmap系统调用允许UNIX程序对其地址空间进行详细控制。它们可用于在进程之间共享内存,将文件映射到进程地址空间,并作为用户级页面错误方案的一部分,如本课程中讨论的垃圾收集算法。在本实验室中,您将把mmap和munmap添加到xv6中,重点关注内存映射文件(memory-mapped files)。
手册页面(运行man 2 mmap)显示了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区域的一部分,但您可以认为它取消映射的位置要么在区域起始位置,要么在区域结束位置,要么就是整个区域(但不会在区域中间“打洞”)。
int munmap(void *addr, size_t len);
您应该实现足够的mmap和munmap功能,以使mmaptest测试程序正常工作。如果mmaptest不会用到某个mmap的特性,则不需要实现该特性。
完成后,您应该会看到以下输出:
$ mmaptest
mmap_test starting
test mmap f
test mmap f: OK
test mmap private
test mmap private: OK
test mmap read-only
test mmap read-only: OK
test mmap read/write
test mmap read/write: OK
test mmap dirty
test mmap dirty: OK
test not-mapped unmap
test not-mapped unmap: OK
test mmap two files
test mmap two files: OK
mmap_test: ALL OK
fork_test starting
fork_test OK
mmaptest: all tests succeeded
$ usertests
usertests starting
...
ALL TESTS PASSED
$
运行usertests以确保一切正常。
好综合的一道题,几乎把所有之前学到的知识都用上了!
首先还是老套路,在三小只添加mmap和munmap
//user/user.h
void *mmap(void*, int, int, int, int, int);
int munmap(void*, int);
//user/usys.pl
entry("mmap");
entry("munmap");
//makefile里面添加 symlinktest
。。。
$U/_mmaptest\
。。。
内核中也需要添加
//kernel/syscall.h
#define SYS_mmap 22
#define SYS_munmap 23
//kernel/syscall.c
extern uint64 sys_mmap(void);
extern uint64 sys_munmap(void);
。。。
[SYS_mmap] sys_mmap,
[SYS_munmap] sys_munmap,
准备工作做完了,可以正式开始了。
首先按照提示3,定义VMA对应的结构体。
//kernel/proc.h
struct vm_area {
int used; // 是否已被使用
uint64 addr; // 起始地址
int len; // 长度
int prot; // 权限
int flags; // 标志位
int vfd; // 对应的文件描述符
struct file* vfile; // 对应文件
int offset; // 文件偏移,本实验中一直为0
};
// Per-process state
struct proc {
struct spinlock lock;
struct vm_area vma[16]; // 虚拟内存区域
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};
然后我们需要将vma数组初始化为0(这步好像可不加)
//kernel/proc.c
static struct proc*
allocproc(void)
{
。。。
found:
。。。
memset(&p->vma, 0, sizeof(p->vma));
return p;
}
接下来是核心,实现sys_mmap()
//kernel/sysfile.c
uint64
sys_mmap(void) {
//首先进行参数的提取: 由于 xv6 中参数类型只有整数 int 类型, 因此 len, offset 参数此处都定义为 int 类型。
uint64 addr; // 起始地址
int len; // 长度
int prot; // 权限
int flags; // 标志位
int vfd; // 对应的文件描述符
struct file* vfile; // 对应文件
int offset; // 文件偏移,本实验中一直为0
uint64 err = 0xffffffffffffffff; //失败则返回
//其次进行类型检查,先判断是否有小于0的参数,再按要求prot是PROT_READ或PROT_WRITE或两者兼有。flags要么是MAP_SHARED(映射内存的修改应写回文件),要么是MAP_PRIVATE(映射内存的修改不应写回文件)。addr和offset为0
// 获取系统调用参数
if(argaddr(0, &addr) < 0 || argint(1, &len) < 0 || argint(2, &prot) < 0 ||
argint(3, &flags) < 0 || argfd(4, &vfd, &vfile) < 0 || argint(5, &offset) < 0)
return err;
// 实验提示中假定addr和offset为0,简化程序可能发生的情况
if(addr != 0 || offset != 0 || len< 0)
return err;
// 文件不可写则不允许拥有PROT_WRITE权限时映射为MAP_SHARED
if(vfile->writable == 0 && (prot & PROT_WRITE) != 0 && flags != MAP_SHARED)
return err;
//分配之前先判断进程的地址空间有没有足够大小的虚拟空间
//获取当前进程的指针
struct proc* p = myproc();
//映射长度加上当前进程的虚拟地址空间大小是否超出了最大虚拟地址空间 MAXVA
if(p->sz + len > MAXVA)
return err;
//遍历查找未使用的VMA结构体
for(int i = 0; i < 16; ++i) {
if(p->vma[i].used == 0) {
p->vma[i].used = 1;
p->vma[i].addr = p->sz;
p->vma[i].len = length;
p->vma[i].flags = flags;
p->vma[i].prot = prot;
p->vma[i].vfile = vfile;
p->vma[i].vfd = vfd;
p->vma[i].offset = offset;
// 增加文件的引用计数
filedup(vfile);
//更新当前进程的虚拟地址空间大小
p->sz += length;
return p->vma[i].addr;
}
}
return err;
}
由于在 sys_mmap() 中对文件映射的内存采用的是懒分配,因此需要修改 kernel/trap.c 中 usertrap() 的代码。
我们可以回忆以下当时懒分配中实现的流程
//kernel/trap.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "spinlock.h"
#include "proc.h"
#include "defs.h"
#include "fcntl.h"
//这块如果还不好使就把file.h中的file结构体粘过来,他这个头文件很烦,也有可能是我电脑的问题
。。。
} else if((which_dev = devintr()) != 0){
// ok
} else if (r_scause() == 13 || r_scause() == 15) {
if(mmap_handler(r_stval(), r_scause()) != 0)
p->killed = 1;
else
p->killed = 1;
} else {
。。。
int mmap_handler(int va, int cause) {
//首先通过遍历当前进程的 VMA 数组中找对应的 VMA 结构体,查找包含发生页面错误的虚拟地址
int i;
struct proc* p = myproc();
// 根据地址查找属于哪一个VMA
for(i = 0; i < 16; ++i) {
if(p->vma[i].used && p->vma[i].addr <= va && va <= p->vma[i].addr + p->vma[i].len - 1) {
break;
}
}
if(i == 16)
return -1;
//然后先进行页表项的权限设置和文件可访问性的检查
// 设置pte的标志位
int pte_flags = PTE_U;
if(p->vma[i].prot & PROT_READ) pte_flags |= PTE_R;
if(p->vma[i].prot & PROT_WRITE) pte_flags |= PTE_W;
if(p->vma[i].prot & PROT_EXEC) pte_flags |= PTE_X;
struct file* vf = p->vma[i].vfile;
// 读导致的页面错误
if(cause == 13 && vf->readable == 0) return -1;
// 写导致的页面错误
if(cause == 15 && vf->writable == 0) return -1;
//使用 kalloc 分配一个物理页面,并使用 memset 清零
void* pa = kalloc();
if(pa == 0)
return -1;
memset(pa, 0, PGSIZE);
//读取文件内容
ilock(vf->ip);
// 计算当前页面读取文件的偏移量,实验中p->vma[i].offset总是0
// 要按顺序读读取,例如内存页面A,B和文件块a,b
// 则A读取a,B读取b,而不能A读取b,B读取a
int offset = p->vma[i].offset + PGROUNDDOWN(va - p->vma[i].addr);
int readbytes = readi(vf->ip, 0, (uint64)pa, offset, PGSIZE);
// 什么都没有读到
if(readbytes == 0) {
iunlock(vf->ip);
kfree(pa);
return -1;
}
iunlock(vf->ip);
//最后即可使用 mappages() 将物理页映射到用户进程的页面值
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, pte_flags) != 0) {
kfree(pa);
return -1;
}
return 0;
}
//kernel/defs.h
// trap.c
extern uint ticks;
void trapinit(void);
void trapinithart(void);
extern struct spinlock tickslock;
void usertrapret(void);
int mmap_handler(int, int);
然后目标是实现sys_munmap()
由于lab对munmap的情况做了简化,只会从头开始munmap,因此在将数据写回磁盘时,直接调用filewrite函数即可,它在内部会自动调用file的偏移
//kernel/sysfile.c
uint64
sys_munmap(void) {
//首先也是对参数的提取, munmap 只有 addr 和 length 两个参数
uint64 addr;
int length;
//其次进行类型检查,二者都要大于0
if(argaddr(0, &addr) < 0 || argint(1, &length) < 0)
return -1;
//接下来根据 addr 和 length ,遍历虚拟内存区域,找到包含要取消映射的地址范围的 VMA
int i;
struct proc* p = myproc();
for(i = 0; i < 16; ++i) {
if(p->vma[i].used && p->vma[i].len >= length) {
// 根据提示,munmap的地址范围只能是
// 1. 起始位置
if(p->vma[i].addr == addr) {
p->vma[i].addr += length;
p->vma[i].len -= length;
break;
}
// 2. 结束位置
if(addr + length == p->vma[i].addr + p->vma[i].len) {
p->vma[i].len -= length;
break;
}
}
}
if(i == 16)
return -1;
// 如果未映射的页面已被修改,并且文件已映射到MAP_SHARED, 将MAP_SHARED页面写回文件系统
if(p->vma[i].flags == MAP_SHARED && (p->vma[i].prot & PROT_WRITE) != 0) {
filewrite(p->vma[i].vfile, addr, length);
}
// 取消页面映射
uvmunmap(p->pagetable, addr, length / PGSIZE, 1);
// 当前VMA中全部映射都被取消
if(p->vma[i].len == 0) {
fileclose(p->vma[i].vfile);
p->vma[i].used = 0;
}
return 0;
}
懒分配中需要修改uvmunmap和uvmcopy检查PTE_V后不再panic
//kernel/vm.c
#include "fcntl.h"
//uvmunmap()
......
if((*pte & PTE_V) == 0)
//panic("uvmunmap: not mapped");
continue;
......
//uvmcopy()
......
if((*pte & PTE_V) == 0)
//panic("uvmcopy: page not present");
continue;
......
终于到最后了,根据提示修改exit将进程的已映射区域取消映射
//kernel/proc.c
......
// 将进程的已映射区域取消映射
for(int i = 0; i < 16; ++i) {
if(p->vma[i].used) {
if(p->vma[i].flags == MAP_SHARED && (p->vma[i].prot & PROT_WRITE) != 0) {
filewrite(p->vma[i].vfile, p->vma[i].addr, p->vma[i].len);
}
fileclose(p->vma[i].vfile);
uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].len / PGSIZE, 1);
p->vma[i].used = 0;
}
}
......
最后修改fork,复制父进程的VMA并增加文件引用计数,以确保子对象具有与父对象相同的映射区域
//kernel/proc.c
......
// 复制父进程的VMA
for(i = 0; i < 16; ++i) {
if(p->vma[i].used) {
memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));
filedup(p->vma[i].vfile);
}
}
......
这个实验是真的全,从添加配置,到内存分配,还有锁的运用,最后包括了虚拟空间的映射。说实话难是真的难,做完了爽也是真的爽。感觉对整个的流程有了进一步的理解和运用,也同时感谢几位参考了的大佬,非常感谢前人栽树,我们这些后人也算是乘了凉。