【写在前面】
这是2023-2024春季学期操作系统课程有关xv6 lab部分的实验报告,参考了很多网络资源,解释也不一定正确,仅作为留档,参考需谨慎。
Lab 地址:6.1810 / Fall 2024
参考:
MIT 6.S081_Zheyuan Zou的博客-CSDN博客
yali-hzy/xv6-labs-2024: MIT 6.1810 assignments
【露说xv6】Lab3-Three Important Parts_哔哩哔哩_bilibili
【露说xv6】Lab3-Problems_哔哩哔哩_bilibili
Print a page table
实验任务分析
实验任务要求将一个进程的页表按照指定格式打印出来。
实现思路:
-
在 kernel/vm.c 中新增函数vmprint实现打印功能,并在 kernel/defs.h 中声明
-
根据提示“The function freewalk may be inspirational. ”阅读freewalk源码如下,可以发现他是一个用于递归地释放一个页表及其关联的所有子页表所占用的内存的函数。所以我们的代码也可以使用递归的逻辑。
-
为了控制打印的格式,具体实现中额外多定义了一个函数void ptePrint(pagetable_t pagetable, int level, uint64 baseP),主要用于按照页表的层次打印指定数量的缩进符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void
freewalk(pagetable_t pagetable)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
freewalk((pagetable_t)child);
pagetable[i] = 0;
} else if(pte & PTE_V){
panic("freewalk: leaf");
}
}
kfree((void*)pagetable);
}
|
具体代码
在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
|
// 用于按照格式递归地打印页表
void ptePrint(pagetable_t pagetable, int level, uint64 baseP) {
if(level < 0) // 遍历完所有页表层级,结束递归调用
return ;
// 遍历页表的每一项(每个页表有 512 个项)
for(uint64 i = 0; i < 512; i++) {
pte_t pte = pagetable[i]; // 获取当前页表项的值
if(pte & PTE_V){ // 判断有效
uint64 next_baseP = baseP + (i << PXSHIFT(level)); // 计算下一级页表项的虚拟地址偏移
pagetable_t next_pgt = (pagetable_t)PTE2PA(pte); // 提取物理地址
uint64 pnn = (uint64)next_pgt >> 12; // 提取物理页帧号
// 打印缩进,表示当前页表项所在的层级
for(int j = level; j < 3; j++)
printf(" ..");
// 打印页表项的详细信息:索引、地址、物理页帧号
printf("%ld: pte %p (", i, (uint64*)pte);
if (pte & PTE_R) printf("R");
if (pte & PTE_W) printf("W");
if (pte & PTE_X) printf("X");
if (pte & PTE_U) printf("U");
printf(") pa %ld(th pages)\n",pnn);
// 递归调用遍历下一级页表
ptePrint(next_pgt, level - 1, next_baseP);
}
}
}
void vmprint(pagetable_t pagetable) {
printf("page table %p\n", pagetable); // 打印根页表的基地址
ptePrint(pagetable, 2, 0); // 从根页表开始 递归遍历和打印
}
|
运行结果

回答问题
示例输出:

问题1:为什么第一对括号为空?32618在物理内存的什么位置,为什么不从低地址开始?结合源代码内容进行解释。
-
第一行的页表为非叶子页表项。在 xv6 的多级页表中,权限标志位(如 PTE_V、PTE_R、PTE_W、PTE_X、PTE_U 等)主要存在于叶子页表项,因为只有叶子页表项直接映射到物理内存页面。
查看源码,mappages 函数用于将物理内存映射到虚拟地址空间,并设置权限标志位。它调用 walk 函数来找到或创建叶子页表项,并设置权限标志位。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
...
a = va;
last = va + size - PGSIZE;
for (;;){
if ((pte = walk(pagetable, a, 1)) == 0) // 此处返回叶子页表项
return -1;
if (*pte & PTE_V)
panic("mappages: remap");
*pte = PA2PTE(pa) | perm | PTE_V; // 设置权限标志位
...
}
|
-
32618 是物理页帧号,物理页帧的大小通常为 4KB = 4096bits(即 PGSIZE),所以 32618 号物理页帧在物理内存中的起始地址为 32618 * PGSIZE。
-
在 xv6 操作系统中,物理内存被分为多个部分,一般低地址区域被内核代码、数据、设备映射占用,因此页表等动态分配的内存位于高地址。阅读源码kernel/kalloc.c中kinit()函数可知,xv6 使用从内核结尾到 PHYSTOP 之间的物理内存为运行时分配提供内存资源。
1
2
3
4
5
6
|
void
kinit()
{
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
|
问题2:这是什么页?装载的什么内容?结合源代码内容进行解释。
问题3:这是什么页,有何功能?为什么没有U标志位?
问题4:这是什么页?装载的什么内容?指出源代码初始化该页的位置。
问题5:这是什么页,为何没有X标志位?
- 内核数据页(
va=0x0000003fffffe000),权限 RW(内核可读/写)。
- 无
X 标志:数据页无需执行权限,防止代码注入攻击,提高安全性。
问题6:这是什么页,为何没有W标志位?装载的内容是什么?为何这里的物理页号处于低地址区域(第7页)?结合源代码对应的操作进行解释。
-
Trampoline 页(跳板页),权限 RX(内核可读/执行),无 W 和 U,因为Trampoline 代码必须是只读的,防止用户或内核修改其内容。
-
装载内容了trampoline.S 中的汇编代码(用户/内核切换逻辑)。
-
Trampoline 页在内核启动时(main() 前)静态分配,占用固定低物理地址(物理页7)。源码中由 kvminit() 直接映射到高虚拟地址,但物理页在低地址保留:
1
2
|
// kernel/vm.c: kvmmake()
kvmmap(kernel_pagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
|
trampoline 符号链接到低地址(kernel/kernel.ld ):
1
2
3
|
. = 0x80000000; // 内核虚拟地址起始
.text : { *(.text .text.*) }
.trampoline : { *(.trampoline) } // 位于0x7000(低物理地址)
|
Use superpages
实验任务分析
实验要求实现对超级页的支持,当用户程序通过 sbrk() 系统调用请求 2MB 或更大的内存时,内核应使用超级页进行内存分配。这需要修改内核的内存管理和页表处理逻辑,使系统能够识别和管理超级页。
这需要实现超级页物理内存的分配与释放,以及超级页虚拟内存的管理。
实现思路:
-
物理内存的分配/释放
- 在
kmem中添加一个run结构体变量用于指向一个空闲超级页(2MB)的链表
- 在
freerange 函数中添加用于释放超级页的物理内存区域的代码
- 仿照
kfree和kalloc函数,添加superfree 函数用于释放一个超级页,并添加superalloc 函数用于分配一个超级页
-
虚拟内存的管理
-
根据提示“A good place to start is sys_sbrk in kernel/sysproc.c, which is invoked by the sbrk system call. Follow the code path to the function that allocates memory for sbrk.”
1
2
3
4
5
6
7
8
9
10
11
12
|
uint64
sys_sbrk(void)
{
uint64 addr;
int n;
argint(0, &n);
addr = myproc()->sz;
if(growproc(n) < 0)
return -1;
return addr;
}
|
可以观察到sys_sbrk调用了growproc函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int
growproc(int n)
{
uint64 sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
if((sz = uvmalloc(p->pagetable, sz, sz + n, PTE_W)) == 0) {
return -1;
}
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}
|
观察到函数growproc通过参数n来调整进程的内存大小,并且调用了uvmalloc 函数来为用户进程分配虚拟内存。由此可以找到有关管理虚拟内存的代码均在vm.c中。
-
新增spuerwalk函数,仿照walk逻辑编写,使其支持在超级页表中查找虚拟地址 va 对应的 PTE。相比walk添加了一个参数l,返回时可以用于标识是超级页(l=1)还是普通页(l=0)
-
修改mappages函数,使其支持超级页虚拟内存映射
-
修改uvmunmap函数,使其支持解除超级页虚拟内存页面映射,使用 superwalk 函数代替 walk 函数来处理超级页
-
修改uvmalloc函数,使其支持超级页虚拟内存分配
-
修改uvmcopy函数,使其在复制内存时能够根据情况选择使用超级页或传统页面进行内存分配和映射
具体代码
物理内存的分配/释放
在kalloc.c中仿照xv6的对普通页的链式管理方式,新增对超级页的管理
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
|
struct {
struct spinlock lock;
struct run *freelist;
#ifdef LAB_PGTBL
struct run *superfreelist; // 对超级页的空闲链表
#endif
} kmem;
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
#ifndef LAB_PGTBL
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
#else // 如果支持超级页
int superpg_num = 10; // 预留超级页
char *superp = (char*)SUPERPGROUNDUP((uint64)pa_end - superpg_num * SUPERPGSIZE);
// 计算超级页的起始地址,从 pa_end 向下对齐到超级页边界
for(; p + PGSIZE <= superp; p += PGSIZE)
kfree(p);// 先释放普通页面部分
for(; superp + SUPERPGSIZE <= (char*)pa_end; superp += SUPERPGSIZE)
superfree(superp); // 再释放超级页部分
#endif
}
#ifdef LAB_PGTBL
// 超级页释放函数
void
superfree(void *pa)
{
struct run *r; // 用于表示空闲内存块
if(((uint64)pa % SUPERPGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("superfree"); // 参数验证:确保 pa 对齐到超级页大小且在合法内存范围内
// Fill with junk to catch dangling refs.
memset(pa, 1, SUPERPGSIZE);
r = (struct run*)pa; // 将 pa 转换为 struct run 类型
acquire(&kmem.lock); // 获取自旋锁
r->next = kmem.superfreelist;
kmem.superfreelist = r; // 将超级页插入空闲链表头部
release(&kmem.lock); // 释放锁
}
// 超级页分配函数
void *
superalloc(void)
{
struct run *r;
acquire(&kmem.lock);
// 从空闲链表中取出一个超级页
r = kmem.superfreelist;
if(r)
kmem.superfreelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, SUPERPGSIZE); // fill with junk
return (void*)r; // 返回分配的超级页地址
}
#endif
|
虚拟内存的管理
在riscv.h中对照PGROUNDDOWN和PGROUNDUP添加SUPERPGROUNDDOWN,用于找到当前a所在物理页帧号,返回所在物理页的开始地址
1
2
|
#define SUPERPGROUNDUP(sz) (((sz)+SUPERPGSIZE-1) & ~(SUPERPGSIZE-1))
#define SUPERPGROUNDDOWN(a) (((a)) & ~(SUPERPGSIZE-1)) // 新增
|
在defs.h中声明新增的函数
1
2
3
|
void * superalloc(void);
void superfree(void *pa);
pte_t * superwalk(pagetable_t, uint64, int, int *);
|
在kernel/vm.c中修改:
spuerwalk函数
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
|
#ifdef LAB_PGTBL
// 类似walk()逻辑编写
// 函数用于在超级页表中查找虚拟地址 va 对应的 PTE。
// 参数l用于指定页表的起始级别,在遍历过程中更新为实际访问的页表级别后返回
pte_t *
superwalk(pagetable_t pagetable, uint64 va, int alloc, int *l)
{
if(va >= MAXVA)
panic("superwalk");
for(int level = 2; level > *l; level--) {
pte_t *pte = &pagetable[PX(level, va)]; // 获取当前层的页表项地址
if(*pte & PTE_V) { // 如果页表项有效
pagetable = (pagetable_t)PTE2PA(*pte); // 转换为物理地址并更新页表指针
if(PTE_LEAF(*pte)) { // 如果是叶节点
*l = level; // 更新实际访问的页表级别
return pte; // 返回页表项地址
}
} else {
if(!alloc || (pagetable = (pde_t*)kalloc()) == 0) // 如果不需要分配或分配失败
return 0;
memset(pagetable, 0, PGSIZE); // 初始化新分配的页表
*pte = PA2PTE(pagetable) | PTE_V; // 更新页表项为有效
}
}
return &pagetable[PX(*l, va)]; // 返回目标页表项地址
}
#endif
|
mappages函数
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
63
64
|
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("mappages: va not aligned");
if((size % PGSIZE) != 0)
panic("mappages: size not aligned");
if(size == 0)
panic("mappages: size");
a = va;
last = va + size - PGSIZE;
for (;;) {
#ifdef LAB_PGTBL
//int sz = PGSIZE;
int use_superpage = 0; // 用于标识是否使用超级页面映射
// 判断是否可以使用超级页面映射
if ((a % SUPERPGSIZE) == 0 && (a + SUPERPGSIZE <= last + PGSIZE) && (perm & PTE_U)) {
use_superpage = 1; // 更改标识
//sz = SUPERPGSIZE;
}
if (use_superpage) {
int l = 1;
if ((pte = superwalk(pagetable, a, 1, &l)) == 0) // 查找或创建超级页面对应的PTE
return -1;
} else {
if ((pte = walk(pagetable, a, 1)) == 0)
return -1;
}
#else // 如果不能使用超级页面映射 就用普通页
if ((pte = walk(pagetable, a, 1)) == 0)
return -1;
#endif
// 检查PTE是否已经被标记为有效
if (*pte & PTE_V)
panic("mappages: remap");
*pte = PA2PTE(pa) | perm | PTE_V; // 将物理地址转换为PTE格式 并加上权限位和有效位
#ifdef LAB_PGTBL
if (use_superpage) { //如果使用超级页
if (a + SUPERPGSIZE == last + PGSIZE) // 检查是否已经映射到最后一个超级页面
break;
// 更新起始地址和物理地址
a += SUPERPGSIZE;
pa += SUPERPGSIZE;
} else {
if (a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
#else //不使用超级页
if (a == last)
break;
a += PGSIZE;
pa += PGSIZE;
#endif
}
return 0;
}
|
uvmunmap函数
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
|
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
int sz;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += sz){
sz = PGSIZE;
#ifdef LAB_PGTBL
int l = 0; // 标志变量 用于确定是超级页还是普通页。
int flag = 0; // 标记是否已经处理过超级页
if((pte = superwalk(pagetable, a, 0, &l)) == 0)
panic("uvmunmap: walk");
#else
if((pte = walk(pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
#endif
if((*pte & PTE_V) == 0) {
printf("va=%ld pte=%ld\n", a, *pte);
panic("uvmunmap: not mapped");
}
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){ // 解除虚拟内存页面映射具体操作
uint64 pa = PTE2PA(*pte); // 从页表项中提取物理地址
#ifdef LAB_PGTBL
if(l == 1) { // 如果是超级页
int perm = *pte & 0xFFF; // 获取权限
*pte = 0; // 清空页表项
flag = 1; // 设置标志
sz = SUPERPGSIZE; // 更新大小为超级页大小
if(a % SUPERPGSIZE != 0){ // 如果虚拟地址未对齐到超级页
// 对齐到超级页边界
for(uint64 i = SUPERPGROUNDDOWN(a); i < va; i += PGSIZE) {
char *mem = kalloc(); // 分配新的物理页面
if(mem == 0)
panic("uvmunmap: kalloc");
mappages(pagetable, i, PGSIZE, (uint64)mem, perm); // 将新分配的页面映射到虚拟地址空间
memmove(mem, (char*)pa + i - SUPERPGROUNDDOWN(a), PGSIZE); // 将数据从超级页复制到新分配的页面
}
a = SUPERPGROUNDUP(a); // 更新虚拟地址
sz = 0; // 更新大小
}
superfree((void*)pa); // 释放超级页
} else
#endif
// 如果是普通页
kfree((void*)pa); // 释放普通页
}
#ifdef LAB_PGTBL
if(flag == 0) // 避免使用超级页时候被重复清除
#endif
*pte = 0;
}
}
|
uvmalloc函数
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
|
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
{
char *mem;
uint64 a;
int sz;
if(newsz < oldsz)
return oldsz;
oldsz = PGROUNDUP(oldsz);
for(a = oldsz; a < newsz; a += sz){
sz = PGSIZE;
#ifdef LAB_PGTBL
// 判断是否可以使用超级页
if (newsz - a >= SUPERPGSIZE && a % SUPERPGSIZE == 0) {
sz = SUPERPGSIZE; // 更新大小为超级页
mem = superalloc(); // 分配超级页大小的物理内存
} else
#endif
mem = kalloc();
if(mem == 0){
uvmdealloc(pagetable, a, oldsz);
return 0;
}
#ifndef LAB_SYSCALL
memset(mem, 0, sz);
#endif
if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
#ifdef LAB_PGTBL
if(sz == SUPERPGSIZE) // 如果分配的是超级页大小内存
superfree(mem); // 释放超级页内存
else
#endif
kfree(mem);
uvmdealloc(pagetable, a, oldsz);
return 0;
}
}
return newsz;
}
|
uvmcopy函数
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
|
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
char *mem;
int szinc;
for(i = 0; i < sz; i += szinc){
szinc = PGSIZE;
#ifdef LAB_PGTBL
int l = 0; // 标志变量 用于确定是普通页还是超级页
if((pte = superwalk(old, i, 0, &l)) == 0)
// 如果是超级页l=1,普通页l=0
panic("uvmcopy: pte should exist");
#else
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
#endif
if((*pte & PTE_V) == 0)
panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
#ifdef LAB_PGTBL
if(l == 1) { // 如果是超级页
szinc = SUPERPGSIZE; // 将地址增量设置为超级页的大小
if((mem = superalloc()) == 0) // 分配超级页大小的内存
goto err;
memmove(mem, (char*)pa, SUPERPGSIZE); // 将超级页大小的物理内存从旧地址复制到新分配的内存地址
if(mappages(new, i, SUPERPGSIZE, (uint64)mem, flags) != 0){ // 将超级页大小的新内存映射到新页表的虚拟地址
superfree(mem); // 释放之前分配的超级页内存
goto err;
}
} else { // 如果是普通页
#endif
if((mem = kalloc()) == 0)
goto err;
memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
kfree(mem);
goto err;
}
#ifdef LAB_PGTBL
}
#endif
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
|
运行结果
