UCORE实验2
实验目的
理解基于段页式内存地址的转换机制
理解页表的建立和使用方法
理解物理内存的管理方法
实验内容
本次实验包含三个部分。首先了解如何发现系统中的物理内存;然后了解如何建立对物理内存的初步管理,即了解连续物理内存管理;最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个比较全面的了解。本实验里面实现的内存管理还是非常基本的,并没有涉及到对实际机器的优化,比如针对 cache 的优化等。如果大家有余力,尝试完成扩展练习。
练习0:填写已有实验
本实验依赖实验1。请把你做的实验1的代码填入本实验中代码中有“LAB1”的注释相应部分。提示:可采用diff和patch工具进行半自动的合并(merge),也可用一些图形化的比较/merge工具来手动合并,比如meld,eclipse中的diff/merge工具,understand中的diff/merge工具等。
分析
可以直接meld手动快速合并,或者自己手动复制粘贴。
练习1:实现 first-fit 连续物理内存分配算法(需要编程)
在实现first fit内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时,需要按照空闲页块起始地址来排序,形成一个有序的链表。可能会修改default_pmm.c中的default_init,default_init_memmap,default_alloc_pages, default_free_pages等相关函数。请仔细查看和理解default_pmm.c中的注释。
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
你的first fit算法是否有进一步的改进空间。
分析
相关背景知识:
探测系统物理内存布局:
当ucore被启动之后,最重要的事情就是知道还有多少内存可用,一般来说,获取内存大小的方法由BIOS中断调用和直接探测两种。BIOS中断调用方法是一般只能在实模式下完成,而直接探测方法必须在保护模式下完成。ucore是通过BIOS中断调用来帮助完成的,由于BIOS中断调用必须在实模式下进行,所以在bootloader进入保护模式前完成这部分工作相对比较合适。通过BIOS中断获取内存可调用参数为e820h的INT 15h BIOS中断,BIOS通过系统内存映射地址描述符(Address Range Descriptor)格式来表示系统物理内存布局。探测功能在bootasm.S中实现:
probe_memory:
movl $0, 0x8000 //对0x8000处的32位单元清零,即给位于0x8000处的struct e820map的成员变量nr_map清零
xorl %ebx, %ebx
movw $0x8004, %di //表示设置调用INT 15h BIOS中断后,BIOS返回的映射地址描述符的起始地址
start_probe:
movl $0xE820, %eax //INT 15的中断调用参数
movl $20, %ecx //设置地址范围描述符的大小为20字节,其大小等于struct e820map的成员变量map的大小
movl $SMAP, %edx //设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定
int $0x15 //调用int 0x15中断,要求BIOS返回一个用地址范围描述符表示的内存段信息
jnc cont //如果eflags的CF位为0,则表示还有内存段需要探测
movw $12345, 0x8000 //探测有问题,结束探测
jmp finish_probe
cont:
addw $20, %di //设置下一个BIOS返回的映射地址描述符的起始地址
incl 0x8000 //递增struct e820map的成员变量nr_map
cmpl $0, %ebx //如果INT0x15返回的ebx为零,表示探测结束,否则继续探测
jnz start_probe
finish_probe:
探查出的信息存放在物理地址0x8000,程序使用结构体struct e820map作为保存地址范围描述符结构的缓冲区来保存内存布局,e820map定义在kern/mm/memlayout.h:
// some constants for bios interrupt 15h AX = 0xE820
#define E820MAX 20 // number of entries in E820MAP
#define E820_ARM 1 // address range memory
#define E820_ARR 2 // address range reserved
struct e820map {
int nr_map;
struct {
uint64_t addr;
uint64_t size;
uint32_t type;
} __attribute__((packed)) map[E820MAX];
};
完成物理内存页管理初始化工作后,其物理地址的分布空间如下:
+----------------------+ <- 0xFFFFFFFF(4GB) ---------------------------- 4GB
| 一些保留内存,例如用于| 保留空间
| 32bit设备映射空间等 |
+----------------------+ <- 实际物理内存空间结束地址 ----------------------------
| |
| |
| 用于分配的 | 可用的空间
| 空闲内存区域 |
| |
| |
| |
+----------------------+ <- 空闲内存起始地址 ----------------------------
| VPT页表存放位置 | VPT页表存放的空间 (4MB左右)
+----------------------+ <- bss段结束处 ----------------------------
|uCore的text、data、bss | uCore各段的空间
+----------------------+ <- 0x00100000(1MB) ---------------------------- 1MB
| BIOS ROM |
+----------------------+ <- 0x000F0000(960KB)
| 16bit设备扩展ROM | 显存与其他ROM映射的空间
+----------------------+ <- 0x000C0000(768KB)
| CGA显存空间 |
+----------------------+ <- 0x000B8000 ---------------------------- 736KB
| 空闲内存 |
+----------------------+ <- 0x00011000(+4KB) uCore header的内存空间
| uCore的ELF header数据 |
+----------------------+ <-0x00010000 ---------------------------- 64KB
| 空闲内存 |
+----------------------+ <- 基于bootloader的大小 bootloader的
| bootloader的 | 内存空间
| text段和data段 |
+----------------------+ <- 0x00007C00 ---------------------------- 31KB
| bootloader和uCore |
| 共用的堆栈 | 堆栈的内存空间
+----------------------+ <- 基于栈的使用情况
| 低地址空闲空间 |
+----------------------+ <- 0x00000000 ---------------------------- 0KB
以页为单位管理物理内存:
在获得可用物理内存范围后,系统需要建立相应的数据结构来管理以物理页(按4KB对齐,且大小为4KB的物理内存单元)为最小单位的整个物理内存,以配合后续涉及的分页管理机制。每个物理页可以用一个Page数据结构来表示。Page定义在kern/mm/memlayout.h:
/* *
* struct Page - Page descriptor structures. Each Page describes one
* physical page. In kern/mm/pmm.h, you can find lots of useful functions
* that convert Page to other data types, such as phyical address.
* */
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表示这页被页表的引用记数,property代表代空闲块的大小(以物理页为大小),仅在当前物理页为连续内存空闲块第一个物理页(Head Page)时生效,而flags用到了两个bit表示页目前具有的两种属性:
/* Flags describing the status of a page frame */
#define PG_reserved 0 // the page descriptor is reserved for kernel or unusable
#define PG_property 1 // the member 'property' is valid
flags设置为PG_reserved时,即PG_reserved为1,该物理页会被保留,不能放到空闲页链表中,即这样的页不是空闲页,不能动态分配与释放。比如目前内核代码占用的空间就属于这样“被保留”的页。
flags设置为PG_property时,即PG_property为1,该物理页为空闲状态,可以被分配,如果为0则该页已经被操作系统分配。
首次匹配(first fit):
将空闲内存块以地址递增的顺序连接,从低地址开始寻找,找到第一个足够大的块,将请求的空间返回给用户,剩余的空闲空间留给后续请求。首次匹配有速度优势(不需要遍历所有空闲块),但有时会让空闲列表开头的部分有很多小块。
有了以上背景知识后,我们来分析defalut_pmm.c,本质上该文件实现了一个类似first fit的算法,我们着重分析其中三个函数:
1.default_init_memmap
static void
default_init_memmap(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
for (; p != base + n; p ++) { //在查找可用内存并分配struct Page数组时就已经将将全部Page设置为reserved
assert(PageReserved(p));
p->flags = p->property = 0; //将Page标记为可用的:ref设为0,清除reserved,设置PG_property,并把property设置为0(不是空闲块的第一个物理页)
set_page_ref(p, 0);
}
base->property = n; //空闲块第一个物理页,property设置为n
SetPageProperty(base);
nr_free += n; //更新空闲块的总和
list_add(&free_list, &(base->page_link)); //'p->page_link'将这个页面链接到'free_list'。
}
这是一个物理页初始化函数,其功能是将所有可用的Page的flags设置为PG_property,引用计数设置为0,property设置为0,初始化page_link空闲块的第一个物理块的property设置为该空闲块的大小,将其加入到空闲列表末尾。
2.default_alloc_pages
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) { //如果请求的内存大小大于空闲块的大小,返回NULL
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); //在while循环中,获取page并检查p->property(记录这个块中空闲物理页的数量)是否>=n.
if (p->property >= n) { //如果找到满足大小的空闲块,跳出循环
page = p;
break;
}
}
if (page != NULL) { //找到满足大小(>=n)的空闲块后,被分配的物理页的flags应该被设置为:'PG_reserved = 1', 'PG_property = 0'。然后,将这些页面从'free_list'中移除。
list_del(&(page->page_link));
if (page->property > n) { //如果p->property>n,我们应该重新计算这个空闲块的剩下空余物理页的数量
struct Page *p = page + n;
p->property = page->property - n;
list_add(&free_list, &(p->page_link));
}
nr_free -= n; //重新计算nr_free(所有空闲块的空闲部分的数量)
ClearPageProperty(page);
}
return page;
}
该函数功能是在空闲列表中搜索第一个空闲块(块大小>=n),如果找到则把找到的page返回。
3.default_free_pages
//参数说明:当n超过已分配块的页数时,回收整个块;当n小于已分配块的页数时,回收n页,剩下的内存不回收;当base指向的内存块不是已分配块的起始地址时,从base开始回收。
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; //重置物理页的属性,如p->ref和p->flags
set_page_ref(p, 0);
}
base->property = n; //头页property设为n
SetPageProperty(base);
list_entry_t *le = list_next(&free_list);
while (le != &free_list) { //尝试在较低或较高的地址合并块。注意,这应该正确改变一些物理页的p->property。
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)); //将合并后的空闲块添加会空闲列表
}
该函数的功能是释放指定大小的已分配的内存块,并且合并空闲块。
4.改进
defalut_pmm.c中包含两个check函数,用来检测是否正确实现了first fit算法。而defalut_pmm.c中的算法要改进的地方在于:first-fit算法要求将空闲内存块按照地址从小到大的方式链接起来,而defalut_pmm.c中的算法没有实现这一点。
几种listadd函数:
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
list_add_after(list_entry_t *listelm, list_entry_t *elm) {
__list_add(elm, listelm, listelm->next);
}
static inline void
list_add(list_entry_t *listelm, list_entry_t *elm) {
list_add_after(listelm, elm);
}
static inline void
list_add_before(list_entry_t *listelm, list_entry_t *elm) {
__list_add(elm, listelm->prev, listelm);
}
default_init_memmap
修改前:该函数将新页面插入链表时,没有按照地址顺序插入。
list_add(&free_list, &(base->page_link));
修改后:按地址顺序插入至双向链表中。
list_add_before(&free_list, &(base->page_link));
default_alloc_pages
修改前:获取到了一个大小足够大的块地址时,程序会先将该页头从链表中断开,切割,并将剩余空间放回链表中。但将剩余空间放回链表时,并没有按照地址顺序插入链表。
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)); //key
}
修改后:将剩余空间放回链表时,按照地址顺序插入链表,而不是直接插入free_list后面。
if (page->property > n) {
struct Page *p = page + n;
p->property = page->property - n;
SetPageProperty(p);
list_add_after(&(page->page_link), &(p->page_link)); //key
}
list_del(&(page->page_link));
default_free_pages
修改前:该函数默认会在函数末尾处,将待释放的页头插入至链表的第一个结点。
list_add(&free_list, &(base->page_link));
修改后:按地址顺序插入至对应的链表结点处。
/*le = list_next(&free_list);
while (le != &free_list) {
p = le2page(le, page_link); //将空闲列表条目转换为页面
if (base + base->property <= p) { //地址大小由空闲块大小来表示
assert(base + base->property != p);
break;
}
le = list_next(le);
}*/
list_add_before(le, &(base->page_link));
修改前,内存分配失败:
修复后,内存分配成功:
练习2:实现寻找虚拟地址对应的页表项(需要编程)
通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。get_pte函数的调用关系图如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iR8VexL8-1651301445675)(https://objectkuan.gitbooks.io/ucore-docs/content/lab2_figs/image001.png)]
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
请描述页目录项(Pag Director Entry)和页表(Page Table Entry)中每个组成部分的含义和以及对ucore而言的潜在用处。
如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
分析
相关背景知识:
建立虚拟页和物理页帧的地址映射关系:
此处采用二级页表来建立线性地址与物理地址之间的映射关系。由于我们已经具有了一个物理内存页管理器default_pmm_manager,支持动态分配和释放内存页的功能,我们就可以用它来获得所需的空闲物理页。在二级页表结构中,页目录表占4KB空间,可通过alloc_page函数获得一个空闲物理页作为页目录表(Page Directory Table,PDT)。同理,ucore也通过这种类似方式获得一个页表(Page Table,PT)所需的4KB空间。在一个简单的两级页表中,页目录为每页页表包含了一项。它由多个页目录项(Page Directory Entries,PDE)组成。PDE(至少)拥有有效位(valid bit)和页帧号(page frame number,PFN),类似于 PTE。但是,正如上面所暗示的,这个有效位的含义稍有不同:如果 PDE 项是有效的,则意味着该项指向的页表(通过PFN)中至少有一页是有效的,即在该 PDE 所指向的页中,至少一个PTE,其有效位被设置为1。如果 PDE 项无效(即等于零),则 PDE的其余部分没有定义。
// 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) -----------/
//
// The PDX, PTX, PGOFF, and PPN macros decompose linear addresses as shown.
// To construct a linear address la from PDX(la), PTX(la), and PGOFF(la),
// use PGADDR(PDX(la), PTX(la), PGOFF(la)).
1.实现get_pte函数
注意,PTE内容的设置是调用者的职责,get_pte只需要给调用者一个可访问的PTE即可。
get_pte函数中用到的一些宏和函数具体功能如下:
MACROs or Functions:
PDX(la) : the index of page directory entry of VIRTUAL ADDRESS la.
虚拟地址la的页目录条目的索引。
KADDR(pa) : takes a physical address and returns the corresponding kernel virtual address.
接收一个物理地址并返回相应的内核虚拟地址。
set_page_ref(page,1) : means the page be referenced by one time.
表示该页被引用一次。
page2pa(page): get the physical address of memory which this (struct Page *) page manages.
获取这个(struct Page *)page所管理的内存的物理地址。
struct Page * alloc_page() : allocation a page.
分配一个页面。
memset(void *s, char c, size_t n) : sets the first n bytes of the memory area pointed by s to the specified value c.
将s所指向的内存区域的前n个字节设置为指定的值c。
DEFINEs:
PTE_P 0x001 //page table/directory entry flags bit : Present,页表/页目录条目标志位:位1,表示物理内存页存在。
PTE_W 0x002 //page table/directory entry flags bit : Writeable,页表/页目录条目标志位:位2,表示物理内存页内容可写。
PTE_U 0x004 //page table/directory entry flags bit : User can access,页表/页目录条目标志位:位3,表示用户态的软件可以读取对应地址的物理内存页内容。
根据注释修改get_pte函数:
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
pde_t *pdep = &pgdir[PDX(la)]; //find page directory entry 获取传入的虚拟地址中所对应的页目录项的物理地址
if (!(*pdep & PTE_P)) { //check if entry is not present 判断该页目录项是否有效,如果无效则需要进行分配
struct Page *page;
if (!create || (page = alloc_page()) == NULL) {
return NULL; //check if creating is needed, then alloc page for page table 如果分配页面失败,或者不允许分配,则返回NULL
}
set_page_ref(page, 1); //set page reference 设置该物理页面的引用次数为1
uintptr_t pa = page2pa(page); //get linear address of page 获取当前物理页面所管理的物理地址
memset(KADDR(pa), 0, PGSIZE); //clear page content using memset 清空该物理页面的数据。需要注意的是使用虚拟地址
*pdep = pa | PTE_U | PTE_W | PTE_P; //set page directory entry's permission 将新分配的页面设置为当前缺失的页目录条目中,之后该页面就是其中的一个二级页面
}
return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)]; //return page table entry 返回在pgdir中对应于la的二级页表项
}
2.页目录项和页表项中每个组成部分的含义和以及对ucore而言的潜在用处。
查看《Intel® 64 and IA-32 Architectures Developer’s Manual: Vol. 3A》手册中的4.4节,得到页目录项和页表项的结构图:
页目录项具体位的功能:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1walgP7X-1651301445676)(https://tva4.sinaimg.cn/large/a081fc05gy1h1oj24l7a0j20ma0c3jtm.jpg)]
bit 0§: Present,用来确认对应的页表是否存在。
bit 1(R/W): read/write,若该位为0 ,则只读,否则可写。
bit 2(U/S): user/supervisor,用来确认用户态下是否可以访问。
bit 3(PWT): page-level write-through,表示是否使用write through缓存写策略。
bit 4(PCD): page-level cache disable,表示是否不对该页进行缓存。
bit 5(A): accessed,用来确认对应页表是否被访问过。
bit 6: 忽略。
bit 7(PS): Page size,这个位用来确定32位分页的页大小,当该位为1且CR4的PSE位为1时,页大小为4M,否则为4K。
bit 11:8: 忽略。
bit 32:12: 页表的PPN(页对齐的物理地址)。
页表具体位的功能:
页表项除了第7,8位与PDE不同,其余位作用均相同。
bit 7(PAT): 如果支持PAT分页,间接决定这项访问的4K页的内存类型;如果不支持,这位保留(必须为 0 )。
bit 8(G): global位。当CR4的PGE位为1时,若该位为1,翻译是全局的;否则,忽略该位。
其中被忽略的位可以被操作系统用于实现各种功能;和权限相关的位可以用来增强ucore的内存保护机制;access位可以用来实现内存页交换算法。
3.出现页访问异常,硬件要做的事情
当启动分页机制以后,如果一条指令或数据的虚拟地址所对应的物理页不在内存中或者访问的类型有误(比如写一个只读页或用户态程序访问内核态的数据等),就会发生页错误异常。
而产生页面异常的原因主要有:
①目标页面不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已经撤销);
②相应的物理页面不在内存中(页表项非空,但Present标志位=0,比如将页表交换到磁盘);
③访问权限不符合(比如企图写只读页面)。
当出现上面情况之一,那么就会产生页面page fault(#PF)异常。产生异常的虚拟地址存储在CR2中,并且将是page fault的错误类型保存在error code中。引发异常后将外存的数据换到内存中,进行上下文切换,退出中断,返回到中断前的状态。
Linux中对于page fault有详细的分类:
练习3:释放某虚地址所在的页并取消对应二级页表项的映射(需要编程)
当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page_remove_pte函数中的注释。为此,需要补全在 kern/mm/pmm.c中的page_remove_pte函数。page_remove_pte函数的调用关系图如下所示:
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?
如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事? 鼓励通过编程来具体完成这个问题
分析
1.实现page_remove_pte函数
注意检查ptep是否有效,如果映射更新,tlb必须手动更新。
page_remove_pte函数中用到的一些宏和函数具体功能如下:
MACROs or Functions:
struct Page *page pte2page(*ptep): get the according page from the value of a ptep.
从一个ptep的值中获得相应的页面。
free_page: free a page
释放一个页面。
page_ref_dec(page): decrease page->ref. NOTICE: ff page->ref == 0 , then this page should be free.
减少page->ref。注意:如果page->ref == 0,那么这个页面应该是空闲的。
tlb_invalidate(pde_t *pgdir, uintptr_t la): Invalidate a TLB entry, but only if the page tables being edited are the ones currently in use by the processor.
使一个TLB条目无效,但是只有正在被编辑的页表是处理器当前使用的页表的情况下生效。
DEFINEs:
PTE_P 0x001: page table/directory entry flags bit : Present,页表/页目录条目标志位:位1,表示物理内存页存在。
//page_remove_pte - free an Page sturct which is related linear address la
// - and clean(invalidate) pte which is related linear address la
//note: PT is changed, so the TLB need to be invalidate
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
if (*ptep & PTE_P) { //check if this page table entry is present,如果传入的页表条目是可用的
struct Page *page = pte2page(*ptep); //find corresponding page to pte,获取该页表项所对应的地址
if (page_ref_dec(page) == 0) //decrease page reference,如果该页的引用次数在减1后为0
free_page(page); //and free this page when page reference reachs 0,释放当前页
*ptep = 0; //clear second page table entry,清空PTE
tlb_invalidate(pgdir, la); //flush tlb,刷新TLB内的数据
}
}
2.数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?
当页目录项或页表项有效时,Page数组中的项与页目录项或页表项存在对应关系。
Page的每一项记录一个物理页的信息,而每个页目录项记录一个页表的信息,每个页表项则记录一个物理页的信息。假设系统中共有N个物理页,那么Page共有N项,第i项对应第i个物理页的信息。而页目录项和页表项的第31~12位构成的20位数分别对应一个物理页编号,因此也就和Page的对应元素一一对应。页目录项和页表项的前20位就可以表明它是哪个Page。
(1)将虚拟地址向下对齐到页大小,换算成物理地址(-KERNBASE), 再将其右移GSHIFT(12)位获得在pages数组中的索引PPN,&pages[PPN]就是所求的Page结构地址。
(2)PTE按位与0xFFF获得其指向页的物理地址,再右移PGSHIFT(12)位获得在pages数组中的索引PPN,&pages[PPN]就PTE指向的地址对应的Page结构。
如果按照书上的知识,VA如下:
页目录项PDE的地址:PDEAddr = PageDirBase +(PDIndex×sizeof(PDE))
页表项PTE的地址:PTEAddr = (PDE.PFN << SHIFT) + (PTIndex * sizeof(PTE))
物理地址:PhysAddr =(PTE.PFN << SHIFT)+ offset
3.虚拟地址与物理地址相等
相关背景知识:
系统执行中地址映射的四个阶段:
在lab1中,我们已经碰到到了简单的段映射,即对等映射关系,保证了物理地址和虚拟地址相等,也就是通过建立全局段描述符表,让每个段的基址为0,从而确定了对等映射关系。在lab2中,由于在段地址映射的基础上进一步引入了页地址映射,形成了组合式的段页式地址映射。从计算机加电,启动段式管理机制,启动段页式管理机制,在段页式管理机制下运行这整个过程中,虚地址到物理地址的映射产生了多次变化,实现了最终的段页式映射关系:
virt addr = linear addr = phy addr + 0xC0000000
第一个阶段是bootloader阶段,这个阶段其虚拟地址,线性地址以及物理地址之间的映射关系与lab1的一样,即:
lab2 stage 1: virt addr = linear addr = phy addr
第二个阶段是从kern_\entry函数开始,到执行enable_page函数(在kern/mm/pmm.c中)之前再次更新了段映射,还没有启动页映射机制。由于gcc编译出的虚拟起始地址从0xC0100000开始,ucore被bootloader放置在从物理地址0x100000处开始的物理内存中。所以当kern_entry函数完成新的段映射关系后,且ucore在没有建立好页映射机制前,CPU按照ucore中的虚拟地址执行,能够被分段机制映射到正确的物理地址上,确保ucore运行正确。这时的虚拟地址,线性地址以及物理地址之间的映射关系为:
lab2 stage 2: virt addr - 0xC0000000 = linear addr = phy addr
此时CPU在寻址时还是只采用了分段机制,一旦执行完enable_paging函数中的加载cr0指令(即让CPU使能分页机制),则接下来的访问是基于段页式的映射关系了。
第三个阶段是从enable_page函数开始,到执行gdt_init函数(在kern/mm/pmm.c中)之前,启动了页映射机制,但没有第三次更新段映射。这是候映射关系是:
lab2 stage 3: virt addr - 0xC0000000 = linear addr = phy addr + 0xC0000000 # 物理地址在0~4MB之外的三者映射关系
virt addr - 0xC0000000 = linear addr = phy addr # 物理地址在0~4MB之内的三者映射关系
请注意pmm_init函数中的一条语句:
boot_pgdir[0] = boot_pgdir[PDX(KERNBASE)];
就是用来建立物理地址在0~4MB之内的三个地址间的临时映射关系virt addr - 0xC0000000 = linear addr = phy addr。
第四个阶段是从gdt_init函数开始,第三次更新了段映射,形成了新的段页式映射机制,并且取消了临时映射关系,即执行语句“boot_pgdir[0] = 0;”把boot_pgdir[0]的第一个页目录表项(0~4MB)清零来取消临时的页映射关系。这时形成了我们期望的虚拟地址,线性地址以及物理地址之间的映射关系:
lab2 stage 4: virt addr = linear addr = phy addr + 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.
*
* */
(1)修改虚拟起始地址
由背景知识知,gcc编译出的虚拟起始地址从0xC0100000开始,ucore被bootloader放置在从物理地址0x100000处开始的物理内存中,因此我们首先得将虚拟起始地址设置为0x100000。而ucore kernel各个部分由组成kernel的各个.o或.a文件构成,且各个部分在内存中地址位置由ld工具根据kernel.ld链接脚本(linker script)来设定。ld工具使用命令-T指定链接脚本。链接脚本主要用于规定如何把输入文件(各个.o或.a文件)内的section放入输出文件(lab2/bin/kernel,即ELF格式的ucore内核)内,并控制输出文件内各部分在程序地址空间内的布局。因此虚拟起始地址应该在kernel.ld中有所定义,其内容如下所示:
/* Simple linker script for the ucore kernel.
See the GNU ld 'info' manual ("info ld") to learn the syntax. */
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(kern_entry)
SECTIONS {
/* Load the kernel at this address: "." means the current address */
. = 0xC0100000; //改为0x100000
.text : {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
PROVIDE(etext = .); /* Define the 'etext' symbol to this value */
.rodata : {
*(.rodata .rodata.* .gnu.linkonce.r.*)
}
/* Include debugging information in kernel memory */
.stab : {
PROVIDE(__STAB_BEGIN__ = .);
*(.stab);
PROVIDE(__STAB_END__ = .);
BYTE(0) /* Force the linker to allocate space
for this section */
}
.stabstr : {
PROVIDE(__STABSTR_BEGIN__ = .);
*(.stabstr);
PROVIDE(__STABSTR_END__ = .);
BYTE(0) /* Force the linker to allocate space
for this section */
}
/* Adjust the address for the data segment to the next page */
. = ALIGN(0x1000);
/* The data segment */
.data : {
*(.data)
}
PROVIDE(edata = .);
.bss : {
*(.bss)
}
PROVIDE(end = .);
/DISCARD/ : {
*(.eh_frame .note.GNU-stack)
}
}
其实从链接脚本的内容可知:内核加载地址:0xC0100000,入口(起始代码)地址: ENTRY(kern_entry),cpu机器类型:i386。我们将0xC0100000,修改为0x100000。
(2)保留临时映射
在上一步中,ucore设置了虚拟地址 0 ~ 4M 到物理地址 0 ~ 4M 的映射以确保开启页表后kern_entry能够正常执行,在将 eip 修改为对应的虚拟地址(加KERNBASE)后就取消了这个临时映射。因为我们要让物理地址等于虚拟地址,所以保留这个映射不变,即将entry.S中movl %eax, __boot_pgdir
这一行清除临时映射的代码注释掉。entry.S部分代码如下:
next:
# unmap va 0 ~ 4M, it's temporary mapping
#xorl %eax, %eax
movl %eax, __boot_pgdir //注释掉
(3)修改KERNBASE
由背景知识得,最后虚拟地址和物理地址的映射关系满足:
physical address + KERNBASE = virtual address
因此我们需要将KERNBASE改为0,即在memlayout.h中修改KERNBASE:
/* All physical memory mapped at this address */
#define KERNBASE 0xc0000000 //改为0
#define KMEMSIZE 0x38000000 // the maximum amount of physical memory
#define KERNTOP (KERNBASE + KMEMSIZE)
由于我们修改了映射关系,因此我们还要在pmm_init中对check_pgdir和check_boot_pgdir的调用给注释掉,免得检查无法通过:
(4)修改boot_map_segment函数
完成以上操作后,理论上虚拟地址和物理地址的映射关系修改完毕,但是ucore会在boot_map_segment中设置页表后异常终止或跳转到别的地方执行。通过gdb调试,发现在设置boot_pgdir[1]时会获取和boot_pgdir[0]相同的页表。也就是说,页目录项 PDE 0 和 PDE 1共同指向同一个页表__boot_pt1,在设置虚拟地址4 ~ 8M 到物理地址 4 ~ 8M 的映射时,同时将虚拟地址地址0 ~ 4M 映射到了 4 ~ 8M ,导致ucore运行异常。
entry.S中这一段关于_boot_pgdir的代码或许能解释为什么会出现上述情况,但由于本人水平有限,因此暂且略过。
# kernel builtin pgdir
# an initial page directory (Page Directory Table, PDT)
# These page directory table and page table can be reused!
.section .data.pgdir
.align PGSIZE
__boot_pgdir:
.globl __boot_pgdir
# map va 0 ~ 4M to pa 0 ~ 4M (temporary)
.long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
.space (KERNBASE >> PGSHIFT >> 10 << 2) - (. - __boot_pgdir) # pad to PDE of KERNBASE
# map va KERNBASE + (0 ~ 4M) to pa 0 ~ 4M
.long REALLOC(__boot_pt1) + (PTE_P | PTE_U | PTE_W)
.space PGSIZE - (. - __boot_pgdir) # pad to PGSIZE
想要解决这个问题,我们可以回到boot_map_segment函数中,将boot_pgdir[1]的Present位给设置为0,让get_pte函数重新分配,避免了boot_pgdir[1]获取错误以及麻烦的代码修改。
//boot_map_segment - setup&enable the paging mechanism
// parameters
// la: linear address of this memory need to map (after x86 segment map)
// size: memory size
// pa: physical address of this memory
// perm: permission of this memory
static void
boot_map_segment(pde_t *pgdir, uintptr_t la, size_t size, uintptr_t pa, uint32_t perm) {
boot_pgdir[1] &= ~PTE_P; //添加这一行
assert(PGOFF(la) == PGOFF(pa));
size_t n = ROUNDUP(size + PGOFF(la), PGSIZE) / PGSIZE;
la = ROUNDDOWN(la, PGSIZE);
pa = ROUNDDOWN(pa, PGSIZE);
for (; n > 0; n --, la += PGSIZE, pa += PGSIZE) {
pte_t *ptep = get_pte(pgdir, la, 1);
assert(ptep != NULL);
*ptep = pa | PTE_P | perm;
}
}
修改前:
修改后:
通过比较eip的值,不难看出修改成功。
实验心得
实验2虽然与上一个实验有所关联,但重点是在物理内存管理上,上一个实验深入探讨了从加电到ucore加载到内存后,ucore要完成基本的内存管理和外设中断管理。而实验2分为三部分,第一步是探测物理内存分布和大小,这一步是在bootloader进入保护模式前实现的。第二步是实现分页机制,最后是实现物理内存页分配算法实现,其中如何实现分页机制是本实验关键。ucore采用的是通过二级页表建立虚拟页和物理页帧的地址映射关系。书上关于分页机制的两大问题,一是高性能开销,而是大内存开销,针对于大内存开销本实验通过段页混合加上二级页表的实现使得书上的理论知识得到了实践。