提示:这是操作系统本人对 MIT 6.S081 的 lab6 实验课的笔记,仅供参考。
前言
提示:以下是本篇文章正文内容,下面案例可供参考
一、实验目标
实现一个写时复制操作,即在进程 fork 后,不立刻复制内存页,而是将子进程虚拟地址指向与父进程相同的物理地址。在父子任意一方尝试对内存页进行修改时,才对内存页进行复制。
二、实验
第一步:
首先在文件kernel/riscv.h
文件中设置一个标志位如下,用于区分该虚拟地址是否是写时复制页。页表项 flags 中,第 8、9、10 位可以由开发人员自定义用途。这里使用第8位来表示该地址是否是写时复制页。
#define PTE_COW (1L << 8) // 是否为写时复制页,使用页表项 flags 中保留的第 8 位表示
第二步:
修改kernel/kalloc.c
文件所有内存管理代码,修改后内容如下。该部分主要是开辟一个数组为 [KERNBASE, PHYSTOP) 区间的每一页物理内存建立一个引用计数,表示拥有该物理页的进程数量。
// Physical memory allocator, for user processes,
// kernel stacks, page-table pages,
// and pipe buffers. Allocates whole 4096-byte pages.
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "riscv.h"
#include "defs.h"
// 用于访问物理页引用计数数组
#define PA2PGREF_ID(p) (((p)-KERNBASE)/PGSIZE) // 这里是为[KERNBASE, PHYSTOP)之间的物理页建立索引
#define PGREF_MAX_ENTRIES PA2PGREF_ID(PHYSTOP)
struct spinlock pgreflock; // 用于 pageref 数组的锁,防止竞态条件引起内存泄漏
int pageref[PGREF_MAX_ENTRIES]; // 从 KERNBASE 开始到 PHYSTOP 之间的每个物理页的引用计数,这是一个很大的数组
// 通过物理地址获得引用计数
#define PA2PGREF(p) pageref[PA2PGREF_ID((uint64)(p))]
extern char end[]; // first address after kernel.
// defined by kernel.ld.
void freerange(void *pa_start, void *pa_end);
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
void
kinit()
{
initlock(&kmem.lock, "kmem");
initlock(&pgreflock, "pgref"); // 初始化锁
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
acquire(&pgreflock); // 获取锁
if(--PA2PGREF(pa) <= 0){ // 如果当前的引用计数小于等于 0,就释放该页
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
// 将释放的页加入到空闲链表中
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
release(&pgreflock);
}
// Allocate one 4096-byte page of physical memory.
// 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
PA2PGREF(r) = 1; // 引用计数初始化为 1
}
return (void*)r;
}
// Decrease reference to the page by one if it's more than one, then
// allocate a new physical page and copy the page into it.
// (Effectively turing one reference into one copy.)
//
// Do nothing and simply return pa when reference count is already
// less than or equal to 1.
//
// 当引用已经小于等于 1 时,不创建和复制到新的物理页,而是直接返回该页本身
void *kcopy_n_deref(void *pa) {
acquire(&pgreflock);
if(PA2PGREF(pa) <= 1) { // 只有 1 个引用,无需复制
release(&pgreflock);
return pa;
}
// 分配新的内存页,并复制旧页中的数据到新页
uint64 newpa = (uint64)kalloc();
if(newpa == 0) {
release(&pgreflock);
return 0; // out of memory
}
memmove((void*)newpa, (void*)pa, PGSIZE);
// 旧页的引用减 1
PA2PGREF(pa)--;
release(&pgreflock);
return (void*)newpa;
}
// 为 pa 的引用计数增加 1
void krefpage(void *pa) {
acquire(&pgreflock);
PA2PGREF(pa)++;
release(&pgreflock);
}
第三步:
修改kernel/vm.c
文件uvmcopy
函数:该函数主要在未修改前,主要用于在fork函数中子进程拷贝父进程的页表项。该函数在修改后,用与在复制父进程的内存到子进程的时候,不立刻复制数据,而是建立指向原物理页的映射,并将父子两端的页表项都设置为不可写。修改后内容如下:
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
// char *mem;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
if(*pte & PTE_W){
// 清除父进程的 PTE_W 标志位,设置 PTE_COW 标志位表示是一个懒复制页(多个进程引用同个物理页)
*pte = (*pte & ~PTE_W) | PTE_COW;
}
flags = PTE_FLAGS(*pte); // 获取当前页表的标志位
// if((mem = kalloc()) == 0)
// goto err;
// memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
// kfree(mem);
goto err;
}
krefpage((void*)pa); // 将物理页的应用数加1
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
第四步:
需要添加两个函数,用于在产生page fault错误时,检查当前虚拟地址是否符合写时复制的页以及进行写时复制操作。在kernel/vm.c
添加如下函数,在kernel/defs.h
添加函数声明。
// 检查一个地址指向的页是否是懒复制页
int uvmcheckcowpage(uint64 va) {
pte_t *pte;
struct proc *p = myproc();
return va < p->sz // 在进程内存范围内
&& ((pte = walk(p->pagetable, va, 0))!=0)
&& (*pte & PTE_V) // 页表项存在
&& (*pte & PTE_COW); // 页是一个懒复制页
}
// 实复制一个懒复制页,并重新映射为可写
int uvmcowcopy(uint64 va) {
pte_t *pte;
struct proc *p = myproc();
if((pte = walk(p->pagetable, va, 0)) == 0)
panic("uvmcowcopy: walk");
// 调用 kalloc.c 中的 kcopy_n_deref 方法,复制页
// (如果懒复制页的引用已经为 1,则不需要重新分配和复制内存页,只需清除 PTE_COW 标记并标记 PTE_W 即可)
uint64 pa = PTE2PA(*pte);
uint64 new = (uint64)kcopy_n_deref((void*)pa); // 将一个懒复制的页引用变为一个实复制的页
if(new == 0)
return -1;
// 重新映射为可写,并清除 PTE_COW 标记
uint64 flags = (PTE_FLAGS(*pte) | PTE_W) & ~PTE_COW;
uvmunmap(p->pagetable, PGROUNDDOWN(va), 1, 0);
if(mappages(p->pagetable, va, 1, new, flags) == -1) {
panic("uvmcowcopy: mappages");
}
return 0;
}
第五步:
修改kernel/trap.c
文件中的usertrap
函数,增加对page fault的检测,并将符合写时复制的内存进行内存拷贝操作。
void
usertrap(void)
{
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// ..........
syscall();
} else if((which_dev = devintr()) != 0){
// ok
} else if((r_scause() == 13 || r_scause() == 15) && uvmcheckcowpage(r_stval())){ // 处理page fault
if(uvmcowcopy(r_stval()) == -1){ // 如果内存不足,则杀死进程
p->killed = 1;
}
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if(p->killed)
exit(-1);
// .........................
usertrapret();
}
第六步:
修改kernel/vm.c
文件中的copyout
函数,该函数主要用于从内核空间向用户空间传递数据,需要进行写操作,因此需要检测写入地址是否是写时复制页,如果是,就要进行内存拷贝。修改后代码如下:
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
if(uvmcheckcowpage(dstva)) // 检查每一个被写的页是否是 COW 页
uvmcowcopy(dstva);
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}
至此,所有操作步骤完成,输入make
命令进行编译,make qemu
启动系统,输入make grade
命令进行测试。