MIT 6.S081 2024
6.S081 Lab: mmap lab
In this blog post, I'll be diving into the details of the mmap
system call as implemented in the 6.S081.
This lab is designed to help students understand the concepts of virtual memory, memory-mapped files and how memory-mapping can be used to efficiently work with large files.
Notice
If you haven't started writing code yet. Don't refer to my code.
This won't make you progress. Reading source code is usually a good habit, but not now.
If you are nearing the completion of this lab, here are a few debugging tips:
print is more efficient
I didn't use gdb in this lab. I think print is usually more efficient than gdb because I know where wrong happens when my code runs badly.
If you know how the code works, don't use gdb.
Reading mmaptest.c
is important
Don't ignore testfile. I don't think anyone can finish this lab and pass all the test examples without reading mmaptest.c
.
It's useful to print logs in mmaptest.c
.
file size is constant
mmap
don't change the file's size. Let me show you an example to illustrate this point.
fileA: size == 5000
addr = mmap(fileA, 10000, ...)
addr[6000] = xxx
This will not cause panic. But this shouldn't extend the file. Check if your code implement this situation correctly.
mmap
doesn't directly complete mapping
If uvmunmap
report not mapped, it might be OK. Here is an example:
p1 = mmap(fileB, ...)
munmap(p1)
This situation, original uvmunmap
will report an error. But it's OK.
So Changing the implement or checking this situation in your munmap is both right.
Find safe virtual memory
XV6 doesn't provide a interface to get safe virtual memory. So we have to do this job.
I can provide a way. It can pass all tests but not really safe.
I use myproc()->sz as the virtual memory mapped.
Other details are clearly stated in the lab description. Don't skip any instructions.
Hint: use differs from Hint: see
Don't usefilewrite
, you should decide the offset to write to.
I won't go into all details. For example I think you know how to add a system call in XV6.
Copying the code shown here won't work. It's not a good way. I provide my code in my github. If this helps you pass the tests, you can give me a star. Thanks.
We focus on functions below:
sysfile.c
sys_mmap
sys_munmap
writeback
(helper function)
file.c
filedup
filewrite
fileclose
vm.c
mappages
uvmunmap
proc.c
usertrap
exit
fork
About 100 lines will be modified.
struct vma
struct vma {
uint64 addr;
uint length;
uint prot; //PROT_READ PROT_WRITE...
uint flags; //MAP_PRIVATE MAP_SHARED
struct file *f;
uint64 start; //set when mmap is called the first time.
};
sys_mmap
This function is easy.
Just find a empty vma in proc struct and fill it.
uint64 sys_mmap(void)
{
struct file *f;
uint64 addr;
int length;
int prot;
int flags;
int offset;
argaddr(0, &addr);
argint(1, &length);
argint(2, &prot);
argint(3, &flags);
argfd(4, 0, &f);
argint(5, &offset);
/*
printf("debug: argstest...\n");
printf("addr = %ld, length = %d, prot = %d, flags = %d, offset = %d\n",
addr, length, prot, flags, offset);
*/
if (!f->writable && (prot & PROT_WRITE) && (flags & MAP_SHARED))
return -1;
filedup(f);
struct proc *p = myproc();
for (int i = 0; i < NVMA; i++) {
struct vma *vma = &p->vmas[i];
if (vma->prot == PROT_NONE) { // found unused vma
vma->addr = p->sz;
vma->length = PGROUNDUP(length);
vma->prot = prot;
vma->flags = flags;
vma->f = f;
vma->start = vma->addr;
//printf("mmap: address = %p\n", (void *)addr);
p->sz += vma->length;
return vma->addr;
}
}
return 0;
}
I use p->sz as the virtual memmory alloced. This way can pass all tests(including some new tests). I saw someone use TRAPOLINES
, might that's a better way?
Don't forget to use filedup
to add file refcnt
, this is mentioned in the lab description.
usertrap
read pagefault cause 0xd
write pagefault cause 0xf
So we should do pagefault handle when r_scause() == 0xd || 0xf
When we capture pagefault, we should check if it's asking for a mmaped page.
If so, we should alloc a physical page(In fact, XV6 only provides physical page alloc interface), and map vma->addr
to it.
I don't recommend to use uvmalloc
to do this because we just need mappage one page per trap.
I used uvmalloc
firstly, and didn't pass. But I don't really have reasons to ask you not do so.
uint64 va = r_stval();
if (va >= p->sz || va > MAXVA ||
PGROUNDUP(va) == PGROUNDDOWN(p->trapframe->sp))
setkilled(p);
else {
struct vma *vma = (struct vma *)0;
for (int i = 0; i < NVMA; i++) {
if (p->vmas[i].prot != PROT_NONE &&
va >= p->vmas[i].addr &&
va < p->vmas[i].addr + p->vmas[i].length) {
vma = &p->vmas[i];
break;
}
}
if (vma) { // mmaped
va = PGROUNDDOWN(va);
uint64 offset = va - vma->addr;
uint64 mem = (uint64)kalloc();
if (mem == 0) {
setkilled(p);
} else {
memset((void *)mem, 0, PGSIZE);
ilock(vma->f->ip);
if (readi(vma->f->ip, 0, mem, offset,
PGSIZE) < 0)
setkilled(p);
iunlock(vma->f->ip);
int flag = PTE_U;
if (vma->prot & PROT_READ)
flag |= PTE_R;
if (vma->prot & PROT_WRITE)
flag |= PTE_W;
if (vma->prot & PROT_EXEC)
flag |= PTE_X;
// cannot write a readonly page
if (r_scause() == 15 &&
!(vma->prot & PROT_WRITE)) {
kfree((void *)mem);
setkilled(p);
}
if (mappages(p->pagetable, va, PGSIZE,
mem, flag) != 0) {
kfree((void *)mem);
setkilled(p);
}
}
} else { // not mmaped
setkilled(p);
}
sys_munmap
before writing code, you'd better know what are the legal parameters for munmap.
Unlike Linux kernel, our munmap implement doesn't occur in the middle of vma because we don't maintain a linked list for vmas.
Here is an example:
vma->addr = 0x8000;
vma->length = 8192;
legal pamameters:
addr = 0x8000 0 <= length <= 8192(automatic alignment)
this case:
vma->addr += length vma->length -= length;
or
0x8000 < addr < 0x8000 + 8192 length = 0x8000 + 8192 - addr
this case:
vma->length -= length;
mmaptest.c
doesn't test other cases, so make sure you have implementations for both of them.
uint64 sys_munmap(void)
{
#define MIN(x, y) ((x) < (y) ? (x) : (y))
uint64 addr;
int length;
argaddr(0, &addr);
argint(1, &length);
length = PGROUNDUP(length);
struct proc *p = myproc();
for (int i = 0; i < NVMA; i++) {
struct vma *v = &p->vmas[i];
if (v->prot != PROT_NONE &&
(v->addr <= addr && addr < v->addr + v->length)) {
if ((v->flags & MAP_SHARED) && (v->prot & PROT_WRITE)) {
int max =
((MAXOPBLOCKS - 1 - 1 - 2) / 2) * BSIZE;
int foff = addr - v->start;
writeback(v->f, addr, foff,
MIN(length, (v->f->ip->size - foff)),
max);
}
uvmunmap(p->pagetable, addr, length / PGSIZE, 1);
v->length -= length;
if (v->length == 0) {
fileclose(v->f);
memset(v, 0, sizeof(*v));
return 0;
}
if (addr == v->addr) {
v->addr += length;
}
}
}
return 0;
}
Please check this:
int foff = addr - v->start;
So I reminded you at the beginning. Don't use filewrite
, you must calculate the offset yourself.
Here is an example:
vma->addr = 0x0000;
vma->length = 8192;
munmap(vma->addr, 4096);
writeback(file_offset = 0);
vma->addr = 0x1000;
vma->length = 4096;
munmap(vma->addr, 4096);
writeback(file_offset = 4096); (file_offset = 4096 - vma->start(0))
If you don't store the address mmaped, calculate file offset can be difficult.
Here is the implementation of writeback
:
// referring filewrite
void writeback(struct file *f, uint64 addr, int foff, int n, int limit)
{
int i = 0;
int r = 0;
while (i < n) {
int n1 = n - i;
if (n1 > limit)
n1 = limit;
begin_op();
ilock(f->ip);
if ((r = writei(f->ip, 1, addr + i, foff, n1)) > 0)
foff += r;
iunlock(f->ip);
end_op();
if (r != n1) {
// error from writei
break;
}
//printf("hello\n");
i += r;
}
}
The last job is to improve the implementation of exit
and fork
, which is pretty easy.
If you read the important points I mentioned carefully, debugging is not difficult either.
Complete code implementation is in my github repository.
There is also a simple implementation of C compiler. You can read it if you are interested.
If any problem, please contact me.
my email: xiongzile99@gmail.com
my github repository url: Withmm
Wish you good day!