0%

lab3.Page tables

Lab 3:Page Table

课程地址:https://pdos.csail.mit.edu/6.828/2020/schedule.html
Lab 地址:https://pdos.csail.mit.edu/6.828/2020/labs/pgtbl.html
代码仓库:https://github.com/ajackchan/my-xv6-riscv/tree/pgtbl

编写一个 vmprint() 函数,接收 pagetable_t 参数,递归打印页表的有效条目,包括页表索引、PTE 信息和物理地址,使用缩进表示不同页表级别。在 exec.c 中调用该函数,打印第一个进程的页表,并确保输出格式与样例一致。

1. 在 kernel/defs.h 中添加 vmprint 函数声明

1
2
3
4
5
6
// kernel/defs.h
......
int copyout(pagetable_t, uint64, char *, uint64);
int copyin(pagetable_t, char *, uint64, uint64);
int copyinstr(pagetable_t, char *, uint64, uint64);
int vmprint(pagetable_t pagetable); // 添加函数声明

2. 实现 vmprint()pgtblprint() 函数

我们在 kernel/vm.c 中实现这两个函数,pgtblprint() 函数负责递归遍历三级页表并打印,而 vmprint() 则作为入口调用 pgtblprint()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// kernel/vm.c

// 递归打印页表的函数,类似于 freewalk,但用于打印页表内容
int pgtblprint(pagetable_t pagetable, int depth) {
// 页表中有 2^9 = 512 个 PTE
for (int i = 0; i < 512; i++) {
pte_t pte = pagetable[i]; // 读取当前页表项
if (pte & PTE_V) { // 如果页表项有效
// 打印页表项,按照要求的格式,先打印缩进
printf("..");
for (int j = 0; j < depth; j++) {
printf(" ..");
}
// 打印页表索引、PTE 值、物理地址
printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));

// 如果该 PTE 指向的是下一级页表而非叶子节点,则递归打印子页表
if ((pte & (PTE_R | PTE_W | PTE_X)) == 0) {
// 该 PTE 指向下一级页表
uint64 child = PTE2PA(pte); // 获取子页表地址
pgtblprint((pagetable_t)child, depth + 1); // 递归进入子页表
}
}
}
return 0;
}

// 页表打印函数入口
int vmprint(pagetable_t pagetable) {
printf("page table %p\n", pagetable); // 打印页表的基地址
return pgtblprint(pagetable, 0); // 调用递归打印函数,深度从0开始
}

3. 在 exec.c 中调用 vmprint

exec.cexec() 函数中,在进程执行后插入对 vmprint() 的调用,以便打印进程的页表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// kernel/exec.c

int exec(char *path, char **argv) {
// 原有代码部分
...

// 在进程页表初始化之后,返回之前打印页表信息
if (p->pid == 1) { // 只打印第一个进程 (init 进程) 的页表
vmprint(p->pagetable);
}

return argc; // this ends up in a0, the first argument to main(argc, argv)

bad:
if (pagetable)
proc_freepagetable(pagetable, sz);
if (ip) {
iunlockput(ip);
end_op();
}
return -1;
}

A kernel page table per process

修改内核,使每一个进程进入内核态后,都能有自己的独立内核页表

1. 在 proc 结构体中添加 kernelpgtbl 字段

修改 kernel/proc.h,为每个进程添加 kernelpgtbl 字段,用于存储进程的独立内核页表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// kernel/proc.h

// Per-process state
struct proc {
struct spinlock lock;

// 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)
pagetable_t kernelpgtbl; // Kernel page table for the process
};

2. 抽象出内核页表的映射逻辑

kernel/vm.c 中实现 kvm_map_pagetable 函数,用于将系统所需的固定内存映射到每个进程的内核页表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// kernel/vm.c

