相关知识
page fault一般指程序访问内存时发生的错误,通过page fault会触发trap机制,进入内核状态。可以实现的一系列虚拟内存功能。这里相关的功能有:
lazy allocation
copy-on-write fork
demand paging
memory mapped files
lazy allocation(延迟分配内存)
先为用户分配一块虚拟地址空间,但并没有映射到物理内存,当需要使用时,才给它映射物理内存。需要注意的是,在映射之前,如果用户释放这块自己以为已经得到的内存,其实内核什么也没做。因为此时物理内存并没有映射,如果释放虚拟内存反而会产生错误。
一个例子便是bss段的内存映射。BSS区域包含了未被初始化或者初始化为0的全局或者静态变量。这里或许有许多许多个矩阵,但是所有的矩阵内容都为0。在物理内存中,我只需要分配一个page,这个page的内容全是0。然后将所有虚拟地址空间的全0的page都map到这一个物理page上。在程序启动的时候能节省大量的物理内存分配。之后在某个时间点,应用程序尝试写BSS中的一个page时,比如说需要更改一两个变量的值,我们会得到page fault。然后可以在物理内存中申请一个新的内存page,将其内容设置为0,更新这个page的mapping关系,首先PTE要设置成可读可写,然后将其指向新的物理page。
在lazy allocation中,如果物理内存已耗尽了该如何办?
如果内存耗尽了,一个选择是撤回page(evict page)。比如说将部分内存page中的内容写回到文件系统再撤回page。一旦你撤回并释放了page,那么你就有了一个新的空闲的page,你可以使用这个刚刚空闲出来的page,分配给刚刚的page fault handler,再重新执行指令。
copy-on-write fork
写时拷贝,创建子进程时,子进程和父进程先共用内存。为了确保进程间的隔离性,我们可以将这里的父进程和子进程的PTE的标志位都设置成只读的。
在某个时间点,当父进程和子进程任一个需要更改内存的内容时,我们会得到page fault。
在得到page fault之后,我们需要拷贝相应的物理page。假设现在是子进程在执行store指令,那么我们会分配一个新的物理内存page,然后将page fault相关的物理内存page拷贝到新分配的物理内存page中,并将新分配的物理内存page映射到子进程。这时,新分配的物理内存page只对子进程的地址空间可见,所以我们可以将相应的PTE设置成可读写,并且我们可以重新执行store指令。实际上,对于触发刚刚page fault的物理page,因为现在只对父进程可见,相应的PTE对于父进程也变成可读写的了。
copy-on-write fork
(1). 修改uvmcopy。每次创建子进程,便会调用此函数为子进程分配物理内存。改为不为子进程分配内存,而是使父子进程共享内存,但禁用PTE_W,同时标记PTE_cow,记得调用kaddrefcnt增加引用计数
(2). 在**kernel/riscv.h**中选取PTE中的保留位定义标记一个页面是否为COW Fork页面的标志位
// 记录应用了COW策略后fork的页面
#define PTE_F (1L << 8)
(3). 在**kalloc.c**中进行如下修改
定义引用计数的全局变量ref,其中包含了一个自旋锁和一个引用计数数组,由于ref是全局变量,会被自动初始化为全0。
这里使用自旋锁是考虑到这种情况:进程P1和P2共用内存M,M引用计数为2,此时CPU1要执行fork产生P1的子进程,CPU2要终止P2,那么假设两个CPU同时读取引用计数为2,执行完成后CPU1中保存的引用计数为3,CPU2保存的计数为1,那么后赋值的语句会覆盖掉先赋值的语句,从而产生错误
struct ref_stru {
struct spinlock lock;
int cnt[PHYSTOP / PGSIZE]; // 引用计数
} ref;
在kinit中初始化ref的自旋锁
修改kalloc和kfree函数,在kalloc中初始化内存引用计数为1,在kfree函数中对内存引用计数减1,如果引用计数为0时才真正删除
(4). 在**trap.c**中进行如下修改
在usertrap()函数中,判断page fault产生类型是否是由于写入内存错误。如果是,判断PTE的cow位是否被标志,如果是,则读取内存引用计数,如果为1,说明只有当前进程引用了该物理内存(其他进程此前已经被分配到了其他物理页面),就只需要改变PTE使能PTE_W;否则就分配物理页面,并将原来的内存引用计数减1
uint64 cowalloc(pagetable_t pagetable, uint64 va)
{
pte_t *pte;
uint64 pa;
char* mem;
if(cowpage(pagetable, va) == 0)
return -1;
pte = walk(pagetable, va, 0); //子进程的页表pte
pa = PTE2PA(*pte); //共用的物理地址
//flags = PTE_FLAGS(*pte);
// 清除PTE_V,否则在mappagges中会判定为remap
if(krefcnt((char*)pa) == 1) {
// 只剩一个进程对此物理地址存在引用
// 则直接修改对应的PTE即可
*pte |= PTE_W;
*pte &= ~PTE_cow;
return pa;
} else {
// 多个进程对物理内存存在引用
// 需要分配新的页面,并拷贝旧页面的内容
mem = kalloc();
if(mem == 0)
return 0;
// 复制旧页面内容到新页
memmove(mem, (char*)pa, PGSIZE);
// 清除PTE_V,否则在mappagges中会判定为remap
*pte &= ~PTE_V;
// 为新页面添加映射
if(mappages(pagetable, va, PGSIZE, (uint64)mem, (PTE_FLAGS(*pte) | PTE_W) & ~PTE_cow) != 0) {
kfree(mem);
*pte |= PTE_V;
return 0;
}
// 将原来的物理内存引用计数减1
kfree((char*)PGROUNDDOWN(pa));
return (uint64)mem;
}
}
(5). 在copyout(由内核copy到用户空间)中处理相同的情况,如果是COW页面,需要
也需要重新为子进程分配内存
mmap系统调用
void *mmap(void *addr, int length, int prot, int flags, int fd, int off);
int munmap(void *addr, int length);
/*addr:映射到的虚拟地址,
length:需要映射的字节数,可能和文件大小不相等,
prot:表示映射到的内存的权限为PROT_READ还是PROT_WRITE。
flag:可以是MAP_SHARED或MAP_PRIVATE,如果是前者就需要将内存中修改的相应部分写回文件中。
fd: 需要映射的已打开的文件描述符。
*/
#define MAXVMA 16
struct vma {
int valid; // whether the vma is valid
uint64 addr; // starting virtual address of vma
int len; // length of vma, unit: bytes
int prot; // permission
int flags; // flag
struct file *f; // pointer to mapped file
int off; // offset of the valid mapped address
int valid_len; // length of the valid mapped address
};
实现系统调用函数
先不分配物理内存,只是找到空闲的vma,初始化vma。
使用COW机制分配真实内存。
uint64
sys_mmap(void)
{
int length, prot, flags, fd;
struct file *f;
if (argint(1, &length)<0 || argint(2, &prot)<0 || argint(3, &flags)<0 || argfd(4, &fd, &f)<0) {
return -1;
}
if (!f->writable && (prot & PROT_WRITE) && (flags & MAP_SHARED)) return -1; // to assert that readonly file could not be opened with PROT_WRITE && MAP_SHARED flags
struct proc *p = myproc();
struct vma *pvma = p->procvma;
//查找空闲的vma
for (int i = 0; i < MAXVMA; i++) {
if(pvma[i].valid == 0) {
pvma[i].addr = p->sz; //进程地址空间的最上方,mmap起始虚拟地址,addr+len为结束地址
pvma[i].f = filedup(f); // increment the refcount for f
pvma[i].flags = flags;
pvma[i].len = PGROUNDUP(length); //映射的文件字节长度
pvma[i].prot = prot;
pvma[i].valid = 1;
pvma[i].off = 0;
pvma[i].valid_len = pvma[i].len;
p->sz += pvma[i].len; //更新进程已使用地址空间,预分配
return pvma[i].addr;
}
}
return -1;
}
出发page fault进入trap时,先判断由于访问虚拟地址引发trap的va是否合法(在vma管理的mmap地址范围内)分配内存,映射到虚拟内存。,将对应的文件内容用放入刚分配的(物理)内存中。
else if (r_scause() == 13 || r_scause() == 15) {
uint64 va = r_stval();
if (va < p->trapframe->sp || va >= p->sz) {
goto exception;
}
struct vma *pvma = p->procvma;
va = PGROUNDDOWN(va);
for (int i = 0; i < MAXVMA; i++) {
if (pvma[i].valid && va >= pvma[i].addr + pvma[i].off && va < pvma[i].addr + pvma[i].off + pvma[i].valid_len) {
// allocate one page in the physical memory
char *mem = kalloc();
if (mem == 0) goto exception;
memset(mem, 0, PGSIZE);
int flag = (pvma[i].prot << 1) | PTE_U; // PTE_R == 2 and PROT_READ == 1
if (mappages(p->pagetable, va, PGSIZE, (uint64)mem, flag) != 0) {
kfree(mem);
goto exception;
}
int off = va - pvma[i].addr;
ilock(pvma[i].f->ip);
readi(pvma[i].f->ip, 1, va, off, PGSIZE);
iunlock(pvma[i].f->ip);
break;
}
}
}
sys_munmap函数
首先需要找到对应的vma,然后根据unmap的大小和起点的不同进行讨论。如果是从vma有效部分的起点开始,当整个vma都被unmap掉时,需要标记这个打开的文件被关闭(但是现在还不能关闭,因为后面可能需要写回硬盘中的文件)。如果是从中间部分开始,则直接unmap一直到结尾
然后判断是否是MAP_SHARED,如果是就需要从内存写回磁盘原文件
sys_munmap(void)
{
uint64 addr;
int length;
if (argaddr(0, &addr) < 0 || argint(1, &length) < 0) {
return -1;
}
struct proc *p = myproc();
struct vma *pvma = p->procvma;
int close = 0;
// find the corresponding vma
for (int i = 0; i < MAXVA; i++) {
if (pvma[i].valid && addr >= pvma[i].addr && addr < pvma[i].addr + pvma[i].len) {
addr = PGROUNDDOWN(addr);
if (addr == pvma[i].addr + pvma[i].off) {
// starting at begin of the valid address of vma
if (length >= pvma[i].valid_len) {
// whole vma is unmmaped
pvma[i].valid = 0;
length = pvma[i].valid_len;
close = 1;
p->sz -= pvma[i].len;
} else {
pvma[i].off += length;
pvma[i].valid_len -= length;
}
} else {
// starting at middle, should unmap until the end
length = pvma[i].addr + pvma[i].off + pvma[i].valid_len - addr;
pvma[i].valid_len -= length;
}
if (pvma[i].flags & MAP_SHARED) {
// write the page back to the file
if (_filewrite(pvma[i].f, addr, length, addr - pvma[i].addr) == -1) return -1;
}
uvmunmap(p->pagetable, addr, PGROUNDUP(length)/PGSIZE, 0); //取消内存映射
if (close) fileclose(pvma[i].f);
return 0;
}
}
return -1;
}
用到了log机制
/* f:文件指针
addr:umap的地址
n: umap的长度
off: 从addr到vma起始地址的偏移
*/
int
_filewrite(struct file *f, uint64 addr, int n, uint off) {
int r, ret = 0;
if(f->writable == 0)
return -1;
if(f->type == FD_PIPE){
ret = pipewrite(f->pipe, addr, n);
} else if(f->type == FD_DEVICE){
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
return -1;
ret = devsw[f->major].write(1, addr, n);
} else if(f->type == FD_INODE){
// write a few blocks at a time to avoid exceeding
// the maximum log transaction size, including
// i-node, indirect block, allocation blocks,
// and 2 blocks of slop for non-aligned writes.
// this really belongs lower down, since writei()
// might be writing a device like the console.
int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE; //一次写回的最大长度
int i = 0;
while(i < n){
int n1 = n - i;
if(n1 > max)
n1 = max;
begin_op();
ilock(f->ip);
if ((r = writei(f->ip, 1, addr + i, off, n1)) > 0)
off += r;
iunlock(f->ip);
end_op();
if(r != n1){
// error from writei
break;
}
i += r;
}
// ret = (i == n ? n : -1);
} else {
panic("filewrite");
}
return ret;
}
问题:释放内存处,
发现物理内存的释放必须是整页整页的释放,而之前munmap指定的addr可能正处于页中间,此时该页整体不应该被释放(或者是地址下移,凑够整页,再释放)
将uvmunmap改成vmaunmap
// Remove n BYTES (not pages) of vma mappings starting from va. va must be
// page-aligned. The mappings NEED NOT exist.
// Also free the physical memory and write back vma data to disk if necessary.
void
vmaunmap(pagetable_t pagetable, uint64 va, uint64 nbytes, struct vma *v)
{
uint64 a;
pte_t *pte;
// printf("unmapping %d bytes from %p\n",nbytes, va);
// borrowed from "uvmunmap"
for(a = va; a < va + nbytes; a += PGSIZE){
if((pte = walk(pagetable, a, 0)) == 0)
panic("sys_munmap: walk");
if(PTE_FLAGS(*pte) == PTE_V)
panic("sys_munmap: not a leaf");
if(*pte & PTE_V){
uint64 pa = PTE2PA(*pte);
if((*pte & PTE_D) && (v->flags & MAP_SHARED)) { // dirty, need to write back to disk
begin_op();
ilock(v->f->ip);
uint64 aoff = a - v->vastart; // offset relative to the start of memory range
if(aoff % PFSIZE != 0) { // if the first page is not a full 4k page
向下多删一段,凑够整页
} else if(aoff + PGSIZE > v->sz){ // if the last page is not a full 4k page
向下多删一段,凑够整页
} else { // full 4k pages
writei(v->f->ip, 0, pa, v->offset + aoff, PGSIZE);
}
iunlock(v->f->ip);
end_op();
}
kfree((void*)pa);
*pte = 0;
}
}
}