page table源码阅读


相关的代码文件主要是kalloc.cvm.cmemlayout.hriscv.h


memlayout.h

这是xv6的物理内存布局定义头文件。首先注释说明的是QEMU虚拟机的物理内存布局。

// Physical memory layout

// qemu -machine virt is set up like this,
// based on qemu's hw/riscv/virt.c:
//
// 00001000 -- boot ROM, provided by qemu
// 02000000 -- CLINT
// 0C000000 -- PLIC
// 10000000 -- uart0 
// 10001000 -- virtio disk 
// 80000000 -- boot ROM jumps here in machine mode
//             -kernel loads the kernel here
// unused RAM after 80000000.

// the kernel uses physical memory thus:
// 80000000 -- entry.S, then kernel text and data
// end -- start of kernel page allocation area
// PHYSTOP -- end RAM used by the kernel

当机器启动时,首先执行entry.S中的汇编启动代码,这是内核的入口,0x80000000是内核启动的物理地址,内核的代码和数据在这里存放。

end是内核代码和数据的结束地址,这个地址之后是内核动态分配内存的区域。

PHYSTOP

PHYSTOP是内核可使用的物理内存上限。

这一段定义了内核代码起始地址和结束地址,DRAM的物理内存上限为128MB。

TRAMPOLINE

接下来是TRAMPOLINE页,它被映射到虚拟地址空间的最高处,在用户空间与内核空间都一样,它的内容在trampoline.S中设置。

KSTACK

KSTACK宏定义用于计算每个进程的内核栈虚拟地址。

内核栈位于trampoline下面,每个内核栈又由实际栈空间和上下保护页组成,保护页有共有部分。

KSTACK计算出的地址是从下保护页开始的,实际栈空间要从KSTACK(p)+PGSIZE开始。

TRAMPOLINE

USYSCALL

下面这段注释介绍了用户进程虚拟地址空间的布局。TRAMPOLINE下面是TRAPFRAME陷阱帧,然后是USYSCALL系统调用页,包含进程ID信息。


riscv.h

这个文件中有很多用于硬件寄存器访问的内联函数,就先跳过。我们主要关注一些宏定义。

MSTATUS

这是RISC-V的机器状态寄存器mstatus相关的宏。先解释一下后面的3L << 11的含义:

3L即为二进制的11<< 11意为左移11位,结果是0b1100000000000,对应mstatus寄存器的11-12位(寄存器位从0开始),这两位是寄存器中的MMP字段(Machine Previous Privilege)。

  • MSTATUS_MPP_MASK用于屏蔽mstatus中的MMP字段,它会覆盖11和12位;

  • MSTATUS_MPP_M表示进入异常前特权级为机器模式,其11和12位均为1;

  • MSTATUS_MPP_S表示进入异常前特权级为监督模式,其11位为1,12位为0;

  • MSTATUS_MPP_U表示进入异常前特权级为用户模式,其11和12位均为0。

MSTATUS_MIE在第3位,MIE=1时允许机器模式下的中断,MIE=0时允许所有机器模式中断。

SSTATUS

SPP标识异常发生前的特权模式;SPIE保存异常前监督模式的中断使能状态,异常发生时,硬件会将当前SIE的值保存到这里,然后禁用中断;UPIE保存异常前用户模式的中断使能状态,与前面同理;SIEUIE则是控制各模式中断使能的。

SIE

这三个宏用于控制监督模式下不同类型中断的使能开关。分别控制外部中断、定时器中断和软件中断。

SATP

这两句用于配置SATP寄存器。

第一句设置其分页模式为SV-39,1000是MODE字段的SV39编码,用来指明分页模式。

SATP寄存器的结构如下。ASID是地址空间标识符;PPN就是根页表的物理页号。

