MIT_JOS_Lab2

内存管理

内存管理的内容

内核页表机制的启动过程: 详细内容可以这篇博客 传送门 ,这里我们简单概述下, BIOS 与 boot loader 都是工作在实模式下的, 启用页表机制是在 entry.S 文件中启动的, 并且将页目录的物理地址存入 CR3 寄存器中, 这是一个静态初始化的页目录与页表, 映射的内存空间为 4MB, 但是我们知道, 内核的大小远大于 4MB, 这 4MB 的部分是用来执行 KERNBASE 上面 4MB 部分的代码, 这一部分代码一定包含了下面的构造真正的内核页表部分的代码.

还有一点就是我们目前已经得到了物理与虚拟地址之间的一个映射, 那就是内核本身的映射, 通过 entry.S 里面使用的初始页表.

这里我不想按照课程的Exercise 来解释, 而是按照操作系统启动的顺序来解释, 我们知道在倒入 ELF 文件之后, 我们从 boot loader 跳转到了 kern/init.c 中的 i386_init(void) 函数, 内核开始启动了, 我们可以看到在 Lab2 中这个函数.

void
i386_init(void)
{
	extern char edata[], end[];
	// Before doing anything else, complete the ELF loading process.
	// Clear the uninitialized global data (BSS) section of our program.
	// This ensures that all static/global variables start out zero.
	memset(edata, 0, end - edata);
	// Initialize the console.
	// Can't call cprintf until after we do this!, 控制台初始化
	cons_init();
	cprintf("6828 decimal is %o octal!\n", 6828);
	// 内存初始化, 
	// Lab 2 memory management initialization functions
	mem_init();
}

我们要初始化内存单元, 也就是启用页表机制. 但是这是内核的页表, 目前还没有用户程序. 我们先看一下此时物理内存的状态, 虚拟内存空间的划分在之前已经说过了, 在这张图的左边, 我们看到了物理内存的分布, 并且我们将内核导入到了物理内存的 0x00100000 的位置.

那我们接下来要做什么呢? 首先, 我们的操作系统其实确定哪些物理地址是空闲的, 哪些存储了数据, 这是我们设计操作系统的时候决定的. 然后我们需要从空闲的物理内存中创建一个页目录. 所以我们来到第一个问题:

物理内存管理

我们的操作系统在管理内存的时候, 管理的基本单元是页(4KB大小), 并且物理地址也是需要页对齐的. 对操作系统而言, 它使用下面的结构体来表示一个物理页的状态.

/*
 * Page descriptor structures, mapped at UPAGES.
 * Read/write to the kernel, read-only to user programs.
 * 存出来一个物理页的基本信息, 并不是物理页本身, 而且我们也不必对物理页的所有信息进行描述
 * 这个结构体与物理页一一对应, 可以通过 page2pa() 获得一个物理页描述符的物理地址
 */
// 描述了一个物理页的状态
struct PageInfo {
    // 这是一个虚拟地址, 指向一个物理页描述符结构体
	// Next page on the free list.
	struct PageInfo *pp_link;
	// 表示有多少个虚拟地址指向该页
	uint16_t pp_ref;
};

物理页的状态可以是空闲的, 此时 pp_ref = 0, 也可以是被占用, 此时 pp_ref = 1, 那么我们就会想到另一个问题, 这个物理页的地址是多少呢? 其实很简单, 因为物理页也是页对齐(PGSIZE alligned)的.

// 注意 pages 的声明, struct PageInfo *pages; 是一个链表, pages 是链表头部
// 所以 pp 减去 pages(基地址)得到第几个物理页, 然后 << PGSHIFT 就可以得到 pp 的物理地址,
// 因为物理地址也是 PGSIZE 对齐的
static inline physaddr_t page2pa(struct PageInfo* pp)
{
	return (pp - pages) << PGSHIFT;
}

接下来我们看一下一些变量的声明. 他们很重要,

