对于操作系统内核中页内存管理有必要理一下。
其中涉及到采用什么数据结构管理内存,内存的分配与释放应该采用什么算法,而这其中要造哪些轮子,以便于达到目的。所谓轮子可以是宏、普通函数或者内联函数,好的轮子可以提高代码的编写效率,可读性也越好。对于已有的轮子要明确,输入什么输出什么。
在分页机制下,物理内存将以页为单位进行组织,页内存管理的需求是:
- 能够分配指定的页数(>=1);
- 能够释放某页。
每一页通过一个结构体描述其状态,所有的页对应一个Page型的数组,大小由管理页的数量而定:
struct Page {
int ref; //页被引用的次数
uint32_t flags; //页的状态标志
unsigned int property; //可以用来记录空闲块的大小(空闲块中包含页若干)
list_entry_t page_link; //链表中的公共部分,链接的媒介
};
为了便于对空闲块进行管理,将空闲块中第一页(头页)对应的Page数组中的那个元素用链表的方式管理起来,元素按照分配的先后顺序插入链表,称此链表为空闲链表。元素中property成员记录了空闲块中的页数。
当分配页时,先遍历空闲链表,寻找property满足要求的空闲块,当找到时将此元素的地址记录下来(假设p),此为分配页的开始,然后根据要分配页的数量n,p+n即为分配页的结束,其间的Page数组元素flags也要进行相关改变。如果n==property,则没有新的空闲块产生,否则会产生新的空闲块,对应Page数组地址为p+n的元素为头页,将其插入空闲链表中,原链表结点删除。
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; //page指向页数组项
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));
SetPageProperty(p); //from ShiHao 用不用对Page数组中的元素进行操作
}
nr_free -= n;
ClearPageProperty(page);
}
return page;
}
对于页的释放需要明确指出要释放的页的位置,以及页的数量,在页释放后应当立即进行合并操作,如果释放后的空闲块其前后有空闲块那么就进行合并。需要注意的是,空闲链表只是空闲块的组织形式,是为了便于查找空闲块,并不会改变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); //释放后,引用清零
}
base->property = n;
SetPageProperty(base); //成为头页,可以用于分配,后面要加入空闲链表
list_entry_t *le = list_next(&free_list);
while (le != &free_list) {
p = le2page(le, page_link); //p是从空闲链表第一个结点开始
/*
* 释放后空闲块**结尾的地址**指向的还是一个空闲块(妙:去空闲链表中找而不是在Page数组中找,
* 因为那样会有超出边界的问题,即base + base->property指向未知的区域,不好判断)
* 随着p在空闲链表中跳转,一个循环下来可以实现释放块和其前后块的合并。
*/
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));
}
le = list_next(le);
}
nr_free += n;
list_add(&free_list, &(base->page_link)); //将新释放的块放置在链表的开始处
}
当线性地址送到页部件翻译时,发现对应的页目录什么也没有,便会产生缺页异常,通常异常产生的情况有这么几种:
- 目标页帧不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已经撤销);
- 相应的物理页帧不在内存中(页表项非空,但Present标志位=0,比如在swap分区或磁盘文件上);
- 不满足访问权限(此时页表项P标志=1,但低权限的程序试图访问高权限的地址空间,或者有程序试图写只读页面)。
当出现上面情况之一,那么就会产生页面page fault(#PF)异常。CPU会把产生异常的线性地址存储在CR2中,并且把表示页访问异常类型的值(简称页访问异常错误码,errorCode)保存在中断栈中。页访问异常错误码有32位,大多数位为0。其他各位的含义为:
- 位0为1表示对应物理页不存在,位0为0表示保护错;
- 位1为1表示写异常(比如写了只读页),位1为0表示读异常;
- 位2为1表示用户访问权限异常(比如用户态程序访问内核空间的数据),位2为0表示内核访问异常。
当上述异常发生时,应当执行中断服务例程,进行相应的处理。