void kvm_map_pagetable(pagetable_t pgtbl) {
// 映射 UART 寄存器
kvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

// 映射 virtio 硬盘接口
kvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

// 映射 CLINT
kvmmap(pgtbl, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

// 映射 PLIC
kvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

// 映射内核代码区(只读和可执行)
kvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

// 映射内核数据区(可读写)
kvmmap(pgtbl, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

// 映射 trampoline 用于 trap entry/exit
kvmmap(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}

3. 实现新的内核页表初始化函数

实现 kvminit_newpgtbl,用于为每个进程创建新的内核页表:

1
2
3
4
5
6
7
pagetable_t kvminit_newpgtbl() {
pagetable_t pgtbl = (pagetable_t) kalloc(); // 分配页表
memset(pgtbl, 0, PGSIZE); // 清空页表

kvm_map_pagetable(pgtbl); // 映射系统所需内存
return pgtbl;
}

4. 修改 kvminit 函数

kvminit 中调用 kvminit_newpgtbl(),为内核初始化全局页表:

1
2
3
void kvminit() {
kernel_pagetable = kvminit_newpgtbl(); // 创建全局内核页表
}

5. 分配进程的内核页表和内核栈

allocproc() 中,为新创建的进程分配独立的内核页表和内核栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// kernel/proc.c

static struct proc* allocproc(void) {
struct proc *p;

for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == UNUSED) {
goto found;
} else {
release(&p->lock);
}
}
return 0;

found:
p->pid = allocpid();

// 分配 trapframe 页
if((p->trapframe = (struct trapframe *)kalloc()) == 0) {
release(&p->lock);
return 0;
}

// 创建用户页表
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}

// 创建进程专属内核页表
p->kernelpgtbl = kvminit_newpgtbl();

// 为进程分配内核栈
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int)0); // 固定地址映射
kvmmap(p->kernelpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;

// 初始化进程上下文
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;

return p;
}

6. 切换进程的内核页表

在调度器 scheduler() 中切换到进程的内核页表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// kernel/proc.c

void scheduler(void) {
struct proc *p;
struct cpu *c = mycpu();

c->proc = 0;
for(;;) {
intr_on();

for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == RUNNABLE) {
p->state = RUNNING;
c->proc = p;

// 切换到进程内核页表
w_satp(MAKE_SATP(p->kernelpgtbl));
sfence_vma();

swtch(&c->context, &p->context);

// 切换回全局内核页表
kvminithart();

c->proc = 0;
}
release(&p->lock);
}
}
}

7. 释放进程的内核页表和内核栈

freeproc() 函数中,释放进程的内核栈和内核页表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// kernel/proc.c

static void freeproc(struct proc *p) {
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;

if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;

// 释放内核栈
void *kstack_pa = (void *)kvmpa(p->kernelpgtbl, p->kstack);
kfree(kstack_pa);
p->kstack = 0;

// 释放进程内核页表
kvm_free_kernelpgtbl(p->kernelpgtbl);
p->kernelpgtbl = 0;

p->state = UNUSED;
}

8. 递归释放页表

实现 kvm_free_kernelpgtbl() 函数,递归释放页表中的所有映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
// kernel/vm.c

void kvm_free_kernelpgtbl(pagetable_t pagetable) {
for(int i = 0; i < 512; i++) {
pte_t pte = pagetable[i];
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0) {
uint64 child = PTE2PA(pte);
kvm_free_kernelpgtbl((pagetable_t)child); // 递归释放子页表
pagetable[i] = 0;
}
}
kfree((void*)pagetable); // 释放页表本身
}

9. 修改 virtio_disk_rw 函数

修改 virtio_disk_rw 中的 kvmpa 调用,使用当前进程的 kernelpgtbl

1
2
3
4
5
6
7
// virtio_disk.c

void virtio_disk_rw(struct buf *b, int write) {
// 使用当前进程的内核页表
disk.desc[idx[0]].addr = (uint64) kvmpa(myproc()->kernelpgtbl, (uint64) &buf0);
// 其他代码不变
}

Simplify copyin/copyinstr

copyincopyinstr 的实现替换为调用新的 copyin_newcopyinstr_new 函数,并为每个进程的内核页表添加用户地址映射,确保这些新函数能够正确处理用户指针的解引用。

1. 工具方法的实现

实现 kvmcopymappingskvmdealloc,用于同步用户页表和内核页表的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// kernel/vm.c

// 将 src 页表的一部分页映射关系拷贝到 dst 页表中。
// 只拷贝页表项,不拷贝实际的物理页内存。
// 成功返回 0,失败返回 -1
int kvmcopymappings(pagetable_t src, pagetable_t dst, uint64 start, uint64 sz) {
pte_t *pte;
uint64 pa, i;
uint flags;

for(i = PGROUNDUP(start); i < start + sz; i += PGSIZE) {
if((pte = walk(src, i, 0)) == 0)
panic("kvmcopymappings: pte should exist");
if((*pte & PTE_V) == 0)
panic("kvmcopymappings: page not present");

pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte) & ~PTE_U; // 设置非用户权限
if(mappages(dst, i, PGSIZE, pa, flags) != 0) {
goto err;
}
}
return 0;

err:
uvmunmap(dst, PGROUNDUP(start), (i - PGROUNDUP(start)) / PGSIZE, 0);
return -1;
}

