ucore-lab2
完成ucore-lab1后总觉得做实验的过程怪怪的,每个练习的关联性并不强,有许多内容需要打开搜索引擎获得答案,难度缺少梯度做起来很难受。直到我某天闲着无聊二刷了lab0和lab1,才发现我所疑惑的一切问题都藏在了参考资料中,可能遇到的坑以及相对应的解决方法,甚至是不同练习之间的联系以及整个设计的框架都在参考资料中有详细的说明。
故我认为ucore的最佳做法是先阅读完实验原理再开始着手做相关练习,不要被内容的顺序所迷惑,这样才能真正有所得。
本周刚刚学完linux的内存管理,并且肝完了小班课的slab原理,顺带了解了一下分段和分页机制,希望在本次的lab2-内存管理模块中能亲手实现内存管理的机制,对内存管理有更深刻的理解。
实验目的
- 理解基于段页式内存地址的转换机制
- 理解页表的建立和使用方法
- 理解物理内存的管理方法
实验内容
- 了解如何发现系统中的物理内存
- 了解如何建立对物理内存的初步管理
- 了解页表相关的操作
练习0 merge
将实验1的代码填入本实验中代码中有“LAB1”的注释相应部分。提示:可采用diff和patch工具进行半自动的合并(merge)
在ucore/labcodes目录下执行:
1 | diff -urN lab2 lab1 > lab1.patch |
物理内存管理
首先对实验的执行流程做个介绍,并进一步介绍如何探测物理内存的大小与布局,如何以页为单位来管理计算机系统中的物理内存,如何设计物理内存页的分配算法,最后比较详细地分析了在80386的段页式硬件机制下,ucore操作系统把段式内存管理的功能弱化,并实现以分页为主的页式内存管理的过程。
实验执行流程概述
lab2中内核启动过程:
- 在bootloader中,完成了对物理内存资源的探测工作;
- ucore kernel在后续执行中能够基于bootloader探测出的物理内存情况进行物理内存管理初始化工作;
- bootloader调用位于lab2/kern/init/entry.S中的kern_entry函数设置堆栈,临时建立了一个段映射关系,为之后建立分页机制的过程做准备
- 调用kern_init函数完成一些输出并对lab1实验结果的检查
- 调用pmm_init函数完成物理内存的管理
- 调用pic_init函数和idt_init函数,执行中断和异常相关的初始化工作
lab2中物理内存管理的过程:
- 初始化物理内存页管理器框架pmm_manager;
- 建立空闲的page链表,这样就可以分配以页(4KB)为单位的空闲内存了;
- 检查物理内存页分配算法;
- 为确保切换到分页机制后,代码能够正常执行,先建立一个临时二级页表;
- 建立一一映射关系的二级页表;
- 使能分页机制;
- 从新设置全局段描述符表;
- 取消临时二级页表;
- 检查页表建立是否正确;
- 通过自映射机制完成页表的打印输出(这部分是扩展知识)
另外,主要注意的相关代码内容包括:
- boot/bootasm.S中探测内存部分(从probe_memory到finish_probe的代码);
- 管理每个物理页的Page数据结构(在mm/memlayout.h中),这个数据结构也是实现连续物理内存分配算法的关键数据结构,可通过此数据结构来完成空闲块的链接和信息存储,而基于这个数据结构的管理物理页数组起始地址就是全局变量pages,具体初始化此数组的函数位于page_init函数中;
- 用于实现连续物理内存分配算法的物理内存页管理器框架pmm_manager,这个数据结构定义了实现内存分配算法的关键函数指针,而同学需要完成这些函数的具体实现;
- 设定二级页表和建立页表项以完成虚实地址映射关系,这与硬件相关,且用到不少内联函数,源代码相对难懂一些。具体完成页表和页表项建立的重要函数是boot_map_segment函数,而get_pte函数是完成虚实映射关键的关键。
我们可以看到从内存探测到内存分配、内存管理(页表和虚实映射)都给出了具体实现的函数,接下来的部分将会给出其具体实现的原理,帮助我们更好的理解内存管理的机制。
探测系统物理内存布局
获取内存大小的方法由 BIOS 中断调用和直接探测两种:
BIOS 中断调用方法是一般只能在实模式下完成
基于INT 15h中断(88h e801h e820h)
NT 15h是一个BIOS中断,用于提供许多与系统硬件和性能相关的服务。在x86架构的计算机中,BIOS中断通常用于执行低级系统任务,例如与键盘、显示器、磁盘驱动器和其他硬件设备的交互。
INT 15h中断提供了许多不同的功能,其中包括:
- 系统内存检测和测试
- 键盘输入和输出
- 磁盘驱动器控制
- 显示器设置和字符生成
- 系统时间和日期查询
- 打印机操作
- 系统电源管理
这些功能可以通过不同的INT 15h子功能代码来访问。在编写x86汇编语言程序时,可以使用INT 15h中断调用这些子功能来执行各种系统任务。
在本实验中,我们通过e820h中断获取内存信息
INT E820h:是一个32位的扩展BIOS中断,用于获取更为详细的系统内存信息。它提供了有关系统内存布局的详细信息,包括物理内存和设备内存的地址范围、内存类型(例如可用、已保留、ACPI等)等。这些信息对于操作系统和引导加载程序非常重要,可以帮助它们了解系统中哪些内存区域可以使用,哪些必须保留。
而直接探测方法必须在保护模式下完成
在 bootloader 进入保护模式之前调用这个 BIOS 中断,并且把 e820 映射结构保存在物理地址0x8000处。具体实现详见boot/bootasm.S。有关探测系统物理内存方法和具体实现的信息参见lab2试验指导的附录A“探测物理内存分布和大小的方法”和附录B“实现物理内存探测”。
探测物理内存分布和大小的方法
BIOS中断调用必须在实模式下进行,所以在bootloader进入保护模式前完成这部分工作相对比较合适。这些部分由boot/bootasm.S中从probe_memory处到finish_probe处的代码部分完成完成。通过BIOS中断获取内存可调用参数为e820h的INT 15h BIOS中断。BIOS通过系统内存映射地址描述符(Address Range Descriptor)格式来表示系统物理内存布局,其具体表示如下:
1 | Offset Size Description |
INT15h BIOS中断的详细调用参数:
1 | eax:e820h:INT 15的中断调用参数; |
此中断的返回值为:
1 | cflags的CF位:若INT 15中断执行成功,则不置位,否则置位; |
这样,我们通过调用INT 15h BIOS中断,递增di的值(20的倍数),让BIOS帮我们查找出一个一个的内存布局entry,并放入到一个保存地址范围描述符结构的缓冲区中,供后续的ucore进一步进行物理内存管理。这个缓冲区结构定义在memlayout.h中:
1 | struct e820map { |
实现物理内存探测
物理内存探测是在bootasm.S中实现的,相关代码很短,如下所示:
1 | probe_memory: |
上述代码正常执行完毕后,在0x8000地址处保存了从BIOS中获得的内存分布信息,此信息按照struct e820map的设置来进行填充。这部分信息将在bootloader启动ucore后,由ucore的page_init函数来根据struct e820map的memmap(定义了起始地址为0x8000)来完成对整个机器中的物理内存的总体管理。
以页为单位管理物理内存
建立对整个计算机的每一个物理页的属性用结构Page来表示,它包含了映射此物理页的虚拟页个数,描述物理页属性的flags和双向链接各个Page结构的page_link双向链表。
1 | struct Page { |
ref表示这页被页表的引用记数
flags表示此物理页的状态标记
进一步查看kern/mm/memlayout.h中的定义,可以看到:
1
2
3/* 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- bit 1表示此页是否是free的,如果设置为1,表示这页是free的,可以被分配
- 如果设置为0,表示这页已经被分配出去了,不能被再二次分配
property用来记录某连续内存空闲块的大小(即地址连续的空闲页的个数)(存在于第一个物理表)
page_link是便于把多个连续内存空闲块链接在一起的双向链表指针(存在于第一个物理表)
在初始情况下,也许这个物理内存的空闲物理页都是连续的,这样就形成了一个大的连续内存空闲块。但随着物理页的分配与释放,这个大的连续内存空闲块会分裂为一系列地址不连续的多个小连续内存空闲块,且每个连续内存空闲块内部的物理页是连续的。那么为了有效地管理这些小连续内存空闲块。定义了一个free_area_t数据结构:
1 | /* free_area_t - maintains a doubly linked list to record free (unused) pages */ |
- list_entry结构的双向链表指针
- nr_free记录当前空闲页的个数的无符号整型变量
有了这两个数据结构,ucore就可以管理起来整个以页为单位的物理内存空间。接下来需要解决两个问题:
管理页级物理内存空间所需的Page结构的内存空间从哪里开始,占多大空间?
空闲内存空间的起始地址在哪里?
根据bootloader给出的内存布局信息找出最大的物理内存地址maxpa(定义在page_init函数中的局部变量),由于x86的起始物理内存地址为0,所以可以得知需要管理的物理页个数为
1 | npage = maxpa / PGSIZE |
这样,我们就可以预估出管理页级物理内存空间所需的Page结构的内存空间所需的内存大小为:
1 | sizeof(struct Page) * npage) |
为了简化起见,从地址0到地址pages+ sizeof(struct Page) * npage)结束的物理内存空间设定为已占用物理内存空间(起始0~640KB的空间是空闲的),地址pages+ sizeof(struct Page) * npage)以上的空间为空闲物理内存空间,这时的空闲空间起始地址为
1 | uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage); |
其实实验二在内存分配和释放方面最主要的作用是建立了一个物理内存页管理器框架,这实际上是一个函数指针列表,定义如下:
1 | struct pmm_manager { |
物理内存页分配算法实现
使用双向链表实现
lab2的第一部分是完成first_fit的分配算法。原理FirstFit内存分配算法上很简单,但要在ucore中实现,需要充分了解和利用ucore已有的数据结构和相关操作、关键的一些全局变量等。
1 | kern_init --> pmm_init-->page_init-->init_memmap--> pmm_manager-->init_memmap |
kern_init()
:是操作系统内核的入口函数,用来初始化整个内核和启动操作系统的各个子系统。pmm_init()
:是物理内存管理(Physical Memory Manager,PMM)子系统的初始化函数,用来初始化PMM的数据结构和其他必要的设置。page_init()
:用来初始化操作系统中的页面(page)数据结构,其中包括可用页面的列表和映射到物理内存的页面。init_memmap()
:用来初始化物理内存映射表,它把操作系统中所有可用的物理内存区域映射到相应的页面。pmm_manager()
:是物理内存管理器的方法,用来初始化物理内存管理器所维护的内存映射表。init_memmap()
是一个函数,用于初始化初始化内存映射。
default_init_memmap需要根据page_init函数中传递过来的参数(某个连续地址的空闲块的起始页,页个数)来建立一个连续内存空闲块的双向链表。这里有一个假定page_init函数是按地址从小到大的顺序传来的连续内存空闲块的。链表头是free_area.free_list,链表项是Page数据结构的base->page_link。这样我们就依靠Page数据结构中的成员变量page_link形成了连续内存空闲块列表。
设计实现
default_init_memmap可大致实现如下:
1 | default_init_memmap(struct Page *base, size_t n) { |
以首Page为起始地址,遍历该链表修改每个页的标志位,最后将此内存的信息写入第一个页中。
如果要分配一个页,那要考虑哪些呢?
考虑实现default_alloc_pages函数
如果要分配一个页,那要考虑哪些呢?
firstfit需要从空闲链表头开始查找最小的地址,通过list_next找到下一个空闲块元素,通过le2page宏可以更加链表元素获得对应的Page指针p。通过p->property可以了解此空闲块的大小。如果>=n,这就找到了!如果<n,则list_next,继续查找。直到list_next== &free_list,这表示找完了一遍了。找到后,就要从新组织空闲块,然后把找到的page返回。所以default_alloc_pages可大致实现如下:
1 | static struct Page * |
default_free_pages函数的实现其实是default_alloc_pages的逆过程,不过需要考虑空闲块的合并问题。
- 需要注意实现时尽量多考虑一些边界情况,这样确保软件的鲁棒性
- 加一些 assert函数,在有错误出现时,能够迅速发现
- 通过le2page宏可以更加链表元素获得对应的Page指针p
实现分页机制
由于ucore OS是基于80386 CPU实现的,所以CPU在进入保护模式后,就直接使能了段机制,并使得ucore OS需要在段机制的基础上建立页机制。
段页式管理基本概念
在 ucore 中段式管理只起到了一个过渡作用,它将逻辑地址不加转换直接映射成线性地址
页式管理将线性地址分成三部分(图中的 Linear Address 的 Directory 部分、 Table 部分和 Offset 部分)。
ucore 的页式管理通过一个二级的页表实现。一级页表的起始物理地址存放在 cr3 寄存器中,这个地址必须是一个页对齐的地址,也就是低 12 位必须为 0。目前,ucore 用boot_cr3(mm/pmm.c)记录这个值。
低12位必须为0是因为在x86架构中,一个页面的大小是2的幂次方,即4KB(2的12次方),如果一个页面的起始地址不是页面大小的整数倍,那么这个页面就会跨越两个页面,这会导致内存访问出错。
建立段页式管理中需要考虑的关键问题
为了实现分页机制,需要建立好虚拟内存和物理内存的页映射关系,即正确建立二级页表。此过程涉及硬件细节,不同的地址映射关系组合,相对比较复杂。总体而言,我们需要思考如下问题:
- 如何在建立页表的过程中维护全局段描述符表(GDT)和页表的关系,确保ucore能够在各个时间段上都能正常寻址?
- 对于哪些物理内存空间需要建立页映射关系?
- 具体的页映射关系是什么?
- 页目录表的起始地址设置在哪里?
- 页表的起始地址设置在哪里,需要多大空间?
- 如何设置页目录表项的内容?
- 如何设置页表项的内容?
系统执行中地址映射的四个阶段
为了建立正确的地址映射关系,ld在链接阶段生成了ucore OS执行代码的虚拟地址,而bootloader与ucore OS协同工作,通过在运行时对地址映射的一系列“腾挪转移”,从计算机加电,启动段式管理机制,启动段页式管理机制,在段页式管理机制下运行这整个过程中,虚地址到物理地址的映射产生了多次变化,实现了最终的段页式映射关系:
1 | virt addr = linear addr = phy addr + 0xC0000000 |
在lab1中:
1 | ENTRY(kern_init) |
这意味着在lab1中通过ld工具形成的ucore的起始虚拟地址从0x100000开始,注意:这个地址是虚拟地址。但由于lab1中建立的段地址映射关系为对等关系,所以ucore的物理地址也是0x100000,而ucore的入口函数kern_init的起始地址。所以在lab1中虚拟地址,线性地址以及物理地址之间的映射关系如下:
1 | lab1: virt addr = linear addr = phy addr |
在lab2中:
1 | ENTRY(kern_entry) |
这意味着lab2中通过ld工具形成的ucore的起始虚拟地址从0xC0100000开始,注意:这个地址也是虚拟地址。入口函数为kern_entry函数(在kern/init/entry.S中)。这与lab1有很大差别。但其实在lab1和lab2中,bootloader把ucore都放在了起始物理地址为0x100000的物理内存空间。
第一阶段–bootloader阶段
从bootloader的start函数(在boot/bootasm.S中)到执行ucore kernel的kern_entry函数之前,其虚拟地址,线性地址以及物理地址之间的映射关系与lab1的一样,即:
1 | 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运行正确。这时的虚拟地址,线性地址以及物理地址之间的映射关系为:
1 | lab2 stage 2: virt addr - 0xC0000000 = linear addr = phy addr |
注意此时CPU在寻址时还是只采用了分段机制。最后后并使能分页映射机制(请查看lab2/kern/mm/pmm.c中的enable_paging函数),一旦执行完enable_paging函数中的加载cr0指令(即让CPU使能分页机制),则接下来的访问是基于段页式的映射关系了。(进入了保护模式?)
第三阶段
从enable_page函数开始,到执行gdt_init函数(在kern/mm/pmm.c中)之前,启动了页映射机制,但没有第三次更新段映射。这时的虚拟地址,线性地址以及物理地址之间的映射关系比较微妙:
1 | lab2 stage 3: virt addr - 0xC0000000 = linear addr = phy addr + 0xC0000000 |
请注意pmm_init
函数中的一条语句:
1 | boot_pgdir[0] = boot_pgdir[PDX(KERNBASE)]; //KERNBASE以上的虚拟地址对应的物理地址在0-4MB之间 |
就是用来建立物理地址在0~4MB之内的三个地址间的临时映射关系virt addr - 0xC0000000 = linear addr = phy addr
。
物理内存的前4MB是被预留出来用于存放一些重要的系统信息和硬件设备的显存等。操作系统在启动阶段需要对这4MB以内的物理内存进行特殊处理,以保证系统能够正常运行。
因此,操作系统在启动阶段需要特别处理0~4MB的物理内存,建立临时的页表映射,使得内核代码和数据可以被正确地加载和执行。而超过4MB的物理内存则需要建立另外的映射关系,以便用户程序可以正常地访问和使用。
第四阶段
从gdt_init函数开始,第三次更新了段映射,形成了新的段页式映射机制,并且取消了临时映射关系,即执行语句“boot_pgdir[0] = 0**;**”把boot_pgdir[0]的第一个页目录表项(0~4MB)清零来取消临时的页映射关系。这时形成了我们期望的虚拟地址,线性地址以及物理地址之间的映射关系:
1 | lab2 stage 4: virt addr = linear addr = phy addr + 0xC0000000 |
在操作系统启动阶段,物理内存的前4MB通常会被保留用于存储一些系统信息和硬件设备的显存等。因此,在建立临时映射关系的时候,操作系统需要将这些区域映射到虚拟地址空间的KERNBASE以上的地址,以便内核代码和数据可以被正确加载和执行。
然而,在建立新的段页式映射机制之后,这个临时映射关系就不再需要了,操作系统会将boot_pgdir[0]的第一个页目录表项(0~4MB)清零,从而取消这个映射关系。
链接地址/虚地址/物理地址/加载地址以及edata/end/text的含义
链接脚本简介
ucore kernel各个部分由组成kernel的各个.o或.a文件构成,且各个部分在内存中地址位置由ld工具根据kernel.ld链接脚本(linker script)来设定。ld工具使用命令-T指定链接脚本。链接脚本主要用于规定如何把输入文件(各个.o或.a文件)内的section放入输出文件(lab2/bin/kernel,即ELF格式的ucore内核)内, 并控制输出文件内各部分在程序地址空间内的布局。下面简单分析一下/lab2/tools/kernel.ld,来了解一下ucore内核的地址布局情况。kernel.ld的内容如下所示:
1 | /* Simple linker script for the ucore kernel. |
其实从链接脚本的内容,可以大致猜出它指定告诉链接器的各种信息:
- 内核加载地址:0xC0100000
- 入口(起始代码)地址: ENTRY(kern_entry)
- cpu机器类型:i386
其最主要的信息是告诉链接器各输入文件的各section应该怎么组合:应该从哪个地址开始放,各个section以什么顺序放,分别怎么对齐等等,最终组成输出文件的各section.
链接地址/加载地址/虚地址/物理地址
1 | /* * |
bootloader把ucore kernel加载到内存时,采用的是加载地址(load addr),这是由于ucore还没有运行,即还没有启动页表映射,导致这时采用的寻址方式是段寻址方式,用的是boot loader在初始化阶段设置的段映射关系,其映射关系(可参看bootasm.S的末尾处有关段描述符表的内容)是:
1 | linear addr = phy addr = virtual addr |
简言之: OS的链接地址(link addr) 在tools/kernel.ld中设置好了,是一个虚地址(virtual addr);而ucore kernel的加载地址(load addr)在boot loader中的bootmain函数中指定,是一个物理地址。
edata/end/text的含义
在基于ELF执行文件格式的代码中,存在一些对代码和数据的表述,基本概念如下:
- BSS段(bss segment):指用来存放程序中未初始化的全局变量的内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
- 数据段(data segment):指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
- 代码段(code segment/text segment):指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
自映射机制(太牛了)
如果我们这时需要按虚拟地址的地址顺序显示整个页目录表和页表的内容,则要查找页目录表的页目录表项内容,根据页目录表项内容找到页表的物理地址,再转换成对应的虚地址,然后访问页表的虚地址,搜索整个页表的每个页目录项。这样过程比较繁琐。
我们需要有一个简洁的方法来实现这个查找。ucore做了一个很巧妙的地址自映射设计,把页目录表和页表放在一个连续的4MB虚拟地址空间中,并设置页目录表自身的虚地址<–>物理地址映射关系。这样在已知页目录表起始虚地址的情况下,通过连续扫描这特定的4MB虚拟地址空间,就很容易访问每个页目录表项和页表项内容。
具体而言,ucore是这样设计的,首先设置了一个常量(memlayout.h):
VPT=0xFAC00000, 这个地址的二进制表示为:
1111 1010 1100 0000 0000 0000 0000 0000
高10位为1111 1010 11,即10进制的1003,中间10位为0,低12位也为0。在pmm.c中有两个全局初始化变量
1 | pte_t * const vpt = (pte_t *)VPT; |
并在pmm_init函数执行了如下语句:
boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W;
1 | ()1111 1010 11|11 1110 1011 | 0000 0000 0000 |
这些变量和语句有何特殊含义呢?其实vpd变量的值就是页目录表的起始虚地址0xFAFEB000,且它的高10位和中10位是相等的,都是10进制的1003。当执行了上述语句,就确保了vpd变量的值就是页目录表的起始虚地址,且vpt是页目录表中第一个目录表项指向的页表的起始虚地址。此时描述内核虚拟空间的页目录表的虚地址为0xFAFEB000,大小为4KB。页表的理论连续虚拟地址空间0xFAC00000~0xFB000000,大小为4MB。因为这个连续地址空间的大小为4MB,可有1M个PTE,即可映射4GB的地址空间。
为什么是0xFAC00000~0xFB000000呢?
页目录表的起始虚地址为0xFAFEB000,页表的起始虚地址为0xFAC00000,因此0xFAC00000这个虚拟地址的高10位对应页目录表中的索引为1003,中10位对应页表中的索引为0,低12位为0。因此,在页目录表中找到索引为1003的目录表项,其中存储的指针为0xFAC00000,指向页表的起始虚地址。而在该页表中,索引为0的页表项中存储的物理地址为0x00000000,即将0xFAC00000这个虚拟地址映射到了物理地址0x00000000。类似地,该页表中的其它页表项也会将相应的虚拟地址映射到对应的物理地址。因此,这个4MB的连续地址空间可以被映射到物理地址0x00000000~0x04000000,即可以被寻址。
练习
练习1 实现 first-fit 连续物理内存分配算法
在实现first fit 内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时,需要按照空闲页块起始地址来排序,形成一个有序的链表。可能会修改default_pmm.c中的default_init,default_init_memmap,default_alloc_pages, default_free_pages等相关函数。请仔细查看和理解default_pmm.c中的注释。
解答
Prepare
1 | be familiar to the struct list in list.h. |
default_pmm.c
1 | * (2) default_init: you can reuse the demo default_init fun to init the free_list and set nr_free to 0. |
调用list_init函数建立物理内存页表,页表数为0
1 | static void |
default_init_memmap
1 | * (3) default_init_memmap: CALL GRAPH: kern_init --> pmm_init-->page_init-->init_memmap--> pmm_manager->init_memmap |
此函数是用来初始化内存映射表的,结合我们了解的前置知识可以知道该函数的设计有两个要点:
该映射表中的元素是Page,我们需要注意初始化Page中的几个参数。
1
2
3
4
5
6struct 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表示这页被页表的引用记数
flags表示此物理页的状态标记
进一步查看kern/mm/memlayout.h中的定义,可以看到:
1
2
3/* 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- bit 1表示此页是否是free的,如果设置为1,表示这页是free的,可以被分配
- 如果设置为0,表示这页已经被分配出去了,不能被再二次分配
注意初始化第一个Page中独占的两个全局参数:
- property用来记录某连续内存空闲块的大小(即地址连续的空闲页的个数)(存在于第一个物理表)
- page_link是便于把多个连续内存空闲块链接在一起的双向链表指针(存在于第一个物理表)
故我们可以写出以下的函数:
1 | static void |
其中根据实验指导书的设计要求,对多种情况做出了判断,便于我们测试函数模块。
在该函数中我们接受Page的基址,以及一个需要分配的页数,通过循环的方式进行遍历,调用PageReserved(p)
查看该块 是非 被占用,若 没被占用则使用p->flags = p->property = 0
对每个块进行初始化标记为已被使用,再使用set_page_ref(p, 0)
添加引用计数。
完成对要求页数的初始化后,对第一个页进行设置:设置该页表中空闲块的数量base->property = n;
以及将该页表加入到页目录中,最终完成了一个页表的初始化。
default_alloc_pages
1 | default_alloc_pages: search find a first free block (block size >=n) in free list and reszie the free block, return the addr |
其实这些函数都在实验指导书中给出了具体的实现原理了
1 | static struct Page * |
为什么用before呢?我们知道题目要求我们的页表项的地址要从小到大排列,由双向链表的性质可以得出以下的图:
整个函数的过程用图可以表示为:
default_free_pages
1 | * (5) default_free_pages: relink the pages into free list, maybe merge small free blocks into big free blocks. |
就如实验指导书中所说:default_free_pages函数的实现其实是default_alloc_pages的逆过程,不过需要考虑空闲块的合并问题。
1 | static void |
该函数会将第一个空闲页面的属性设置为要释放的页面数量,并将其标记为可用空闲页面。然后,它遍历所有的空闲页面,将与要释放页面相邻的页面合并成一个大页面,并从空闲页面列表中删除旧的页面。
引用计数是用于跟踪页面是否被占用的技术,当页面被占用时,引用计数会增加。而空闲页数量是指当前系统中可用的未被占用的物理页面的数量。
在引用计数的实现中,当一个进程需要访问某个页面时,它会增加该页面的引用计数,表示该页面正在被使用。当该进程完成使用后,它会将该页面的引用计数减少,如果该页面的引用计数为0,表示该页面可以被释放并添加到空闲页列表中,从而增加空闲页数量。
可能的优化
在使用 first-fit 算法分配内存后,可能会留下一些非常小的空闲块。为了避免这种情况,可以在分配内存时,如果剩余空闲块的大小小于一个阈值,可以将该块合并到前一个或后一个空闲块中,从而减少碎片化。
练习2:实现寻找虚拟地址对应的页表项
通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。get_pte函数的调用关系图如下所示:
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
- 请描述页目录项(Pag Director Entry)和页表(Page Table Entry)中每个组成部分的含义和以及对ucore而言的潜在用处。
- 如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
解答
1 | * Some Useful MACROs and DEFINEs, you can use them in below implementation. |
1 | pde_t *pdep = &pgdir[PDX(la)]; |
解答2
如果 ucore 执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
答:
- 将触发
页访问异常
的虚地址保存到cr2
寄存器中 - 设置错误代码,触发 14 号中断,也就是
缺页错误
- 抛出
Page Fault
异常,将外存的数据读到内存中(应该是读存在硬盘上的虚拟内存分页文件) - 进行
上下文切换
,退出中断,返回到中断前的状态
练习3
当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构 Page 做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。补全 page_remove_pte 函数。
解答
1 | static inline void |
TLB(Translation Lookaside Buffer)转换检测缓冲区是一个内存管理单元,是用于改进虚拟地址到物理地址转换速度的缓存。 其中每一行都保存着一个由单个PTE(Page Table Entry,页表项)组成的块。 如果没有TLB,则每次取数据都需要两次访问内存。
解答2
数据结构 Page 的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是什么?
当页目录项或页表项有效时,Page
数组中的项与页目录项或页表项存在对应关系。实际上,每个页目录项记录一个页表的信息,每个页表项记录一个物理页的信息。页目录表中存放着数个页表项,这些页表项中存放了某个二级页表所在物理页的信息,包括该二级页表的物理地址,但使用线性地址的头部PDX(Page Directory Index)
来索引页目录表。而页表(二级页表)与页目录(一级页表)具有类似的特性,页表中的页表项指向所管理的物理页的物理地址,使用线性地址的中部PTX(Page Table Index)
来索引页表。页目录项保存的物理页面地址(即某个页表),以及页表项保存的物理页面地址都对应于Page
数组中的某一页。
解答3
如果希望虚拟地址与物理地址相等,则需要如何修改 lab2 才能完成此事?
- 先将
tools/kernel.ld
中的第10行
的内核加载地址
从0xC0100000
修改为0x00100000
1 | // 代码第10行 修改前: |
- 再将
kern/mm/memlayout.h
中的第56行
的内核偏移地址
从0xC0000000
修改为0x00000000
1 | // 代码第56行 修改前: |
- 关闭页机制,这一步是为了保证运行时不报错。将
kern/init/entry.S
中开启页机制
的那一段代码删除即可。否则在分页模式,取消掉boot_pgdir[0]
的页表下kern_init
会被置于一个根本无法访问到的地址。
1 | # 代码第14-17行 修改前: |