阅读前注意事项:
1、我的博客从lab2之后,如果没有特殊说明,所有标注的代码行数位置,以labcodes_answer(答案包)里的文件为准!!!因为你以后会发现做实验用meld软件比较费时费力,对于咱们学校的验收不如直接对着答案来;
2、感谢网上的各路前辈大佬们,本人在这学期初次完成实验的过程中,各位前辈们的博客给了我很多有用的指导;本人的博客内容在现有的内容上,做了不少细节的增补内容,有些地方属个人理解,如果有错在所难免,还请各位大佬们批评指正;
3、所有实验的思考题,我把它规整到了文章最后;
4、所有实验均默认不做challenge,对实验评分无影响。
一、实验内容
本次实验包含三个部分。首先了解如何发现系统中的物理内存;然后了解如何建立对物理内存的初步管理, 即了解连续物理内存管理;最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个比较全面的了解。本实验里面实现的内存管理还是非常基本的,并没有涉及到对实际机器的优化,比如针对 cache 的优化等。
二、目的
理解基于段页式内存地址的转换机制;
理解页表的建立和使用方法;
理解物理内存的管理方法。
三、实验设计思想和流程
练习0:填写已有实验
本实验依赖实验1。请把你做的实验1的代码填入本实验中代码中有“LAB1”的注释相应部分。提示:可采用diff和patch工具进行半自动的合并(merge),也可用一些图形化的比较/merge工具来手动合并,比如meld,eclipse中的diff/merge工具,understand中的diff/merge工具等。
经过比较,需要更改的文件为:
kdebug.c
trap.c
其余地方无需修改,可以直接使用。
练习1:实现 first-fit 连续物理内存分配算法(需要编程)
在实现first fit 内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时,需要按照空闲页块起始地址来排序,形成一个有序的链表。可能会修改default_pmm.c中的default_init,default_init_memmap,default_alloc_pages, default_free_pages等相关函数。请仔细查看和理解default_pmm.c中的注释。
First fit算法是一种思想比较简单,速度非常快的分配算法:
物理内存页管理器顺着双向链表进行搜索空闲内存区域,直到找到一个足够大的空闲区域,因为它尽可能少地搜索链表。如果空闲区域的大小和申请分配的大小正好一样,则把这个空闲区域分配出去,成功返回;否则将该空闲区分为两部分,一部分区域与申请分配的大小相等,把它分配出去,剩下的一部分区域形成新的空闲区。其释放内存的设计思路很简单,只需把这块区域重新放回双向链表中即可。
首先,让我们来看几个关键的数据结构:
物理页数据结构Page(kern/mm/memlayout.h,99——104行):
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
};
一个物理页数据结构含有四个成员,分别的解释如下:
1、ref,注释中可以翻译为“引用的计数器”,ref表示的是,这个页被页表的引用记数,也就是映射此物理页的虚拟页个数。如果这个页被页表引用了,即在某页表中有一个页表项设置了一个虚拟页到这个Page管理的物理页的映射关系,就会把Page的ref加一;反之,若页表项取消,即映射关系解除,就会把Page的ref减一。
2、flags表示此物理页的状态标记,有两个标志位状态,为1的时候,代表这一页是free状态,可以被分配,但不能对它进行释放;如果为0,那么说明这个页已经分配了,不能被分配,但是可以被释放掉。
3、property用来记录某连续空闲页的数量,这里需要注意的是用到此成员变量的这个Page一定是连续内存块的开始地址(第一页的地址)。
4、page_link是便于把多个连续内存空闲块链接在一起的双向链表指针,连续内存空闲块利用这个页的成员变量page_link来链接比它地址小和大的其他连续内存空闲块,释放的时候只要将这个空间通过指针放回到双向链表中。
管理所有连续的空闲内存空间块的数据结构free_area_t
(kern/mm/mmlayout.h,122——125行):
typedef struct {
list_entry_t free_list; // the list header
unsigned int nr_free; // # of free pages in this free list
} free_area_t;
在初始情况下,也许这个物理内存的空闲物理页都是连续的,这样就形成了一个大的连续内存空闲块。但随着物理页的分配与释放,这个大的连续内存空闲块会分裂为一系列地址不连续的多个小连续内存空闲块,且每个连续内存空闲块内部的物理页是连续的。
那么为了有效地管理这些小连续内存空闲块。所有的连续内存空闲块可用一个双向链表管理起来,便于分配和释放,为此定义了一个free_area_t数据结构,包含了一个list_entry结构的双向链表指针和记录当前空闲页的个数的无符号整型变量nr_free。其中的链表指针指向了空闲的物理页。
该数据结构中含有两个成员:
free_list:一个list_entry结构的双向链表指针。
nr_free:记录当前空闲页的个数。
其中,在(kern/mm/pmm.c,57行)定义了一个该数据结构的实例,用于管理实际分配当中的空闲空间。
那么,ucore如何确定使用哪个分配算法呢?这里涉及到了一个物理内存管理类pmm_manager(kern/mm/pmm.h,13——23行)
struct pmm_manager {
const char *name; // XXX_pmm_manager's name
void (*init)(void); // initialize internal description&management data structure
// (free block list, number of free block) of XXX_pmm_manager
void (*init_memmap)(struct Page *base, size_t n);
// setup description&management data structcure according to the initial free physical memory space
struct Page *(*alloc_pages)(size_t n);
// allocate >=n pages, depend on the allocation algorithm
void (*free_pages)(struct Page *base, size_t n);
// free >=n pages with "base" addr of Page descriptor structures(memlayout.h)
size_t (*nr_free_pages)(void); // return the number of free pages
void (*check)(void); // check the correctness of XXX_pmm_manager
};
总的来说,一个物理内存管理类包含这个类的名字和数个函数:包括初始化、分配页、释放页等操作,可以看做类似于lab6中的调度的五元组的绑定方法。
同样,我们所实现的函数,也是通过名字绑定的,具体的绑定过程在(kern/mm/pmm.c,285+行):
const struct pmm_manager default_pmm_manager = {
.name = "default_pmm_manager",
.init = default_init,
.init_memmap = default_init_memmap,
.alloc_pages = default_alloc_pages,
.free_pages = default_free_pages,
.nr_free_pages = default_nr_free_pages,
.check = default_check,
};
接下来,我们来实现First fit算法的相关函数:default_init,default_init_memmap,default_alloc_pages, default_free_pages。
一、default_init(kern/mm/pmm.c,63——66行):
default_init(void) {
list_init(&free_list);
nr_free = 0;
}
这个初始化很简单,直接调用库函数list_init初始化掉free_area_t(管理所有连续的空闲内存空间块的数据结构free_area_t)的双向链表和空闲块数。
这里有两个宏定义,在(kern/mm/pmm.c,59——60行):
#define free_list (free_area.free_list)
#define nr_free (free_area.nr_free)
从这里看出来,初始化的对象是free_area_t。
二、default_init_memmap(kern/mm/default_pmm.c,68——83行):
init_memmap函数主要实现的是一个根据现有的内存情况构建空闲块列表的初始状态的功能。何时应该执行这个函数呢,因此我们需要研究一下这个函数是如何被调用的。
调用过程是:kern_init --> pmm_init-->page_init-->init_memmap。
(1)kern_init(kern/init/init.c,18——44行)
这个函数是进入ucore操作系统之后,第一个执行的函数,对于内核进行初始化。在其中,调用了初始化物理内存的函数pmm_init。
(2)pmm_init(kern/mm/pmm.c,285——339行)
这个函数主要是完成对于整个物理内存的初始化,页初始化只是其中的一部分,调用位置偏前,函数之后的部分可以不管,直接进入page_init函数。
(3)page_init(kern/mm/pmm.c,189——239行)
static void
page_init(void) {
struct e820map *memmap = (struct e820map *)(0x8000 + KERNBASE);
//首先声明一个e820map类的对象memmap,与物理内存相关,在本实验中,我们获取内存信息的方式是通过e820中断(一种特殊的内核中断模式)
uint64_t maxpa = 0;
cprintf("e820map:\n");
int i;
for (i = 0; i < memmap->nr_map; i ++) {
//这里可以看做一个遍历,第一轮遍历是遍历物理地址空间,获取物理地址的最大值maxpa。(探测物理内存布局)
uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
cprintf(" memory: %08llx, [%08llx, %08llx], type = %d.\n",
memmap->map[i].size, begin, end - 1, memmap->map[i].type);
if (memmap->map[i].type == E820_ARM) {
if (maxpa < end && begin < KMEMSIZE) {
maxpa = end;
}
}
}
if (maxpa > KMEMSIZE) {
maxpa = KMEMSIZE;
} //maxpa不能大于所允许的最大值,这个最大值宏定义在memlayout的第57行,物理地址所允许的最大值为0x38000000
extern char end[];
npage = maxpa / PGSIZE; //创建的页数量等于物理地址最大值除以页大小,其中页大小为4096字节,即4KB,该定义在mmu.h的第226行。
pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);
for (i = 0; i < npage; i ++) {
SetPageReserved(pages + i);
} //将所有的页设置为保留页,在实际初始化页面init_memmap的时候,又会更改回非保留,推测是在初始化过程中这样处理,是为了防止页面被分配,结构被破坏。
uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);
for (i = 0; i < memmap->nr_map; i ++) {
uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;
if (memmap->map[i].type == E820_ARM) { //第二次遍历物理内存,这一次遍历,主要是调用init_memmap初始化各个页表。
if (begin < freemem) {
begin = freemem;
}
if (end > KMEMSIZE) {
end = KMEMSIZE;
}
if (begin < end) {
begin = ROUNDUP(begin, PGSIZE);
end = ROUNDDOWN(end, PGSIZE);
if (begin < end) {
init_memmap(pa2page(begin), (end - begin) / PGSIZE);
}
}
}
}
}
page_init函数主要是完成了一个整体物理地址的初始化过程,包括设置标记位,探测物理内存布局等操作。上面函数的注释中,标出了几个关键位置代码。
但是,其中最关键的部分,也是和实验相关的页初始化,交给了init_memmap函数处理。
这里我们主要研究一下init_memmap传入的两个参数:
1、pa2page(begin):(kern/mm/pmm.h,87——93行)
static inline struct Page *
pa2page(uintptr_t pa) {
if (PPN(pa) >= npage) {
panic("pa2page called with invalid pa");
}
return &pages[PPN(pa)];
}
其中PPN是物理地址页号,该函数的作用是,返回传入参数pa开始的第一个物理页,其基地址base。
2、end-begin / PGSIZE
由于end和begin都是循环中记录位置的标记,PGSIZE为4KB的页大小,这里就代表物理页的个数。
(4)init_memmap(kern/mm/default_pmm.c,68——83行)
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 = 0;
SetPageProperty(p); //更改页的状态
p->property = 0;
set_page_ref(p, 0);
list_add_before(&free_list, &(p->page_link));
}
nr_free += n;
//first block
base->property = n;
}
我们可以看出来,从上一轮传过来的参数,是从基地址开始的第一个物理页和待初始化的物理页的数量。
首先,用一个页结构p,存储传下来的base页面,之后对于紧随其后的n个页面进行遍历判断是否为预留页(之前,因为防止初始化页面被分配或破坏,已经设置了预留页),如果这里已经设置为了预留页(assert断言),那么就可以对它进行初始化:
1、将其标记位flag清零,调用SetPageProperty(kern/mm/memlayout.h,113行)将flag置1,表示当前页为空。(应该是一个可以优化的点)
#define SetPageProperty(page) set_bit(PG_property, &((page)->flags))
其中,PG_property的定义在(kern/mm/memlayout.h,108行)
#define PG_property 1 // the member 'property' is valid
2、将其连续空页数量设置为0,即p->property。
3、映射到此物理页的虚拟页数量置为0,调用set_page_ref函数(kern/mm/pmm.h,123——126行)
set_page_ref(struct Page *page, int val) {
page->ref = val;
}
4、插入到双向链表中,free_list因为宏定义的原因,指的是free_area_t中的list结构。
5、基地址连续空闲页数量加n,且空闲页数量加n。
三、default_alloc_pages(kern/mm/default_pmm.c,85——116行)//分配函数
static struct Page *
default_alloc_pages(size_t n) {
assert(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);//删链表
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;
}
alloc_page,这个函数是用来分配空闲页的。
首先判断空闲页的大小是否大于所需的页块大小。
如果需要分配的页面数量n,已经大于了空闲页的数量,那么直接return NULL分配失败。
过了这一个检查之后,遍历整个空闲链表。如果找到合适的空闲页,即p->property >= n(从该页开始,连续的空闲页数量大于n),即可认为可分配,重新设置标志位。具体操作是调用SetPageReserved(pp)和ClearPageProperty(pp),设置当前页面预留,以及清空该页面的连续空闲页面数量值。
然后从空闲链表,即free_area_t中,记录空闲页的链表,删除此项。
如果当前空闲页的大小大于所需大小。则分割页块。具体操作就是,刚刚分配了n个页,如果分配完了,还有连续的空间,则在最后分配的那个页的下一个页(未分配),更新它的连续空闲页值。如果正好合适,则不进行操作。
最后计算剩余空闲页个数并返回分配的页块地址。
四、default_free_pages(kern/mm/default_pmm.c,118——162行)
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);
if(p>base){
break;
}
} //找到释放的位置
//list_add_before(le, base->page_link);
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;
p = le2page(le,page_link) ; //此时,p已经到达了插入完释放数量空页的后一个页的位置上。此时,一般会满足base+n==p,因此,尝试向后合并空闲页。如果能合并,那么base的连续空闲页加上p的连续空闲页,且p的连续空闲页置为0,;如果之后的页不能合并,那么p的property一直为0,下面的代码不会对它产生影响。
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; //不断更新前一个页p的property值,并清除base
}
le = list_prev(le);
p = le2page(le,page_link);
}
}
nr_free += n; //最后的最后,空闲页数量加n
return ;
}
首先还是观察它的参数,参数和分配页的函数是相同的,因此可以类似比较。
default_free_pages主要完成的是对于页的释放操作,首先有一个assert语句断言这个基地址所在的页是否为预留,如果不是预留页,那么说明它已经是free状态,无法再次free,也就是之前所述,只有处在占用的页,才能有free操作。
之后,声明一个页p,p遍历一遍整个物理空间,直到遍历到base所在位置停止,开始释放操作。
找到了这个基地址之后呢,就可以将空闲页重新加进来(之前在分配的时候,删除了),之后就是一系列与初始化空闲页一样的设置标记位操作了。
之后,如果插入基地址附近的高地址或低地址可以合并,那么需要更新相应的连续空闲页数量,向高合并和向低合并。
练习2:实现寻找虚拟地址对应的页表项(需要编程)
通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。
概念解释:
(1)三种地址
x86体系结构将内存地址分成三种:逻辑地址(也称虚地址)、线性地址和物理地址。
逻辑地址即是程序指令中使用的地址。
物理地址是实际访问内存的地址。
逻辑地址通过段式管理的地址映射可以得到线性地址,线性地址通过页式管理的地址映射得到物理地址。get pte函数是给出了线性地址,即linear address。
三者的关系是:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。
有时我们也把逻辑地址称为虚拟地址。因为和虚拟内存空间的概念类似,逻辑地址也是和实际物理内存容量无关的。逻辑地址和物理地址的“差距”是0xC0000000,是由于虚拟地址->线性地址->物理地址映射正好差这个值。这个值是由操作系统指定的。
本次实验相关的实现内容,主要是线性地址和物理地址,和逻辑地址基本无关,逻辑地址和线性地址的转换关系,已经在之前的函数调用中处理完毕,等到函数调用到get_pte的时候,只需要使用线性地址操作即可。
(2)相关可能涉及到的函数调用和其他概念解释
页式管理将线性地址分成三部分(图中的 Linear Address 的 Directory 部分、 Table 部分和 Offset 部分)。ucore 的页式管理通过一个二级的页表实现。一级页表存放在高10位中,二级页表存放于中间10位中,最后的12位表示偏移量,据此可以证明,页大小为4KB(2的12次方,4096)。
pde_t 全称为page directory entry,也就是一级页表的表项,前10位;
pte_t 全称为page table entry,表示二级页表的表项,中10位。
获取directory,table部分:(kern/mm/mmu.h,204——207行):
#define PDX(la) ((((uintptr_t)(la)) >> PDXSHIFT) & 0x3FF)
#define PTX(la) ((((uintptr_t)(la)) >> PTXSHIFT) & 0x3FF)
la是线性地址,32位,需要提取出该字段内容,才能获取页表内容。
其中,PDXSHIFT的值为22,右移22位,再与10个1与,就可以获取directory;
PTXSHIFT的值为11,右移10位,再与11个1与,由于地址对齐的原因,0x3FF的11位之前都是0,这样就能提取table部分。
set_page_ref(page,1): 设置此页被引用一次
page2pa(page): 得到page管理的那一页的物理地址
KADDR(pa): 返回pa对应的虚拟地址(线性地址),注释里面如此说:takes a physical address and returns the corresponding kernel virtual address.
因此有函数实现如下:
get pte(kern/mm/pmm.c,349——395行):
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)];
}
首先尝试使用PDX函数,获取一级页表的位置,如果获取成功,可以直接返回一个东西。
如果获取不成功,那么需要根据create标记位来决定是否创建这一个二级页表(注意,一级页表中,存储的都是二级页表的起始地址)。如果create为0,那么不创建,否则创建。
既然需要查找这个页表,那么页表的引用次数就要加一。
之后,需要使用memset将新建的这个页表虚拟地址,全部设置为0,因为这个页所代表的虚拟地址都没有被映射。
接下来是设置控制位。这里应该设置同时设置上PTE_U、PTE_W和PTE_P,分别代表用户态的软件可以读取对应地址的物理内存页内容、物理内存页内容可写、物理内存页存在。
如果原来就有二级页表,或者新建立了页表,最后,只需返回对应项的地址即可。
其中,PDE_ADDR被定义在(kern/mm/mmu.h,219——220行):
#define PTE_ADDR(pte) ((uintptr_t)(pte) & ~0xFFF)
#define PDE_ADDR(pde) PTE_ADDR(pde)
其实只是一个与操作,最后用KADDR返回二级页表所对应的线性地址,因为这里不是要求物理地址,而是需要找对应的二级页表项,在查询完二级页表之前,都还是属于虚拟地址的范畴。
练习3:释放某虚地址所在的页并取消对应二级页表项的映射(需要编程)
当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page_remove_pte函数中的注释。为此,需要补全在 kern/mm/pmm.c中的page_remove_pte函数。
实现如下:
page_remove_pte(kern/mm/pmm.c,413——447行)
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
if (*ptep & PTE_P) { //PTE_P代表页存在
struct Page *page = pte2page(*ptep);
if (page_ref_dec(page) == 0) {
free_page(page);
}
*ptep = 0;
tlb_invalidate(pgdir, la)
}
}
传入参数和寻找二级页表的时候差不多,传入的是一个一级、二级地址,和一个线性地址,用于确认虚拟地址(线性地址)所在页的位置。
首先,需要确认这个页是存在的,使用*ptep和上一题中定义的标记位PTE_P(0x001,代表页存在)。如果这个页存在,那么使用pte2page获取其物理地址。
下面有一个函数的调用:page_ref_dec(kern/mm/pmm.h,135——139行)
page_ref_dec(struct Page *page) {
page->ref -= 1;
return page->ref;
}
很明显看出来,这个函数试探一下当前这个页被引用的次数,如果只被上一级(二级页表)引用了一次,那么减一以后就是0,页和对应的二级页表都可以直接被释放(将二级页表置0是取消映射)。
如果还有更多的页表应用了它,那就不能释放掉这个页,但是取消对应二级页表项的映射,也就是把映射的入口(传入的二级页表)释放为0,之后调用了tlb_invalidate函数:
tlb_invalidate:(kern/mm/pmm.c,488——495行)
// 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);
}
}
这里面的rcr3和invlpg应该是库函数,没有找到在哪里实现了,函数的大致翻译过来意思是,使TLB条目无效,但仅当正在编辑的页表是处理器当前正在使用的页表时。具体作用,应当是去除那些已经被清除关系的二级页表,更新pgdir,也就是一级页表。
四、思考题
思考题1:你的first fit算法是否有进一步的改进空间
我认为还有提升空间。比如在free操作中,寻找需要free的base地址的时候,依靠的是遍历,通过改进算法,可以直接将base地址传入,无需遍历,直接找到位置开始操作,减少时间开销。
思考题2:数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?
有关系,数据结构page是最低级的页表,目录项是一级页表,存储的内容是页表项的起始地址(二级页表),而页表项是二级页表,存储的是每个页表的开始地址,这些内容之间的关系时通过线性地址高低位不同功能的寻址体现的。
思考题3:请描述页目录项(Pag Director Entry)和页表(Page Table Entry)中每个组成部分的含义和以及对ucore而言的潜在用处。
页目录项是一级页表,存储了各二级页表的起始地址,页表是二级页表,存储了各个页的起始地址。一个虚拟地址(线性地址)通过页机制翻译得到最终的物理地址。
页表的主要作用是:假如在系统里面,物理内存和虚拟内存是一一对应的,那么在进程空间里面就会存在很多的页表,同时也会占据很多的空间,那么,为了解决这个问题就出现了多级页表。
思考题4:如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
参考网上资料,总结缺页中断发生时的事件顺序如下:
1) 硬件陷入内核,在堆栈中保存程序计数器。大多数机器将当前指令的各种状态信息保存在特殊的CPU寄存器中。
2) 启动一个汇编代码例程保存通用寄存器和其他易失的信息,以免被操作系统破坏。这个例程将操作系统作为一个函数来调用。
3) 当操作系统发现一个缺页中断时,尝试发现需要哪个虚拟页面。通常一个硬件寄存器包含了这一信息,如果没有的话,操作系统必须检索程序计数器,取出这条指令,用软件分析这条指令,看看它在缺页中断时正在做什么。
4) 一旦知道了发生缺页中断的虚拟地址,操作系统检查这个地址是否有效,并检查存取与保护是否一致。如果不一致,向进程发出一个信号或杀掉该进程。如果地址有效且没有保护错误发生,系统则检查是否有空闲页框。如果没有空闲页框,执行页面置换算法寻找一个页面来淘汰。
5) 如果选择的页框“脏”了,安排该页写回磁盘,并发生一次上下文切换,挂起产生缺页中断的进程,让其他进程运行直至磁盘传输结束。无论如何,该页框被标记为忙,以免因为其他原因而被其他进程占用。
6) 一旦页框“干净”后(无论是立刻还是在写回磁盘后),操作系统查找所需页面在磁盘上的地址,通过磁盘操作将其装入。该页面被装入后,产生缺页中断的进程仍然被挂起,并且如果有其他可运行的用户进程,则选择另一个用户进程运行。
7) 当磁盘中断发生时,表明该页已经被装入,页表已经更新可以反映它的位置,页框也被标记为正常状态。
8) 恢复发生缺页中断指令以前的状态,程序计数器重新指向这条指令。
9) 调度引发缺页中断的进程,操作系统返回调用它的汇编语言例程。
10) 该例程恢复寄存器和其他状态信息
思考题5:如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事? 鼓励通过编程来具体完成这个问题。
我们知道,物理地址和虚拟地址之间存在一定偏移offset:
phy addr + KERNBASE = virtual addr
通常,这个offset,就是KERNBASE是由操作系统决定的,而ucore中,对其也是有定义的,定义在(kern/mm/memlayout.h,55——58行)
/* All physical memory mapped at this address */
#define KERNBASE 0xC0000000
#define KMEMSIZE 0x38000000 // the maximum amount of physical memory
KERNBASE为虚拟地址空间中的内核基址,即偏移量。
我们看到,在ucore中,虚拟地址和物理地址之间的偏移量为0xC0000000,将其改为0即可完成题目要求。
五、运行结果
1、qemu中出现页表信息:
2、如果一切正常,make grade得分应当为50/50。