Lab3-page tables && MIT6.1810操作系统工程【持续更新】

Lab:page tables

​ 在这个lab中6.1810 / Fall 2025,要求我们先阅读xv6课本的Chapter 3 Page tables(第三章)。要求我们探索xv6当中关于页表的内容。并且要求我们实现一些页表相关功能的实现(例如:虚地址和物理地址的映射/解除映射,页表的创建和释放等)。

​ 并且官网也给出了提示:

  • kernel/memlayout.h当中存放了内存布局,页表大小相关的常量就在此。
  • kernel/vm.c当中是页表相关逻辑的实现,接下来的大部分lab内容就在此实现。
  • kernel/kalloc.c当中存放的时内存分配相关的逻辑,在新建/删除页表时会用到这里的函数。

Speed up system calls (简单)

​ 在这个lab当中,要求我们在 xv6 中添加一个新的 用户可读的只读内存映射(USYSCALL) ,用来让用户态程序在不陷入内核的情况下,直接读取部分内核数据(如 pid),并正确处理其 创建、映射、访问与释放的完整生命周期

如何将一个**用户可读的只读内存映射(USYSCALL)**添加到进程页表内?以及如何删除该映射?

前言和注意事项 :在xv6当中的有关进程的创建/释放,进程页表的创建/释放的过程都在kernel/proc.h,并且按照官网的说法,我们需要将进程的pid 存放到内存当中,这样在调用**gitpid系统调用** 时,则直接选择从内存空间当中读取该pid,大大提高了执行效率,并且不用陷入到内核态;这就意味着我们需要在进程的结构体 当中添加一个成员用于指向存放当前进程的pid的空间,为了之后的读取。

一、分配物理内存

​ 前面提到过,进程的结构体成员当中有指向进程pid的指针(struct usyscall *),因此,我们需要先给他分配物理内存(由内核分配)。

c 复制代码
p->usyscall = (struct usyscall *)kalloc(); //分配物理内存

二、初始化内容:

​ 将当前进程的pid 存放到刚才的指针p->usyscall所指向的空间中。

c 复制代码
p->usyscall->pid = p->pid;

// 以下是xv6提前写好的,改进后的ugetpid方法
int
ugetpid(void)
{
  struct usyscall *u = (struct usyscall *)USYSCALL;  //通过虚拟地址USYSCALL访问特点内存
  return u->pid;
}

​ 为什么我们必须通过struct usyscall *来访问,而不是直接返回进程结构体当中的pid呢?

​ 答:首先,xv6有内核页表用户页表 ,并且用户态下的进程只能看得见内存。因为进程的结构体存放在内核页表当中,在用户态下我们只能访问到用户页表,所以准确来说我们只能通过虚拟内存 搭配页表机制 的方式来访问存放在该物理空间当中内容。我们在内核态下通过p->usyscall = (struct usyscall *)kalloc(); 分配的内存似乎也是被内核所管理,但是我们将USYSCALL这个虚拟地址和物理地址相映射了起来,因此我们可以通过在用户态下访问该虚拟地址的方式下访问到具体的物理地址当中的值。

三、创建用户页表:

​ 众所周知,OS当中的进程采用页表机制来将进程的虚地址映射到物理地址上,所以说无论我们是否要添加映射到页表中,我们都必不可免地要创建一个用户页表。

c 复制代码
p->pagetable = proc_pagetable(p);

四、建立虚拟地址 到 物理地址映射:

​ 说白了就是在用户页表中添加一个新的页表项,所以这一步的操作要在页表的相关逻辑 当中进行,该页表项用于映射到刚才分配的物理内存。在kernel/defs.h当中,我们可以看到**mappages**的声明(该函数用于添加映射到页表)。

​ 注意:页表机制是将进程的虚拟地址映射为内存中真实的物理地址,所以在添加新的映射时,要一并给出这些参数以及映射大小和权限。

c 复制代码
// 映射 USYSCALL
  if(mappages(pagetable,
              USYSCALL,  //虚拟地址
              PGSIZE,  // 映射大小
              (uint64)p->usyscall, //物理地址
              PTE_R | PTE_U | PTE_V) < 0){  // 官网要求设置的权限
    uvmfree(pagetable, 0);
    return 0;
  }

xv6的权限(添加权限的目的是防止"篡改","非法访问"等等操作):

含义
PTE_R 用户可读
PTE_W 防止用户写
PTE_X 防止执行
PTE_U 用户态可访问
PTE_V 映射有效

五、删除/释放映射:

​ 首先在页表释放的相关逻辑 当中进行释放映射的操作,在kernel/defs.h当中,我们可以看到uvmunmap的声明(该函数用于删除/释放映射到页表)。