size_t npages;			// Amount of physical memory (in pages), 表示物理内存的大小
pde_t *kern_pgdir;		// Kernel's initial page directory, 内核的页目录
struct PageInfo *pages;		// Physical page state array, 物理页状态数组, 记录了物理页的状态, 
// 问题: 如何获得物理页的物理地址, 根据下面的内容我们直到, 直接使用 pages 数组的下标就可以访问物理地址了, 因为物理页以及以 PGSIZE 为单位划分
static struct PageInfo *page_free_list;	// Free list of physical pages, 空闲物理页链表, 将空闲的物理页连成一个链表

接下来我们看内存初始化的第一个函数. 按照我们操作系统开机的流程, 第一个就是初始化一个页目录.

	// create initial page directory.
	// 分配一个物理页大小的虚拟空间, 对于页目录来说, 这里使用 boot_alloc
	kern_pgdir = (pde_t*)boot_alloc(PGSIZE);
	// 初始化页目录
	memset(kern_pgdir, 0, PGSIZE);

那么问题来了, 在哪里创建这个页目录呢? 在刚才的 中我们已经知道了虚拟内存与物理内存的布局. 接下来, 我们使用下面的函数, 分配PGSIZE(4KB) 大小的内存给页目录.

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: 根据 ELF 文件的格式, 这里就是内核数据段的末尾
    // 也可以根据前面所使用的查看内核 ELF 文件的格式来查看, 得到 bss 段的内容如下:
    // Idx Name          Size      VMA       LMA       File off  Algn
    // 9 .bss          00000648  f0113060  00113060  00014060  2**5
    // 内核文件在虚拟内存中, 数据段在代码段的上面, bss 就是数据段的结尾
	// the first virtual address that the linker did *not* assign
	// to any kernel code or global variables.
	if (!nextfree) {
		extern char end[];
		// 将地址 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.
	// 这里的 free memory 是在 KERNBASE 上面的虚拟地址空间中的 free memory, 也就是内核的数据段
	result = nextfree;
    // 这里是一个虚拟地址
	if(n > 0)
	{
		nextfree = ROUNDUP(result+n, PGSIZE);
        // 这里相当于在 free memory 分出一部分内存, 以 PGSIZE 对齐
	}
	cprintf("boot_alloc memory at %x, next memory allocate at %x\n", result, nextfree);
	return result;
}

这里我们在内核的虚拟空间中给页目录分配了地址, 它在内核结尾的上面, 然后我们得到并用 memset 初始化了这个页目录. 然后我们向页目录中加入第一项, 然后我们必须要知道的一点是, 页目录的大小是 4KB, 页目录本身也是一个页表, 页表的大小是 4KB, 一共有 \(2^{10}\) 个页表, 所以我们通常说页表的大小是 4MB.

这里我们知道页目录的大小和一个页表的大小都是 4KB, 页目录自身也是一个页表, 他可以映射的虚拟空间大小是 4MB, 这 4MB 就是页表, 页表虽然在物理地址上不一定是连续的, 因为物理地址是按需分配, 但是在虚拟地址上一定是连续的. 下面我们就是映射了页表的虚拟地址也就是 UVPT. 我们可以通过这个虚拟地址来访问页表.

	// Recursively insert PD in itself as a page table, to form
	// a virtual page table at virtual address UVPT.
	// (For now, you don't have understand the greater purpose of the
	// following line.)
	// Permissions: kernel R, user R
	// 左边是 PDX[UVPT] 是虚拟地址UVPT 对应的页目录项, 页目录项的内容应该是一个页表的物理地址
	// 右边是我们刚才获得内核页目录的物理地址, 
	// 所以 kern_pgdir 现在既是页目录, 也是页表, 虚拟地址 UVPT 对应的页表刚好就是 kern_pgdir,
	kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;

到此, 我们分配了一个页目录, 要想完成虚拟地址到物理地址的映射, 我们知道映射的基本单位是页(4KB), 所以我们用下面这个pages 数组来管理物理页,

	// 下面是描述物理内存的状态
	// 物理页描述符的数组, 
	// 这里的 npages 我没写, 是通过 i386_detect_memory(); 得到的,
	// 这里我们还是在内核的顶部, 分配了一个数组, 也就是说在虚拟地址空间与物理地址上都划分了这一块来存储 pages 数组.
	pages = (struct PageInfo*)boot_alloc(sizeof(struct PageInfo) * npages);
	// pages 是物理内存状态数组
	memset(pages, 0, sizeof(struct PageInfo) * npages);

