实验汲取知识
- 基于段页式内存地址的转换机制
- 页表的建立和使用方法
- 物理内存的管理方法
首先了解如何发现系统中的物理内存;然后了解如何建立对物理内存的初步管理,即了解连续物理内存管理;最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个比较全面的了解。
本次采用的首次适应算法firstfit分配
如何设计物理内存页的分配算法,最后比较详细地分析了在80386的段页式硬件机制下,ucore操作系统把段式内存管理的功能弱化,并实现以分页为主的页式内存管理的过程。
实验工作内容
在代码分析上,建议根据执行流程来直接看源代码,并可采用GDB源码调试的手段来动态地分析ucore的执行过程。内存管理相关的总体控制函数是pmm_init函数,它完成的主要工作包括:
初始化物理内存页管理器框架pmm_manager;
初始化连续物理页,加入到空闲链表
//<<它初始化了一段连续的物理页,并将它们添加到空闲页链表中。>>
static void default_init_memmap(struct Page *base, size_t n) {//初始化分配内存
assert(n > 0); // 确保要初始化的页数大于0
struct Page *p = base; // 定义一个指向物理页的指针,并初始化为输入参数 base,即起始页
// 循环遍历从 base 到 base + n - 1 的物理页
for (; p != base + n; p ++) {
assert(PageReserved(p)); // 确保当前页 p 是保留页(已被内核占用)
// 将当前页的标志位 flags 和属性 property 清零,表示这些页现在是空闲的,没有被任何进程占用
p->flags = p->property = 0;
// 将当前页的引用计数设置为 0,表示当前页没有被引用
set_page_ref(p, 0);
}
// 将起始页 base 的属性 property 设置为 n,表示这个连续的内存块包含了 n 个页
base->property = n;
// 将起始页 base 标记为保留页,表示这个内存块已经被占用
SetPageProperty(base);
// 增加空闲页计数器 nr_free,表示有 n 个页现在是空闲的
nr_free += n;
// 将起始页 base 添加到空闲页链表 free_list 中,表示这个内存块是空闲的,可以用于后续的分配操作
list_add(&free_list, &(base->page_link));
}
分配物理页,并把剩余合并
//<<这段代码的主要功能是根据请求的页数从空闲页链表中分配一页或多页的物理内存。 >>
static struct Page *default_alloc_pages(size_t n) {
assert(n > 0); // 确保请求分配的页数大于0
if (n > nr_free) { // 如果请求的页数大于空闲页数,则无法满足分配要求,返回 NULL
return NULL;
}
struct Page *page = NULL; // 初始化分配的页为 NULL
list_entry_t *le = &free_list; // 初始化链表指针 le 指向空闲页链表的头部
// 遍历空闲页链表,查找第一个空闲块大小大于等于请求页数的页
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; // 计算剩余空闲页的地址,pos来算的
p->property = page->property - n; // 更新剩余空闲页的大小
list_add(&free_list, &(p->page_link)); // 将剩余空闲页添加到空闲页链表中
}
nr_free -= n; // 更新空闲页数
ClearPageProperty(page); // 清除空闲页的保留标志
}
return page; // 返回分配的页的指针
}
释放物理页
//这段代码主要实现了释放内存页的功能。首先,它确保了要释放的内存页数量大于 0,并且要释放的内存页已经被标记为保留页。
//然后,它将要释放的内存页添加到空闲页链表中,并尝试将连续的空闲页合并起来。
//最后,它更新了空闲页的数量并结束了函数的执行。
static void default_free_pages(struct Page *base, size_t n) {
assert(n > 0); // 确保要释放的内存页数量大于 0
assert(PageReserved(base)); // 确保要释放的内存页已经被标记为保留页,即已被内核占用,保留页就是内核占用
// 初始化指向空闲页链表的指针 le,声明循环中的指针 p
list_entry_t *le = &free_list;
struct Page * p;
// 循环遍历空闲页链表,找到一个比 base 大的空闲页地址,p 指向的是第一个大于 base 的空闲页
while((le = list_next(le)) != &free_list) {
p = le2page(le, page_link);
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); // 设置页的引用计数为 0
ClearPageProperty(base); // 清除页的保留属性
SetPageProperty(base); // 设置页的属性为保留
base->property = n; // 设置页的属性为 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; // 结束函数
}
通过线性地址获取二级页表,返回虚拟地址
//get_pte - get pte and return the kernel virtual address of this pte for la
// - if the PT contains this pte didn't exist, alloc a page for PT
// 通过线性地址(linear address)得到一个页表项(二级页表项)(Page Table Entry),并返回该页表项结构的内核虚拟地址
// 如果应该包含该线性地址对应页表项的那个页表不存在,则分配一个物理页用于存放这个新创建的页表(Page Table)
// parameter: 参数
// pgdir: the kernel virtual base address of PDT 页目录表(一级页表)的起始内核虚拟地址
// la: the linear address need to map 需要被映射关联的线性虚拟地址
// create: a logical value to decide if alloc a page for PT 一个布尔变量决定对应页表项所属的页表不存在时,是否将页表创建
// return vaule: the kernel virtual address of this pte 返回值: la参数对应的二级页表项结构的内核虚拟地址
pte_t * get_pte(pde_t *pgdir, uintptr_t la, bool create) {
/* LAB2 EXERCISE 2: YOUR CODE
*
* If you need to visit a physical address, please use KADDR()
* please read pmm.h for useful macros
*
* Maybe you want help comment, BELOW comments can help you finish the code
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* PDX(la) = the index of page directory entry of VIRTUAL ADDRESS 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 * 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.
* DEFINEs:
* PTE_P 0x001 // page table/directory entry flags bit : Present
* PTE_W 0x002 // page table/directory entry flags bit : Writeable
* PTE_U 0x004 // page table/directory entry flags bit : User can access
*/
#if 0
pde_t *pdep = NULL; // (1) find page directory entry
if (0) { // (2) check if entry is not present
// (3) check if creating is needed, then alloc page for page table
// CAUTION: this page is used for page table, not for common data page
// (4) set page reference
uintptr_t pa = 0; // (5) get linear address of page
// (6) clear page content using memset
// (7) set page directory entry's permission
}
return NULL; // (8) return page table entry
#endif
// PDX(la) 根据la的高10位获得对应的页目录项(一级页表中的某一项)索引(页目录项)
// &pgdir[PDX(la)] 根据一级页表项索引从一级页表中找到对应的页目录项指针
pde_t *pdep = &pgdir[PDX(la)];
// 判断当前页目录项的Present存在位是否为1(对应的二级页表是否存在)
if (!(*pdep & PTE_P)) {
// 对应的二级页表不存在
// *page指向的是这个新创建的二级页表基地址
struct Page *page;
if (!create || (page = alloc_page()) == NULL) {
// 如果create参数为false或是alloc_page分配物理内存失败
return NULL;
}
// 二级页表所对应的物理页 引用数为1
set_page_ref(page, 1);
// 获得page变量的物理地址
uintptr_t pa = page2pa(page);
// 将整个page所在的物理页格式胡,全部填满0
memset(KADDR(pa), 0, PGSIZE);
// la对应的一级页目录项进行赋值,使其指向新创建的二级页表(页表中的数据被MMU直接处理,为了映射效率存放的都是物理地址)
// 或PTE_U/PTE_W/PET_P 标识当前页目录项是用户级别的、可写的、已存在的
*pdep = pa | PTE_U | PTE_W | PTE_P;
}
// 要想通过C语言中的数组来访问对应数据,需要的是数组基址(虚拟地址),而*pdep中页目录表项中存放了对应二级页表的一个物理地址
// PDE_ADDR将*pdep的低12位抹零对齐(指向二级页表的起始基地址),再通过KADDR转为内核虚拟地址,进行数组访问
// PTX(la)获得la线性地址的中间10位部分,即二级页表中对应页表项的索引下标。这样便能得到la对应的二级页表项了
return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
}
释放映射
//总的来说,这段代码的作用是从页表中移除指定线性地址 la 相关的页表项,并释放与之相关的物理页,最后确保 TLB 中的条目失效,以保证内存访问的一致性和正确性。
//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) {
/* LAB2 EXERCISE 3: YOUR CODE
*
* Please check if ptep is valid, and tlb must be manually updated if mapping is updated
*
* Maybe you want help comment, BELOW comments can help you finish the code
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* struct Page *page pte2page(*ptep): get the according page from the value of a ptep
* free_page : free a page
* page_ref_dec(page) : decrease page->ref. NOTICE: ff page->ref == 0 , then this page should be free.
* 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.
* DEFINEs:
* PTE_P 0x001 // page table/directory entry flags bit : Present
*/
#if 0
if (0) { //(1) check if this page table entry is present
struct Page *page = NULL; //(2) find corresponding page to pte
//(3) decrease page reference
//(4) and free this page when page reference reachs 0
//(5) clear second page table entry
//(6) flush tlb
}
#endif
if (*ptep & PTE_P) {
struct Page *page = pte2page(*ptep);
if (page_ref_dec(page) == 0) {
free_page(page);
}
*ptep = 0;
tlb_invalidate(pgdir, la);
}
}
总结
pmm_manager
接口:在实现 pmm_manager
框架时,理解了每个函数的作用和接口约定。这些函数包括初始化函数 init
、内存映射初始化函数 init_memmap
、分配页面函数 alloc_pages
、释放页面函数 free_pages
等,以及处理各种异常情况和错误场景,比如内存不足、页表项不存在、TLB 刷新等。运用了 pmm.h
中定义的宏和函数,如 PDX
、PTX
、KADDR
、page2pa
等,这些宏和函数可以帮助我更方便地操作物理内存管理相关的数据结构和地址转换
PDX
和PTX
宏用于从线性地址中提取页目录索引和页表索引。给定一个线性地址,这些宏通过位运算操作,提取出对应的页目录项和页表项在页目录和页表数组中的索引。
KADDR
宏用于将物理地址转换为内核虚拟地址。在操作系统内核中,为了方便对物理地址进行访问,需要将物理地址映射到虚拟地址空间中。KADDR
宏就是用来完成这一转换的。
page2pa
函数用于从struct Page
结构体中提取出物理地址。在操作系统中,用struct Page
结构体来表示一个物理页,其中包含了页的属性和地址等信息。page2pa
函数就是从这个结构体中提取出物理地址的函数。