练习0:填写已有实验
本实验依赖实验1。请把你做的实验1的代码填入本实验中代码中有“LAB1”的注释相应部分。提示:可采用diff和patch工具进行半自动的合并(merge),也可用一些图形化的比较/merge工具来手动合并,比如meld,eclipse中的diff/merge工具,understand中的diff/merge工具等。
练习1:实现 first-fit 连续物理内存分配算法
算法原理
要求空闲分区链以地址递增的次序链接。在分配内存时,从链首开始顺序查找,直至找到一个大小能满足要求的分区为止;然后再按照作业的大小,从该分取中划出一块内存空间分配给请求者,余下的空闲分区仍留在空闲链中。若从链首直到链尾都不能找到一个能满足要求的分区,则此次内存分配失败,返回。该算法倾向于优先利用内存中低地址部分的空闲分区,从而保留了高址部分的大空闲区。这给为以后到达的大作业分配大的内存空间创造了条件,其缺点是低址部分不断被划分,会留下许多难以利用的、很小的空闲分区,而每次查找又都是从低址部分开始,这无疑会增加查找可用空闲分区时的开销。
改写 default_init_memmap()
其中用到的结构体定义
struct list_entry {
struct list_entry *prev, *next; //父节点,子节点
};
typedef struct list_entry list_entry_t; //重命名
typedef struct {
list_entry_t free_list; //链表头部
unsigned int nr_free; //空闲块的个数
} free_area_t; //链表头部结构
struct Page {
int ref; //映射此物理页的虚拟页的个数
uint32_t flags; //物理页属性
unsigned int property; //连续空页有多少(只在地址最低页有值)
list_entry_t page_link; // 双向链接各个Page结构的page_link
};
其中用到的函数和宏定义
free_area_t free_area;
#define free_list (free_area.free_list)
#define nr_free (free_area.nr_free)
#define PG_reserved 0
#define PG_property 1
#define SetPageProperty(page) \
set_bit(PG_property, &((page)->flags)) //设置为保留页
#define PageReserved(page) \
test_bit(PG_reserved, &((page)->flags)) //检查是否为保留页
static inline void
list_add_before(list_entry_t *listelm, list_entry_t *elm) { //使用头插法将空闲页插入链表中
__list_add(elm, listelm->prev, listelm);
}
static inline void
__list_add(list_entry_t *elm, list_entry_t *prev, list_entry_t *next) {
prev->next = next->prev = elm;
elm->next = next;
elm->prev = prev;
}
static inline void
set_page_ref(struct Page *page, int val) {
page->ref = val;
}
实现default_init_memmap()
static void
default_init_memmap(struct Page *base, size_t n) {
assert(n > 0); //判断n是否大于0
struct Page *p = base;
for (; p != base + n; p ++) { //初始化n块物理页
assert(PageReserved(p)); //检查此页是否为保留页
p->flags = p->property= 0; //标志位清0
SetPageProperty(p); //设置标志位为1
set_page_ref(p, 0); //清除引用此页的虚拟页的个数
//加入空闲链表
list_add_before(&free_list, &(p->page_link));
}
nr_free += n; //计算空闲页总数
base->property = n; //修改base的连续空页值为n
}
总结
传入物理页基地址,和物理页的个数(个数必须大于0),然后对每一块物理页进行设置:先判断是否为保留页,如果不是,则进行下一步。将标志位清0,连续空页个数清0,然后将标志位设置为1,将引用此物理页的虚拟页的个数清0。然后再加入空闲链表。最后计算空闲页的个数,修改物理基地址页的property的个数为n。
改写 default_alloc_pages()
程序用到的函数定义
static inline list_entry_t *
list_next(list_entry_t *listelm) { //返回下一个节点
return listelm->next;
}
static inline void
list_del(list_entry_t *listelm) { //删除当前节点
__list_del(listelm->prev, listelm->next);
}
static inline void
__list_del(list_entry_t *prev, list_entry_t *next) {
prev->next = next;
next->prev = prev;
}
宏定义
#define offsetof(type, member) \
((size_t)(&((type *)0)->member))
#define to_struct(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
#define le2page(le, member) \
to_struct((le), struct Page, member)
#define ClearPageReserved(page) \
clear_bit(PG_reserved, &((page)->flags))
实现default_alloc_pages()
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0); //判断n是否大于0
if (n > nr_free) { //需要分配页的个数小于空闲页的总数,直接返回
return NULL;
}
list_entry_t *le, *len; //空闲链表的头部和长度
le = &free_list; //空闲链表的头部
while((le=list_next(le)) != &free_list) {//遍历整个空闲链表
struct Page *p = le2page(le, page_link); //转换为页结构
if(p->property >= n){ //找到合适的空闲页
int i;
for(i=0;i<n;i++){
len = list_next(le);
struct Page *pp = le2page(le, page_link); //转换页结构
SetPageReserved(pp); //设置每一页的标志位
ClearPageProperty(pp);
list_del(le); //将此页从free_list中清除
le = len;
}
if(p->property>n){ //如果页块大小大于所需大小,分割页块
(le2page(le,page_link))->property = p->property-n;
}
ClearPageProperty(p);
SetPageReserved(p);
nr_free -= n; //减去已经分配的页块大小
return p;
}
}
return NULL;
}
总结
这个函数是用来分配空闲页的。首先判断空闲页的大小是否大于所需的页块大小。如果小于空闲页的大小。则遍历整个空闲链表。如果找到合适的空闲页,则重新设置标志位。然后从空闲链表中删除此页。如果当前空闲页的大小大于所需大小。则分割页块。如果合适则不进行操作,最后计算剩余空闲页个数并返回分配的页块地址.
改写 default_free_pages()
用到的函数定义
static inline list_entry_t *
list_prev(list_entry_t *listelm) {
return listelm->prev;
}
实现 default_free_pages()
static void
default_free_pages(struct Page *base, size_t n) {
assert(n > 0);
assert(PageReserved(base)); //检查需要释放的页块是否已经被分配
list_entry_t *le = &free_list;
struct Page * p;
while((le=list_next(le)) != &free_list) { //寻找合适的位置
p = le2page(le, page_link); //获取链表对应的Page
if(p>base){
break;
}
}
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->property = n; //设置连续大小为n
//如果是高位,则向高地址合并
p = le2page(le,page_link) ;
if( base+n == p ){
base->property += p->property;
p->property = 0;
}
//如果是低位且在范围内,则向低地址合并
le = list_prev(&(base->page_link));
p = le2page(le, page_link);
if(le!=&free_list && p==base-1){ //满足条件,未分配则合并
while(le!=&free_list){
if(p->property){ //连续
p->property += base->property;
base->property = 0;
break;
}
le = list_prev(le);
p = le2page(le,page_link);
}
}
nr_free += n;
return ;
}
总结
这个函数的作用是释放已经使用完的页,把他们合并到freelist中。在freelist中查找合适的位置以供插入。改变被释放页的标志位,以及头部的计数器尝试在freelist中向高地址或低地址合并。
练习2:实现寻找虚拟地址对应的页表项
段页式管理总体框架
在保护模式中,x86 体系结构将内存地址分成三种:逻辑地址(也称虚地址)、线性地址和物理地址。逻辑地址即是程序指令中使用的地址,物理地址是实际访问内存的地址。逻辑地址通过段式管理的地址映射可以得到线性地址,线性地址通过页式管理的地址映射得到物理地址。
相关定义
PDX(la): 返回虚拟地址la的页目录索引
KADDR(pa): 返回物理地址pa相关的内核虚拟地址
set_page_ref(page,1): 设置此页被引用一次
page2pa(page): 得到page管理的那一页的物理地址
struct Page * alloc_page() : 分配一页出来
memset(void * s, char c, size_t n) : 设置s指向地址的前面n个字节为字节‘c’
PTE_P 0x001 表示物理内存页存在
PTE_W 0x002 表示物理内存页内容可写
PTE_U 0x004 表示可以读取对应地址的物理内存页内容
类型定义
这里涉及到三个类型pte_t、pde_t和uintptr_t。通过查看定义:
typedef unsigned int uint32_t;
typedef uint32_t uintptr_t;
typedef uintptr_t pte_t;
typedef uintptr_t pde_t;
可知它们其实都是unsigned int类型。在此做区分,是为了分清概念。
获取一级页表项地址
对于32位的线性地址,我们可以将它拆分成三部分
通过查看mmu.h文件,我们可以知道它的定义
// A linear address 'la' has a three-part structure as follows:
//
// +--------10------+-------10-------+---------12----------+
// | Page Directory | Page Table | Offset within Page |
// | Index | Index | |
// +----------------+----------------+---------------------+
// \--- PDX(la) --/ \--- PTX(la) --/ \---- PGOFF(la) ----/
// \----------- PPN(la) -----------/
//
// page directory index
#define PDX(la) ((((uintptr_t)(la)) >> PDXSHIFT) & 0x3FF)
// page table index
#define PTX(la) ((((uintptr_t)(la)) >> PTXSHIFT) & 0x3FF)
// page number field of address
#define PPN(la) (((uintptr_t)(la)) >> PTXSHIFT)
// offset in page
#define PGOFF(la) (((uintptr_t)(la)) & 0xFFF)
所以,PDX(la)的作用就是得到一级页表项对应的入口地址。
获取一级页表项
pde_t *pdep = &pgdir[PDX(la)]; //尝试获得页表
pde_t全称为 page directory entry,也就是一级页表的表项(注意:pgdir实际不是表项,而是一级页表本身。实际上应该新定义一个类型pgd_t来表示一级页表本身)。pte_t全称为 page table entry,表示二级页表的表项。uintptr_t表示为线性地址,由于段式管理只做直接映射,所以它也是逻辑地址。pgdir给出页表起始地址。通过查找这个页表,我们可以得到一级页表项(二级页表的入口地址)。
不存在二级页表项
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; //设置控制位
}
如果在查找二级页表项时,发现对应的二级页表不存在,则需要根据create参数的值来处理是否创建新的二级页表。如果create参数为0,则get_pte返回NULL;如果create参数不为0,则get_pte需要申请一个新的物理页(通过alloc_page来实现,可在mm/pmm.h中找到它的定义),再在一级页表中添加页目录项指向表示二级页表的新物理页。注意,新申请的页必须全部设定为零,因为这个页所代表的虚拟地址都没有被映射。
然后设置控制位:
*pdep = pa | PTE_U | PTE_W | PTE_P; //设置控制位
当建立从一级页表到二级页表的映射时,需要注意设置控制位。这里应该设置同时设置 上PTE_U、PTE_W和PTE_P(定义可在mm/mmu.h)。如果原来就有二级页表,或者新建立了页表,则只需返回对应项的地址即可。
设一个32bit线性地址la有一个对应的32bit物理地址pa,如果在以la的高10位为索引值的页目录项中的存在位(PTE_P)为0,表示缺少对应的页表空间,则可通过alloc_page获得一个空闲物理页给页表,页表起始物理地址是按4096字节对齐的,这样填写页目录项的内容为
页目录项内容 = (页表起始物理地址 &0x0FFF) | PTE_U | PTE_W | PTE_P
其中:
PTE_U:位3,表示用户态的软件可以读取对应地址的物理内存页内容
PTE_W:位2,表示物理内存页内容可写
PTE_P:位1,表示物理内存页存在
返回地址
return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)]; //返回页表的地址
如果原来就有二级页表,或者新建立了页表,则只需返回对应项的地址即可。
实现get_pte()
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
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)];
//KADDR(PDE_ADDR(*pdep)):这部分是由页目录项地址得到关联的页表物理地址, 再转成虚拟地址
//PTX(la):返回虚拟地址la的页表项索引
//最后返回的是虚拟地址la对应的页表项入口地址
}
练习3:释放某虚地址所在的页并取消对应二级页表项的映射
PTE_P
#define PTE_P 0x001 // Present
tlb_invalidate函数
// invalidate a TLB entry, but only if the page tables being
// edited are the ones currently in use by the processor.
void
tlb_invalidate(pde_t *pgdir, uintptr_t la) {
if (rcr3() == PADDR(pgdir)) {
invlpg((void *)la);
}
}
当修改的页表是进程正在使用的那些页表,使之无效。
page_ref_dec函数
static inline int
page_ref_dec(struct Page *page) {
page->ref -= 1; //引用数减一
return page->ref;
}
减少该页的引用次数,返回剩下引用次数。
实现page_remove_pte()
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); //释放页
}
*ptep = 0; //该页目录项清零
tlb_invalidate(pgdir, la);
//修改的页表是进程正在使用的那些页表,使之无效
}
}
总结
判断此页被引用的次数,如果仅仅被引用一次,则这个页也可以被释放。否则,只能释放页表入口。