c 复制代码
uvmunmap(pagetable, USYSCALL, 1, 0); //释放USYSCALL

​ 之后在进程释放的相关逻辑 进行释放之前访问的物理空间的操作,在kernel/defs.h当中,我们可以看到kfree的声明(该函数用于释放分配的内存)。

c 复制代码
kfree(p->usyscall);

六、深入了解进程和页表的底层逻辑:

函数(kernel/proc.c) 负责什么
allocproc 分配"进程资源"(pid、usyscall、trapframe、kstack)(第一,二,三步在此进行)
freeproc 释放"进程资源" (第五步后半部分在此进行)
proc_pagetable 构造页表结构 (第五步前半部分在此进行)
proc_freepagetable 拆除页表结构 (第四步在此进行)

​ 由此我们可以得知页表的生命周期几乎伴随整个进程。

代码的相关内容:

c 复制代码
/* 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();
  p->state = USED;
  // 分配物理内存
  p->usyscall = (struct usyscall *)kalloc();
  if(p->usyscall == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  // 初始化内容
  p->usyscall->pid = p->pid;

  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // An empty user page table.
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // Set up new context to start executing at forkret,
  // which returns to user space.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;

  return p;
}

// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
  // 释放之前分配的物理内存
  if(p->usyscall){
    kfree((void*)p->usyscall);
    p->usyscall = 0;
  }
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  p->pagetable = 0;
  p->sz = 0;
  p->pid = 0;
  p->parent = 0;
  p->name[0] = 0;
  p->chan = 0;
  p->killed = 0;
  p->xstate = 0;
  p->state = UNUSED;
}

// Create a user page table for a given process, with no user memory,
// but with trampoline and trapframe pages.
pagetable_t
proc_pagetable(struct proc *p)
{
  pagetable_t pagetable;

  // An empty page table.
  pagetable = uvmcreate();
  if(pagetable == 0)
    return 0;

  // 映射 USYSCALL(也是关键部分)
  if(mappages(pagetable,
              USYSCALL,  //虚拟地址
              PGSIZE,  // 映射大小
              (uint64)p->usyscall,  //物理地址
              PTE_R | PTE_U | PTE_V) < 0){ // 权限
    uvmfree(pagetable, 0);
    return 0;
  }
  

  // map the trampoline code (for system call return)
  // at the highest user virtual address.
  // only the supervisor uses it, on the way
  // to/from user space, so not PTE_U.
  if(mappages(pagetable, TRAMPOLINE, PGSIZE,
              (uint64)trampoline, PTE_R | PTE_X) < 0){
    uvmfree(pagetable, 0);
    return 0;
  }

  // map the trapframe page just below the trampoline page, for
  // trampoline.S.
  if(mappages(pagetable, TRAPFRAME, PGSIZE,
              (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

  return pagetable;
}

// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, USYSCALL, 1, 0);  //释放/删除USYSCALL对应的映射
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  uvmfree(pagetable, sz);
}

Print a page table (简单)

​ 这个lab要求我们实现一个打印页表的函数,同时也能帮助我们理解xv6当中,页表是如何实现的。在本次实验前,这门课程的作者已经将kpgtbl()这个系统调用添加到内核当中了,现在我们要做的就是完善kernel/vm.c当中的**vmprint()** 函数,这个函数接收一个pagetable_t(页表类型)的参数。

xv6当中的页表是怎样的?

零、专业词汇阐述

名称 含义
VA 虚拟地址(CPU 使用)
PTE 页表项(映射 + 权限)
PA 物理地址(RAM 索引)
PPN 物理页号(PA 的高位)

一、虚拟地址va的结构和xv6当中的三级页表

​ 根据本课程对应的课本xv6 book 当中的第三章,我们可以得知在xv6当中,虚拟地址va的位数为64位,并且我们只使用低39位,高25位用于扩展。相信你在看到这里时肯定学过操作系统这门课程,在任何一本操作系统的教科书当中,对于虚拟地址va的构成的描述都是低n位是页内偏移地址,用于定位某页内的页表项,剩下的高位都是索引,用于定位到某一页。

​ 在xv6当中,页表的每页大小为4096B每个页表项(PTE)的大小为8B ,所以一个页表的当中有4096/8 = 512个PTE。所以39位的虚拟地址va当中,低12位为页内偏移量,省下的27位用于索引页表。

​ xv6采用三级页表,也就是说27位的索引地址,每9位构成一个层级,类似一个树。以下内容是39位虚拟地址的构成。

​ PS:(床图网站随时可能失效,所以下面我尽量使用文字来进行描述)。

c 复制代码
|VPN[2] | VPN[1] | VPN[0]|页内偏移|
  9        9        9		12       共39位
一级索引  二级索引  三级索引  页内偏移量   总位数
  根                叶子

​ 寻址时,先访问VPN[2]当中的某个PTE,该PTE指向VPN[1],之后从VPN[1]中选取新的PTE,再次通过新的PTE寻址VPN[0],用VPN[0]获得最终的PTE后即可获得PNN(物理页号)。最后通过对PNN操作得到PA(物理地址)。整个过程类似寻找树的叶子结点那样,一层一层向下寻找。

二、为什么xv6采用三级页表?

​ 进程在创建之初,必须且至少拥有一个页表。

如果采用一级页表设计,为了满足这一必须的条件,操作系统必须一次性分配一张覆盖整个虚拟地址空间的页表,即使进程只使用其中极小的一部分(大部分内存空间会浪费掉),也必须遵守该规定。

​ 而在采用三级页表的设计中,进程创建时只需要分配一个 4KB 的根页表页,其余页表页在虚拟地址空间被实际使用时才按需分配。

二、PTE的内容

​ 已知每个PTE的大小为8B,即一共64位。其中低10位(90位)为flags(权限位/标记),剩下的高位(5310位共44bit)为PNN(物理页框号,分配内存之时,OS从空闲页框表当中的表头取下来的)。最后的10位(63~54位)暂时未用,置为0。

​ flags的内容:

含义
V 是否有效
R 可读
W 可写
X 可执行
U 用户可访问
A/D 硬件访问/修改标记

​ 当一个 PTE 的 R/W/X 任一位为 1 时,该 PTE 是叶子结点,指向真实物理页;

​ 若 R/W/X 全为 0 且 V=1,则该 PTE 指向下一级页表。

​ 页表的本质除了指明虚拟地址映射到哪里外,还可以决定这个地址是否可读,是否可写,是否可执行,是否可用户态访问/执行

三、PNN如何转为物理地址

​ xv6当中规定物理地址的位数为56位,由PNN和va的低12位拼接而成,具体操作手法如下:

​ 1、首先讲PTE右移10位,这样低10位的flags会消失。

​ 2、之后讲PTE左移12位,这样低12位的空白正好可以由虚拟地址的低12位偏移量进行填补。

​ 3、我们现在需要将虚拟地址va的第12位进行填补,所以我们将va和0xFFF相与,这样va就只剩下了第12位的偏移量。

​ 4、将PTE和va相加或着进行"逻辑或"操作,这样就拼接好了一个完整的物理地址。

​ 注意:在xv6当中,以上的操作都有着对应的宏,在编码时可以直接使用宏操作。

该lab的实现和代码相关内容

一、个人的解析和官网提示

  1. 打印格式:第一行显示 vmprint 的参数。之后,每个页表项(PTE)对应一行,包括那些指向树中更深层次页表页的页表项。每个页表项行都缩进若干个 "..",以表示其在树中的深度。每个页表项行都会显示其虚拟地址、页表项位以及从该页表项中提取的物理地址。不要打印无效的页表项。
  2. kernel/riscv.h的文件末尾,有关于va转pa的宏。
  3. freewalk这个函数也许会带来启发。
  4. 在printf调用中使用%p,以官网上示例所示的方式打印完整的64位十六进制页表项(PTE)和地址。

二、代码相关内容

c 复制代码
##在kernel/vm.c文件内:

static void
vmprint_walk(pagetable_t pagetable, int level, uint64 va){
  //每个页表521个PTE
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    // pte有效 并且 V位为1则不是叶子结点
    if((pte & PTE_V) == 0)
      continue;
    // 将传入的PA物理地址(此时PA第12位为空)和偏移量相加合并为完整的物理地址
    uint64 newva = va | ((uint64)i << (12 + 9 * level));

    // 打印层级, depth = 2 - level
    for(int d = 0; d < 2 - level; d++)
      printf(" ..");

    printf("%p: pte %p pa %p\n",
           (void*)newva,
           (void*)pte,
           (void*)PTE2PA(pte));

    // 不是叶子结点则向下递归
    if((pte & (PTE_R | PTE_W | PTE_X)) == 0){
      // PTE2PA是将pte转为了物理地址PA(此时低12位为空)
      pagetable_t child = (pagetable_t)PTE2PA(pte);
      vmprint_walk(child, level - 1, newva);
    }
  }

}

#if defined(LAB_PGTBL) || defined(SOL_MMAP) || defined(SOL_COW)
void
vmprint(pagetable_t pagetable) {
  // your code here
  // 打印第一行,之后递归进行遍历
  printf("page table %p\n", pagetable);
  vmprint_walk(pagetable, 2, 0);
  
}
#endif

Use superpages (困难)

​ 这个lab可以说是最难的lab。卡了我快20个小时。当用户通过sbrk()申请内存时,如果申请的内存**≥2MB**时,xv6不再使用传统的三级页表(即大小为4K的页),而是采用二级页表(即1个2MB的超级页)。并且相关的函数也要适配处理超级页的功能。

​ 采用超级页后的地址结构如下:

复制代码
|VPN[2] | VPN[1](包含VPN[0])|页内偏移|
  9        9        9		   12       共39位
一级索引         二级索引      页内偏移量   总位数
  根              叶子
===============================
level-2 (512GB)
  |
level-1 (2MB)   ← ★ superpage 在这里(第一层)
  |
level-0 (4KB)   ← 普通的页面在这里(第0层)

​ 起始该lab的某些地方的写法是有迹可循的,你可以直接照搬之前原因的部分代码。

顺腾摸瓜寻找需要修改的内容

​ 一、在kernel/kalloc.c文件当中的函数是负责分配页表内存的,目前这里只有普通页的内容,我们需要添加超级页的相关内容。在kmem中添加一个run结构,让其指向一个超级页的空闲页表。 之后在freerange函数当中仿照普通页的内存分配逻辑,照葫芦画瓢写一个超级页的内存分配逻辑。同时仿照kfreekalloc写一个superallocsuperfree,这两个分别是超级页的分配和释放。

​ 二、sbrk()当中调用了growproc()函数,使用参数n 调整内存的大小。当n 为有效值时则调用uvmalloc函数来对用户进行虚拟内存的分配**(这里需要修改uvmalloc)** 。进一步进入uvmalloc函数当中,其中涉及了mappages函数,该函数负责为每个页表项映射物理地址**(这里需要修改mappages)** ;同时也涉及了uvmdealloc函数,该函数的功能是释放用户页面,其内部涉及uvmunmap函数,这个函数是页面释放的具体实现**(这里需要修改uvmunmap)** 。在mappages函数当中涉及了walk函数,该函数负责返回虚拟地址 va 对应的页表项(PTE)的地址**(这里需要修改walk)**。

​ 三、官网说了,通过用户程序pgtbltest来测试超级页功能是否完成,所以我们顺藤摸瓜在kernel/pgtbltest.c当中发现superpg_kfork函数调用了fork进程来创建新进程,打算让新的进程采用超级页。所以我们再次顺腾摸瓜找到了kfork函数,里面涉及了uvmcopy函数,这个函数负责将父进程的页表复制给子进程(把父进程的数据拷贝一份给子进程),(这里需要修改uvmcopy)

代码相关内容

这一小节本人一开始没做出来,因此参考了很多大佬的博客和视频才得以做出,以下代码参考了这位大佬的博客→mit6.1810]Lab3: page tables

​ 1、在kalloc.c当中照葫芦画瓢添加对超级页的管理。

c 复制代码
struct {
  struct spinlock lock;
  struct run *freelist;
  struct run *superfreelist;   // 仿照上面的freelist
} 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;
  // 计算超级页的起始地址,从 pa_end 向下对齐到超级页边界
  char *superp = (char*)SUPERPGROUNDUP((uint64)pa_end - superpg_num * SUPERPGSIZE);
  // 先释放普通页面部分
  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;
  // 参数验证:确保 pa 对齐到超级页大小且在合法内存范围内
  if(((uint64)pa % SUPERPGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("superfree"); 
  
  memset(pa, 1, SUPERPGSIZE);
  r = (struct run*)pa;
  //加锁
  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);
   // 返回分配的超级页地址
  return (void*)r;
}
#endif

​ 2、kalloc.h当中,我们给普通页分配内存时用到了PGROUNDUP,于是超级页的内存分配也需要类似的内容。我们顺腾摸瓜找到riscv.h,在里面仿照PGROUNDUPPGROUNDDOWN,新增SUPERPGROUNDUPSUPERPGROUNDDOWN

c 复制代码
#define SUPERPGROUNDUP(sz)  (((sz)+SUPERPGSIZE-1) & ~(SUPERPGSIZE-1))
#define SUPERPGROUNDDOWN(a) (((a)) & ~(SUPERPGSIZE-1)) 

​ 3、在defs.h中添加下刚才的新增的声明。

c 复制代码
void *          superalloc(void);
void            superfree(void *pa);
pte_t *         superwalk(pagetable_t, uint64, int, int *);

接下来的内容都在kernel/vm.c当中实现

​ 4、添加uvmalloc函数。

c 复制代码
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;
}

​ 5、修改mappages函数。

c 复制代码
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 use_superpage = 0; // 用于标识是否使用超级页面映射
    // 判断是否可以使用超级页面映射
    if ((a % SUPERPGSIZE) == 0 && (a + SUPERPGSIZE <= last + PGSIZE) && (perm & PTE_U)) {
      use_superpage = 1; // 更改标识
    }
    // 如果是超级页则设置l为1,代表接下来在superwalk当中到1层后停止
    // 传统的walk会走到level0,之后返回pte(页表项地址)
    // 而改进过的superwalk可以被人为操控停到指定的层级。
    //  层级从高到底为:2 1 0
    if (use_superpage) {
      int l = 1;
      if ((pte = superwalk(pagetable, a, 1, &l)) == 0)
        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格式 并加上权限位和有效位
    // 这里就是添加映射的核心
    *pte = PA2PTE(pa) | perm | PTE_V; 
#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;
}

​ 6、修改uvmunmap函数。

**注意:**在释放整个页时涉及三种情况(页只会在其对应的虚拟地址被完全 unmap 时被释放):

  1. 第一种情况是释放普通页,已知每个普通页都是4KB,并且xv6的三级页表的最低级也都是4KB,所以直接释放即可。
  2. 第二种情况是释放超级页(整块释放),超级页的大小为2M,因为xv6的三级页表的第二层是表示超级页的层级(如果第二层 PTE 是 leaf 并且覆盖 2MB,则是超级页),此时在地址对其的情况下并且释放该页不会对其它的页造成影响则直接释放即可。
  3. 第三种情况是释放超级页(非整块释放,可能比一块小也可能比一块大),众所周知,在操作系统当中,一个 leaf PTE 要么映射整个 4KB 页框,要么映射整个 2MB 页框,不能只映射其中一部分。所以,当我们释放内存时,被释放的内存大小没有超过一个超级页 或者 超过了一个超级页,那么就必然导致有一个页的完整性被打破,从而违反操作系统对单个页完整性的规定。所以我们要将哪些被破坏了完整性的超级页进行降级操作,使得其降为普通页。降级的过程就是再开辟新的普通页,然后将原来超级页的内容(正常存在无需释放的内容)复制到新的普通页,之后我们删除/释放原来的超级页。

问: 为什么2MB的超级页的完整性被破坏后就必须降级为4KB的普通页?
**答:**xv6支持3级页表,普通页(4KB)已经是最小的硬件映射粒度,不能再细分,所以不存在"普通页被部分破坏后再降级"的问题。

c 复制代码
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;
  }
}

​ 7、仿照walk添加superwalk。

c 复制代码
#ifdef LAB_PGTBL
// 参数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 {
      //页表项无效则尝试分配,分配失败返回0
      if(!alloc || (pagetable = (pde_t*)kalloc()) == 0) 
        return 0;
      // 初始化新分配的页表
      memset(pagetable, 0, PGSIZE);
      // 更新页表项为有效
      *pte = PA2PTE(pagetable) | PTE_V; 
    }
  }
  // 返回目标页表项地址
  return &pagetable[PX(*l, va)]; 
}
#endif

​ 8、添加uvmcopy函数。

c 复制代码
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;
}

写在后面

​ 这一lab,尤其是最后的用户页表lab确实非常难,一开始花费了好长时间都没做出来,好在网络上有很多大佬对该lab进行了讲解和提供了成品代码,使得本人在后续的研究中才得以明白该lab的底层逻辑。

相关推荐
十年磨一剑~4 小时前
Linux程序接收到sigpipe信号崩溃处理
linux
geshifei4 小时前
Sched ext回调3——select_cpu(linux 6.15.7)
linux·ebpf
代码游侠4 小时前
C语言核心概念复习——网络协议与TCP/IP
linux·运维·服务器·网络·算法
你真是饿了4 小时前
6.库制作与原理
linux·服务器
Zach_yuan5 小时前
深入浅出 JSONCpp
linux·服务器·网络·c++
北京迅为6 小时前
《【北京迅为】itop-3568开发板NPU使用手册》- 第 7章 使用RKNN-Toolkit-lite2
linux·人工智能·嵌入式·npu
Dragon~Snow6 小时前
Linux Centos9 安装 Elasticsearch
linux·elasticsearch·jenkins
熊延6 小时前
麒麟V10系统安装部署elasticsearch
linux·运维·服务器·elasticsearch·搜索引擎·全文检索
Jia ming7 小时前
跟踪器与事件使用举例
linux·事件·跟踪器