接下来对物理内存的描述进行初始化, 在分配物理内存, 设置页目录机制之前, 物理内存中有很多地方已经不是空闲的了, 所以对 pages 的描述就会有所改变, 比如说物理内存中分配给 IO 段的内存, 直到内核的数据段的末尾都是已经已经分配过的物理内存, 这一部分已经被分配, 所以不在空闲链表中. 下面就初始化 pages 数组,

void page_init(void)
{
    // 获取 IO 数据段的物理地址
	size_t io_hole_start_page = (size_t)IOPHYSMEM / PGSIZE;
	// If n==0, returns the address of the next free page without allocating anything.
	// 使用 boot_alloc(0) 找出未被分配的物理地址, 就是内核分配的末尾的物理地址, 就是在 pages 和 页目录后面
	size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE;

	size_t i;
	for (i = 0; i < npages; i++) {
		if(i == 0){
			pages[i].pp_ref = 1;
			pages[i].pp_link = NULL;
		}
		if(io_hole_start_page <= i && i < kernel_end_page)
		{
			pages[i].pp_ref = 1;
			pages[i].pp_link = NULL;
		}
		else
		{
			pages[i].pp_ref = 0;
			// 这一步是形成空闲链表, 注意这个链表在构建的时候是逆向的,
      // page_free_list 一直指向第一个空闲页
			pages[i].pp_link = page_free_list;
			page_free_list = &pages[i];
		}
		
	}
}

分配了物理页空闲链表就实现了对物理内存的更好的管理, 我们不仅知道了物理内存的状态, 还知道了哪些物理页是空闲的, 注意: page_free_list 空闲链表构成的方法是通过 pages[i] 的 pp_link 构造的, page_free_list 是空闲链表的开头, 内容是物理页描述符, 而不是真正的物理页. 描述了物理页之后, 分配一个物理页就很简单了, 因为 page_free_list 一直指向第一个空闲页, 这里不难理解

struct PageInfo * page_alloc(int alloc_flags)
{
	// Fill this function in
	struct PageInfo *new_alloc = page_free_list;
	if(new_alloc == NULL)
	{
		// 分配失败, 没有空闲的空间
		cprintf("page_alloc: out of free memory\n");
	}
    // 将 page_free_list 向后移动一步, 表示一个物理页被占用
	page_free_list = new_alloc->pp_link;
    // 这一页已经被分配了, 所以 pp_link == NULL
	new_alloc->pp_link = NULL;
	if(alloc_flags & ALLOC_ZERO)
	{
    // 关于这里为什么能使用 page2kva 获得虚拟地址, 我的理解是, 在上一个函数中, 其实只有 Kernel 后面的
    // 物理内存才能使用
		memset(page2kva(new_alloc), 0, sizeof(struct PageInfo));
	}
	return new_alloc;
}

我们是重要注意的是, 物理内存的描述就是 pages, 所以释放与分配的过程对 pages 的操作相反:

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.
	if(pp->pp_ref == 0 && pp->pp_link == NULL)
	{
		pp->pp_link = page_free_list;
		page_free_list = pp;
	}
	else
	{
        // 释放一个页之前, 要判断是否被使用
		panic("page_free: pp->pp_ref is nonzero or pp->pp_link is not NULL\n");
	}
	
}

这一部分我们主要看了两部分, 物理内存的管理, 主要是物理内存空间的初始化, 也就是 pages 数组初始化, 还有就是在物理内存上分配一页, 或者释放一页, 这里我们都是直接传入 struct PageInfo 结构体的指针, 因为我们用这个结构体来描述一个物理页. 另一个就是我们在内存中初始化了一个页目录. 这个页目录在物理内存中也是存在的, 并且就在内核的上方.

虚拟内存管理

虚拟地址, 线性地址, 物理地址

