引言 #
前面已经通过lab1的这篇博文了解了内存分页的实现细节,接下来就谈谈如何具体实现内存分页
物理载体 #
通过了解KERNBASE对操作系统的影响这篇博文我们知道,其实内存分页就是完成对物理存在的一种分割和隔离,所以我们在完成内存分页系统设计之前必须要构建一个载体,完成对物理存在的一种表示。
在xv6中声明一个动态数组来代表物理内存,每一个值代表一块4k的内存页。我们主要通过offset - base
得到偏移倍数,每个偏移量为4K
,也就是通过(offset - base ) << 12
得到物理地址,我们看一下这个数组成员:PageInfo结构体
struct PageInfo {
struct PageInfo *pp_link;
uint16_t pp_ref;
};
主要存在两个值:一个为下一个可用的地址指针,一个为引用次数。
引用次数比较好理解,但是这个pp_link
有什么用呢。其实你可用把这个结构体看做成一个由链表组成的堆栈,我们只需要保留栈顶值(page_free_list),由于它保存下一个值地址,这样通过不断的push、pop,就能维持一个可用物理内存栈。
二级指针的妙用 #
由于前面的博客原理已经介绍的很详细了,我就不再累赘了,在这里我提一下源码中二级指针的妙用,虽然它只有短短几行,但是运行的结果却是让人大开眼界,体会到指针的神奇威力。
这段代码出现在kern/pmap.c
的check_page_free_list
函数中
if (only_low_memory) {
// Move pages with lower addresses first in the free
// list, since entry_pgdir does not map all pages.
struct PageInfo *pp1, *pp2;
struct PageInfo **tp[2] = { &pp1, &pp2 };
for (pp = page_free_list; pp; pp = pp->pp_link) {
int pagetype = PDX(page2pa(pp)) >= pdx_limit;
*tp[pagetype] = pp;
tp[pagetype] = &pp->pp_link;
}
*tp[1] = 0;
*tp[0] = pp2;
page_free_list = pp1;
}
主要的作用是将“栈底”的元素移到“栈顶”,首先它使用了两个一级指针(pp1、pp2),还有两个二级指针分别指向(pp1、pp2)
首先int pagetype = PDX(page2pa(pp)) >= pdx_limit;
判断物理地址是否为大于4M还是小于4M,我们把物理内存页分成两组
- 小于4M A组
- 大于4M B组
对于小于4M的组,分两种情况
- 第一个小于4M的内存页(page1)
- *tp[0] (也就是pp1) 存贮了pp的值,也就是pp1 = page1
- tp[0] 存贮了pp -> link 的地址(这个没有什么用)
- A组第二个以及以后的内存页(page2)
- 上一个地址的值等于pp(没什么用)
- tp[0] 存贮了下一个空闲地址的值
对于B组来说也是一样的,最重要的是for
循环结束后实现的交换
*tp[1] = 0;
B组最后一个的pp_link
地址地址设置为NULL,也就相当于把他放到栈底
*tp[0] = pp2;
A组最后一个变成pp_link
地址设置pp2,也就是B组的第一个接到了A组的最后面去了
page_free_list = pp1;
栈顶变成A组第一个,通过这样的“乾坤大挪移”术就将A组部分移到B组前面去了,也就实现了先使用低地址的物理内存的作用
总结 #
理解内存分页必须要了解背后的原理,了解了原理看具体实现的时候才能事半功倍。