MIT6828 学习笔记 011 (lab pgtbl)

MIT6828 学习笔记 011 (lab pgtbl)

RayAlto OP

1. 给系统调用加速

一些操作系统(如 Linux )通过在一块只读区域和用户空间共享数据来为某些系统调用加速。现在给 Xv6 的 getpid 系统调用加速:当进程被创建时,在 USYSCALL (kernel/memlayout.h:74) 处映射一个只读分页,在这个分页的起点保存一个 struct usyscall (kernel/memlayout:76) ,把 pid 放进去。

思路:要给新创建的进程映射新的分页表,先看看创建进程的系统调用的实现,发现 sys_forkkernel/sysproc.c:24 ,它直接调用了 fork (kernel/proc.c:279) ,里面用 allocproc (kernel/proc.c:109) 分配新的进程,其中 proc_pagetable (kernel/proc.c:176) 用来创建分页表,只要在 proc_pagetable 里映射 USYSCALL 就可以了:

点击展开:我的答案
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
diff --git a/kernel/proc.c b/kernel/proc.c
index 959b778..438450b 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -158,8 +158,13 @@ freeproc(struct proc *p)
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
- if(p->pagetable)
+ if(p->pagetable){
+ struct usyscall* usyscall = (struct usyscall*)walkaddr(p->pagetable, USYSCALL);
+ if(usyscall != 0){
+ kfree(usyscall);
+ }
proc_freepagetable(p->pagetable, p->sz);
+ }
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
@@ -202,6 +207,18 @@ proc_pagetable(struct proc *p)
return 0;
}

+ // map the usyscall page just below the trapframe page, for
+ // user/user.h:31 ugetpid()
+ struct usyscall* usyscall = kalloc();
+ usyscall->pid = p->pid;
+ if(mappages(pagetable, USYSCALL, PGSIZE,
+ (uint64)(usyscall), PTE_R | PTE_U) < 0){
+ uvmunmap(pagetable, TRAMPOLINE, 1, 0);
+ uvmunmap(pagetable, TRAPFRAME, 1, 0);
+ uvmfree(pagetable, 0);
+ return 0;
+ }
+
return pagetable;
}

@@ -212,6 +229,7 @@ proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
+ uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, sz);
}

问:还有那些 Xv6 系统调用可以利用这个共享分页实现提速?并解释原因。答:别的我不敢确定,但 uptime 系统调用应该可以通过这个方法提速,因为进程只需要读取 ticks (kernel/trap.c:10) ,内核更新这个数据也只需要写入 ticks ,用户进行 uptime 调用后不需要进入内核,直接读取数据即可。

2. 打印一个分页表

定义一个 vmprint(pagetable_t pgtbl) 函数,放在 exec (kernel/exec.c:22) 的 return 之前,打印出第一个进程的分页表(声明在 kernel/defs.h ,定义在 kernel/vm.c

点击展开:我的答案
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
55
56
57
58
59
60
61
62
diff --git a/kernel/defs.h b/kernel/defs.h
index a3c962b..487f4f2 100644
--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -173,6 +173,7 @@ uint64 walkaddr(pagetable_t, uint64);
int copyout(pagetable_t, uint64, char *, uint64);
int copyin(pagetable_t, char *, uint64, uint64);
int copyinstr(pagetable_t, char *, uint64, uint64);
+void vmprint(pagetable_t);

// plic.c
void plicinit(void);
diff --git a/kernel/exec.c b/kernel/exec.c
index e18bbb6..e7e7a02 100644
--- a/kernel/exec.c
+++ b/kernel/exec.c
@@ -128,6 +128,10 @@ exec(char *path, char **argv)
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);

+ // print page table for the first process
+ if(p->pid == 1)
+ vmprint(p->pagetable);
+
return argc; // this ends up in a0, the first argument to main(argc, argv)