对于 x86 而言, 虚拟地址段选择器与段内偏移, 准确的说, 线性地址就是段内偏移, 也就是进行页表映射之前的地址, 物理地址是访问 RAM 内存的地址, 是转换后的地址, 对于xv6 JOS 而言, 没有使用段机制, 在前面的 Lab1 中在页表机制启动之前, boot loader 阶段 cpu 仍然工作在实模式下. 启动页表机制与在保护模式下, 程序的内存引用, 也就是程序中的偏移与指针,都是建立在虚拟地址上的. 而进入了保护模式之后, 内核有时候有需要读或者更改他知道的物理地址对应的内存, 这时候就需要将物理地址再转化为虚拟地址, 然后引用虚拟地址.

现在我们已经得到了一个页目录了, 现在要向页目录添加内容, 注意页目录的页目录项是页表的物理地址,

下面的内容主要是页表的管理, 最重要的工作就是将虚拟地址与物理地址对应起来, 在保护模式下, 对应的方式就是通过页表来实现. 需要实现的函数如下:

在这之前, 我们需要意识到, 指针是虚拟地址, 而不是物理地址, 那么我们在使用指针修改数据的时候, 需要先得到虚拟地址. 下面一步十分重要, 我们知道对于一个虚拟地址 va, 我们要先获得它的页目录项, 然后通过页目录项得到它的那一个页表的物理地址, 然后找到页表项, 页表项的内容就是 va 对应的物理地址. 现在我们要修改这个页表项的内容, 必须要知道的是这个页表项的虚拟地址, 而不是物理地址.

// 对于一个虚拟地址找到页表中对应页表项地址, 页表项的内容就是
pte_t* pgdir_walk(pde_t* pgdir, const void* va, int create)
{
	// Fill this function in
	pte_t* result;
	// 如果页目录对应的目录项内容为空, 表示页表中该页还未创造
	if (!(pgdir[PDX(va)] & PTE_P)) {
		// create == 0, return NULL
		if (create == 0) {
			return NULL;
		}
		else {
			// 新建一页, 在物理内存中新分配一页
			struct PageInfo* temp = page_alloc(ALLOC_ZERO);
			if (temp == NULL) {
				return NULL;
			}
			else {
				temp->pp_ref += 1;
				// 新分配的一页是页表, 下面设置页目录 目录项的内容
				pgdir[PDX(va)] = page2pa(temp) | PTE_P | PTE_W | PTE_U;
			}
		}
	}
	// (pgdir[PDX(va)])这一步是根据页目录项得到 va 对应的页目录项的内容
	// 该目录项存储的是 va 对应的页表的物理地址, 还有权限等等, 通过 PTE_ADDR 获取这个物理地址
	// 将这个物理地址转化为虚拟地址, 再加上 10 位的页表项, 就是虚拟地址, 这个虚拟地址就是 va 对应的那个页表的虚拟地址
        // 这样做能够保证页表的虚拟地址都在 UVPT 中, 并且是按照 4B对齐的, 这里和前面 UVPT 构造页表的虚拟地址真的很神奇
	result = (pte_t*)KADDR(PTE_ADDR(pgdir[PDX(va)])) + PTX(va);
	return result;
}

这个过程可以用下面这张图来表示.

页表映射

整个过程中最重要的数据是:

变量及含义 内容 例子
*pgdir, 页目录的起始地址 页目录的内容应该是对应的页表的物理地址 pgdir[PDX(va)] = page2pa(temp)
pgdir[PDX(va)] 页表的物理地址与权限信息 页目录项的内容, (PTE_ADDR(pgdir[PDX(va)])) 这里使用PTE_ADDR的原因也是,目录项中不仅有物理地址,还有权限信息,
pgdir_walk(pgdir, (void *)temp_va, 1) 页表的虚拟地址 存储的是虚拟地址对应的物理地址 pte_t *pte = pgdir_walk(pgdir, (void *)temp_va, 1); //获取当前va对应的页表的页表项地址

下面这个函数就是如何在虚拟地址与物理地址之间添加一个映射, 这里最核心的就是上面函数pgdir_walk 做的事情, 先获得 va对应的页表项的指针, 页表项的内容就是物理页的物理地址.

