Lab2
实验目的
• 操理解基于段页式内存地址的转换机制
• 理解页表的建立和使用方法
• 理解物理内存的管理方法
实验内容
本次实验包含三个部分。首先了解如何发现系统中的物理内存;然后了解如何建立对物理内存的初步管理,即了解连续物理内存管理;最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个比较全面的了解。本实验里面实现的内存管理还是非常基本的,并没有涉及到对实际机器的优化,比如针对 cache 的优化等。
练习0、填写已有实验
练习1、实现First-Fit(首次适配)连续物理内存分配算法
需要实现的函数有default_init,default_init_memmap,default_alloc_pages,default_free_pages,均在kern/mm/default_pmm.c下,以下是对该c文件中注释的翻译:
首次适配算法(FFMA):分配器维护一个空闲列表,收到写入内存请求时,会从头开始找空闲列表中第一个能装下目标大小的空位,如果空位大于写入块大小,则剩余部分成为新的空闲列表项。
实现流程:
-
准备:free_area_t结构维护空闲列表头,该结构实现应基于libs/list.h中的双链表(可参考Lab 0对ucore双链表的介绍),其中可调用函数有list_init, list_add(list_add_after), list_add_before, list_del, list_next, list_prev。可以使用一些宏将这种list转换为特定结构(这里的free_area_t可以使用mm/memlayout.h中的le2page宏转换实现,le即list_entry)。
-
default_init实现:初始化 free_list 空闲列表,并将 nr_free **空闲页(不是块数!)**总数为0。
static void
default_init(void) {
list_init(&free_list);
nr_free = 0;
}
- default_init_memmap实现:初始化一个空闲块,需要的参数为基址和包含的页数目
static void
default_init_memmap(struct Page *base, size_t n) { // base为空闲块基址,n为需要分配的页数
assert(n > 0);
for (struct Page *p = base; p != base + n; ++p) {
assert(PageReserved(p)); // 确认该页未被内核使用(也就是已分配)
p->flags = p->property = 0; // 该页不是空闲块的首页
set_page_ref(p, 0); // 空闲列表页没有引用
SetPageProperty(p); // 将空闲块状态置入flags
list_add_before(&free_list, &(p->page_link)); // 将该页链到空闲列表
}
base->property = n; // 首页的property设为页数n
nr_free += n; // 空闲列表中含有的空闲页数(不是块数!)
}
调用链 kern_init --> pmm_init–>page_init–>init_memmap–> pmm_manager->init_memmap
在mm/memlayout.h中初始化了页,其具有如下属性:
p->flags(设为PG_property值1表示为空闲块中的首页,值0表示该页及所在块已分配或该页不是首页;PG_reserved值1表示为内核保留,不能用于分配/释放,否则值为0);
p->property(若为块的第一页,值为其中块中的页数n,否则值为0);
p->ref设为0,表示处于空闲状态没有引用;
p->page_link将该页链接到空闲列表,例如list_add_before(&free_list, &(p->page_link));
最后nr_free总计空闲块个数
- default_alloc_pages(分配)实现:找符合FFMA的第一个空闲块,返回分配出来的块地址,解除该块和链表之间的p->page_link并更新剩余空闲块大小,PG_reserved设为1,PG_property设为0;如果找不到符合要求的空闲块,返回0
除了首次适配,笔者还拓展实现了最佳/最差/下次适配算法
经测试首次/下次匹配算法运行正常,最佳/最差匹配算法会使OS停止响应,原因尚未分析。
// list_entry_t *le_start = &free_list; // 如果为下次匹配算法(最快),需要维护下次开始遍历的起点
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) {
return NULL;
}
list_entry_t *le;
le = &free_list; // 下次匹配:le = le_start
/* 最佳/最差匹配使用以下代码段替代while循环,用于寻找符合要求的块首。注意将while中的if()语句放到while之外,最佳/最差算法区别只在不等号方向上
struct Page *biggest = NULL;
struct Page *p;
while((le=list_next(le)) != &free_list) {
p = le2page(le, page_link);
if(p->property >= n && (biggest != NULL || biggest->property < p->property)) {
biggest = p;
}
}
p = biggest;
if(p != NULL) ...
*/
// 注意是逐页遍历,而不是逐块遍历!!
while((le=list_next(le)) != &free_list) { // 下次匹配:&free_list改为le_start
struct Page *p = le2page(le, page_link);
// 满足该条件说明是能分配出这么多空间,而且p是块首页
if(p->property >= n){ // 最佳/最差匹配将条件改为p != NULL
//将块首后面的所有页属性设置好
for(int i = 0; i < n; ++i){
struct Page *pp = le2page(le, page_link); // pp代表的是遍历的当前页
SetPageReserved(pp); // 设置pp即将被使用
ClearPageProperty(pp);// 清除pp维护的块首信息
list_del(le); // 从空闲列表删除该页
le = list_next(le); // 更替到下一页
}
// 如果已被分配的块p还有余下的空页,则le所在的页刚好就是剩余空块的首页
if(p->property > n){
(le2page(le, page_link))->property = p->property - n;
}
nr_free -= n;
return p;
}
}
// le_start = le; // 下次匹配算法加入该行
return NULL;
}
- default_free_pages(释放)实现:将页重新链回空闲列表,还需要合并一些连续的小空闲块成为大空闲块,并重置页的各属性值。合并小块时p->property会发生改变,注意处理。
static void
default_free_pages(struct Page *base, size_t n) {
assert(n > 0);
assert(PageReserved(base)); // 确保是块首页
list_entry_t *le = list_next(&free_list);
struct Page *p = base;
// 遍历空闲列表,找一个在base右侧的页le(按地址顺序放回)
while((le = list_next(le)) != &free_list) {
p = le2page(le, page_link);
if(p > base) {
break;
}
}
// 在le前逐个填入base块下的各页
for(p=base;p<base+n;p++){
list_add_before(le, &(p->page_link));
}
base->flags = 0;
set_page_ref(base, 0);
ClearPageProperty(base);
SetPageProperty(base); // 将空闲块状态放入base->flags
base->property = n; // 空闲块维护包含页数
p = le2page(le,page_link) ;
if(base + n == p ){ // 右边紧邻le所在页,则合并
base->property += p->property;
p->property = 0; // 不再是块首页
}
le = list_prev(&(base->page_link)); // 看base的前一页
p = le2page(le, page_link);
if(le != &free_list && p == base - 1){ // 如果base左侧紧邻p且p不是表头
while(le!=&free_list){
if(p->property){ // 左侧p块合并base块
p->property += base->property;
base->property = 0; // base不再是块首
break;
}
le = list_prev(le); // p不是块首页,则继续向左找到块首页为止
p = le2page(le,page_link);
}
}
nr_free += n; // 更新空闲列表总页数
}
练习2、实现寻找虚拟地址对应的页表项
通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。get_pte函数的调用关系图如下所示:
kern/mm/pmm.c中get_pte注释翻译如下:
- 功能:获取页表项(Page Table Entry, PTE)并返回它线性地址(Linear Address, LA)在内核对应的虚拟地址(Virtual Address, VA);如果**页表(Page Table, PT)**包含了一个不存在的页表项,为页表分配一个页。
- 参数:
pgdir **页目录表(Page Directory Table, PDT)**在内核的虚拟基地址;
la 待映射的线性地址;
create 布尔值,是否要为页表分配一个页;
返回值为页表项在内核的虚拟地址。 - 实现:可能用到的宏和定义
KADDR(pa) 将物理地址转换为内核的虚拟地址
PDX(la) 返回位于la的页目录项的索引,Page Directory Index
set_page_ref(page, 1) 该页被引用1次
page2pa(page) 获取page的物理地址,Page to Physical Address
alloc_page() 分配一个新页
PTE_P 页表/页目录项标记:存在Present
PTE_W 页表/页目录项标记:可写Weitable
PTE_U 页表/页目录项标记:用户可访问User can access
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
#if 0 // 预编译指令,当不想执行某段代码,是一种除了注释外的好办法,启用用#if 1即可
pde_t *pdep = &pgdir[PDX(la)]; // 获取线性地址的页目录项索引,在页目录中找到该项,页目录项维护的就是页表基地址
if (!(*pdep & PTE_P)) { // 该页目录项不存在(是个空页表)
struct Page *page;
if (!create || (page = alloc_page()) == NULL) { // 是否为空页表创建新页,否则返回空
return NULL;
}
set_page_ref(page, 1); // 建立一个引用
uintptr_t pa = page2pa(page); // 获取该页的物理地址
memset(KADDR(pa), 0, PGSIZE); // 获取该页的虚拟地址,并清空该页(防止内存泄漏)
*pdep = pa | PTE_U | PTE_W | PTE_P; // 将该页放入页目录项,并打上相应标记
}
return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)]; // 返回该页表项虚拟基地址
#endif
问题
• 请描述页目录项(Pag Director Entry)和页表(Page Table Entry)中每个组成部分的含义和以及对ucore而言的潜在用处。
PD是维护PDE的列表,而每个PDE维护了一个PT基地址,每个PTE页表项维护了物理页到虚拟内存的映射地址和使用状态。
• 如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?(以下答案参考自https://blog.csdn.net/CNRalap/article/details/124512925)
当启动分页机制以后,如果一条指令或数据的虚拟地址所对应的物理页不在内存中或者访问的类型有误(比如写一个只读页或用户态程序访问内核态的数据等),就会发生页错误异常。
而产生页面异常的原因主要有:
①目标页面不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已经撤销);
②相应的物理页面不在内存中(页表项非空,但Present标志位=0,比如将页表交换到磁盘);
③访问权限不符合(比如企图写只读页面)。
当出现上面情况之一,那么就会产生页面page fault(#PF)异常。产生异常的虚拟地址存储在CR2中,并且将是page fault的错误类型保存在error code中。引发异常后将外存的数据换到内存中,进行上下文切换,退出中断,返回到中断前的状态。
练习3、释放某虚地址所在的页并取消对应二级页表项的映射
当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page_remove_pte函数中的注释。为此,需要补全在 kern/mm/pmm.c中的page_remove_pte函数。page_remove_pte函数的调用关系图如下所示:
- 注释翻译:
该函数需要检查页表项(PTE)指针是否有效。如果页表项映射更新,TLB也要手动更新(旁路转换缓冲,俗称快表,负责维护最近访问的页表VA->PA映射)。
一些使用的宏和定义:
pte2page(*ptep) 根据页表项指针获取指定页;
free_page 释放页
page_ref_dec(page) 页引用-1,注意如果页引用数为0,记得释放。
tlb_invalidate(pde_t *pgdir, uintptr_t la) 使一个TLB项失效,仅在被改动的页表是处理器正在使用时该函数可用
PTE_P 页表项/页目录项存在 - 实现:
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
if (*ptep & PTE_P) { // 检查页表项是否存在
struct Page *page = pte2page(*ptep); // 页表项对应页
if (page_ref_dec(page) == 0) { // 减少页引用
free_page(page); // 如果引用数为0,释放页
}
*ptep = 0; // 表明该页表项已失效
tlb_invalidate(pgdir, la); // TLB删除该页信息
}
}
问题
• 数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?
存在对应关系。
pages数组每一页都维护一个物理页的信息,每个PDE维护一个页表信息,每个PTE维护一个物理页信息。PDE和PTE的前20位(第31~12位)分别对应一个物理页编号,即和pages数组的物理页一一对应。
将VA向下对齐到页大小,转换成PA后,将其右移12位获得在pages数组的索引PPN,&pages[PPN]即PTE指向地址对应的Page结构。
• 如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事? 鼓励通过编程来具体完成这个问题。
lab2为段页式,其映射建立流程如下:
1)bootloader阶段:VA = LA = PA
2)kern/init/entry.S/kern_entry函数到kern/mm/pmm.c/enable_page函数之前,更新段映射:
VA - 0xC0000000 = LA =PA
3)enable_page函数到kern/mm/pmm.c/gdt_init函数之前,启动页映射
VA = LA = PA + 0xC0000000 (PA在0~4MB之间)
VA = LA = PA (PA在4MB之外)
4)gdt_init再次更新段映射:
VA = LA = PA + 0xC0000000
/* *
* Virtual memory map: Permissions
* kernel/user
*
* 4G -----------> +---------------------------------+
* | |
* | Empty Memory (*) |
* | |
* +---------------------------------+ 0xFB000000
* | Cur. Page Table (Kern, RW) | RW/-- PTSIZE
* VPT ----------> +---------------------------------+ 0xFAC00000
* | Invalid Memory (*) | --/--
* KERNTOP ------> +---------------------------------+ 0xF8000000
* | |
* | Remapped Physical Memory | RW/-- KMEMSIZE
* | |
* KERNBASE -----> +---------------------------------+ 0xC0000000
* | |
* | |
* | |
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped.
* "Empty Memory" is normally unmapped, but user programs may map pages
* there if desired.
*
* */
扩展练习 Challenge 1 Buddy System(伙伴系统)分配算法
Buddy System算法把系统中的可用存储空间划分为存储块(Block)来进行管理, 每个存储块的大小必须是2的n次幂(Pow(2, n)), 即1, 2, 4, 8, 16, 32, 64, 128…
• 参考伙伴分配器的一个极简实现, 在ucore中实现buddy system分配算法,要求有比较充分的测试用例说明实现的正确性,需要有设计文档。
可以采用完全二叉树,高层节点大块,孩子节点即左右分半。这样搜索合并只需对数级复杂度,且能够减少外部碎片。缺点是块大小不是按需分配,有内部碎片。
扩展练习 Challenge 2 任意大小的内存单元Slub分配算法
slub算法,实现两层架构的高效内存单元分配,第一层是基于页大小的内存分配,第二层是在第一层基础上实现基于任意大小的内存分配。可简化实现,能够体现其主体思想即可。
• 参考linux的slub分配算法,在ucore中实现slub分配算法。
此处参考https://blog.csdn.net/StuGeek/article/details/118708800。