第二句用于构造SATP寄存器值,将根页表的物理地址转换为符合SATP寄存器格式的值。SATP_SV39设置分页模式;(((uint64)pagetable) >> 12完成物理地址到PPN的转换,物理地址需按4KB对齐,右移12位也就相当于取高44位作为PPN。

RISC-V的Sv39分页模式下,物理地址根据规范最大支持56位。物理地址由PPN和页内偏移组成。

pte_t和pagetable_t

pte_t对应页表项的64位数据结构、pagetable_t则是页表指针。

PG宏

分别定义了页的大小以及页偏移量(用于地址转换)。

定义超级页的大小为2MB,SUPERPGROUNDUP是地址对齐宏。

这两个是普通页的地址对齐宏,PGROUNDUP用于向上对齐,PGROUNDDOWN向下对齐。

PTE

这定义的是PTE中的标志位。

再下面就是跟页表处理相关的一些宏定义。

PTE_LEAF判断是否为叶子页表项,通过检查是否具有R/W/X任一权限。

PA2PTE将物理地址转换为页表项,先取物理地址的高44位,再右移保留10位作为标志位。PTE2PA同理。PTE_FLAGS提取PTE标志位。

PX宏

这是关于虚拟地址的宏定义。PXMASK是9位掩码,用于提取低9位;PXSHIFT用于计算各级页表的位偏移量;PX提取指定层级的页表索引。

MAXVA

最后是最大虚拟地址。


kalloc.c

这部分代码主要关于xv6系统的物理内存管理。

这是kernel的结束地址。

struct

kinit

kinit()初始化内存分配器的自旋锁,然后将内核结束后的物理内存加入空闲链表。

freerange

其中调用的freerange()函数用于将指定范围内的物理内存页初始化为空闲状态。

kfree

kfree()用于释放指定的物理页,安全检查包括是否对齐、是否在内核页中、以及是否超出物理内存上限。memset填充垃圾数据,然后将其加入空闲链表(头插法)。

kalloc

kalloc()用于分配物理页。它会取空闲链表头部指向的物理页,填充垃圾数据后返回该页指针。


vm.c

这部分代码主要关于xv6的虚拟内存管理。

kvminit

kernel_pagetable是内核页表指针。

kvmmake

kvmmake()用于创建内核页表,这个页表是直接映射的。

kvminithart

这是激活内核页表的函数,用于完成MMU(内存管理单元)的页表切换操作,启用分页并将SATP指向内核页表。

walk

walk()用于查找虚拟地址对应的页表项。PX查找页表项的索引,在前面的头文件中定义的。如果PTE有效,就用PTE2PA转化为物理地址并赋值给pagetable,来指向下一级的页表。后面的#ifdef LAB_PGTBL是大页情况,无需再遍历下级页表。else处理的是PTE无效,需要分配的情况。最后返回最终层的PTE指针。

walkaddr

walkaddr()用于查找虚拟地址对应的物理地址。

kvmmap

kvmmap()用于内核页表映射。

mappages

mappages()用于将虚拟地址范围 [va, va+size) 映射到物理地址范围 [pa, pa+size),并设置页表项(PTE)的权限。首先获取当前虚拟地址对应的PTE,然后检查是否已经被占用,再设置PTE的物理地址(PA2PTE)和权限。

uvmunmap

uvmunmap()用于解除虚拟内存映射,do_free决定是否释放物理页。

uvmcreate

uvmcreate()用于创建空的用户页表。

uvmfirst

uvmfirst()初始化第一个用户进程的地址空间,将初始化代码映射到虚拟地址0

uvmalloc

uvmalloc()用于扩展用户进程的内存空间。

uvmdealloc

uvmdealloc()缩小用户进程的内存空间。

freewalk

freewalk()用于递归释放页表页,在进程退出时彻底释放页表占用的所有物理内存。

uvmfree

uvmfree()用于释放用户内存,先接触映射,再递归释放页表页占用的物理内存。

uvmcopy

uvmcopy()用于将父进程的页表和物理内存复制给子进程。在使用paflags获取父进程信息后,直接用memmove把内容复制给kalloc新分配的子进程物理页,然后用mappages映射。

uvmclear

uvmclear()取消用户模式的访问权限。

copy

copyout函数用于从内核中复制数据到用户空间,copyin相反,copystr用于从用户空间复制字符串到内核。

Last updated