MIT——6.828:操作系统工程——第2章:实验二:内存管理
在本实验中,为操作系统编写内存管理代码,分为三个部分
第一部分是物理内存管理,使得内核可以分配内存并稍后释放它。物理内存管理将以 4096 字节为单位运行,称为 pages。这一部分的任务是维护记录哪些物理页面是空闲的,哪些是已分配的,以及有多少进程正在共享每个已分配页面的数据结构。编写代码来分配和释放内存页。
第二部分是虚拟内存。一个虚拟地址如何被映射到物理地址,将实现一些函数来操作页目录和页表从而达到映射的目的。
第三部分是内核的地址空间。将结合第一部分和第二部分的成果,来对内核地址空间进行映射。
1. 物理内存管理
练习1:在kern/pmap.c文件中,必须实现以下函数的代码(可能按照给定的顺序)。
`boot_alloc()`
`mem_init()` (仅限于调用 `check_page_free_list(1)`)
`page_init()`
`page_alloc()`
`page_free()`
check_page_free_list()和check_page_alloc()测试物理页面分配器。您应该启动JOS并查看check_page_alloc()是否报告成功。修复代码,使其通过。您可能会发现添加自己的assert()来验证您的假设是否正确是有帮助的。
答:通过实验一,我们可以知道目前的物理内存分布
接下来阅读代码,从实验一可知,进入内核后调用i386_init
函数,该函数首先初始化控制台,然后调用mem_init
初始化内存。在mem_init
中,首先调用i386_detect_memory
获取可用的物理内存页的数量,并将其保存在全局变量npages
和npages_basemem
中。然后,调用boot_alloc
初始化页目录,boot_alloc
实现如下
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; // virtual address of next byte of free memory
char *result;
// Initialize nextfree if this is the first time.
// 'end' is a magic symbol automatically generated by the linker,
// which points to the end of the kernel's bss segment:
// the first virtual address that the linker did *not* assign
// to any kernel code or global variables.
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
// Allocate a chunk large enough to hold 'n' bytes, then update
// nextfree. Make sure nextfree is kept aligned
// to a multiple of PGSIZE.
//
// LAB 2: Your code here.
result = nextfree;
nextfree = ROUNDUP((char*)result + n, PGSIZE);
return result;
}
boot_alloc
定义一个static局部变量nextfree
,第一次调用时将其指向容纳内核代码段和数据段的物理页的下一页(end
是内核bss段的结束位置)的地址。然后以一页大小为倍数(PGSIZE)分配足够容纳所申请大小的空间,返回首页地址并使nextfree
指向下一页。
接下来,根据注释
Allocate an array of npages 'struct PageInfo's and store it in 'pages'.
The kernel uses this array to keep track of physical pages: for
each physical page, there is a corresponding struct PageInfo in this
array. 'npages' is the number of physical pages in memory. Use memset
to initialize all fields of each struct PageInfo to 0.
分配npages大小的结构体PageInfo的数组并将其存储在pages中。
内核使用此数组跟踪物理页:对于每个物理页,此数组中都有一个相应的结构体PageInfo。
npages是内存中物理页的数量。使用memset将每个结构体PageInfo的所有字段初始化为0。
因此,在mem_init
补充代码
// Your code goes here:
pages = (struct PageInfo*)boot_alloc(sizeof(struct PageInfo) * npages); //分配空间保存pages数组
memset(pages, 0, sizeof(struct PageInfo) * npages);
接下来,mem_init
调用page_init
初始化物理页管理链表,这时我们查看page_init
的注释
// The example code here marks all physical pages as free.
// However this is not truly the case. What memory is free?
// 1) Mark physical page 0 as in use.
// This way we preserve the real-mode IDT and BIOS structures
// in case we ever need them. (Currently we don't, but...)
// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
// is free.
// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
// never be allocated.
// 4) Then extended memory [EXTPHYSMEM, ...).
// Some of it is in use, some is free. Where is the kernel
// in physical memory? Which pages are already in use for
// page tables and other data structures?
//
// Change the code to reflect this.
// NB: DO NOT actually touch the physical memory corresponding to
// free pages!
// 这里的示例代码将所有物理页面标记为可用。
//
// 1) 将物理页0标记为正在使用。
// 通过这种方式,我们保留了实模式的IDT和BIOS结构,以防我们需要它。
// 2)剩余的基本内存[PGSIZE,npages_basemem * PGSIZE)是免费的。
// 3) 然后是IO孔[IOPHYSEM,EXTPHYSEM],这是绝对不能分配的。
// 4) 然后扩展内存[EXTPHYSEM,…]。
// 有些正在使用,有些是免费的。内核在物理内存中的位置?哪些页面已用于页面表和其他数据结构?
// 更改代码以反映这一点。
// 注意:不要实际触摸空闲页面对应的物理内存!
这里提示我们部分物理页是可用的,部分是不可用的,那么,哪些是可用的,哪些是不可用的呢?根据物理内存的分布以及注释的提升,大致可以分为3个部分
- 0x00000 ~ 0x1000(4KB):这部分保存实模式的IDT,不可用
- 0x1000 ~ 0xA0000(640KB):这部分叫basemem,可用
- 0xA0000 ~ 0x100000(1MB):这部分叫IO Hole,不可用
- 0x100000 ~ 物理页管理数组所在页:这部分保存页目录,物理页管理数组以及内核代码段和数据段,不可用。之后都是可用的。
因此,上述i386_detect_memory()
统计有多少可用的物理内存,将总共的可用物理内存页数保存到全局变量npages中,basemem部分可用的物理内存页数保存到npages_basemem中。
由此,page_init
函数编写如下
page_init(void)
{
size_t i;
size_t io_hole_start_page = (size_t)IOPHYSMEM / PGSIZE;
size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE;// 转换为物理地址,因为boot_alloc返回虚拟地址
for (i = 0; i < npages; i++) {
if (i == 0) {
pages[i].pp_ref = 1; // pp_ref当前页的引用计数
pages[i].pp_link = NULL; // pp_link下一个空闲页
}
else if (i >= io_hole_start_page && i <= kernel_end_page)
{
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
else {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
}
接着,我们实现page_alloc
和page_free
函数。根据注释
// Allocates a physical page. If (alloc_flags & ALLOC_ZERO), fills the entire
// returned physical page with '\0' bytes. Does NOT increment the reference
// count of the page - the caller must do these if necessary (either explicitly
// or via page_insert).
//
// Be sure to set the pp_link field of the allocated page to NULL so
// page_free can check for double-free bugs.
//
// Returns NULL if out of free memory.
//
// Hint: use page2kva and memset
//分配物理页面。如果(alloc_flags& alloc_ZERO),则用“\0”字节填充整个返回的物理页。不增加页面的引用计数 - 如果需要,调用者必须执行这些操作(显式或通过page_insert)。
//确保将已分配页面的pp_link字段设置为NULL,以便page_free可以检查是否存在双自由错误。
//如果可用内存不足,则返回NULL。
//提示:使用page2kva和memset
得知page_alloc
是分配一个可用的物理页,编写代码如下
struct PageInfo *
page_alloc(int alloc_flags)
{
// Fill this function in
struct PageInfo* temp = page_free_list;
if (temp == NULL) {
cprintf("page_alloc: out of free memory\n");
return NULL; // 没有空闲页返回NULL
}
page_free_list = page_free_list->pp_link;
temp->pp_link = NULL;
if (alloc_flags & ALLOC_ZERO) {
void * addr = page2kva(temp); // 找到该页对应的内核虚拟地址
memset(addr, '\0', PGSIZE);
}
return temp;
}
进而可知,page_free
即为释放一个物理页,编写代码如下
void
page_free(struct PageInfo *pp)
{
// Fill this function in
// Hint: You may want to panic if pp->pp_ref is nonzero or
// pp->pp_link is not NULL.
// 提示:如果pp->pp_ref为非零或pp->pp_link不为NULL,您可能会感到恐慌。
if (pp->pp_ref != 0 || pp->pp_link != NULL) {
cprintf("page_free: pp->pp_ref is nonzero or pp->pp_link is not NULL\n");
return;
}
pp->pp_link = page_free_list;
page_free_list = pp;
return;
}
至此,我们得到了pages数组保存所有物理页的信息,page_free_list链表记录所有空闲的物理页。可以用page_alloc()和page_free()进行对物理页分配和回收。
现在的物理内存分布
2. 虚拟内存
练习 2:如果您还没有阅读 Intel 80386 参考手册 的第 5 章和第 6 章 ,请阅读。仔细阅读有关页转换和基于页的保护的部分(5.2 和 6.4)。我们建议您还浏览有关细分的部分;虽然 JOS 使用分页硬件来实现虚拟内存和保护,但在 x86 上无法禁用段转换和基于段的保护,因此您需要对它有一个基本的了解。
答:详见5.2 分页机制与6.4 页保护
可参考从零实现一个操作系统内核——寻址机制:段机制和页机制
2.1 虚拟、线性和物理地址
在x86术语中,虚拟地址由段选择子和段内的偏移量组成。线性地址是在段转换之后但页面转换之前获得的地址。物理地址是在段和页转换之后最终获得的地址,也是最终通过硬件总线输出到RAM的地址。
Selector +--------------+ +-----------+
---------->| | | |
| Segmentation | | Paging |
Software | |-------->| |----------> RAM
Offset | Mechanism | | Mechanism |
---------->| | | |
+--------------+ +-----------+
Virtual Linear Physical
A C指针是虚拟地址的“偏移”部分。在boot/boot.S中,我们安装了一个全局描述符表(GDT),通过将所有段基地址设置为0并将边界设置为0xffffffff,该表有效地禁用了段转换。因此,“选择子”没有起作用,线性地址等于虚拟地址。
回想一下,在实验室1的第3部分中,我们安装了一个简单的页表,以便内核可以在其链接地址0xf0100000处运行,即使它实际上是加载到0x00100000处ROM BIOS上方的物理内存中。此页表仅映射[0,4MB)内存。在您将在本实验室为JOS设置的虚拟地址空间布局中,我们将扩展此布局,以映射从虚拟地址0xf0000000开始的第一个256MB物理内存,并映射虚拟地址空间的许多其他区域。
从CPU上执行的代码来看,一旦我们进入保护模式(这是我们在boot/boot.S中输入的第一件事),就无法直接使用线性或物理地址。所有内存引用都被解释为虚拟地址,并由MMU转换,这意味着C中的所有指针都是虚拟地址。
JOS内核通常需要将地址作为不透明值或整数进行操作,而不需要对它们进行解引用(*),例如在物理内存分配器中。有时这些是虚拟地址,有时是物理地址。为了帮助记录代码,JOS源代码区分了两种情况:uintptr_t类型表示不透明的虚拟地址,physpaddr_t表示物理地址。这两种类型实际上都只是32位整数(uint32_t)的同义词,因此编译器不会阻止您将一种类型分配给另一种类型!因为它们是整数类型(而不是指针),所以如果你试图解引用它们,编译器会报错。
JOS内核可以通过首先将uintptr_t转换为指针类型来解引用它。相反,内核不能智能地解引用物理地址,因为MMU转换所有内存引用。如果将phyaddr_t转换为指针并解引用它,则可以加载并存储到生成的地址(硬件会将其解释为虚拟地址),但可能无法获得所需的内存位置。
总结:
C type | Address type |
---|---|
T* | Virtual |
uintptr_t | Virtual |
physaddr_t | Physical |
JOS 内核有时需要读取或修改它只知道物理地址的内存。例如,向页表添加映射可能需要分配物理内存来存储页目录,然后初始化该内存。但是,内核无法绕过虚拟地址转换,因此无法直接加载和存储到物理地址。JOS 将所有物理内存从物理地址 0 开始重新映射到虚拟地址 0xf0000000 的原因之一是为了帮助内核读写它只知道物理地址的内存。为了将一个物理地址翻译成内核真正可以读写的虚拟地址,内核必须在物理地址上加上0xf0000000,才能在重映射区域中找到其对应的虚拟地址。你应该使用KADDR(pa) 完成这个加法。 |
|
JOS内核有时还需要能够在给定存储内核数据结构的内存的虚拟地址的情况下找到物理地址。内核全局变量和boot_alloc()分配的内存位于内核加载的区域,从0xf0000000开始,即我们映射所有物理内存的区域。因此,为了将该区域中的虚拟地址转换为物理地址,内核可以简单地减去0xf0000000。你应该使用PADDR(va) 完成这个减法。 |
问题:假设以下JOS内核代码是正确的,那么变量x应该具有什么类型,uintptr_t或physaddr_t?
_mystery_t_ x;
char* value = return_a_pointer();
*value = 10;
x = (_mystery_t_) value;
答:uintptr_t类型
练习3:虽然GDB只能通过虚拟地址访问QEMU的内存,但在设置虚拟内存时能够检查物理内存通常很有用。查看实验室工具指南中的QEMU监视器命令,特别是xp命令,它允许您检查物理内存。要访问QEMU监视器,请在终端中按Ctrl-a c(相同的绑定返回到串行控制台)。
使用QEMU监视器中的xp命令和GDB中的x命令检查相应物理和虚拟地址的内存,并确保看到相同的数据。
我们的QEMU补丁版本提供了一个info-pg命令,它可能也很有用:它显示了当前页表的紧凑但详细的表示,包括所有映射的内存范围、权限和标志。Stock QEMU还提供了一个info mem命令,该命令显示了映射的虚拟地址范围以及使用的权限。
2.2 引用计数
在未来的实验室中,您通常会将同一物理页面同时映射到多个虚拟地址(或多个环境的地址空间)。您将在与物理页对应的结构PageInfo
的pp_ref
字段中保留对每个物理页的引用数。当物理页面的此计数为零时,该页面可以被释放,因为它不再被使用。一般来说,这个计数应该等于物理页面在所有页面表中出现在UTOP之下的次数(UTOP之上的映射大多是在启动时由内核设置的,不应该被释放,因此不需要引用计数)。我们还将使用它来跟踪我们保存到页目录页的指针数量,进而跟踪页目录对页表页的引用数量。
使用page_alloc
时要小心。它返回的页面将始终具有0的引用计数,因此,一旦对返回的页面执行了某些操作(例如将其插入到页面表中),pp_ref
应该立即递增。有时这由其他函数(例如page_insert
)处理,有时调用page_alloc
的函数必须直接执行。
2.2 页表管理
现在您将编写一组程序来管理页表:插入和删除线性到物理的映射,并在需要时创建页表页。
练习4:在文件kern/pmap.c中,您必须实现以下函数的代码。
pgdir_walk()
boot_map_region()
page_lookup()
page_remove()
page_insert()
check_page()
,调用自mem_init()
,测试您的页表管理例程。在继续之前,您应该确保它报告成功。
答:首先来看pgdir_walk()
的注释
// Given 'pgdir', a pointer to a page directory, pgdir_walk returns
// a pointer to the page table entry (PTE) for linear address 'va'.
// This requires walking the two-level page table structure.
//
// The relevant page table page might not exist yet.
// If this is true, and create == false, then pgdir_walk returns NULL.
// Otherwise, pgdir_walk allocates a new page table page with page_alloc.
// - If the allocation fails, pgdir_walk returns NULL.
// - Otherwise, the new page's reference count is incremented,
// the page is cleared,
// and pgdir_walk returns a pointer into the new page table page.
//
// Hint 1: you can turn a PageInfo * into the physical address of the
// page it refers to with page2pa() from kern/pmap.h.
//
// Hint 2: the x86 MMU checks permission bits in both the page directory
// and the page table, so it's safe to leave permissions in the page
// directory more permissive than strictly necessary.
//
// Hint 3: look at inc/mmu.h for useful macros that manipulate page
// table and page directory entries.
// 给定“pgdir”(指向页目录的指针),pgdir_walk返回指向线性地址“va”的页表条目(PTE)的指针。
// 这需要遍历两级页表结构。
// 相关的页表页可能还不存在。
// 如果这是真的,并且create == false,那么pgdir_walk返回NULL。
// 否则,pgdir_walk将调用page_alloc分配一个新的页表页。
// - 如果分配失败,pgdir_walk将返回NULL。
// - 否则,新页面的引用计数将递增,页面将被清除,pgdir_walk将返回指向新页表页的指针。
// 提示1:您可以将PageInfo * 转换为它从kern / pmap.h使用page2pa()引用的页面的物理地址。
// 提示2:x86 MMU检查页面目录和页面表中的权限位,因此,将页面目录中的权限保留得比严格需要的更宽松是安全的。
// 提示3:查看inc / mmu.h,以获取操作页表和页目录条目的有用宏。
pgdir_walk()
接受参数
pde_t *pgdir
const void *va
int create
返回指向线性地址va
的页表条目(PTE)的指针。
代码如下
static pde_t*
find_pde(pde_t* pgdir, const void* va) {
uintptr_t PDX_T = PDX(va);
pde_t* PDE_T = pgdir + PDX_T; // 以指针加上偏移量,指针类型偏移
return PDE_T;
}
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
pde_t* PDE_T = find_pde(pgdir, va);
if (!((*PDE_T) & PTE_P)) {
if (create == 0) {
return NULL;
}
else {
struct PageInfo* temp = page_alloc(ALLOC_ZERO); //分配的物理页值清零
if (temp == NULL) {
return NULL;
}
temp->pp_ref++;
*PDE_T = page2pa(temp) | PTE_P | PTE_U | PTE_W;
}
}
return (pte_t *)KADDR(PTE_ADDR(*PDE_T)) + PTX(va); //指针加上偏移量,指针偏移
}
接下来是boot_map_region()
// Map [va, va+size) of virtual address space to physical [pa, pa+size)
// in the page table rooted at pgdir. Size is a multiple of PGSIZE, and
// va and pa are both page-aligned.
// Use permission bits perm|PTE_P for the entries.
//
// This function is only intended to set up the ``static'' mappings
// above UTOP. As such, it should *not* change the pp_ref field on the
// mapped pages.
//
// Hint: the TA solution uses pgdir_walk
// 将虚拟地址空间的[va,va+size)映射到以pgdir为根的页表中的物理[pa,pa+size)。大小是PGSIZE的倍数,并且va和pa是页对齐的。
// 对条目使用perm | PTE_P权限位。
// 此函数仅用于在UTOP之上设置“静态”映射。因此,它不应该更改映射物理页上的pp_ref字段。
// 提示:TA解决方案使用pgdir_walk
boot_map_region()
接受参数
pde_t *pgdir
uintptr_t va
size_t size
physaddr_t pa
int perm
将虚拟地址空间的[va,va+size)
映射到以pgdir
为根的页表中的物理[pa,pa+size)
代码如下
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
int n;
if (size % PGSIZE != 0) { // 计算需要的物理页数量
n = size / PGSIZE + 1;
}
else {
n = size / PGSIZE;
}
uintptr_t temp_va = va;
physaddr_t temp_pa = pa;
for (int i = 0; i < n; ++i) {
pte_t* PTE_T = pgdir_walk(pgdir, temp_va, 1);
if (PTE_T != NULL) {
*PTE_T = temp_pa | perm;
temp_pa += PGSIZE;
temp_va += PGSIZE;
}
else {
panic("boot_map_region(): out of memory\n");
}
}
}
然后是page_lookup()
// Return the page mapped at virtual address 'va'.
// If pte_store is not zero, then we store in it the address
// of the pte for this page. This is used by page_remove and
// can be used to verify page permissions for syscall arguments,
// but should not be used by most callers.
//
// Return NULL if there is no page mapped at va.
//
// Hint: the TA solution uses pgdir_walk and pa2page.
//
// 返回映射到虚拟地址“va”的物理页。
// 如果pte_store不为零,那么我们将在其中存储此页的pte地址。这由page_remove使用,可用于验证syscall参数的页面权限,但大多数调用者不应使用。
// 如果va.处没有映射页,则返回NULL。
// 提示:TA解决方案使用pgdir_walk和pa2page。
page_lookup()
接受参数
pde_t *pgdir
void *va
pte_t **pte_store
返回映射到虚拟地址 va
的物理页的指针
代码如下
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
pte_t* PTE_T = pgdir_walk(pgdir, va, 0);
if (PTE_T == NULL) {
return NULL;
}
if (!((*PTE_T) & PTE_P)) {
return NULL;
}
if (pte_store != NULL) {
*pte_store = PTE_T;
}
physaddr_t pa = PTE_ADDR(*PTE_T); // va对应的物理地址(删去权限位)
struct PageInfo* pp = pa2page(pa);
return pp;
}
然后是page_remove()
//
// Unmaps the physical page at virtual address 'va'.
// If there is no physical page at that address, silently does nothing.
//
// Details:
// - The ref count on the physical page should decrement.
// - The physical page should be freed if the refcount reaches 0.
// - The pg table entry corresponding to 'va' should be set to 0.
// (if such a PTE exists)
// - The TLB must be invalidated if you remove an entry from
// the page table.
//
// Hint: The TA solution is implemented using page_lookup,
// tlb_invalidate, and page_decref.
//
// 取消映射虚拟地址“va”处的物理页。
// 如果该地址处没有物理页面,则无提示地不执行任何操作。
// 详细信息:
// - 物理页上的引用计数应递减。
// - 如果引用计数达到0,则应释放物理页。
// - 与“va”对应的pg表条目应设置为0。
// (如果存在这样的PTE)
// - 如果从页表中删除条目,则TLB必须无效。
// 提示:TA解决方案是使用page_lookup、tlb_validate和page_decref实现的。
page_remove()
接受参数
pde_t *pgdir
void *va
取消虚拟地址va
处映射的物理页。
代码如下
void
page_remove(pde_t *pgdir, void *va)
{
pte_t* pte_store;
struct PageInfo* page_va = page_lookup(pgdir, va, &pte_store);
if (page_va == NULL) {
return;
}
*pte_store = 0;
page_decref(page_va);
tlb_invalidate(pgdir, va);
}
最后是page_insert()
// Map the physical page 'pp' at virtual address 'va'.
// The permissions (the low 12 bits) of the page table entry
// should be set to 'perm|PTE_P'.
//
// Requirements
// - If there is already a page mapped at 'va', it should be page_remove()d.
// - If necessary, on demand, a page table should be allocated and inserted
// into 'pgdir'.
// - pp->pp_ref should be incremented if the insertion succeeds.
// - The TLB must be invalidated if a page was formerly present at 'va'.
//
// Corner-case hint: Make sure to consider what happens when the same
// pp is re-inserted at the same virtual address in the same pgdir.
// However, try not to distinguish this case in your code, as this
// frequently leads to subtle bugs; there's an elegant way to handle
// everything in one code path.
//
// RETURNS:
// 0 on success
// -E_NO_MEM, if page table couldn't be allocated
//
// Hint: The TA solution is implemented using pgdir_walk, page_remove,
// and page2pa.
//
// 将物理页“pp”映射到虚拟地址“va”。
// 页表项的权限(低12位)应设置为“perm | PTE_P”。
// 要求
// - 如果已经有一个页面映射到“va”,那么它应该调用page_remove()取消。
// - 如有必要,应根据需要分配一个页表并将其插入到“pgdir”中。
// - 如果插入成功,pp->pp_ref应该递增。
// - 如果之前映射“va”的物理页的TLB必须无效。
// 提示:确保考虑在同一pgdir中的同一虚拟地址重新插入同一pp时会发生什么。
// 但是,尽量不要在代码中区分这种情况,因为这经常会导致细微的错误;有一种优雅的方法可以在一个代码路径中处理所有事情。
// 返回:成功时为0
// -E_NO_MEM,如果无法分配页表
// 提示:TA解决方案使用pgdir_walk、page_remove和page2pa实现。
page_insert()
接受参数
pde_t *pgdir
struct PageInfo *pp
void *va, int perm
将物理页pp
映射到虚拟地址va
,成功返回0,失败返回-E_NO_MEM
代码如下
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t* PTE_T = pgdir_walk(pgdir, va, 1);
if (PTE_T == NULL) {
return -E_NO_MEM;
}
if (!((*PTE_T) & PTE_P)) {
*PTE_T = page2pa(pp) | perm | PTE_P;
pp->pp_ref++;
pgdir[PDX(va)] |= perm | PTE_P;
}
else {
if (pa2page(*PTE_T) == pp) { //在同一pgdir中的同一虚拟地址重复插入同一pp的处理方法
*PTE_T = 0;
*PTE_T = page2pa(pp) | perm | PTE_P;
return 0;
}
page_remove(pgdir, va);
*PTE_T = page2pa(pp) | perm | PTE_P;
pp->pp_ref++;
pgdir[PDX(va)] |= perm | PTE_P;
}
return 0;
}
至此,代码补充完整。
3. 内核地址空间
JOS将处理器的32位线性地址空间分为两部分。我们将在实验 3 中开始加载和运行的用户环境(进程)将控制下半部分的布局和内容,而内核始终保持对上半部分的完全控制。分界线由inc/memlayout.h
中的符号ULIM
任意定义,为内核保留了大约256MB的虚拟地址空间。这就解释了为什么我们需要在实验1中为内核提供如此高的链接地址:否则内核的虚拟地址空间中就没有足够的空间来同时映射到其下方的用户环境中。
Virtual memory map: Permissions
kernel/user
4 Gig --------> +------------------------------+
| | RW/--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
: . :
: . :
: . :
|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/--
| | RW/--
| Remapped Physical Memory | RW/--
| | RW/--
KERNBASE, ----> +------------------------------+ 0xf0000000 --+
KSTACKTOP | CPU0's Kernel Stack | RW/-- KSTKSIZE |
| - - - - - - - - - - - - - - -| |
| Invalid Memory (*) | --/-- KSTKGAP |
+------------------------------+ |
| CPU1's Kernel Stack | RW/-- KSTKSIZE |
| - - - - - - - - - - - - - - -| PTSIZE
| Invalid Memory (*) | --/-- KSTKGAP |
+------------------------------+ |
: . : |
: . : |
MMIOLIM ------> +------------------------------+ 0xefc00000 --+
| Memory-mapped I/O | RW/-- PTSIZE
ULIM, MMIOBASE --> +------------------------------+ 0xef800000
| Cur. Page Table (User R-) | R-/R- PTSIZE
UVPT ----> +------------------------------+ 0xef400000
| RO PAGES | R-/R- PTSIZE
UPAGES ----> +------------------------------+ 0xef000000
| RO ENVS | R-/R- PTSIZE
UTOP,UENVS ------> +------------------------------+ 0xeec00000
UXSTACKTOP -/ | User Exception Stack | RW/RW PGSIZE
+------------------------------+ 0xeebff000
| Empty Memory (*) | --/-- PGSIZE
USTACKTOP ---> +------------------------------+ 0xeebfe000
| Normal User Stack | RW/RW PGSIZE
+------------------------------+ 0xeebfd000
| |
| |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
. .
. .
. .
|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
| Program Data & Heap |
UTEXT --------> +------------------------------+ 0x00800000
PFTEMP -------> | Empty Memory (*) | PTSIZE
| |
UTEMP --------> +------------------------------+ 0x00400000 --+
| Empty Memory (*) | |
| - - - - - - - - - - - - - - -| |
| User STAB Data (optional) | PTSIZE
USTABDATA ----> +------------------------------+ 0x00200000 |
| Empty Memory (*) | |
0 ------------> +------------------------------+ --+
3.1 权限与故障隔离
由于内核和用户内存都存在于每个环境的地址空间中,因此我们必须在x86页表中使用权限位,以允许用户代码仅访问地址空间的用户部分。否则,用户代码中的错误可能会覆盖内核数据,导致崩溃或更细微的故障;用户代码还可以窃取其他环境的私有数据。注意,可写权限位(PTE_W)同时影响用户和内核代码!
用户环境将无权访问ULIM以上的任何内存,而内核将能够读取和写入该内存。对于地址范围[UTP,ULIM),内核和用户环境都具有相同的权限:它们可以读取但不能写入此地址范围。此地址范围用于向用户环境公开只读的某些内核数据结构。最后,UTOP下面的地址空间供用户环境使用;用户环境将设置访问此内存的权限。
现在,您将在UTOP之上设置地址空间的内核部分。inc/memlayout.h显示了您应该使用的布局。您将使用刚刚编写的函数来设置适当的线性内存到物理内存的映射。
练习五:.在调用check_page()后,填写mem_init()中缺少的代码。
您的代码现在应该通过check_kern_pgdir()
和check_page_installed_pgdir()
检查。
答:首先查看注释
// Map 'pages' read-only by the user at linear address UPAGES
// Permissions:
// - the new image at UPAGES -- kernel R, user R
// (ie. perm = PTE_U | PTE_P)
// - pages itself -- kernel RW, user NONE
// Your code goes here:
// 映射用户只读的“pages”在线性地址UPAGES处
// 权限:
// -UPAGES的新映像--内核R,用户R(即perm=PTE_U|PTE_P)
// -页本身--内核RW,用户NONE
// 代码如下:
根据上面的虚拟地址映射表,UPAGES — UVPT
存储物理页数组,权限为内核和用户可读perm=PTE_U|PTE_P
。代码如下
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U | PTE_P);
同理
//////////////////////////////////////////////////////////////////////
// Use the physical memory that 'bootstack' refers to as the kernel
// stack. The kernel stack grows down from virtual address KSTACKTOP.
// We consider the entire range from [KSTACKTOP-PTSIZE, KSTACKTOP)
// to be the kernel stack, but break this into two pieces:
// * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory
// * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed; so if
// the kernel overflows its stack, it will fault rather than
// overwrite memory. Known as a "guard page".
// Permissions: kernel RW, user NONE
// Your code goes here:
// 使用“bootstack”所指的物理内存作为内核堆栈。内核堆栈从虚拟地址KSTACKTOP向下扩展。
// 我们将[KSTACKTOP - PTSIZE,KSTACKTOP)的整个范围视为内核堆栈,
// 但将其分为两部分: * [KSTACKTOP - KSTKSIZE,KSTACKTOP](由物理内存支持
// [KSTACKTOP - PTSIZE,KSTACKTOP - KSTKSSIZE)(不支持);因此,如果内核溢出堆栈,它将出错,而不是覆盖内存。被称为“保护页”。
// 权限:内核RW,用户NONE
// 您的代码如下:
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_P | PTE_W);
//////////////////////////////////////////////////////////////////////
// Map all of physical memory at KERNBASE.
// Ie. the VA range [KERNBASE, 2^32) should map to
// the PA range [0, 2^32 - KERNBASE)
// We might not have 2^32 - KERNBASE bytes of physical memory, but
// we just set up the mapping anyway.
// Permissions: kernel RW, user NONE
// Your code goes here:
// 映射KERNBASE的所有物理内存。
// Ie.VA范围[KERNBASE,2 ^ 32)应该映射到PA范围[0,2 ^ 32 - KERNBASE)
// 我们可能没有2 ^ 32 - KERNBASE字节的物理内存,但我们只是设置了映射。
// 权限:内核RW,用户NONE
// 您的代码如下:
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_P | PTE_W);
问题:
2. 此时页面目录中的哪些条目(行)已填充?它们映射了哪些地址,指向何处?换句话说,尽可能多地填写此表:
Entry | Base Virtual Address | Points to (logically): |
---|---|---|
1023 | ? | 前4MB物理内存的页表 |
1022 | ? | |
. | ||
. | ||
. | ||
2 | 0x00800000 | |
1 | 0x00400000 | |
0 | 0x00000000 |
- 我们已经将内核和用户环境放在同一个地址空间中。为什么用户程序无法读取或写入内核内存?哪些具体机制保护内核内存?
答:在part1进行物理页初始化时,我们并未将存储内核的物理页放入空闲页链表,这也就使得用户程序无法利用这部分的物理内存。同时页表项中有读写保护位,以及PTE_U
区分内核和用户,MMU组件在映射虚拟地址时实现这种保护。 - 该操作系统可以支持的最大物理内存量是多少?为什么?
答:以32位为地址,最大支持4GB物理内存。 - 如果我们实际上拥有最大数量的物理内存,那么管理内存有多少空间开销?这个开销是如何分解的?
答:
物理页数组:4GB/PGSIZE = 2^20 页
2^20 * 6B = 6MB
空闲页链表:4B
页目录:1024 * 4B = 4KB
页表:1024 * 1024 * 4B = 4MB
共需要:10MB + 4KB + 4B
- 重新查看kern/entry.S和kern/entrypgdir.c中的页表设置。在我们打开分页之后,EIP仍然是一个较低的数字(略超过1MB)。我们在什么时候过渡到在KERNBASE之上的EIP上运行?是什么使我们能够在启用分页和开始在KERNBASE之上的EIP上运行之间继续以低EIP执行?为什么需要这种过渡?
答:在
mov $relocated, %eax
jmp *%eax
之后跳转到KERNBASE之上的EIP运行。
entrypgdir.c中的临时页表entry_pgdir
将 0xf0000000 到 0xf0400000 范围内的虚拟地址转换为物理地址 0x00000000 到 0x00400000,以及将虚拟地址 0x00000000 到 0x00400000 转换为物理地址 0x00000000 到 0x00400000。因此在mem_init
初始化页表页目录之前,mmu使用这个临时页表对虚拟地址进行映射。
在开启分页机制前,eip中存储的是线性地址,在开启分页机制后跳转之前,eip中存储的下一条指令的地址仍是线性地址,mmu会以分页的方式将该地址作为虚拟地址映射为物理地址。而临时页表将虚拟地址 0x00000000 到 0x00400000 转换为物理地址 0x00000000 到 0x00400000,巧妙的避免了错误的发生。
至此实验完成,运行可看到
4. 小结
本实验实现了物理内存与虚拟内存的管理,以及地址空间的映射。具体而言,可分为三个部分。
- 创建管理物理内存的数据结构和函数,可总结为下图
1.每个物理页对应一个PageInfo结构体,结构体中包含引用计数pp_ref与指向下一个结构体的指针pp_link。所有物理页的对应的PageInfo结构体组成pages数组,使用一个链表page_free_list来管理空闲物理页。
2. pa2page()与page2pa()用来实现物理页首地址与PageInfo结构体地址的转换,page_alloc()从page_free_list链表中去取一个PageInfo结构体返回,可以使用该物理页。
3. page_free(struct PageInfo* pp)将pp放回链表page_free_list中,表示该物理页空闲。
- 提供修改页表与页目录的函数,实现虚拟页与物理页的映射。
PDX = LA >> 22;
PTX = (LA >> 12)&0x3FF;
PDE = ∗(CR3 + 4 ∗ PDX);
PTE = ∗((PDE&0xFFFFF000) + 4 ∗ PTX);
PA =(PTE&0xFFFFF000) + (LA&0xFFF);
1.page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)。提供物理页与虚拟地址的映射,通过页目录指针pgdir与虚拟地址结合找到对应的页表项(PTE),将物理页首地址结合权限perm存储到该PTE中。
2.page_remove(pde_t *pgdir, void *va)。解除物理页与虚拟地址va的映射关系,对应PTE清零。
- 将虚拟地址空间映射到物理内存
ps:页目录与页表为何在UVPT处?这是因为在./kern/pmap.c
文件中的mem_init
函数中的代码
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
我们为页目录分配了一个物理页然后将其初始化为0,同时将这个物理页的首地址存入页目录的PDX(UVPT)
。按照页访问规则,访问UVPT-ULIM
地址处的内存时
就可访问任意页表的任意页表项(PTE)的物理地址。