You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

652 lines
28 KiB
Markdown

2 years ago
# 21 | 土地需求扩大与保障:如何分配和释放虚拟内存?
你好我是LMOS。
今天,我们继续研究操作系统如何实现虚拟内存。在上节课,我们已经建立了虚拟内存的初始流程,这节课我们来实现虚拟内存的核心功能:写出分配、释放虚拟地址空间的代码,最后实现虚拟地址空间到物理地址空间的映射。
这节课的配套代码,你可以点击[这里](https://gitee.com/lmos/cosmos/tree/master/lesson19~21/Cosmos)下载。
## 虚拟地址的空间的分配与释放
通过上节课的学习,我们知道整个虚拟地址空间就是由一个个虚拟地址区间组成的。那么不难猜到,分配一个虚拟地址空间就是在整个虚拟地址空间分割出一个区域,而释放一块虚拟地址空间,就是把这个区域合并到整个虚拟地址空间中去。
### 虚拟地址空间分配接口
我们先来研究地址的分配,依然从虚拟地址空间的分配接口开始实现,一步步带着你完成虚拟 空间的分配。
在我们的想像中分配虚拟地址空间应该有大小、有类型、有相关标志还有从哪里开始分配等信息。根据这些信息我们在krlvadrsmem.c文件中设计好分配虚拟地址空间的接口如下所示。
```
adr_t vma_new_vadrs_core(mmadrsdsc_t *mm, adr_t start, size_t vassize, u64_t vaslimits, u32_t vastype)
{
adr_t retadrs = NULL;
kmvarsdsc_t *newkmvd = NULL, *currkmvd = NULL;
virmemadrs_t *vma = &mm->msd_virmemadrs;
knl_spinlock(&vma->vs_lock);
//查找虚拟地址区间
currkmvd = vma_find_kmvarsdsc(vma, start, vassize);
if (NULL == currkmvd)
{
retadrs = NULL;
goto out;
}
//进行虚拟地址区间进行检查看能否复用这个数据结构
if (((NULL == start) || (start == currkmvd->kva_end)) && (vaslimits == currkmvd->kva_limits) && (vastype == currkmvd->kva_maptype))
{//能复用的话,当前虚拟地址区间的结束地址返回
retadrs = currkmvd->kva_end;
//扩展当前虚拟地址区间的结束地址为分配虚拟地址区间的大小
currkmvd->kva_end += vassize;
vma->vs_currkmvdsc = currkmvd;
goto out;
}
//建立一个新的kmvarsdsc_t虚拟地址区间结构
newkmvd = new_kmvarsdsc();
if (NULL == newkmvd)
{
retadrs = NULL;
goto out;
}
//如果分配的开始地址为NULL就由系统动态决定
if (NULL == start)
{//当然是接着当前虚拟地址区间之后开始
newkmvd->kva_start = currkmvd->kva_end;
}
else
{//否则这个新的虚拟地址区间的开始就是请求分配的开始地址
newkmvd->kva_start = start;
}
//设置新的虚拟地址区间的结束地址
newkmvd->kva_end = newkmvd->kva_start + vassize;
newkmvd->kva_limits = vaslimits;
newkmvd->kva_maptype = vastype;
newkmvd->kva_mcstruct = vma;
vma->vs_currkmvdsc = newkmvd;
//将新的虚拟地址区间加入到virmemadrs_t结构中
list_add(&newkmvd->kva_list, &currkmvd->kva_list);
//看看新的虚拟地址区间是否是最后一个
if (list_is_last(&newkmvd->kva_list, &vma->vs_list) == TRUE)
{
vma->vs_endkmvdsc = newkmvd;
}
//返回新的虚拟地址区间的开始地址
retadrs = newkmvd->kva_start;
out:
knl_spinunlock(&vma->vs_lock);
return retadrs;
}
//分配虚拟地址空间的接口
adr_t vma_new_vadrs(mmadrsdsc_t *mm, adr_t start, size_t vassize, u64_t vaslimits, u32_t vastype)
{
if (NULL == mm || 1 > vassize)
{
return NULL;
}
if (NULL != start)
{//进行参数检查开始地址要和页面4KB对齐结束地址不能超过整个虚拟地址空间
if (((start & 0xfff) != 0) || (0x1000 > start) || (USER_VIRTUAL_ADDRESS_END < (start + vassize)))
{
return NULL;
}
}
//调用虚拟地址空间分配的核心函数
return vma_new_vadrs_core(mm, start, VADSZ_ALIGN(vassize), vaslimits, vastype);
}
```
上述代码中依然是接口函数进行参数检查然后调用核心函数完成实际的工作。在核心函数中会调用vma\_find\_kmvarsdsc函数去查找virmemadrs\_t结构中的所有kmvarsdsc\_t结构找出合适的虚拟地址区间。
需要注意的是,**我们允许应用程序指定分配虚拟地址空间的开始地址,也可以由系统决定,但是应用程序指定的话,分配更容易失败,因为很可能指定的开始地址已经被占用了。**
接口的实现并不是很难,接下来我们继续完成核心实现。
### 分配时查找虚拟地址区间
在前面的核心函数中我写上了vma\_find\_kmvarsdsc函数但是我们并没有实现它现在我们就来完成这项工作主要是根据分配的开始地址和大小在virmemadrs\_t结构中查找相应的kmvarsdsc\_t结构。
它是如何查找的呢举个例子吧比如virmemadrs\_t结构中有两个kmvarsdsc\_t结构A\_kmvarsdsc\_t结构表示0x10000x4000的虚拟地址空间B\_kmvarsdsc\_t结构表示0x70000x9000的虚拟地址空间。
这时我们分配2KB的虚拟地址空间vma\_find\_kmvarsdsc函数查找发现A\_kmvarsdsc\_t结构和B\_kmvarsdsc\_t结构之间正好有0x40000x7000的空间刚好放得下0x2000大小的空间于是这个函数就会返回A\_kmvarsdsc\_t结构否则就会继续向后查找。
明白了原理,我们就来写代码。
```
//检查kmvarsdsc_t结构
kmvarsdsc_t *vma_find_kmvarsdsc_is_ok(virmemadrs_t *vmalocked, kmvarsdsc_t *curr, adr_t start, size_t vassize)
{
kmvarsdsc_t *nextkmvd = NULL;
adr_t newend = start + (adr_t)vassize;
//如果curr不是最后一个先检查当前kmvarsdsc_t结构
if (list_is_last(&curr->kva_list, &vmalocked->vs_list) == FALSE)
{//就获取curr的下一个kmvarsdsc_t结构
nextkmvd = list_next_entry(curr, kmvarsdsc_t, kva_list);
//由系统动态决定分配虚拟空间的开始地址
if (NULL == start)
{//如果curr的结束地址加上分配的大小小于等于下一个kmvarsdsc_t结构的开始地址就返回curr
if ((curr->kva_end + (adr_t)vassize) <= nextkmvd->kva_start)
{
return curr;
}
}
else
{//否则比较应用指定分配的开始、结束地址是不是在curr和下一个kmvarsdsc_t结构之间
if ((curr->kva_end <= start) && (newend <= nextkmvd->kva_start))
{
return curr;
}
}
}
else
{//否则curr为最后一个kmvarsdsc_t结构
if (NULL == start)
{//curr的结束地址加上分配空间的大小是不是小于整个虚拟地址空间
if ((curr->kva_end + (adr_t)vassize) < vmalocked->vs_isalcend)
{
return curr;
}
}
else
{//否则比较应用指定分配的开始、结束地址是不是在curr的结束地址和整个虚拟地址空间的结束地址之间
if ((curr->kva_end <= start) && (newend < vmalocked->vs_isalcend))
{
return curr;
}
}
}
return NULL;
}
//查找kmvarsdsc_t结构
kmvarsdsc_t *vma_find_kmvarsdsc(virmemadrs_t *vmalocked, adr_t start, size_t vassize)
{
kmvarsdsc_t *kmvdcurrent = NULL, *curr = vmalocked->vs_currkmvdsc;
adr_t newend = start + vassize;
list_h_t *listpos = NULL;
//分配的虚拟空间大小小于4KB不行
if (0x1000 > vassize)
{
return NULL;
}
//将要分配虚拟地址空间的结束地址大于整个虚拟地址空间 不行
if (newend > vmalocked->vs_isalcend)
{
return NULL;
}
if (NULL != curr)
{//先检查当前kmvarsdsc_t结构行不行
kmvdcurrent = vma_find_kmvarsdsc_is_ok(vmalocked, curr, start, vassize);
if (NULL != kmvdcurrent)
{
return kmvdcurrent;
}
}
//遍历virmemadrs_t中的所有的kmvarsdsc_t结构
list_for_each(listpos, &vmalocked->vs_list)
{
curr = list_entry(listpos, kmvarsdsc_t, kva_list);
//检查每个kmvarsdsc_t结构
kmvdcurrent = vma_find_kmvarsdsc_is_ok(vmalocked, curr, start, vassize);
if (NULL != kmvdcurrent)
{//如果符合要求就返回
return kmvdcurrent;
}
}
return NULL;
}
```
结合前面的描述和代码注释,我们发现**vma\_find\_kmvarsdsc函数才是这个分配虚拟地址空间算法的核心实现**,真的这么简单?是的,对分配虚拟地址空间,真的结束了。
不过,这个分配的虚拟地址空间可以使用吗?这个问题,等我们解决了虚拟地址空间的释放,再来处理。
### 虚拟地址空间释放接口
有分配就要有释放,否则再大的虚拟地址空间也会用完,下面我们就来研究如何释放一个虚拟地址空间。我们依然从设计接口开始,这次我们只需要释放的虚拟空间的开始地址和大小就行了。我们来写代码实现吧,如下所示。
```
//释放虚拟地址空间的核心函数
bool_t vma_del_vadrs_core(mmadrsdsc_t *mm, adr_t start, size_t vassize)
{
bool_t rets = FALSE;
kmvarsdsc_t *newkmvd = NULL, *delkmvd = NULL;
virmemadrs_t *vma = &mm->msd_virmemadrs;
knl_spinlock(&vma->vs_lock);
//查找要释放虚拟地址空间的kmvarsdsc_t结构
delkmvd = vma_del_find_kmvarsdsc(vma, start, vassize);
if (NULL == delkmvd)
{
rets = FALSE;
goto out;
}
//第一种情况要释放的虚拟地址空间正好等于查找的kmvarsdsc_t结构
if ((delkmvd->kva_start == start) && (delkmvd->kva_end == (start + (adr_t)vassize)))
{
//脱链
list_del(&delkmvd->kva_list);
//删除kmvarsdsc_t结构
del_kmvarsdsc(delkmvd);
vma->vs_kmvdscnr--;
rets = TRUE;
goto out;
}
//第二种情况要释放的虚拟地址空间是在查找的kmvarsdsc_t结构的上半部分
if ((delkmvd->kva_start == start) && (delkmvd->kva_end > (start + (adr_t)vassize)))
{ //所以直接把查找的kmvarsdsc_t结构的开始地址设置为释放虚拟地址空间的结束地址
delkmvd->kva_start = start + (adr_t)vassize;
rets = TRUE;
goto out;
}
//第三种情况要释放的虚拟地址空间是在查找的kmvarsdsc_t结构的下半部分
if ((delkmvd->kva_start < start) && (delkmvd->kva_end == (start + (adr_t)vassize)))
{//所以直接把查找的kmvarsdsc_t结构的结束地址设置为释放虚拟地址空间的开始地址
delkmvd->kva_end = start;
rets = TRUE;
goto out;
}
//第四种情况要释放的虚拟地址空间是在查找的kmvarsdsc_t结构的中间
if ((delkmvd->kva_start < start) && (delkmvd->kva_end > (start + (adr_t)vassize)))
{//所以要再新建一个kmvarsdsc_t结构来处理释放虚拟地址空间之后的下半虚拟部分地址空间
newkmvd = new_kmvarsdsc();
if (NULL == newkmvd)
{
rets = FALSE;
goto out;
}
//让新的kmvarsdsc_t结构指向查找的kmvarsdsc_t结构的后半部分虚拟地址空间
newkmvd->kva_end = delkmvd->kva_end;
newkmvd->kva_start = start + (adr_t)vassize;
//和查找到的kmvarsdsc_t结构保持一致
newkmvd->kva_limits = delkmvd->kva_limits;
newkmvd->kva_maptype = delkmvd->kva_maptype;
newkmvd->kva_mcstruct = vma;
delkmvd->kva_end = start;
//加入链表
list_add(&newkmvd->kva_list, &delkmvd->kva_list);
vma->vs_kmvdscnr++;
//是否为最后一个kmvarsdsc_t结构
if (list_is_last(&newkmvd->kva_list, &vma->vs_list) == TRUE)
{
vma->vs_endkmvdsc = newkmvd;
vma->vs_currkmvdsc = newkmvd;
}
else
{
vma->vs_currkmvdsc = newkmvd;
}
rets = TRUE;
goto out;
}
rets = FALSE;
out:
knl_spinunlock(&vma->vs_lock);
return rets;
}
//释放虚拟地址空间的接口
bool_t vma_del_vadrs(mmadrsdsc_t *mm, adr_t start, size_t vassize)
{ //对参数进行检查
if (NULL == mm || 1 > vassize || NULL == start)
{
return FALSE;
}
//调用核心处理函数
return vma_del_vadrs_core(mm, start, VADSZ_ALIGN(vassize));
}
```
结合上面的代码和注释,我相信你能够看懂。需要注意的是,处理释放虚拟地址空间的四种情况。
因为分配虚拟地址空间时,我们**为了节约kmvarsdsc\_t结构占用的内存空间**,规定只要分配的虚拟地址空间上一个虚拟地址空间是连续且类型相同的,我们就**借用**上一个kmvarsdsc\_t结构而不是重新分配一个kmvarsdsc\_t结构表示新分配的虚拟地址空间。
你可以想像一下一个应用每次分配一个页面的虚拟地址空间不停地分配而每个新分配的虚拟地址空间都有一个kmvarsdsc\_t结构对应这样物理内存将很快被耗尽。
### 释放时查找虚拟地址区间
上面释放虚拟地址空间的核心处理函数vma\_del\_vadrs\_core函数中调用了vma\_del\_find\_kmvarsdsc函数用于查找要释放虚拟地址空间的kmvarsdsc\_t结构可是为什么不用分配虚拟地址空间时那个查找函数vma\_find\_kmvarsdsc
这是因为释放时查找的要求不一样。释放时仅仅需要保证释放的虚拟地址空间的开始地址和结束地址他们落在某一个kmvarsdsc\_t结构表示的虚拟地址区间就行所以我们还是另写一个函数代码如下。
```
kmvarsdsc_t *vma_del_find_kmvarsdsc(virmemadrs_t *vmalocked, adr_t start, size_t vassize)
{
kmvarsdsc_t *curr = vmalocked->vs_currkmvdsc;
adr_t newend = start + (adr_t)vassize;
list_h_t *listpos = NULL;
if (NULL != curr)
{//释放的虚拟地址空间落在了当前kmvarsdsc_t结构表示的虚拟地址区间
if ((curr->kva_start) <= start && (newend <= curr->kva_end))
{
return curr;
}
}
//遍历所有的kmvarsdsc_t结构
list_for_each(listpos, &vmalocked->vs_list)
{
curr = list_entry(listpos, kmvarsdsc_t, kva_list);
//释放的虚拟地址空间是否落在了其中的某个kmvarsdsc_t结构表示的虚拟地址区间
if ((start >= curr->kva_start) && (newend <= curr->kva_end))
{
return curr;
}
}
return NULL;
}
```
释放时查找虚拟地址区间的函数非常简单仅仅是检查释放的虚拟地址空间是否落在查找kmvarsdsc\_t结构表示的虚拟地址区间中而可能的四种变换形式交给核心释放函数处理。到这里我们释放虚拟地址空间的功能就实现了。
## 测试环节:虚拟空间能正常访问么?
我们已经实现了虚拟地址空间的分配和释放,但是我们从未访问过分配的虚拟地址空间,也不知道能不能访问,会有什么我们没有预想到的结果。保险起见,我们这就进入测试环节,试一试访问一下分配的虚拟地址空间。
### 准备工作
想要访问一个虚拟地址空间,当然需要先分配一个虚拟地址空间,所以我们要做点准备工作,写点测试代码,分配一个虚拟地址空间并访问它,代码如下。
```
//测试函数
void test_vadr()
{
//分配一个0x1000大小的虚拟地址空间
adr_t vadr = vma_new_vadrs(&initmmadrsdsc, NULL, 0x1000, 0, 0);
//返回NULL表示分配失败
if(NULL == vadr)
{
kprint("分配虚拟地址空间失败\n");
}
//在刷屏幕上打印分配虚拟地址空间的开始地址
kprint("分配虚拟地址空间地址:%x\n", vadr);
kprint("开始写入分配虚拟地址空间\n");
//访问虚拟地址空间把这空间全部设置为0
hal_memset((void*)vadr, 0, 0x1000);
kprint("结束写入分配虚拟地址空间\n");
return;
}
void init_kvirmemadrs()
{
//……
//调用测试函数
test_vadr();
return;
}
```
你大概已经猜到这个在init\_kvirmemadrs函数的最后调用的test\_vadr函数一旦执行一定会发生异常。为了显示这个异常我们要在异常分发器函数中写点代码。代码如下所示。
```
//cosmos/hal/x86/halintupt.c
void hal_fault_allocator(uint_t faultnumb, void *krnlsframp)
{
//打印异常号
kprint("faultnumb is :%d\n", faultnumb);
//如果异常号等于14则是内存缺页异常
if (faultnumb == 14)
{//打印缺页地址这地址保存在CPU的CR2寄存器中
kprint("异常地址:%x,此地址禁止访问\n", read_cr2());
}
//死机,不让这个函数返回了
die(0);
return;
}
```
上述代码非常简单,下面我们来测试一下,看看最终结果。
### 异常情况与原因分析
所有的代码已经准备好了我们进入Cosmos目录下执行make vboxtest指令等Cosmos跑起来的时候你会看到如下所示的情况。
![](https://static001.geekbang.org/resource/image/85/b1/85dbd081f523fdedb71c34f091989eb1.jpg?wh=1044x921 "访问虚拟地址异常截图")
上图中显示我们分配了0x1000大小的虚拟地址空间其虚拟地址是0x5000接着对这个地址进行访问最后产生了缺页异常缺页的地址正是我们分配的虚拟空间的开始地址。
为什么会发生这个缺页异常呢因为我们访问了一个虚拟地址这个虚拟地址由CPU发送给MMU而MMU无法把它转换成对应的物理地址CPU的那条访存指令无法执行了因此就产生一个缺页异常。于是CPU跳转到缺页异常处理的入口地址kernel.asm文件中的exc\_page\_fault标号处开始执行代码处理这个缺页异常。
因为我们仅仅是分配了一个虚拟地址空间就对它进行访问所以才会缺页。既然我们并没有为这个虚拟地址空间分配任何物理内存页面建立对应的MMU页表那我们可不可以分配虚拟地址空间时就分配物理内存页面并建立好对应的MMU页表呢
这当然可以解决问题但是现实中往往是等到发生缺页异常了才分配物理内存页面建立对应的MMU页表。**这种延迟内存分配技术在系统工程中非常有用,因为它能最大限度的节约物理内存。**分配的虚拟地址空间,只有实际访问到了才分配对应的物理内存页面。
### 开始处理缺页异常
准确地说缺页异常是从kernel.asm文件中的exc\_page\_fault标号处开始但它只是保存了CPU的上下文然后调用了内核的通用异常分发器函数最后由异常分发器函数调用不同的异常处理函数如果是缺页异常就要调用缺页异常处理的接口函数。
这个函数之前还没有写呢,下面我们一起来实现它,代码如下所示。
```
//缺页异常处理接口
sint_t vma_map_fairvadrs(mmadrsdsc_t *mm, adr_t vadrs)
{//对参数进行检查
if ((0x1000 > vadrs) || (USER_VIRTUAL_ADDRESS_END < vadrs) || (NULL == mm))
{
return -EPARAM;
}
//进行缺页异常的核心处理
return vma_map_fairvadrs_core(mm, vadrs);
}
//由异常分发器调用的接口
sint_t krluserspace_accessfailed(adr_t fairvadrs)
{//这里应该获取当前进程的mm但是现在我们没有进程才initmmadrsdsc代替
mmadrsdsc_t* mm = &initmmadrsdsc;
//应用程序的虚拟地址不可能大于USER_VIRTUAL_ADDRESS_END
if(USER_VIRTUAL_ADDRESS_END < fairvadrs)
{
return -EACCES;
}
return vma_map_fairvadrs(mm, fairvadrs);
}
```
上面的接口函数非常简单不过我们要在cosmos/hal/x86/halintupt.c文件的异常分发器函数中来调用它代码如下所示。
```
void hal_fault_allocator(uint_t faultnumb, void *krnlsframp)
{
adr_t fairvadrs;
kprint("faultnumb is :%d\n", faultnumb);
if (faultnumb == 14)
{ //获取缺页的地址
fairvadrs = (adr_t)read_cr2();
kprint("异常地址:%x,此地址禁止访问\n", fairvadrs);
if (krluserspace_accessfailed(fairvadrs) != 0)
{//处理缺页失败就死机
system_error("缺页处理失败\n");
}
//成功就返回
return;
}
die(0);
return;
}
```
接口函数和调用流程已经写好了,下面就要真正开始处理缺页了。
### 处理缺页异常的核心
在前面缺页异常处理接口时调用了vma\_map\_fairvadrs\_core函数来进行缺页异常的核心处理、那缺页异常处理究竟有哪些操作呢
这里给你留个悬念,我先来写个函数,你可以结合自己的观察,想想它做了什么,代码如下所示。
```
sint_t vma_map_fairvadrs_core(mmadrsdsc_t *mm, adr_t vadrs)
{
sint_t rets = FALSE;
adr_t phyadrs = NULL;
virmemadrs_t *vma = &mm->msd_virmemadrs;
kmvarsdsc_t *kmvd = NULL;
kvmemcbox_t *kmbox = NULL;
knl_spinlock(&vma->vs_lock);
//查找对应的kmvarsdsc_t结构
kmvd = vma_map_find_kmvarsdsc(vma, vadrs);
if (NULL == kmvd)
{
rets = -EFAULT;
goto out;
}
//返回kmvarsdsc_t结构下对应kvmemcbox_t结构
kmbox = vma_map_retn_kvmemcbox(kmvd);
if (NULL == kmbox)
{
rets = -ENOMEM;
goto out;
}
//分配物理内存页面并建立MMU页表
phyadrs = vma_map_phyadrs(mm, kmvd, vadrs, (0 | PML4E_US | PML4E_RW | PML4E_P));
if (NULL == phyadrs)
{
rets = -ENOMEM;
goto out;
}
rets = EOK;
out:
knl_spinunlock(&vma->vs_lock);
return rets;
}
```
通过对上述代码的观察,你就能发现,以上代码中做了三件事。
首先查找缺页地址对应的kmvarsdsc\_t结构没找到说明没有分配该虚拟地址空间那属于非法访问不予处理然后查找kmvarsdsc\_t结构下面的对应kvmemcbox\_t结构它是用来挂载物理内存页面的最后分配物理内存页面并建立MMU页表映射关系。
下面我们分别来实现这三个步骤。
### 缺页地址是否合法
要想判断一个缺页地址是否合法我们就要确定它是不是已经分配的虚拟地址也就是看这个虚拟地址是不是会落在某个kmvarsdsc\_t结构表示的虚拟地址区间。
因此我们要去查找相应的kmvarsdsc\_t结构如果没有找到则虚拟地址没有分配即这个缺页地址不合法。这个查找kmvarsdsc\_t结构的函数可以这样写。
```
kmvarsdsc_t *vma_map_find_kmvarsdsc(virmemadrs_t *vmalocked, adr_t vadrs)
{
list_h_t *pos = NULL;
kmvarsdsc_t *curr = vmalocked->vs_currkmvdsc;
//看看上一次刚刚被操作的kmvarsdsc_t结构
if (NULL != curr)
{//虚拟地址是否落在kmvarsdsc_t结构表示的虚拟地址区间
if ((vadrs >= curr->kva_start) && (vadrs < curr->kva_end))
{
return curr;
}
}
//遍历每个kmvarsdsc_t结构
list_for_each(pos, &vmalocked->vs_list)
{
curr = list_entry(pos, kmvarsdsc_t, kva_list);
//虚拟地址是否落在kmvarsdsc_t结构表示的虚拟地址区间
if ((vadrs >= curr->kva_start) && (vadrs < curr->kva_end))
{
return curr;
}
}
return NULL;
}
```
这个函数非常简单核心逻辑就是用虚拟地址和kmvarsdsc\_t结构中的数据做比较大于等于kmvarsdsc\_t结构的开始地址并且小于kmvarsdsc\_t结构的结束地址就行了。
### 建立kvmemcbox\_t结构
kvmemcbox\_t结构可以用来挂载物理内存页面msadsc\_t结构而这个msadsc\_t结构是由虚拟地址区间kmvarsdsc\_t结构代表的虚拟空间所映射的物理内存页面。一个kmvarsdsc\_t结构必须要有一个kvmemcbox\_t结构才能分配物理内存。除了这个功能kvmemcbox\_t结构还可以在内存共享的时候使用。
现在我们一起来写个函数实现建立kvmemcbox\_t结构代码如下所示。
```
kvmemcbox_t *vma_map_retn_kvmemcbox(kmvarsdsc_t *kmvd)
{
kvmemcbox_t *kmbox = NULL;
//如果kmvarsdsc_t结构中已经存在了kvmemcbox_t结构则直接返回
if (NULL != kmvd->kva_kvmbox)
{
return kmvd->kva_kvmbox;
}
//新建一个kvmemcbox_t结构
kmbox = knl_get_kvmemcbox();
if (NULL == kmbox)
{
return NULL;
}
//指向这个新建的kvmemcbox_t结构
kmvd->kva_kvmbox = kmbox;
return kmvd->kva_kvmbox;
}
```
上述代码非常简单knl\_get\_kvmemcbox函数就是调用kmsob\_new函数分配一个kvmemcbox\_t结构大小的内存空间对象然后其中实例化kvmemcbox\_t结构的变量。
### 映射物理内存页面
现在我们正式给虚拟地址分配对应的物理内存页面建立对应的MMU页表使虚拟地址到物理地址可以转换成功数据终于能写入到物理内存之中了。
这个步骤完成,就意味着缺页处理完成了,我们来写代码吧。
```
adr_t vma_map_msa_fault(mmadrsdsc_t *mm, kvmemcbox_t *kmbox, adr_t vadrs, u64_t flags)
{
msadsc_t *usermsa;
adr_t phyadrs = NULL;
//分配一个物理内存页面挂载到kvmemcbox_t中并返回对应的msadsc_t结构
usermsa = vma_new_usermsa(mm, kmbox);
if (NULL == usermsa)
{//没有物理内存页面返回NULL表示失败
return NULL;
}
//获取msadsc_t对应的内存页面的物理地址
phyadrs = msadsc_ret_addr(usermsa);
//建立MMU页表完成虚拟地址到物理地址的映射
if (hal_mmu_transform(&mm->msd_mmu, vadrs, phyadrs, flags) == TRUE)
{//映射成功则返回物理地址
return phyadrs;
}
//映射失败就要先释放分配的物理内存页面
vma_del_usermsa(mm, kmbox, usermsa, phyadrs);
return NULL;
}
//接口函数
adr_t vma_map_phyadrs(mmadrsdsc_t *mm, kmvarsdsc_t *kmvd, adr_t vadrs, u64_t flags)
{
kvmemcbox_t *kmbox = kmvd->kva_kvmbox;
if (NULL == kmbox)
{
return NULL;
}
//调用核心函数flags表示页表条目中的相关权限、存在、类型等位段
return vma_map_msa_fault(mm, kmbox, vadrs, flags);
}
```
上述代码中调用vma\_map\_msa\_fault函数做实际的工作。首先它会调用vma\_new\_usermsa函数在vma\_new\_usermsa函数内部调用了我们前面学过的页面内存管理接口分配一个物理内存页面并把对应的msadsc\_t结构挂载到kvmemcbox\_t结构上。
接着获取msadsc\_t结构对应内存页面的物理地址最后是调用hal\_mmu\_transform函数完成虚拟地址到物理地址的映射工作它主要是建立MMU页表在cosmos/hal/x86/halmmu.c文件中我已经帮你写好了代码我相信你结合前面MMU相关的课程你一定能看懂。
vma\_map\_phyadrs函数一旦成功返回就会随着原有的代码路径层层返回。至此处理缺页异常就结束了。
## 重点回顾
今天这节课我们学习了如何实现虚拟内存的分配与释放,现在我把重点为你梳理一下。
首先,我们实现了虚拟地址空间的分配与释放。这是虚拟内存管理的核心功能,通过查找地址区间结构来确定哪些虚拟地址空间已经分配或者空闲。
然后我们解决了缺页异常处理问题。我们分配一段虚拟地址空间并没有分配对应的物理内存页面而是等到真正访问虚拟地址空间时才触发了缺页异常。这时我们再来处理缺页异常中分配物理内存页面的工作建立对应的MMU页表映射关系。**这种延迟分配技术可以有效节约物理内存。**
至此从物理内存页面管理到内存对象管理再到虚拟内存管理我们一层一层地建好了Cosmos的内存管理组件。内存可以说是专栏的重中之重以后Cosmos内核的其它组件也都要依赖于内存管理组件。
## 思考题
请问x86 CPU的缺页异常是第几号异常缺页的地址保存在哪个寄存器中
欢迎你在留言区跟我交流互动,也感谢你坚持不懈跟我学习,如果你身边有对内存管理感兴趣的朋友,记得把今天这节课分享给他。
我是LMOS我们下节课见。