// 将程序内存从 oldsz 缩减到 newsz,用于内核页表内程序内存映射与用户页表程序内存映射之间的同步。
uint64 kvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz) {
if (newsz >= oldsz)
return oldsz;

if (PGROUNDUP(newsz) < PGROUNDUP(oldsz)) {
int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
uvmunmap(pagetable, PGROUNDUP(newsz), npages, 0);
}
return newsz;
}

2. 去除 CLINT 映射

修改 kvm_map_pagetable 函数,去除 CLINT 的映射,并在 kvminit 函数中为全局内核页表单独添加 CLINT 的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// kernel/vm.c

void kvm_map_pagetable(pagetable_t pgtbl) {
// uart registers
kvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

// virtio mmio disk interface
kvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

// 去掉 CLINT 的映射
// kvmmap(pgtbl, CLINT, CLINT, 0x10000, PTE_R | PTE_W);

// PLIC
kvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

// 映射内核代码区
kvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X);

// 映射内核数据区
kvmmap(pgtbl, (uint64)etext, (uint64)etext, PHYSTOP - (uint64)etext, PTE_R | PTE_W);

// 映射 trampoline
kvmmap(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}

void kvminit() {
kernel_pagetable = kvminit_newpgtbl();

// 全局内核页表仍然需要映射 CLINT
kvmmap(kernel_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
}

3. 在 exec 中增加内存范围检测

exec 函数中增加检测,确保用户程序内存不会超过 PLIC 的地址范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// kernel/exec.c

int exec(char *path, char **argv) {
// 其他代码省略...

// 加载程序到内存
for (i = 0, off = elf.phoff; i < elf.phnum; i++, off += sizeof(ph)) {
if (readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if (ph.type != ELF_PROG_LOAD)
continue;
if (ph.memsz < ph.filesz)
goto bad;
if (ph.vaddr + ph.memsz < ph.vaddr)
goto bad;

uint64 sz1;
if ((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
goto bad;

// 检查是否超出 PLIC 范围
if (sz1 >= PLIC) {
goto bad;
}

sz = sz1;
if (ph.vaddr % PGSIZE != 0)
goto bad;
if (loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}

// 清除旧的内核页表映射,重新映射程序内存到内核页表
uvmunmap(p->kernelpgtbl, 0, PGROUNDUP(oldsz) / PGSIZE, 0);
kvmcopymappings(pagetable, p->kernelpgtbl, 0, sz);

// 提交新页表
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry;
p->trapframe->sp = sp;
proc_freepagetable(oldpagetable, oldsz);

return argc;

bad:
iunlockput(ip);
end_op();
return -1;
}

4. 同步页表映射

fork()growproc()userinit() 中同步用户页表和内核页表的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// kernel/proc.c

// fork 中添加映射同步
int fork(void) {
// 其他代码省略...

if (uvmcopy(p->pagetable, np->pagetable, p->sz) < 0 ||
kvmcopymappings(np->pagetable, np->kernelpgtbl, 0, p->sz) < 0) {
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;

// 其他代码省略...
}

// growproc 中同步映射变化
int growproc(int n) {
uint sz = myproc()->sz;

if (n > 0) {
uint64 newsz;
if ((newsz = uvmalloc(myproc()->pagetable, sz, sz + n)) == 0)
return -1;

// 内核页表中的映射同步
if (kvmcopymappings(myproc()->pagetable, myproc()->kernelpgtbl, sz, n) != 0) {
uvmdealloc(myproc()->pagetable, newsz, sz);
return -1;
}
sz = newsz;
} else if (n < 0) {
uvmdealloc(myproc()->pagetable, sz, sz + n);
sz = kvmdealloc(myproc()->kernelpgtbl, sz, sz + n);
}

myproc()->sz = sz;
return 0;
}

// userinit 中的映射同步
void userinit(void) {
struct proc *p = allocproc();

// 初始化代码,加载用户程序
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;

// 同步程序内存映射到进程的内核页表中
kvmcopymappings(p->pagetable, p->kernelpgtbl, 0, p->sz);

// 其他代码省略...
}

5. 替换 copyincopyinstr 实现

copyincopyinstr 实现替换为调用新的 copyin_newcopyinstr_new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// kernel/vm.c

// 新的函数声明
int copyin_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len);
int copyinstr_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max);

// 将 copyin 和 copyinstr 替换为新的实现
int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len) {
return copyin_new(pagetable, dst, srcva, len);
}

int copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max) {
return copyinstr_new(pagetable, dst, srcva, max);
}

-------------本文结束感谢您的阅读-------------