在进行内存管理之前,首先需要知道哪些内存可用,获取内存大小的方法有BIOS中断调用和直接探测两种。BIOS 中断获取内存布局有三种方式,都是基于INT 15h中断,分别为88h ,e801h,e820h,这里不详叙。ucore的物理内存探测是在bootasm.S中实现的,即probe_memory。该代码执行完毕后,探测得到的信息会存储在0x8000,以供后续使用。
以页为单位管理内存
获得可用物理内存范围后,系统需要建立相应的数据结构来管理以物理页(按4KB对齐,且大小为4KB的物理内存单元)为最小单位的整个物理内存,以配合后续涉及的分页管理机制。
每个物理页可以用一个 Page数据结构来表示,如下所示:
ref表示这页被页表的引用记数
flag有很多作用,其不同bit有不同含义,比如bit 0表示此页是否被保留(reserved),bit 1表示此页是否是free的
property在不同的页分配算法中有不同的含义,本实验中用来记录某连续内存空闲块的大小,用到此成员变量的这个Page比较特殊,是这个连续内存空闲块地址最小的一页(即头一页, Head Page)
page_link是便于把多个连续内存空闲块链接在一起的双向链表指针,同样的,用到此成员变量的这个Page是这个连续内存空闲块地址最小的一页。
为了有效地管理小的连续内存空闲块。所有的连续内存空闲块可用一个双向链表管理起来,便于分配和释放,为此定义了一个free_area_t数据结构
成员为一个list_entry结构的双向链表指针和记录当前空闲页的个数的无符号整型变量nr_free。其中的链表指针指向了空闲的物理页。
接下来需要解决两个问题:
• 管理页级物理内存空间所需的Page结构的内存空间从哪里开始,占多大空间?
• 空闲内存空间的起始地址在哪里?
首先由探测得到的最大物理内存地址maxpa(定义在page_init函数中的局部变量),可以得知需要管理的物理页个数为
npage=maxpa/PGSIZE
这样Page结构的内存空间所需的内存大小为
sizeof(struct Page) * npage)
Page结构的其实地址为pages,由于ucore占据了地址空间,其结束地址为end,将pages放在新的page上,按end按页大小取整为:
pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);
那么空闲内存的起始地址为:
uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);
实验二在内存分配和释放方面最主要的作用是建立了一个物理内存页管理器框架,这实际上是一个函数指针列表,定义如下
重点是实现init_memmap/ alloc_pages/ free_pages这三个函数
物理内存页分配算法实现
first_fit分配算法需要维护一个查找有序(地址按从小到大排列)空闲块(以页为最小单位的连续地址空间)的数据结构,而双向链表是一个很好的选择
kern/mm/pmm.h中定义了一个通用的分配算法的函数列表,用pmm_manager 表示。其中init函数就是用来初始化free_area变量的, first_fit分配算法可直接重用default_init函数的实现。init_memmap函数需要根据现有的内存情况构建空闲块列表的初始状态
通过分析代码,可以知道:
default_init_memmap函数根据每个物理页帧的情况来建立空闲页链表,且空闲页块应该是根据地址高低形成一个有序链表。根据上述变量的定义,default_init_memmap可大致实现如下:
如果要分配一个页,那要考虑哪些呢?这里就需要考虑实现default_alloc_pages函数,注意参数n表示要分配n个页。另外,需要注意实现时尽量多考虑一些边界情况,这样确保软件的鲁棒性,default_alloc_pages可大致实现如下:
default_free_pages函数的实现其实是default_alloc_pages的逆过程,不过需要考虑空闲块的合并问题。这里就不再细讲了。注意,上诉代码只是参考设计,不是完整的正确设计。更详细的说明位于lab2/kernel/mm/default_pmm.c的注释中
实现分页机制
x86 体系结构将内存地址分成三种:逻辑地址(也称虚地址)、线性地址和物理地址。逻辑地址即是程序指令中使用的地址,物理地址是实际访问内存的地址。逻 辑地址通过段式管理的地址映射可以得到线性地址,线性地址通过页式管理的地址映射得到物理地址
页式管理将线性地址分成三部分(图中的 Linear Address 的 Directory 部分、 Table 部分和 Offset 部分)。ucore 的页式管理通过一个二级的页表实现。一级页表的起始物理地址存放在 cr3 寄存器中,这个地址必须是一个页对齐的地址,也就是低 12 位必须为 0
在lab2中,为了建立正确的地址映射关系,ld在链接阶段生成了ucore OS执行代码的虚拟地址,而bootloader与ucore OS协同工作,通过在运行时对地址映射的一系列“腾挪转移”,从计算机加电,启动段式管理机制,启动段页式管理机制,在段页式管理机制下运行这整个过程中,虚地址到物理地址的映射产生了多次变化,实现了最终的段页式映射关系:
下面,我们来看看这是如何一步一步实现的。观察一下链接脚本,即tools/kernel.ld文件
这意味着lab2中通过ld工具形成的ucore的起始虚拟地址从0xC0100000开始,注意:这个地址也是虚拟地址。入口函数为kern_entry函数(在kern/init/entry.S中)
第一个阶段是bootloader阶段,即从bootloader的start函数(在boot/bootasm.S中)到执行ucore kernel的kern_\entry函数之前,其虚拟地址,线性地址以及物理地址之间的映射关系与lab1的一样,即:
第二个阶段从从kern_\entry函数开始,到执行enable_page函数(在kern/mm/pmm.c中)之前再次更新了段映射,还没有启动页映射机制。由于gcc编译出的虚拟起始地址从0xC0100000开始,ucore被bootloader放置在从物理地址0x100000处开始的物理内存中
所以当kern_entry函数完成新的段映射关系后,且ucore在没有建立好页映射机制前,CPU按照ucore中的虚拟地址执行,能够被分段机制映射到正确的物理地址上,确保ucore运行正确。这时的虚拟地址,线性地址以及物理地址之间的映射关系为
第三个阶段从enable_page函数开始,到执行gdt_init函数(在kern/mm/pmm.c中)之前,启动了页映射机制,但没有第三次更新段映射。这时的虚拟地址,线性地址以及物理地址之间的映射关系比较微妙
请注意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)清零来取消临时的页映射关系。这时形成了我们期望的虚拟地址,线性地址以及物理地址之间的映射关系:
建立虚拟页和物理页帧的地址映射关系
建立二级页表
整个页目录表和页表所占空间大小取决与二级页表要管理和映射的物理页数。假定当前物理内存0~16MB,每物理页(也称Page Frame)大小为4KB,则有4096个物理页,也就意味这有4个页目录项和4096个页表项需要设置
一个页目录项(Page Directory Entry,PDE)和一个页表项(Page Table Entry,PTE)占4B。即使是4个页目录项也需要一个完整的页目录表(占4KB)。而4096个页表项需要16KB(即4096*4B)的空间,也就是4个物理页,16KB的空间。所以对16MB物理页建立一一映射的16MB虚拟页,需要5个物理页,即20KB的空间来形成二级页表。
为把0~KERNSIZE(明确ucore设定实际物理内存不能超过KERNSIZE值,即0x38000000字节,896MB,3670016个物理页)的物理地址一一映射到页目录项和页表项的内容,其大致流程如下:
1.先通过alloc_page获得一个空闲物理页,用于页目录表;
2.调用boot_map_segment函数建立一一映射关系,具体处理过程以页为单位进行设置,即
设一个32bit线性地址la有一个对应的32bit物理地址pa,如果在以la的高10位为索引值的页目录项中的存在位(PTE_P)为0,表示缺少对应的页表空间,则可通过alloc_page获得一个空闲物理页给页表,页表起始物理地址是按4096字节对齐的,这样填写页目录项的内容为
页目录项内容 = (页表起始物理地址 &0x0FFF) | PTE_U | PTE_W | PTE_P
进一步对于页表中以线性地址la的中10位为索引值对应页表项的内容为
页表项内容 = (pa & ~0x0FFF) | PTE_P | PTE_W
PTE_U:位3,表示用户态的软件可以读取对应地址的物理内存页内容
PTE_W:位2,表示物理内存页内容可写
PTE_P:位1,表示物理内存页存在
ucore 的内存管理经常需要查找页表:给定一个虚拟地址,找出这个虚拟地址在二级页表中对应的项。通过更改此项的值可以方便地将虚拟地址映射到另外的页上。可完成此功能的这个函数是get_pte函数。它的原型为
pte_t *get_pte (pde_t *pgdir, uintptr_t la, bool create)
下面的调用关系图可以比较好地看出get_pte在实现上诉流程中的位置:
pde_t全称为 page directory entry,也就是一级页表的表项(注意:pgdir实际不是表 项,而是一级页表本身。实际上应该新定义一个类型pgd_t来表示一级页表本身)。pte t全 称为 page table entry,表示二级页表的表项。uintptr t表示为线性地址,由于段式管理只做直接映射,所以它也是逻辑地址。
引入进程的概念之后每个进程都会有自己的页 表
有可能根本就没有对应的二级页表的情况,所以二级页表不必要一开始就分配,而是等到需要的时候再添加对应的二级页表。
当建立从一级页表到二级页表的映射时,需要注意设置控制位。这里应该设置同时设置 上PTE_U、PTE_W和PTE_P(定义可在mm/mmu.h)。如果原来就有二级页表,或者新建立了页表,则只需返回对应项的地址即可。
虚拟地址只有映射上了物理页才可以正常的读写。在完成映射物理页的过程中,除了要象上面那样在页表的对应表项上填上相应的物理地址外,还要设置正确的控制位。
只有当一级二级页表的项都设置了用户写权限后,用户才能对对应的物理地址进行读写。 所以我们可以在一级页表先给用户写权限,再在二级页表上面根据需要限制用户的权限,对物理页进行保护。由于一个物理页可能被映射到不同的虚拟地址上去(譬如一块内存在不同进程 间共享),当这个页需要在一个地址上解除映射时,操作系统不能直接把这个页回收,而是要先看看它还有没有映射到别的虚拟地址上。这是通过查找管理该物理页的Page数据结构的成员变量ref(用来表示虚拟页到物理页的映射关系的个数)来实现的
page_insert函数将物理页映射在了页表上。可参看page_insert函数的实现来了解ucore内核是如何维护这个变量的。当不需要再访问这块虚拟地址时,可以把这块物理页回收并在将来用在其他地方。取消映射由page_remove来做,这其实是page insert的逆操作。
建立好一一映射的二级页表结构后,接下来就要使能分页机制了,这主要是通过enable_paging函数实现的,这个函数主要做了两件事:
1 通过lcr3指令把页目录表的起始地址存入CR3寄存器中;
2 通过lcr0指令把cr0中的CR0_PG标志位设置上。