bad:
diff --git a/kernel/vm.c b/kernel/vm.c
index 9f69783..0a342df 100644
--- a/kernel/vm.c
+++ b/kernel/vm.c
@@ -437,3 +437,30 @@ copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
return -1;
}
}
+
+void
+vmprint_level(pagetable_t pagetable, int level){
+ if(pagetable == 0)
+ // not a valid pagetable
+ return;
+ for(int i = 0; i < 512; i++){
+ pte_t pte = pagetable[i];
+ if(!(pte & PTE_V))
+ // not valid.
+ continue;
+ for(int i = 0; i <= level; i++)
+ printf(" ..");
+ printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
+ if((pte & (PTE_R|PTE_W|PTE_X)) == 0)
+ // this PTE points to a lower-level page table.
+ vmprint_level((pagetable_t)PTE2PA(pte), level + 1);
+ }
+}
+
+// Print the contents of a page table.
+void
+vmprint(pagetable_t pagetable)
+{
+ printf("page table %p\n", pagetable);
+ vmprint_level(pagetable, 0);
+}

我的一次运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp
1 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,
format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

xv6 kernel is booting

page table 0x0000000087f6b000
..0: pte 0x0000000021fd9801 pa 0x0000000087f66000
.. ..0: pte 0x0000000021fd9401 pa 0x0000000087f65000
.. .. ..0: pte 0x0000000021fd9c1b pa 0x0000000087f67000
.. .. ..1: pte 0x0000000021fd9017 pa 0x0000000087f64000
.. .. ..2: pte 0x0000000021fd8c07 pa 0x0000000087f63000
.. .. ..3: pte 0x0000000021fd8817 pa 0x0000000087f62000
..255: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..511: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..509: pte 0x0000000021fda013 pa 0x0000000087f68000
.. .. ..510: pte 0x0000000021fdd007 pa 0x0000000087f74000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
init: starting sh
$ QEMU: Terminated
  • %p 具体怎么打印地址是编译器实现的,没有统一标准,我的测试下 x86_64 的 GCC 和 Clang 都输出 0x 开头的 48-bit 的地址,而 x86_64 的 MinGW GCC 则输出没有 0x 开头的 64-bit 的地址。
  • PTE 的 X, W, R flag 的意义: RISC-V Privilleged Table 4.5

3. 检测哪些分页被访问过

RISC-V 会在 TLB (Translation Look-aside Buffer) 未命中时设置分页表的 A flag ,实现函数 pgaccess() ,第一个参数是分页表起点地址,第二个参数是要检测的分页表的数量,第三个参数用于接收结果,从低 bit 到高 bit 保存从低地址到高地址的分页表的 A flag

点击展开:我的答案
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
diff --git a/kernel/riscv.h b/kernel/riscv.h
index 20a01db..e71c193 100644
--- a/kernel/riscv.h
+++ b/kernel/riscv.h
@@ -343,6 +343,7 @@ typedef uint64 *pagetable_t; // 512 PTEs
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // user can access
+#define PTE_A (1L << 6) // accessed

// shift a physical address to the right place for a PTE.
#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index 88644b2..e30a8aa 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -75,6 +75,31 @@ int
sys_pgaccess(void)
{
// lab pgtbl: your code here.
+ struct proc* proc = myproc();
+ uint64 base_addr = 0;
+ int len = 0;
+ uint64 mask_addr = 0;
+ uint64 mask = 0;
+ pte_t* pte = 0;
+ argaddr(0, &base_addr);
+ argint(1, &len);
+ argaddr(2, &mask_addr);
+ if(len > sizeof(mask) * 8){
+ panic("sys_pgaccess: too much pages to check");
+ }
+ base_addr = PGROUNDDOWN(base_addr);
+ for(int i = 0; i < len; i++){
+ pte = walk(proc->pagetable, base_addr + (uint64)i * PGSIZE, 0);
+ if(pte == 0 || !(*pte & PTE_V))
+ // not valid/allocated
+ panic("sys_pgaccess: PTE not valid/allocated");
+ if(*pte & PTE_A){
+ mask |= 1L << i;
+ // clear A flag
+ *pte &= (~PTE_A);
+ }
+ }
+ copyout(proc->pagetable, mask_addr, (char*)&mask, sizeof(mask));
return 0;
}
#endif

RISC-V 手册说修改这些 flag 需要保证原子操作,但我是个铸币,没找到能用的机制/锁,所以干脆就没管,反正测试能过