00_前言
本文主要是理解xv6内核源码对虚拟内存部分的描述。主要是阅读文件:kernel/vm.c
,接下来是对剩下的操纵用户态的函数作用的描述。
01_代码
8.walkaddr函数
walkaddr函数是walk函数的一层封装,专门用来查找用户页表中特定虚拟地址va所对应的物理地址。所以需要注意:
- 它只用来查找用户页表
- 返回的是物理地址,而非像walk函数那样只返回最终层的PTE
源码:
// Look up a virtual address, return the physical address,
// or 0 if not mapped.
// Can only be used to look up user pages.
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
pte_t *pte;
uint64 pa;
// 如果虚拟地址大于最大虚拟地址,返回0
// 物理地址为0的地方是未被使用的地址空间
// Question: 为什么不像walk函数一样直接陷入panic?
if(va >= MAXVA)
return 0;
// 调用walk函数,直接在用户页表中找到最低一级的PTE
pte = walk(pagetable, va, 0);
// 如果此PTE不存在,或者无效,或者用户无权访问
// 都统统返回0(为什么不陷入panic?)
if(pte == 0)
return 0;
if((*pte & PTE_V) == 0)
return 0;
if((*pte & PTE_U) == 0)
return 0;
// 从PTE中截取下来物理地址页号字段,直接返回
pa = PTE2PA(*pte);
return pa;
}
调用这个函数的函数主要是:
- copyin
- copyout
- copyinstr
这三个函数是vm.c的核心,专门负责内核态和用户态之间数据拷贝的
9.freewalk函数
这个函数的作用就是专门用来回收页表页的内存的,因为页表是多级的结构,所以此函数的实现用到了递归,调用这个函数时应该保证叶子级别页表的映射关系全部解除并释放(这将会由后面的uvmunmap函数负责),因为此函数专门用来回收页表页。
uvmunmap函数和freewalk函数结合,成功实现了页表页、物理页的全面释放。
源码:
// Recursively free page-table pages.
// All leaf mappings must already have been removed.
// 译:递归地释放页表页,所有的叶级别页表映射关系必须已经被解除
void
freewalk(pagetable_t pagetable)
{
// there are 2^9 = 512 PTEs in a page table.
// 每一个页表都正好有512个页表项PTE,所以要遍历它们并尝试逐个释放
for(int i = 0; i < 512; i++){
// 取得对应的PTE
pte_t pte = pagetable[i];
// 注意,这里通过标志位的设置来判断是否到达了叶级页表
// 如果有效位为1,且读位、写位、可执行位都是0
// 说明这是一个高级别(非叶级)页表项,且此项未被释放,应该去递归地释放
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);
// 释放完毕之后,将原有的PTE全部清空,表示已经完全释放
pagetable[i] = 0;
// 如果有效位为1,且读位、写位、可执行位有一位为1
// 表示这是一个叶级PTE,且未经释放,这不符合本函数调用条件,会陷入一个panic
} else if(pte & PTE_V){
panic("freewalk: leaf");
}
// 这里隐藏了一个逻辑,即if(pte & PTE_V == 0)
// 这说明当前PTE已经被释放,不用再次释放了,直接遍历下一个PTE
}
// 最后释放页表本身占用的内存,回收,回到上一层递归
kfree((void*)pagetable);
}
10.uvmcreate函数
这个函数的作用很简单,就是为用户进程分配一个页表页并返回指向此页的指针。
源码:
// create an empty user page table.
// returns 0 if out of memory.
// 译:创建一个空的用户页表,当内存耗尽时返回空指针
pagetable_t
uvmcreate()
{
pagetable_t pagetable;
// 分配一个内存页
pagetable = (pagetable_t) kalloc();
if(pagetable == 0)
return 0;
// 将此页表的每一个PTE完全清空
memset(pagetable, 0, PGSIZE);
return pagetable;
}
11.uvminit函数
这个函数的作用是将initcode
加载到用户页表的0地址上,initcode
是启动第一个进程时所需要的一些代码。这个函数的作用就是将initcode
映射到用户地址空间中。首先给出xv6中用户地址空间的设计,可以看到从虚拟地址0开始存放的是进程的代码段,对于操作系统启动的第一个进程而言,这个位置放置的就是initcode
代码。
// Load the user initcode into address 0 of pagetable,
// for the very first process.
// sz must be less than a page.
// 译:将用户的initcode加载到页表的0地址
// 仅为第一个进程而服务
// 代码的尺寸必须小于一个页(4096 bytes)
void
uvminit(pagetable_t pagetable, uchar *src, uint sz)
{
// mem虽然是一个指针,但是因为内核地址空间中虚拟地址和物理地址
// 在RAM上是直接映射的,所以它其实也就等于物理地址
char *mem;
// 如果要求分配的大小大于一个页面,则陷入panic
if(sz >= PGSIZE)
panic("inituvm: more than a page");
// 分配一页物理内存作为initcode的存放处,memset用来将当前页清空
mem = kalloc();
memset(mem, 0, PGSIZE);
// 在页表中加入一条虚拟地址0 <-> mem的映射,相当于将initcode成功映射到了虚拟地址0
mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
// 将initcode的代码一个字节一个字节地搬运到mem地址
memmove(mem, src, sz);
}
这段函数中调用了memmove
函数,它被定义在kernel/string.c
中,实现的功能是从src地址拷贝n个字节到dst地址,并返回指向目的地址的指针。
memmove
函数的实现:
void*
memmove(void *dst, const void *src, uint n)
{
// 声明两个活动的指针,用来实时更新拷贝的过程
const char *s;
char *d;
// 如果n等于0,说明不需要拷贝字符,直接返回dst
if(n == 0)
return dst;
s = src;
d = dst;
// case1:src与dst字符串部分重叠时,倒序对字节进行复制,这样可以避免覆盖问题
if(s < d && s + n > d){
// 调整指针到字符串尾部,准备开始倒序复制
s += n;
d += n;
while(n-- > 0)
*--d = *--s;
} else
// 否则直接正序复制即可
while(n-- > 0)
*d++ = *s++;
return dst;
}
在memmove
函数的实现中最重要的就是分类讨论,分类讨论是为了分析潜在的Src和Dst的地址重叠问题,重叠时反向复制(从终点开始复制到起点),否则正向复制(从起点复制到终点)。
12.uvmunmap函数
(1)函数的作用
这个函数的作用是取消用户进程页表中指定范围的映射关系。当调用这个函数时,系统就知道从指定的虚拟地址va
开始,释放一段连续的虚拟地址范围的映射关系,该范围由npages
个页面组成。由于页表的管理是以页面为单位的,因此va
必须是一个页对齐的地址,即它必须是页大小的整数倍,以确保内存管理系统能够正确处理这些映射关系。
(2)具体代码
// Remove npages of mappings starting from va. va must be
// page-aligned. The mappings must exist.
// Optionally free the physical memory.
// 译:从虚拟地址va开始移除npages个页面的映射关系
// va必须是页对齐的,映射必须存在
// 释放物理内存是可选的
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
// va不是页对齐的,陷入panic
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
// 通过遍历释放npages * PGSIZE大小的内存
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
// 如果虚拟地址在索引过程中对应的中间页表页不存在,陷入panic
// 回顾一下,walk函数返回0,只有一种情况,那就是某一级页表页在查询时发现不存在
if((pte = walk(pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
// 查找成功,但发现此PTE不存在,陷入panic
if((*pte & PTE_V) == 0)
panic("uvmunmap: not mapped");
// 查找成功,但发现此PTE除了valid位有效外,其他位均为0
// 这暗示这个PTE原本不应该出现在叶级页表(奇怪的错误),陷入panic
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
// 否则这是一个合法的,应该被释放的PTE
// 如果do_free被置位,那么还要释放掉PTE对应的物理内存
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
// 最后将PTE本身全部清空,成功解除了映射关系
*pte = 0;
}
}
13.uvmdealloc函数
(1)函数的作用
这个函数的主要目的是调整用户进程的虚拟地址空间大小,它可以通过回收不再需要的页面来缩小地址空间,也可以通过增加新的页面来扩展地址空间。oldsz
表示当前的空间大小,而newsz
是调整后的目标大小。函数返回调整后的新地址空间大小。
关键点:
这个函数既可以扩展地址空间(如果newsz
大于oldsz
),也可以缩小地址空间(如果newsz
小于oldsz
),因此不一定会导致地址空间缩小。
(2)具体代码
// Deallocate user pages to bring the process size from oldsz to
// newsz. oldsz and newsz need not be page-aligned, nor does newsz
// need to be less than oldsz. oldsz can be larger than the actual
// process size. Returns the new process size.
// 译:回收用户页,使得进程的内存大小从oldsz变为newsz。oldsz和newsz不一定要是
// 页对齐的,newsz也不一定要大于oldsz。oldsz可以比当前实际所占用的内存大小更大。
// 函数返回进程新占用的内存大小
uint64
uvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
// 如果新的内存大小比原先内存还要大,那么什么也不用做,直接返回oldsz即可
if(newsz >= oldsz)
return oldsz;
// 如果newsz经过圆整后占据的页面数小于oldsz
// PGROUNDUP宏定义的讲解见上一篇博客
if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
// 计算出来要释放的页面数量
int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
// 调用uvmunmap,清空叶级页表的PTE并释放物理内存
// 因为我们使用了PGROUNDUP来取整页面数量,所以这里可以保证va是页对齐的
// 因为用户地址空间是从地址0开始紧密排布的, 所以PGROUNDUP(newsz)对应着新内存大小的结束位置
// 注意do_free置为1,表示一并回收物理内存
uvmunmap(pagetable, PGROUNDUP(newsz), npages, 1);
}
return newsz;
}
14.uvmalloc函数
(1)函数的作用
uvmalloc
和uvmdealloc
分别用于为用户进程分配内存和回收内存。
uvmalloc
申请新的内存并将其映射到用户进程的虚拟地址空间uvmdealloc
则释放不再需要的内存。
(2)具体代码
// Allocate PTEs and physical memory to grow process from oldsz to
// newsz, which need not be page aligned. Returns new size or 0 on error.
// 译:分配PTE和物理内存来将分配给用户的内存大小从oldsz提升到newsz
// oldsz和newsz不必是页对齐的
// 成功时返回新的内存大小,出错时返回0
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
char *mem;
uint64 a;
// 如果新的内存大小更小,不用分配,直接返回旧内存大小
if(newsz < oldsz)
return oldsz;
// 计算原先内存大小需要至少多少页,因为进程地址空间紧密排列
// 所以这里oldsz指向的其实是原先已经使用内存的下一页,崭新的一页
oldsz = PGROUNDUP(oldsz);
// 开始进行新内存的分配
for(a = oldsz; a < newsz; a += PGSIZE){
// 获取一页新的内存
mem = kalloc();
// 如果mem为空指针,表示内存耗尽
// 释放之前分配的所有内存,返回0表示出错
if(mem == 0){
uvmdealloc(pagetable, a, oldsz);
return 0;
}
// 如果分配成功,则将新分配的页面全部清空
memset(mem, 0, PGSIZE);
// 并在当前页表项中建立起来到新分配页表的映射
// mappages函数的讲解见完全解析系列博客(1)
if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
// 如果mappages函数调用返回值不为0,表明在调用walk函数时索引到的PTE无效
// 释放之前分配的所有内存,返回0表示出错
kfree(mem);
uvmdealloc(pagetable, a, oldsz);
return 0;
}
}
// 如果成功跳出循环,表示执行成功,返回新的内存空间大小
return newsz;
}
15.uvmcopy函数
(1)函数的作用
uvmcopy
函数是为fork
系统调用服务的,它会将父进程的整个地址空间全部复制到子进程中,这包括页表本身和页表指向的物理内存中的数据。
uvmcopy
会复制父进程的页表结构和页表所指向的物理内存内容,这种复制通常包括堆(heap)、栈(stack)、数据段、代码段,这样子进程在初始阶段能够与父进程共享内存。
16.uvmclear函数
(1)函数的作用
uvmclear函数专门用来清除一个PTE的用户使用权限,用于exec函数来设置守护页。
17.uvmfree函数
uvmunmap
用于取消叶级页表的映射关系,freewalk
用于释放页表页,两者结合可以完全释放内存空间吗。uvmfree
函数就是两者的一个简单结合和封装,用于完全释放用户的地址空间。
Comments | NOTHING