// 将物理地址与虚拟地址对应起来
static void boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
	// Fill this function in
	size_t pgs = size / PGSIZE;
	if (size % PGSIZE != 0) {
		pgs++;
	}	
	for (int i = 0; i < pgs; i++) {
		pte_t *pte = pgdir_walk(pgdir, (void *)va, 1);
		if (pte == NULL) {
			panic("boot_map_region(): out of memory\n");
		}
		*pte = pa | PTE_P | perm;
		pa += PGSIZE;
		va += PGSIZE;
	}
}

在建立映射之后, 我们还要考虑的一个问题是, 插入一个物理页, 上面的函数是知道一段数据的物理地址和虚拟地址的时候进行映射, 现在我们只知道一个虚拟地址, 以及一个物理页(物理页其实很容易获得, 在物理内存管理那里), 怎么建立他们之间的映射呢? 其实就是插入一个物理页.

// 向页表中插入一页, 本质是将一个物理页与虚拟地址对应起来
int page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
	// Fill this function in
	// 查找虚拟地址对应的页表的页表项地址
	pte_t *temp = pgdir_walk(pgdir, va, 1);
	if(temp == NULL) {
		return -E_NO_MEM;
	}
	pp->pp_ref += 1;
	// *temp 是页表项存储的数据, 判断 PTE_P 位是否为 1, 
	// 为 1表示页表中该页表项已经存储了物理地址, 虚拟地址 va 已经被映射
	if((*temp) & PTE_P) {
		page_remove(pgdir, va); 
	}
	// pp - pages 实际是物理页号, 将其转化为物理地址
	physaddr_t pa = page2pa(pp); 
	// 更新页表中页表项的数据
	*temp = pa | perm | PTE_P;    //修改PTE
	pgdir[PDX(va)] |= perm;
	
	return 0;
}

这里也要注意一下, (*temp) & PTE_P) 表示页表项已经存了映射了, 所以要删除, 删除映射就是下面这个函数,

// 删除虚拟地址对应的物理页

void
page_remove(pde_t* pgdir, void* va)
{
	// Fill this function in

	// 查找虚拟地址对应的页表项地址
	pte_t* temp_pte = pgdir_walk(pgdir, va, 0);
	if (temp_pte == NULL) {
		return;
	}
	// 找出虚拟地址对应的物理页
	struct PageInfo* pp = page_lookup(pgdir, va, &temp_pte);
	if (pp == NULL) {
		return;
	}
	page_decref(pp);
	// 页表项还是存在的, 但是页表项的内容为空, 这样再使用 page_lookup也不会找到对应的物理页
	*temp_pte = 0;
	tlb_invalidate(pgdir, va);
}

look up, 找出虚拟地址对应的物理页的过程, 这个过程相当于将虚拟地址与物理页之间的映射关系复现了一遍, 如果理解其中的关键参数, 那么整个过程其实十分的流畅与明确,

// 返回虚拟地址对应的物理页面, 也就是物理页号
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t* *pte_store)
{
	// Fill this function in
	// 注意这里传进来了一个二级指针, 目的是修改原指针
	pte_t *temp_pte = pgdir_walk(pgdir, va, 0);

	if (temp_pte == NULL) {
		return NULL;
	}
    // 表示页表项的内容为空
	if (!(*temp_pte) & PTE_P) {
		return NULL;
	}
	// 这里是从页表项的内容中中获取物理地址
	physaddr_t pa = PTE_ADDR(*temp_pte);
	struct PageInfo* pp = pa2page(pa);								//物理地址对应的PageInfo结构地址
    // 在后面的函数调用中, 我们知道这里的 pre_store, 与 temp_pte 是对于同一个虚拟地址对应的页表项地址, 所以应该是相同的
	if (pte_store != NULL) {
		*pte_store = temp_pte;
	}
	return pp;
}

内核空间映射

在 JOS 中, 进程的地址空间被分为两部分, 用户进程与内核进程, 这个分布在 inc/memlayout.h 描述的十分详细, 划分空间的主要原因是内核程序与用户程序的功能不同, 内核程序主要负责资源的调度与内存的分配, 而这里的很多操作对用户来说是透明的, 所以对内核空间与用户空间的权限也是不同的, 内核的代码与数据用户是没有权限修改与访问的, 但是 [UTOP,ULIM] 这一部分是页表与页目录部分, 用户是可读的.

