操作系统ucore Lab2笔记
知识点
物理内存管理
理论课知识点
-
操作系统的内存管理方式
- 重定位(relocation)
- 分段(segmentation)
- 分页(paging)
- 虚拟存储(vitual memory)
-
连续内存分配算法
-
最先匹配(First Fit Allocation):
空闲分区列表按地址排序
分配过程时,搜索第一个合适的分区
释放分区时,检查是否可与临近的空闲分区合并 -
最优匹配(Best Fit Allocation):
空闲分区列表按地址排序
分配过程时,搜索一个合适的分区(寻找一个比所需空间大最少的)
释放分区时,检查是否可与临近的空闲分区合并 -
最差匹配(Worst Fit Allocation):
空闲分区列表按地址排序
分配过程时,选择最大的分区
释放分区时,检查是否可与临近的空闲分区合并
-
-
页与页帧
- 帧(frame):物理内存被划分为大小相同的帧,表示为二元组(f, o) /* (帧号,帧内偏移) */
- 页(page):进程逻辑地址空间被划分为大小相同的页,表示为二元组(p, o) /(页号,页内偏移)/
- 页表:页表保存了物理地址——逻辑地址的映射关系(页到帧的映射)
-
快表和多级页表
旨在解决页表存储空间过大的问题- 快表(Translation Look-aside Buffer, TLB):缓存近期访问的页表项
- 多级页表:通过间接引用将页号分为k级,上级页表记录下级页表的索引,通过上级访问下级,无法直接访问下级
-
反置页表
基于HASH映射值查找对应页表项中的帧号
实验课的理解
代码的理解在课程提供的gitbookhttps://chyyuu.gitbooks.io/ucore_os_docs/content/有较为全面的介绍。这里主要记载一些阅读代码时注意到的点。
- 连续内存的分配算法
实验中使用到的算法是最先匹配策略,算法的代码实现在kern/mm/default_pmm.c. 实验要求按照自己的设计思想修改可能涉及到的函数default_init,default_init_memmap,default_alloc_pages, default_free_pages。但阅读代码之后,发现原来的代码就已经实现了最先匹配的算法,不做修改也是可行的。与答案的代码相比,两者的区别主要是设计理念的不同,而不是答案是前者的补充。
为了理解算法,得先知道算法相关的这几个函数是如何被调用的,可以发现pmm_init函数调用了大部分的函数。位于kern/mm/pmm.c的pmm_init的功能是完成内存管理的初始化,需要感知到被探测到的物理内存,于是向前了解到了位于boot/bootasm.S的内容。于是,例如理解default_init_memmap,可以从
kern_init --> pmm_init-->page_init-->init_memmap--> pmm_manager->default_init_memmap
去理解代码。
现在记录一下修改前的代码是如何实现算法的。算法的几个函数都在维护一个全局链表free_area.free_list(free_list)与空闲内存数量free_area.nr_free(nr_free), free_list为链表入口,链表节点为
struct list_entry {
struct list_entry *prev, *next;
};
是一个典型的双向链表节点的结构,除了指向前后节点的指针别无他物。但显然,链表节点需要包含更多的信息来记录一些东西,ucore这里借助了结构体page来将链表节点包装起来,也就是说将节点作为page的成员出现。page的结构如下:
struct Page {
int ref; // page frame's reference counter
uint32_t flags; // array of flags that describe the status of the page frame
unsigned int property; // the num of free block, used in first fit pm manager
list_entry_t page_link; // free list link
};
为什么不将ref, flags, property这些数据成员通通作为链表节点list_entry的成员就好了,而要把数据成员和指针分开分别操作呢?我的看法是主要因为这里的算法是管理连续内存的,每个链表节点代表一个空闲内存块,可以包含多个内存页,所以链表节点list_entry是可以比Page的数量少的,分开来操作可以减少声明的数量,增加系统执行的效率。而且看宏函数le2page(le, member)实现的功能是将链表节点le所在地址向前(向低)偏移一定的长度,并返回一个Page *指针。
// convert list entry to page
#define le2page(le, member) \
to_struct((le), struct Page, member)
#define to_struct(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
#define offsetof(type, member) \
((size_t)(&((type *)0)->member))
offsetof()的功能是测量结构体成员相比结构体首地址的地址偏移量,返回值代表了偏移的字节数(地址的数值是以字节为单位的)。具体做法是将地址0强制转换成结构体指针,代表在地址0处申请了一个结构体,并取其成员的地址自然得到了偏移量。
to_struct()将传入的指针强制转换成char *, 刚开始看很困惑,为什么一定是char *, 这里和字符操作好像没什么关系。后来想到char类型刚好是一个字节的长度,所以char *可以得到以字节为度量的地址,刚好和offsetof相恰,找到链表想要转换成结构体的地址,并将该地址的类型强制转换。这相当于申请了一个结构体,同时初始化了指定的结构成员,但基本上只涉及到地址操作,所以操作效率很高。在算法代码中,可以看到le2page的使用率非常高。
回到算法涉及到的函数。default_init初始化了free_list和nr_free; default_init_memmap在探测到的物理内存的开头地址处紧凑地写入多个结构体Page的信息,Page的个数就是页的个数,并对内存的权限进行设置。
static void
default_init_memmap(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
for (; p != base + n; p ++) {
assert(PageReserved(p));
p->flags = p->property = 0; //初始化,设置内存页为空闲
set_page_ref(p, 0);
}
base->property = n; //空闲块的首页需要设置一下自己包含了多少页
SetPageProperty(base); //第一个Page(base)可以分配释放
nr_free += n;
list_add(&free_list, &(base->page_link));
//特别注意list_add的位置,没有在for循环里,与答案的代码可以进行对比;
//另外还要注意的是链表头&free_list没有代表一个空闲块,只是作为双向链表的入口
}
default_alloc_page()根据外界需要的内存页数调整空闲内存块所在的链表free_list
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) {
return NULL;
}
struct Page *page = NULL;
list_entry_t *le = &free_list;
while ((le = list_next(le)) != &free_list) {
struct Page *p = le2page(le, page_link); //空闲块的首页
if (p->property >= n) {
page = p;
break; //第一个符合条件的空闲块
}
}
if (page != NULL) { //找到了符合条件的页
list_del(&(page->page_link)); //这个地址已经不是空闲块的起始了,从链表删除
if (page->property > n) {
struct Page *p = page + n;
p->property = page->property - n;
list_add(&free_list, &(p->page_link)); //将剩余的空闲块处理完后插链表
}
nr_free -= n;
ClearPageProperty(page); //用户态不可访问
}
return page;
}
default_free_page(),有借就有还,在归还被分配的内存的同时检查是否有合并的分区。
static void
default_free_pages(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
for (; p != base + n; p ++) {
assert(!PageReserved(p) && !PageProperty(p)); //不要释放了本来就是空闲状态的内存,防止释放越界
p->flags = 0; //页未使用的标志
set_page_ref(p, 0); //被0个对象引用
}
base->property = n;
SetPageProperty(base);
list_entry_t *le = list_next(&free_list);
while (le != &free_list) {
p = le2page(le, page_link);
le = list_next(le);
if (base + base->property == p) { //分区可合并
base->property += p->property;
ClearPageProperty(p);
list_del(&(p->page_link));
}
else if (p + p->property == base) { //另一种情况,欲释放内存在已有内存块之前
p->property += base->property;
ClearPageProperty(base);
base = p;
list_del(&(p->page_link));
}
}
nr_free += n;
list_add(&free_list, &(base->page_link));
}
答案的代码则是一种不同的思路,用双向链表代表已分配的内存,而不是空闲内存块。由于内存分配每次的大小变化较大,链表节点的地址的变化估计会很频繁,干脆每个节点代表一个页,每个页不可再分,内存的分配与释放与链表的节点联动,是一种比较直接的做法。但是链表会变得很长,在沿着链表操作的时候,操作时间会明显增加;而且每个Page的page_link全不为空,也占有更大的运行内存。
剩下的练习2和练习3可以仔细看看pmm.c的pmm_init(),其中的英文注释挺详尽的,可以补充不少知识。
练习2练习3参考https://chyyuu.gitbooks.io/ucore_os_docs/content/lab2/lab2_3_3_5_3_setup_paging_map.html
Tip:关注一下boot_pgdir可以更好地理解来龙去脉~
关于KERN_BASE高虚拟地址(0xC0000000)的解释https://wiki.osdev.org/Higher_Half_bare_bones

浙公网安备 33010602011771号