文章目录
实验三 页表
一、代码理解
在开始编码之前,请阅读xv6 书的第 3 章和相关文件:
kern/memlayout.h,捕获内存的布局。
kern/vm.c,包含大部分虚拟内存(VM)代码。
kernel/kalloc.c,包含分配和释放物理内存的代码。
1.对于内存布局定义的理解
1. **硬件设备映射**:
- **UART0**、**VIRTIO0**、**CLINT**、**PLIC**等定义了硬件设备的物理地址和中断号。这些地址是硬件设计时确定的,操作系统需要知道这些地址才能正确地与设备通信。
- 例如,UART0用于串行通信,操作系统需要知道其寄存器地址`0x10000000L`和中断号`10`来初始化和处理串行通信。
2. **内核加载和执行**:
- 内核代码和数据从`0x80000000`开始加载,这是因为在RISC-V架构中,通常从高地址开始加载内核,以避免与低地址的硬件设备冲突。
- `KERNBASE`和`PHYSTOP`定义了内核使用的物理内存范围,确保内核有足够的内存空间来运行。
- 如KERNBASE + 128*1024*1024:这个表达式将KERNBASE的地址加上128MB,结果是内核可用物理内存的上限地址。
3. **中断处理**:
- **PLIC**和**CLINT**的定义是为了处理中断。PLIC负责管理外部中断,而CLINT处理核心局部中断(如定时器中断)。
- 通过定义这些寄存器的地址,内核可以配置中断优先级、使能中断、处理中断请求等。
4. **内核和用户空间隔离**:
- `TRAMPOLINE`和`TRAPFRAME`的定义是为了处理内核和用户空间之间的切换。`TRAMPOLINE`页用于执行从用户空间到内核空间的切换代码,而`TRAPFRAME`用于保存用户进程的状态
- 其中TRAMPOLINE宏定义了一个特定的页,这个页用于存放从用户空间到内核空间切换时所需的代码和数据,通常称为“trampoline page”。这个页被映射到虚拟地址空间的高端,以便在处理中断、异常或系统调用时,能够快速且安全地切换到内核空间。
- 其中#define KSTACK(p) (TRAMPOLINE - ((p)+1)* 2*PGSIZE)
TRAMPOLINE - ((p)+1)* 2*PGSIZE:这个表达式计算出第p个内核栈的起始地址,它位于TRAMPOLINE页的下方,每个内核栈之间由保护页隔开。
- 这种布局确保了内核和用户空间之间的安全隔离,防止用户程序直接访问或修改内核数据。
5. **内存保护和安全性**:
- 通过将内核和用户空间分开,并使用特定的内存布局(如`KSTACK`定义的内核栈),可以防止用户程序越界访问内核内存,从而提高系统的安全性。
6. **灵活性和可扩展性**:
- 这种内存布局提供了一定的灵活性,使得操作系统可以根据需要动态分配和管理内存。例如,通过定义`KSTACK`来为每个处理器核心分配内核栈,可以支持多核处理器的并发执行。
2.对虚拟内存的理解
在操作系统中,用户页(User Pages)通常是指分配给用户进程的内存页面。这些页面在虚拟内存系统中使用,并且它们的地址是虚拟地址,而不是物理地址。虚拟内存系统允许每个进程拥有自己独立的地址空间,这样进程就可以使用连续的虚拟地址空间来访问可能分散在物理内存中的数据。
在vm.c代码中,有几个函数涉及到用户页的管理:
- `uvmcreate()`:创建一个新的用户页表。
- `uvminit()`:将用户初始代码加载到页表中。
- `uvmalloc()`:为用户进程分配内存。
- `uvmdealloc()`:释放用户进程的内存。
- `uvmcopy()`:复制用户页表和物理内存。
- `uvmclear()`:标记用户页表项为无效。
这些函数主要负责管理用户进程的虚拟内存,包括分配、释放和复制内存页面。它们确保用户进程的虚拟地址空间正确映射到物理内存,并根据需要更新页表。
3.对分配和释放物理内存的理解–删除或者分配物理内存为啥不需更改相应的页表?
在提供的代码中,`kalloc()` 和 `kfree()` 函数确实只负责物理内存的分配和释放,它们并不直接修改页表。页表的修改通常是在其他地方进行的,这些地方涉及到将虚拟地址映射到新分配的物理地址,或者解除虚拟地址到物理地址的映射。
- **分配物理内存** (`kalloc()`):当 `kalloc()` 分配一个物理页面时,它返回一个指向该页面的指针。然而,这个页面还没有被映射到任何虚拟地址空间。如果这个页面需要被用于某个特定的虚拟地址(例如,为用户进程分配内存、为内核栈分配内存等),那么需要调用额外的函数来更新页表,将相应的虚拟地址映射到这个新分配的物理地址。
- **释放物理内存** (`kfree()`):当 `kfree()` 释放一个物理页面时,它只是将这个页面归还到内核的空闲列表中。如果这个页面之前被映射到某个虚拟地址,那么在调用 `kfree()` 之前,通常需要先更新页表,解除该虚拟地址到物理地址的映射,以确保不再有虚拟地址引用这个物理页面。
二、Print a page table
1.题目描述
定义一个名为vmprint() 的函数。它应该接受一个pagetable_t参数,并以下面描述的格式打印该页表。
在 exec.c 中的return argc之前插入if(p->pid==1) vmprint(p->pagetable) ,以打印第一个进程的页表。如果您通过了make grade的pte 打印输出测试,您将获得此作业的满分。
2.题目思考
RISC-V 是64位结构,高25位保留限制了虚拟内存是512GB。它的物理地址限制在56位。它的逻辑地址寻址是采用三级页表的形式,9 bit 一级索引找到二级页表,9 bit 二级索引找到三级页表,9 bit 三级索引找到内存页,最低 12 bit 为页内偏移(即一个页 4096 bytes)。打印页表可以参照walk函数。
3.提交实验
1.首先,在kernel/defs.h文件中声明,以便于后续的调用能找到。
void vmprint(pagetable_t pagetable);
2.其次,在kernel/vm.c修改freewalk函数打印页表
void vmprint(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) { // 如果页表项有效
// 简化版打印页表项信息
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.
uint64 child = PTE2PA(pte);
pgtblprint_simplified((pagetable_t)child);
}
}
}
}
3.最后,按照题目要求修改exec文件。
vmprint(p->pagetable); // 按照实验要求,在 exec 返回之前打印一下页表。
return argc;
三、A kernel page table per process
1.题目描述
您的第一项工作是修改内核,以便每个进程在内核中执行时都使用自己的内核页表副本。修改struct proc以维护每个进程的内核页表,并修改调度程序以在切换进程时切换内核页表。
对于此步骤,每个进程的内核页表应与现有的全局内核页表相同。如果用户测试运行正常, 则您通过了实验的这一部分。
2.题目思考
题目的意思就是需要我们摒弃用户指针,在内核态中给每个进程单独的页表。我们从创建进程,切换进程,和释放进程的角度来考虑内核页表。基本上只要改动两个文件,vm.c和proc.c;其余就是根据其中的函数定义改一下声明。
3.提交实验
1.创建进程
首先 在proc.h文件中加入内核页表的字段
pagetable_t kernelpgtbl;
在proc.c中,我们要给进程创建时分配内核页表,而不是像之前一样进程共用内核页表。
// initialize the proc table at boot time.
void
procinit(void)
{
struct proc *p;
initlock(&pid_lock, "nextpid");
for(p = proc; p < &proc[NPROC]; p++) {
initlock(&p->lock, "proc");
// 这里删除了为所有进程预分配内核栈的代码,变为创建进程的时候再创建内核栈,见 allocproc()
}
kvminithart();
}
原本只有内核那一整个页表有那个地址映射,然后我们为我们自定义的内核页表(给之后的进程使用)创建抵制映射,我们就需要改进这个映射函数。
修改后这个函数的主要目的是在给定的页表中,从起始虚拟地址 va 到 va + size - 1 的范围内,将虚拟地址映射到物理地址 pa,并设置相应的权限。如果任何页面已经映射或映射过程中发生错误,函数将返回错误代码或调用 panic 函数。
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + size - 1);
for(;;){
if((pte = walk(pagetable, a, 1)) == 0)
return -1;
if(*pte & PTE_V)
panic("remap");
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
至此,我们给每个指定页表映射,然后我们要修改相应逻辑kvmmap函数和调用了它的函数。
2.切换进程
在我们的proc.c文件中void scheduler(void)中,我们要改变其逻辑使得切换进程时候,响应内核页表也切换。
// 切换到进程独立的内核页表
w_satp(MAKE_SATP(p->kernelpgtbl));
sfence_vma(); // 清除快表缓存
// 调度,执行进程
swtch(&c->context, &p->context);
// 切换回全局内核页表
kvminithart();
3.释放进程
释放进程时候我们也要释放对应的内核页表;相应进程释放时调用它。
// kernel/vm.c
// 递归释放一个内核页表中的所有 映射
void
kvm_free_kernelpgtbl(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];
uint64 child = PTE2PA(pte);
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
kvm_free_kernelpgtbl((pagetable_t)child);
pagetable[i] = 0;
}
}
kfree((void*)pagetable);
}
四、Simplify copyin/copyinstr
1.题目描述
将kernel/vm.c中的copyin 函数 体替换为对copyin_new 的调用(在 kernel/vmcopyin.c中定义);对copyinstr和copyinstr_new执行相同操作。
将用户地址的映射添加到每个进程的内核页表,以便 copyin_new和copyinstr_new可以正常工作。如果usertests运行正常并且所有make grade测试都通过, 则您通过了此作业。
2.题目思考
先考虑原来是怎么映射的,使用断点追踪;然后考虑新映射的增加。
3.提交实验
为了完成这个实验,我们需要让每个进程的内核页表维护一份用户页表映射的副本,以便利用CPU的硬件寻址功能,提高效率。以下是精简的步骤进行实验:
1. 实现工具方法
在 kernel/vm.c
中,实现 kvmcopymappings
和 kvmdealloc
函数:
int kvmcopymappings(pagetable_t src, pagetable_t dst, uint64 start, uint64 sz);
uint64 kvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz);
2. 修改 kvm_map_pagetable
在 kernel/vm.c
中,去除对 CLINT 的映射:
void kvm_map_pagetable(pagetable_t pgtbl) {
kvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);
kvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
kvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
}
void kvminit() {
kernel_pagetable = kvminit_newpgtbl();
kvmmap(kernel_pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
}
3. 在 exec
中加入检查,防止程序内存超过 PLIC
在 kernel/exec.c
中修改:
int exec(char *path, char **argv) {
// Load program into memory.
for(i = 0, off = elf.phoff; i < elf.phnum; i++, off += sizeof(ph)) {
if (sz1 >= PLIC) {
goto bad;
}
}
// ...
}
4. 同步映射
在 fork
中同步进程内核页表和用户页表
在 kernel/proc.c
中修改:
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;
}
// ...
}
在 exec
中同步内核页表并清除旧映射
在 kernel/exec.c
中修改:
int exec(char *path, char **argv) {
uvmunmap(p->kernelpgtbl, 0, PGROUNDUP(oldsz) / PGSIZE, 0);
kvmcopymappings(pagetable, p->kernelpgtbl, 0, sz);
// ...
}
在 growproc
中同步内核页表
在 kernel/proc.c
中修改:
int growproc(int n) {
if (n > 0) {
if (kvmcopymappings(p->pagetable, p->kernelpgtbl, p->sz, n) != 0) {
uvmdealloc(p->pagetable, newsz, p->sz);
return -1;
}
} else if (n < 0) {
sz = kvmdealloc(p->kernelpgtbl, p->sz, p->sz + n);
}
// ...
}
在 userinit
中同步初始进程页表
在 kernel/proc.c
中修改:
void userinit(void) {
kvmcopymappings(p->pagetable, p->kernelpgtbl, 0, p->sz);
// ...
}
5. 替换 copyin
和 copyinstr
实现
在 kernel/vm.c
中修改:
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);
}
6. 测试
确保所有更改完成后,运行用户测试和 make grade
确保所有测试通过。
make qemu
make grade
五、思考体会
1.页表是怎么使用偏移来映射物理地址和逻辑地址的
在RISC-V架构中,页表用于将虚拟地址(逻辑地址)映射到物理地址。页内偏移是虚拟地址的一部分,它直接对应于物理页内的偏移量,用于确定在物理页内的具体位置。通过这种方式,页表和页内偏移共同工作,实现了从虚拟地址到物理地址的映射。以下是详细的步骤:
1. **虚拟地址分解**:
- 虚拟地址被分为几个部分:页表索引和页内偏移。
- 在RISC-V的三级页表系统中,虚拟地址的前27位(9位一级索引 + 9位二级索引 + 9位三级索引)用于查找页表项(PTE),而最后的12位是页内偏移。
2. **页表查找**:
- 使用一级索引(9位)在顶级页表中查找,找到一个二级页表的物理地址。
- 使用二级索引(9位)在二级页表中查找,找到一个三级页表的物理地址。
- 使用三级索引(9位)在三级页表中查找,找到一个物理页的基地址。
3. **物理地址生成**:
- 找到物理页的基地址后,将这个基地址与虚拟地址中的页内偏移(12位)组合,形成最终的物理地址。
- 物理地址 = 物理页基地址 + 页内偏移
4. **访问物理内存**:
- 使用生成的物理地址访问实际的物理内存。
这个过程是由硬件(通常是内存管理单元,MMU)自动完成的。当CPU尝试访问一个虚拟地址时,MMU会执行上述步骤,将虚拟地址转换为物理地址,然后访问物理内存。如果页表中没有有效的映射(即页表项无效或权限不足),MMU会触发一个缺页异常,操作系统必须处理这个异常,可能需要分配新的物理内存页并更新页表。
六、lec5调用约定笔记
1.讲了汇编流程:预处理编译汇编链接
2.对比精简指令集RISC- V和复杂指令集CISC(X86)
3.汇编代码举例讲解。例如kernel.asm是整个内核的编译语言
4.寄存器讲解
5.函数调用和堆栈
6.结构体在内存中的布局
7.关于国内外课程的一点体会:
我去年学了《嵌入式系统设计》,恰巧主要讲的也是arm汇编,老师花了大半个学期给我们讲定义讲语法,然后抽象思考流程,下学期再做实验。我老师理论知识很扎实这门课讲得很好,但是我学的时候很吃力因为全靠抽象的想象。我当时理论课专业第一,但是一到实验课却有一种啥也没学过的错觉;后来才发觉我理论学得全是汇编语言,实验全都要求C。假如这两门课衔接得更好,我觉得也不比国外差。
今天听的这个讲座相当于花九十分钟总结了我之前所有学的理论课,至于具体细节它更倾向于学生课后自行探讨,例如一些语法他直接给学生抛资料。
国内是“先打好扎实的理论基础再实践”;国外是“边实践边理论”。哪边更好我也说不出来,我只能说取决于老师和课程以及资料的配置。
最后“先理论再实践”是最合适我国的实践情况的,因为我们的课设置得太多太多了,如果按照mit这种“边实践边理论”的设置,我敢信很多大学生都不能有时间去用心思考。
标签:pagetable,--,PTE,内存,虚拟地址,lab3,内核,MIT,页表
From: https://blog.csdn.net/m0_56243424/article/details/139985098