我们知道, 操作系统写到目前为止, 我们使用的虚拟地址到物理地址的映射都是通过 entrypgdir 的, 也就是 KERNBASE 到 物理地址0 的映射. 这个页表的权限是可写的, 这是危险的权限, 并且它只映射了一部分, 所以我们要映射其他部分, 并且修改一些权限. 第一个就是使得kern_pgdir自身作为一个页表.

	kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;

Exercise 5. Fill in the missing code in mem_init() after the call to check_page().

对于后面的问题. 其实这部分就是将虚拟地址空间与物理地址空间进行映射, 我们重新映射了 pages 数组, 内核栈, 和内核文件.

	// 将虚拟地址的  UPAGES 与 pages映射
	boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
	// 这一部分是内核栈部分, KSTACKTOP 是内核栈顶部的虚拟地址
	boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
	// 这一部分是内核的 kernbase 向上部分, 也就是内核部分的数据, 
	boot_map_region(kern_pgdir, KERNBASE, 0xffffffff-KERNBASE, 0, PTE_W);
  1. What entries (rows) in the page directory have been filled in at this point? What
    addresses do they map and where do they point? In other words, fill out this table
    as much as possible:
Entry Base Virtual Address Points to (logically):
1023 0xffc00000 Page table for top 4MB of phys memory
... ... ...
960 0xf0000000 Page table for [0,4)MB of phys memory
959 0xefc00000 Kernel Stack and Invalid Memory
... ... ...
957 0xef400000 UVPT, User read-only virtual page table
956 0xef000000 UPAGES, Read-only copies of the Page structures

上面的表格对应来自上面使用 boot_map_region 的三种情况, 分别是,

  1. UPAGES 与 pages映射, 将UPAGES 与 pages的物理地址 map 起来, pages本身是虚拟地址, 页表是在内存中的, 但是是使用 pages描述的
  2. UVPT, 将页目录与虚拟地址对应
  3. 内核栈, 将内核栈的虚拟地址部分与物理地址部分对应
  4. KERNBASE 上面的一部分, 使用的是 boot_map_region, 直接将地址对应, 映射的物理内存的大小为256MB.

中间的几个问题都十分简单,我们来看看最后一个

Revisit the page table setup in kern/entry.S and kern/entrypgdir.c. Immediately after we turn on paging, EIP is still a low number (a little over 1MB). At what point do we transition to running at an EIP above KERNBASE? What makes it possible for us to continue executing at a low EIP between when we enable paging and when we begin running at an EIP above KERNBASE? Why is this transition necessary?

这一步我们需要理解 JOS 页表机制的启动过程, 在 Bootloader 之后, 进入内核开始执行, 这一部分的代码在物理内存中就是在 1MB 上方, 然后是在一部分代码中, 首先执行 entry.S , 该过程通过 kern/entrypgdir.c 建立了一个临时的页目录, 这个页目录映射的空间大小为 4MB, 但是建立页目录的过程与页目录建立完, 我们CPU使用的都是一个 low EIP, 可以看做此时, EIP 由物理地址表示. 顺序上, 我们设置了 cr0 寄存器后, 表示开启页表机制.

  mov $relocated, %eax
  jmp *%eax

之后,eip到了KERNBASE之上. 既能在低eip分页,也能在高位上运行的原因在于, kern/entrypgdir.c 建立的页表不仅虚拟地址[KERNBASE, KERNBASE+4MB) 被映射到物理地址[0, 4MB),同时虚拟地址[0, 4MB)也被映射到同一段物理地址. 所以当eip在物理地址[0, 4MB)上时,它既在低位的[0, 4MB),也在高位[KERNBASE, KERNBASE+4MB)。这是一个过渡的状态,过一会儿页索引会被加载进来而虚拟地址[0, 4MB)就被舍弃了.
最后我们看一下, 完成页表之后虚拟内存与物理内存的状态, 以及他们之间的映射关系.

posted @ 2020-04-04 21:17  虾野百鹤  阅读(511)  评论(0编辑  收藏  举报