目录
2.4.6 boot_map_region_large( ) 函数
0 准备文件
和实验一类似,注意git的冲突处理,此处不再赘述。
1 物理页管理
The operating system must keep track of which parts of physical RAM are free and which are currently in use. JOS manages the PC's physical memory with page granularity so that it can use the MMU to map and protect each piece of allocated memory.
You'll now write the physical page allocator. It keeps track of which pages are free with a linked list of struct Page
objects, each corresponding to a physical page. You need to write the physical page allocator before you can write the rest of the virtual memory implementation, because your page table management code will need to allocate physical memory in which to store page tables.
这个实验主要是为了让读者了解内核RAM如何分配和释放内存,首先要手动写一个物理页面分配器(physical page allocator),这个分配器用来维护一个数据结构,该数据结构记录了物理内存空闲页面的链表。
Exercise 1. In the file kern/pmap.c, you must implement code for the following functions
(probably in the order given).
boot_alloc()
mem_init() (only up to the call to check_page_free_list(1))
page_init()
page_alloc()
page_free()
check_page_free_list() and check_page_alloc() test your physical page allocator. You should boot JOS
and see whether check_page_alloc() reports success. Fix your code so that it
passes. You may find it helpful to add your own assert()s to verify that your assumptions are correct.
操作系统必需跟踪哪些物理 RAM 是空闲的,哪些正在使用。这个 exercise 主要编写物理页面分配器。它利用一个 PageInfo 结构体组成的链表记录哪些页面空闲,每个结构体对应一个物理页。因为页表的实现需要分配物理内存来存储页表,在虚拟内存的实现之前,我们需要先编写物理页面分配器。
1.1 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.
if (n == 0) {
return nextfree;
}
result = nextfree;
nextfree += ROUNDUP(n, PGSIZE);
return result;
}
要写出这个函数需要搞懂几个概念,n是什么,end是什么,ROUNDUP宏是什么,nextfree是什么:
首先,n是分配n个byte,意思就是现在要分配n个byte,占用n个byte。
第二,end是指向内核的 bss 段的末尾的一个指针。
进入 $ROOT/obj/kern 目录,键入objdump -h kernel,查看文件结构可以发现,bss已经位于内核最终,所以,end是向内核的 bss 段的末尾的一个指针,也就是第一个未使用的虚拟内存地址。
第三,ROUNDUP宏。
进入 $ROOT/inc 目录下的 types.h 可以看到ROUNDUP的原定义。
// Round up to the nearest multiple of n
#define ROUNDUP(a, n) \
({ \
uint32_t __n = (uint32_t) (n); \
(typeof(a)) (ROUNDDOWN((uint32_t) (a) + __n - 1, __n)); \
})
定义为:用多个大小为n的页面聚集最近的a个Byte。(Round up to the nearest multiple of n)
最后,nextfree 是下一个空闲地址的虚拟地址。
函数开头写到:
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
如果 nextfree 是第一次被使用,那么初始化这个变量,如果不是第一次被使用,那么就找到下一个空闲地址的虚拟地址。找到下一个空闲地址的虚拟地址的方法是 用 PGSIZE 为大小的页面聚集 以end地址处的内容 个byte。
PGSIZE是一个物理页面的大小 4KB=4096B,定义在 inc/mmu.h 中,其中还有一个后面要用的重要常量PTSIZE,为一个页表对应实际物理内存的大小 1024*4KB=4MB
那么接下来的情况应该是 nextfree 不是第一次被使用:
if (n == 0) {
return nextfree;
}
result = nextfree;
nextfree += ROUNDUP(n, PGSIZE);
return result;
1)如果分配的n是0的话,那么就意味着不用分配了,直接把nextfree的指针输出即可。
2)如果分配的n不是0,那么就意味着有分配内容,就要 用 PGSIZE 为大小的页面聚集 n 个byte。那么下一个空闲页面(nextfree)就是 从当前开始算起加上 分配的页面之后的页面序号。
1.2 mem_init ( )函数
// Set up a two-level page table:
// kern_pgdir is its linear (virtual) address of the root
//
// This function only sets up the kernel part of the address space
// (ie. addresses >= UTOP). The user part of the address space
// will be set up later.
//
// From UTOP to ULIM, the user is allowed to read but not write.
// Above ULIM the user cannot read or write.
void
mem_init(void)
{
uint32_t cr0;
size_t n;
// Find out how much memory the machine has (npages & npages_basemem).
i386_detect_memory();
// Remove this line when you're ready to test this function.
panic("mem_init: This function is not finished\n");
//
// create initial page directory.
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);
//
// 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
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
//
// 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.
// Your code goes here:
pages = (struct PageInfo *) boot_alloc(npages*sizeof(struct PageInfo));
memset(pages, 0, npages*sizeof(struct PageInfo));
//
// Now that we've allocated the initial kernel data structures, we set
// up the list of free physical pages. Once we've done so, all further
// memory management will go through the page_* functions. In
// particular, we can now map memory using boot_map_region
// or page_insert
page_init();
check_page_free_list(1);
check_page_alloc();
check_page();
//
// Now we set up virtual memory
//
// 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:
//
// 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:
//
// 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:
// Check that the initial page directory has been set up correctly.
check_kern_pgdir();
// Switch from the minimal entry page directory to the full kern_pgdir
// page table we just created. Our instruction pointer should be
// somewhere between KERNBASE and KERNBASE+4MB right now, which is
// mapped the same way by both page tables.
//
// If the machine reboots at this point, you've probably set up your
// kern_pgdir wrong.
lcr3(PADDR(kern_pgdir));
check_page_free_list(0);
// entry.S set the really important flags in cr0 (including enabling
// paging). Here we configure the rest of the flags that we care about.
cr0 = rcr0();
cr0 |= CR0_PE|CR0_PG|CR0_AM|CR0_WP|CR0_NE|CR0_MP;
cr0 &= ~(CR0_TS|CR0_EM);
lcr0(cr0);
// Some more checks, only possible after kern_pgdir is installed.
check_page_installed_pgdir();
}
这个函数不怎么好理解,得一行行硬看:
首先,这个函数用到了刚才提到的数据结构 PageInfo ,就要先看了解到PageInfo的具体定义是什么:
1.2.1 函数中用到的 PageInfo 结构
进入 $ROOT/inc/memlayout.h 中可以看到PageInfo的定义:
struct PageInfo {
// Next page on the free list.
struct PageInfo *pp_link;
// pp_ref is the count of pointers (usually in page table entries)
// to this page, for pages allocated using page_alloc.
// Pages allocated at boot time using pmap.c's
// boot_alloc do not have valid reference count fields.
uint16_t pp_ref;
};
这个 PageInfo 中包括了两个变量:
pp_link:指向下一个空闲的页。
pp_ref:这是指向该页的指针数量。在启动时间时,Pages没有合法的指针,即指针为NULL。
1.2.2 初始化分配首页
// create initial page directory.
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);
文段头部写到,kern_pgdir 是虚拟地址的首部,那么在 kern_pgdir 作为页表头部的指针进行首页的分配。
1.2.3 分配npages大小的数组
分配数组大小为 npages,数据单元是 PageInfo,那么所占用空间就是 : npages*sizeof(struct PageInfo)
将分配完的数据指针放入pages变量中,可以模仿1.2.2 中初始化分配首页的步骤来写下面的代码:
// 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.
// Your code goes here:
pages = (struct PageInfo *) boot_alloc(npages*sizeof(struct PageInfo));
memset(pages, 0, npages*sizeof(struct PageInfo));
only up to the call to check_page_free_list(1),所以我就不往下分析了先。
1.3 page_init( )函数
// Initialize page structure and memory free list.
// After this is done, NEVER use boot_alloc again. ONLY use the page
// allocator functions below to allocate and deallocate physical
// memory via the page_free_list.
//
void
page_init(void)
{
size_t i;
// 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...)
pages[0].pp_ref = 1;
// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
// is free.
for (i = 1; i < npages_basemem; i++) {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
// never be allocated.
for (i = IOPHYSMEM/PGSIZE; i < EXTPHYSMEM/PGSIZE; i++) {
pages[i].pp_ref = 1;
}
// 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?
//
size_t first_free_physical_address = PADDR(boot_alloc(0));
for (i = EXTPHYSMEM/PGSIZE; i < first_free_physical_address/PGSIZE; i++) {
pages[i].pp_ref = 1;
}
for (i = first_free_physical_address/PGSIZE; i < npages; i++) {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
初始化页表结构和空闲页的链表。
这个函数中已经写好的代码片段是假设所有的物理页面都为空的情况,当然这在实际中是不可能的。
所以要初始化空闲页的链表的话就要明确哪些内存是空闲的。
步骤如下:
1)给0页面标记为正在使用的状态。
2)剩下的基础内存是空闲的。
3)IO洞 不能被分配。
4)扩展内存有些被用,有些空闲。
看完了这四句话整个人都不好了,第一句第二句还知道什么意思,剩下的是什么意思呢?
1.3.1 给初始页面标记为正在使用状态
// 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...)
pages[0].pp_ref = 1;
刚开始的时候如果被占用的页面所拥有的指针数量是1,那么模仿下面已存在的代码可以得到以上的这段代码,将指针数量标记为1即可。
1.3.2 剩下的基础内存是空闲的
什么叫基础内存,大概意思就是在头上有一部分空着的吧。所以直接复制下面的那段代码稍作修改就可以了。
// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
// is free.
for (i = 1; i < npages_basemem; i++) {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
1.3.3 IO洞 不能被分配
IO洞是个什么意思?
注释中写道是从IOPHYSMEM到EXTPHYSMEM之间的空间就是IO hole。
// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
// never be allocated.
for (i = IOPHYSMEM/PGSIZE; i < EXTPHYSMEM/PGSIZE; i++) {
pages[i].pp_ref = 1;
}
1.3.4 IO洞后有些被用,有些空闲
首先,如果需要物理地址转虚拟地址可以通过 KADDR(physical_address)
,反之用PADDR(virtual_address)
。
其次,梳理一下上面的几条,页面中第一个页是被占用的,不可被使用的,从第二个页面到Base页结束是可被使用的,其后,IO洞里面的页不可被使用,再随后,也就是文中提到的扩展内存之后,有些可以被用,有些空闲,这就取决于被用和不可被用的临界点在哪里。
那么我们想到了可被使用的第一个页面的位置可以用 boot_alloc() 函数查询到,如果说想找到第一个页面的位置把参数写0就可以了。在这里boot_alloc()获取的是第一个可用的虚拟地址,那么需要把它转换为物理地址,用到了PADDR这个宏。
所以,意思就是从 EXTPHYSMEM 到 PADDR(boot_alloc(0)) 都不可使用,以后可以使用。
// 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?
//
size_t first_free_physical_address = PADDR(boot_alloc(0));
for (i = EXTPHYSMEM/PGSIZE; i < first_free_physical_address/PGSIZE; i++) {
pages[i].pp_ref = 1;
}
for (i = first_free_physical_address/PGSIZE; i < npages; i++) {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
1.4 page_alloc( )函数
先看注释,该函数是分配物理页面。
1)这个函数有一个参数是 alloc_flags ,如果 alloc_flags & ALLOC_ZERO 则把所有返回的物理页面都填充 ‘ \0’。
2)page2kva 函数的作用就是通过物理页获取其内核虚拟地址,分配后的页面需要将 pp_link 指针设置为 NULL。
// 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
struct PageInfo *
page_alloc(int alloc_flags)
{
// Fill this function in
if (page_free_list == NULL) {
return NULL;
}
struct PageInfo *allocated_page = page_free_list;
page_free_list = page_free_list->pp_link;
allocated_page->pp_link = NULL;
if (alloc_flags & ALLOC_ZERO)
memset(page2kva(allocated_page),'\0', PGSIZE);
return allocated_page;
}
1.5 page_free( )函数
释放页面
如果pp->pp_ref不为0说明这个页面不是空的,如果pp->pp_link不为NULL说明接下来链表还有空页面,意思就是不能free,否则会出现问题。
如果排除以上条件,那么就可以free,free的步骤就是把pp_link从以前的NULL变成现在下一个可用页面,也就是page_free_list。
//
// Return a page to the free list.
// (This function should only be called when pp->pp_ref reaches 0.)
//
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){
panic("ARE YOU SERIOUS? Double check failed when dealloc page");
return;
}
pp->pp_link = page_free_list;
page_free_list = pp;
}
1.6 总结
在这个练习里面总共写了四个函数。boot_alloc( )函数,mem_init ( )函数,page_init()函数,page_alloc( )函数,page_free( )函数。
纵向分析一下这五个函数,但从名字来说可以分三类,boot,memory和page层面。
boot,启动层面,boot_alloc() 做了启动时先分配了N个byte空间的功能。这一步做完就有了一定的空间。
memory,内存层面,mem_init() 引入了数据结构Page_Info,做完这一步就将引入的空间在内存中划分成了页的层次。刚才分配了n个byte的空间,那么在这里就同样对应着npage*sizeof(Page_Info)个byte的页面,也就是npage个页面。
page,页面层面,page_init()是页面的初始化,初始化做的就是标记页表中哪些可以用,哪些不能用,把能用的链接起来。page_alloc( )就是分配这些能用的页面,page_free()就是释放掉页面。
所以,事实上,这个实验基本就完成了内存层面初始化页面以及页面的基本操作(包括删除和分配)。
退回$ROOT,现在应该make grade是20分,第一个练习完成。
2 虚拟内存
2.1 物理内存和虚拟内存的转换
这张图大概就帮我复习了一下物理内存和虚拟内存的转换:
我们代码中的 C 指针就是虚拟地址(Virtual Address)中的 offset,通过描述符表和段选择子(Selector),通过分段机制转换为线性地址(虚拟地址),因为JOS中设置的段基址为0,所以线性地址就等于offset。在未开启分页之前,线性地址就是物理地址。而在我们开启分页之后,线性地址经过 CPU 的MMU部件的页式转换得到物理地址。
开启分页后,当处理器碰到一个线性地址后,它的MMU部件会把这个地址分成 3 部分,分别是页目录索引(Directory)、页表索引(Table)和页内偏移(Offset), 这 3 个部分把原本 32 位的线性地址分成了 10+10+12 的 3 个片段。每个页表的大小为4KB(因为页内偏移为12位)。
举例:现在要将线性地址 0xf011294c 转换成物理地址。首先取高 10 位(页目录项偏移)即960(0x3c0),中间 10 位(页表项偏移)为274(0x112),偏移地址为1942(0x796)。
首先,处理器通过 CR3 取得页目录,并取得其中的第 960 项页目
录项,取得该页目录项的高 20 位地址,从而得到对应的页表物理页的首地址,再次取得页表中的第274项页表项,并进而取得该页表项的首地址,加上线性地址的低12位偏移地址1942,从而得到物理地址。
由上面也可知道,每个页目录表有1024个页目录项,每个页目录项占用4字节,一个页目录表占4KB内存。而每个页目录项都指向一个有1024个页表项的页表,每个页表项也占用4字节,因此JOS中页目录和页表一共要占用 1025 * 4KB = 4100KB 约4MB的内存。而通常我们说每个用户进程虚拟地址空间为4GB,其实就是每个进程都有一个页目录表,进程运行时将页目录地址装载到CR3寄存器中,从而每个进程最大可以用4GB内存。在JOS中,为了简单起见,只用了一个页目录表,整个系统的线性地址空间4GB是被内核和所有其他的用户程序所共用的。
分页管理中,页目录以及页表都存放在内存中,而由于CPU 和内存速度的不匹配,这样地址翻译时势必会降低系统的效率。为了提高地址翻译的速度,x86处理器引入了地址翻译缓存TLB(旁路转换缓冲)来缓存最近翻译过的地址。当然缓存之后会引入缓存和内存中页表内容不一致的问题,可以通过重载CR3使整个TLB内容失效或者通过 invlpg 指令。
2.2 uintptr_t 和 physaddr_t
uintptr_t表示不透明的虚拟地址,physaddr_t表示物理地址。
如果要引用虚拟地址,需要将类型先由整型转换成指针类型。
Assuming that the following JOS kernel code is correct, what type should variable x
have, uintptr_t
or physaddr_t
?
mystery_t x;
char* value = return_a_pointer();
*value = 10;
x = (mystery_t) value;
很显然,这个应该是 uintptr_t ,因为内核中的数据操作数据都是以内核虚拟地址进行。
2.3 Reference Counting
这里提到了虚拟内存空间,其中提到了UTOP这样的地址,首先可以在 inc/memlayout.h 找到虚拟内存的布局。
/*
* 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 ------------> +------------------------------+ --+
*
* (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped.
* "Empty Memory" is normally unmapped, but user programs may map pages
* there if desired. JOS user programs map pages temporarily at UTEMP.
*/
可以从这个页表结构找一下虚拟地址是如何对应上的物理地址:
首先,根据虚拟地址vaddr = UVPT[31:22] PDX|PTX|00 在虚拟内存空间里查询,根据二级页表翻译机制:
1)页目录。系统首先取出vaddr的前10位,即UVPT[31:22],去页目录里面查询,并取得其中的页目
2)页表项和偏移地址。再取出vaddr中间10位,即PDX,和vaddr的后面的偏移值,组合之后在页表找到最终的页面物理地址。
由文段可知,地址从 [0,ULIM)区间为分配给用户的,其余的为分配给操作系统的。
2.4 页表管理
Exercise 4. In the file kern/pmap.c, you must implement code for the following functions.
pgdir_walk()
boot_map_region()
boot_map_region_large() // Map all phy-mem at KERNBASE as large pages
page_lookup()
page_remove()
page_insert()
check_page()
, called from mem_init()
, tests your page table management routines. You should make sure it reports success before proceeding.
现在要写的代码在 kern/pmap.c 中,都是关于虚拟地址和物理地址转换的代码。
每一个物理页面对应一个Page的结构体和一个物理页号PPN(Pysical Page Number)和物理首地址。
在 pmap.h 中有这样几个定义:
static inline physaddr_t
page2pa(struct PageInfo *pp)
{
return (pp - pages) << PGSHIFT;
}
static inline struct PageInfo*
pa2page(physaddr_t pa)
{
if (PGNUM(pa) >= npages)
panic("pa2page called with invalid pa");
return &pages[PGNUM(pa)];
}
static inline void*
page2kva(struct PageInfo *pp)
{
return KADDR(page2pa(pp));
}
一个Page对应的物理地址就是page2pa(*PageInfo),
而一个物理地址对应的Page则是pa2page(physaddr_t),
最后,将一个Page对应的虚拟内核地址的函数是page2kva(*PageInfo)。
以上的三个函数都是可以在下面我们要写的函数中用到的。
2.4.1 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.
//
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
pde_t pde = pgdir[PDX(va)];
if (!(pde & PTE_P))
if (!create)
return NULL;
else {
struct PageInfo *pp = page_alloc(true);
if (!pp)
return NULL;
(pp->pp_ref)++;
pgdir[PDX(va)] = page2pa(pp) | PTE_U | PTE_P | PTE_W;
return (pte_t *) page2kva(pp) + PTX(va);
}
return (pte_t *) KADDR(PTE_ADDR(pde)) + PTX(va);
}
这个函数的作用是实现虚拟地址到物理地址的翻译过程。
在提示3里面写道 inc/mmu.h 中有很多可以调用的宏,那么就可以先来看一下这个文件中的内容:
// A linear address 'la' has a three-part structure as follows:
//
// +--------10------+-------10-------+---------12----------+
// | Page Directory | Page Table | Offset within Page |
// | Index | Index | |
// +----------------+----------------+---------------------+
// \--- PDX(la) --/ \--- PTX(la) --/ \---- PGOFF(la) ----/
// \---------- PGNUM(la) ----------/
这段注释注意如下几个宏的使用:
PDX:一个虚拟地址的页目录索引,也是地址的前10位。
PTX:一个虚拟地址的页表索引,也是地址的中间10位。
PGOFF:一个虚拟地址的页偏移,也是地址的后12位。
使用方法如注释就可以得知。
// Page table/directory entry flags.
#define PTE_P 0x001 // Present
#define PTE_W 0x002 // Writeable
#define PTE_U 0x004 // User
#define PTE_PWT 0x008 // Write-Through
#define PTE_PCD 0x010 // Cache-Disable
#define PTE_A 0x020 // Accessed
#define PTE_D 0x040 // Dirty
#define PTE_PS 0x080 // Page Size
#define PTE_G 0x100 // Global
这一段就是表示页目录以及页表索引的各种状态。(存在,可写,用户,不允许缓存,可达,脏读,页面大小,是否全局等)
按照注释中所写,整个程序的步骤如下:
当页目录索引内不存在 va 对应的表项时,即虚拟地址没有对应的物理地址,需要根据create判断是否要为其分配一个物理页面用作二级页表,这里需要设置权限,由于一级页表和二级页表都有权限控制,所以一般的做法是,放宽一级页表的权限,主要由二级页表来控制权限,在提示2中写道,要注意去掉页目录索引(PDX)和页表索引(PTX)的权限位以保证安全操作。
当页目录索引内存在 va 对应的表项时,即虚拟地址有对应的物理地址,该页面的指针数量应该加一,页面清空,并返回一个新页表页的指针。
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// 取得虚拟地址的页目录项 PDX(va), 并得到物理地址中所对应的页目录,命名为 pde
pde_t pde = pgdir[PDX(va)];
// 如果该物理页面 pde 不存在
if (!(pde & PTE_P))
// pde 不存在且不允许创建
if (!create)
return NULL;
// pde 不存在且允许创建
else {
// 新建页面 pp
struct PageInfo *pp = page_alloc(true);
// 如果新建页面失败
if (!pp)
return NULL;
// 新建页面的指针数量增长1
(pp->pp_ref)++;
// 新建页面取消限制权限
pgdir[PDX(va)] = page2pa(pp) | PTE_U | PTE_P | PTE_W;
// 取得虚拟地址的页表项 PTX(va),并找到新建页面所对应的地址
return (pte_t *) page2kva(pp) + PTX(va);
}
// 如果该页目录的物理地址 pde存在,说明该地址已分配,则返回已分配过的地址
return (pte_t *) KADDR(PTE_ADDR(pde)) + PTX(va);
}
写的注释已经很详尽了,这里还想说一下我一开始觉得比较奇怪的地方。
开头说了,该函数的作用是将虚拟地址转成物理地址的一个过程,而无论是KADDR还是page2kva都明明是一个物理地址或一个物理页面结构转换虚拟地址的函数,为什么return回来的会是一个虚拟地址呢?
这是因为现在转换的只是具体到了一个页面,如果按照地址的方式表示的话,那么只是精确到了“页目录+页表”。一级页表中存放的地址是物理地址,而返回的必须是虚拟地址且必须去掉权限位,回忆一下整个过程:
1)判断找到虚拟地址的 “页目录”的地址所对应的值,pgdir[页目录]是进入了该目录的过程,如果页面不存在,但它可以被创建,那么就新创建一个空页面,他的指针数量加一;
2)取消权限的时候,取消的是在物理页中的权限限制,必须在物理地址中进行,所以用到了page2pa,或者用PADDR(page2kva),这样都能将新建的页面转换成物理地址;
3)返回地址的时候是返回了一个“页目录+页表”的组合,找地址的时候,页表的地址存储都是在虚拟内存中的,所以要去虚拟内存中找到页表中所对应的物理地址。他实际是找到了一个二级页表的位置,也就是最后一级页表的位置。
2.4.2 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
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
for (uintptr_t end = va+size; va != end; pa += PGSIZE,va += PGSIZE){
pte_t *t = pgdir_walk(pgdir,va,1);
*t = pa|perm|PTE_P;
}
}
函数 pgdir_walk( ) 的作用是去找到虚拟地址对应的页的物理地址,那么 boot_map_region( ) 的作用就是找到了所对应的物理地址之后,把实际的虚拟地址映射到物理地址页中去。映射范围从va开始,va+size结束,size的大小是PGSIZE的n倍,所以找到了虚拟地址对应的页面之后,将 物理地址 写入这个 页表地址中去。
2.4.3 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.
//
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
pte_t *pte = pgdir_walk(pgdir,va,0);
if (!pte || !(*pte & PTE_P))
return NULL;
if (pte_store != 0)
*pte_store = pte;
return pa2page(PTE_ADDR(*pte));
}
从注释中理解的话,该函数是把va处的虚拟地址映射的物理地址的页面返回。如果第三个参数 pte_store 不是0,那么就把把它代表的地址放到这个页面中。
2.4.4 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.
//
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
pte_t *pte;
struct PageInfo *pp = page_lookup(pgdir,va,&pte);
if (pp){
page_decref(pp);
*pte = 0;
tlb_invalidate(pgdir,va);
}
}
这个函数的目的是取消物理地址映射的虚拟地址。如果没有对应的虚拟地址就什么也不做。
具体做法如注释:
1)减少物理地址所映射的页面数量。(用page_lookup找到va虚拟地址对应的物理地址之后再进行操作)
2)物理页面应该被释放。(用page_decref实现)
3)va虚拟地址对应的页表入口应该被设置为0。
4)TLB 翻译缓存 必须变为不可用状态如果移除了页表入口。(用tlb_invalidate实现)
2.4.5 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.
//
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t *pte = pgdir_walk(pgdir,va,1);
if (!pte)
return -E_NO_MEM;
// 如果 va 对应的页面存在
if (*pte & PTE_P) {
if (PTE_ADDR(*pte) == page2pa(pp)){
*pte = page2pa(pp)|perm|PTE_P;
return 0;
}
page_remove(pgdir, va);
}
// 如果 va 对应的页面不存在
(pp->pp_ref)++;
*pte = page2pa(pp)|perm|PTE_P;
return 0;
}
函数目的是把虚拟地址的情况映射到物理页面信息pp中。
步骤如注释:
1)如果虚拟地址va已经有映射的物理页面,那么他应该先移除映射,用page_remove实现。
2)如果必要,页表要分配并插入到pgdir中。
3)如果插入成功,结构中的引用次数要增加。
4)如果页面已经存在的地址就是va那么TLB必须被无效,用tlb_invalidate实现。
2.4.6 boot_map_region_large( ) 函数
// 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 *PTSIZE*.
// Use permission bits perm|PTE_P|PTE_PS 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
static void
boot_map_region_large(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
int i;
for (i = 0; i < size; i += 0x400000) {
pde_t * target_pde = &pgdir[PDX(va+i)];
if ((*target_pde & (PTE_P | PTE_PS)) != (PTE_P | PTE_PS)){
if(*target_pde & PTE_P){
cprintf("DANGEROUS!COVER OLD PT,UNTRACK PT\n");
}
*target_pde = (pa + i) | perm | PTE_P | PTE_PS;
}
}
}
我们消耗了很多物理页来记录对KERNBASE的映射.用PTE_PS ("Page Size")位
做一个更加空间效率高的 来映射顶部的256MB PD.参考文档
实现boot_map_region_large()
函数,并且在loading cr3以前启用PSE(page size extension ) before loading cr3. 然后调用boot_map_region_large()
来映射 KERNBASE.
可以在参考文档中3.9章看到 它会变成[31..22][21..0]
的划分,一个页变成了4MB页,并且看到PSE flag (bit 4) in control register CR4 and the PS flag in PDE — Set to 1 to enable the page size extension for 4-MByte pages.
也就是我们的走页表过程会变成,只有CR3->PD->PAGE
,
那一共三个步骤
- 实现
boot_map_region_large
这里按照上面只有一层PD也就是PDE直接指向页 - 修改PSE位
- 替换 原来KERNBASE的
boot_map_region
函数
打开 $ROOT/kern/entry.S
# Turn on page size extension.
movl %cr4, %eax
orl $(CR4_PSE), %eax
movl %eax, %cr4
2.5 总结
这个练习主要是练习了虚拟地址和物理地址的转换,以及页表之间的映射。包含了六个函数。
pgdir_walk()
boot_map_region()
boot_map_region_large()
page_lookup()
page_remove()
page_insert()
pgdir_walk( ) 是最基础的一个函数,它主要根据 虚拟地址 找到 它所对应页表的物理地址;
boot_map_region( ) 作用是将一段 连续的虚拟地址映射到 它所对应的物理地址中,比如,将 [va, va+size)映射到 [pa, pa+size);
boot_map_region_large( ) 这个函数看起来是和上一个函数有一定关联的,它的作用是
page_lookup( ) 作用是 va处的虚拟地址已经映射过的物理地址的页面结构体返回;
page_remove( ) 作用是 解绑物理地址和虚拟地址的映射关系;
page_insert( ) 作用是 将va处的虚拟地址与物理地址进行绑定,如果已存在关系,那么先删除后再绑定。
以上操作可以看出,其实这些函数是完成了 物理地址和虚拟地址对应关系的一系列增删改查。
在 $ROOT 中运行 make grade 命令可以看到得分变为40。
running JOS: (7.6s)
Physical page allocator: OK
Page management: OK
Score: 40/80
3 内核地址空间
/*
* 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 ------------> +------------------------------+ --+
*
* (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped.
* "Empty Memory" is normally unmapped, but user programs may map pages
* there if desired. JOS user programs map pages temporarily at UTEMP.
*/
上图再一次复制,是为了方便观看如何对内核进行虚拟地址与物理地址的对应。
3.1 虚拟地址与物理地址的实际对应与权限赋予
这里因为要进行虚拟地址与物理地址的映射,所以用到了一个函数:
boot_map_region( ) 作用是将一段 连续的虚拟地址映射到 它所对应的物理地址中,比如,将 [va, va+size)映射到 [pa, pa+size);
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
来看一下他的参数,第一个是页面的目录,第二个是虚拟地址,第三是映射范围大小,第四是对应物理地址,第五是赋予的权限。
那么我们先来看一下要映射的第一部分:
这一部分的映射,起点是UPAGES,大小是PTSIZE,物理地址是 PADDR(pages) ,权限是 PTE_U|PTE_P ,这里不写PTE_P是因为函数内部已经自己默认赋予此权限了。
//
// Now we set up virtual memory
//
// 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:
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
第二部分的映射如下:
这一部分的映射,起点是KSTACKTOP-KSTKSIZE,大小是KSTKSIZE,物理地址是 PADDR(bootstack) ,权限是 PTE_W ,这里不写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:
boot_map_region_large(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
第三部分的映射如下:
这一部分的映射,起点是KERNBASE,大小是 2^32 - KERNBASE,物理地址是 0,权限是 PTE_W ,这里不写PTE_P是因为函数内部已经自己默认赋予此权限了。
//
// 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:
boot_map_region(kern_pgdir, KERNBASE, -KERNBASE, 0, PTE_W);
运行 make grade
[root@VM_0_8_centos jos-2019-spring]# make grade
make clean
make[1]: 进入目录“/root/job/jos-2019-spring”
rm -rf obj .gdbinit jos.in qemu.log
make[1]: 离开目录“/root/job/jos-2019-spring”
./grade-lab2
make[1]: 进入目录“/root/job/jos-2019-spring”
make[1]: 离开目录“/root/job/jos-2019-spring”
make[1]: 进入目录“/root/job/jos-2019-spring”
+ as kern/entry.S
+ cc kern/entrypgdir.c
+ cc kern/init.c
+ cc kern/console.c
+ cc kern/monitor.c
+ cc kern/pmap.c
+ cc kern/kclock.c
+ cc kern/printf.c
+ cc kern/kdebug.c
+ cc lib/printfmt.c
+ cc lib/readline.c
+ cc lib/string.c
+ ld obj/kern/kernel
ld: warning: section `.bss' type changed to PROGBITS
+ as boot/boot.S
+ cc -Os boot/main.c
+ ld boot/boot
boot block is 382 bytes (max 510)
+ mk obj/kern/kernel.img
make[1]: 离开目录“/root/job/jos-2019-spring”
running JOS: (6.9s)
Physical page allocator: OK
Page management: OK
Kernel page directory: OK
Page management 2: OK
Large kernel page size: OK
Score: 80/80
已经变成了满分。
3.2 关于映射的一些问题
Question
2)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 ? Page table for top 4MB of phys memory
1022 ? ?
. ? ?
. ? ?
. ? ?
2 0x00800000 ?
1 0x00400000 ?
0 0x00000000 [see next question]
3)(From Lecture 3) We have placed the kernel and user environment in the same address space. Why will user programs not be able to read or write the kernel's memory? What specific mechanisms protect the kernel memory?
What is the maximum amount of physical memory that this operating system can support? Why?
4)How much space overhead is there for managing memory, if we actually had the maximum amount of physical memory? How is this overhead broken down?
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?
5)What is the maximum amount of physical memory that this operating system can support? Why?
6)How much space overhead is there for managing memory, if we actually had the maximum amount of physical memory? How is this overhead broken down?
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?
Entry | Base Virtual Address | Points to (logically) |
1023 | 0xffc00000 | Page table for [252,256) MB of phys memory |
... | ... | ... |
961 | 0xf0400000 | Page table for [4,8) MB of phys memory |
960 | 0xf0000000 | Page table for [0,4) MB of phys memory |
959 | 0xefc00000 | ... |
958 | 0xef800000 | ULIM |
... | ... | ... |
3 | 0x00800000 | |
2 | 0x00800000 | |
1 | 0x00400000 | |
0 | 0x00000000 | [see next question] |
1)第一个问题就是在找页面对应的虚拟地址以及映射的物理地址范围的对应。那么就不得不再提一遍说过的刚才的虚拟地址的结构,前10位是页面,所以一共有1024个目录项,每一个目录项的大小是2^10 个页,最后每个页面有2^12个条目。一个页面能够映射4MB的物理内存(PTSIZE = 4MB)。
2)由于页表可以设置权限位,如果没有将 PTE_U 置 0 则用户无权限读写。
3)所有的空闲的物理页面一开始都储存在了pages这样一个数组中,这个数组存储的基本单元是PageInfo,那么一个PageInfo的大小是sizeof(struct PageInfo))=8 B
yte,也就是说最多512K个页面,每个页面容量 2 ^ 12 = 4K。所以512K*4K=2GB。
4)刚才通过上一问,所有的 PageInfo 结构组成的数组的大小是 4MB,最多有 512 K 个页面,那么就有 0.5M 个页面,一条地址信息32位,也就是4B,那么就是页面信息要用4B*0.5M=2MB 记录,目录项本身4K,所以要6MB+4KB。
5)语句jmp *%eax
即转到 eax 所存的地址执行,在这里完成了跳转。relocated 部分代码主要设置了栈指针以及调用 kern/init.c。由于在 kern/entrypgdir.c 中将 0~4MB 和 KERNBASE ~ KERNBASE + 4 MB 的虚拟地址都映射到了 0~4MB 的物理地址上,因此无论 EIP 在高位和低位都能执行。必需这么做是因为如果只映射高位地址,那么在开启分页机制的下一条语句就会crash。
3.3 Challenge
Challenge! Extend the JOS kernel monitor with commands to:
Display in a useful and easy-to-read format all of the physical page mappings (or lack thereof) that apply to a particular range of virtual/linear addresses in the currently active address space. For example, you might enter
'showmappings 0x3000 0x5000'
to display the physical page mappings and corresponding permission bits that apply to the pages
at virtual addresses 0x3000, 0x4000, and 0x5000.
Explicitly set, clear, or change the permissions of any mapping in the current address space.
Dump the contents of a range of memory given either a virtual or physical address range. Be sure the dump code behaves correctly when the range extends across page boundaries!
Do anything else that you think might be useful later for debugging the kernel. (There's a good chance it will be!)
首先,形式是作为一个字符串输入的,我们需要先把它转换为数字形式的地址。
uint32_t xtoi(char* buf) {
uint32_t res = 0;
buf += 2; //0x...
while (*buf) {
if (*buf >= 'a') *buf = *buf-'a'+'0'+10;//aha
res = res*16 + *buf - '0';
++buf;
}
return res;
}
再写一个格式化输出的函数:
void pprint(pte_t *pte) {
cprintf("PTE_P: %x, PTE_W: %x, PTE_U: %x\n",
*pte&PTE_P, *pte&PTE_W, *pte&PTE_U);
}
最后:
int
showmappings(int argc, char **argv, struct Trapframe *tf)
{
if (argc == 1) {
cprintf("Usage: showmappings 0xbegin_addr 0xend_addr\n");
return 0;
}
uint32_t begin = xtoi(argv[1]), end = xtoi(argv[2]);
cprintf("begin: %x, end: %x\n", begin, end);
for (; begin <= end; begin += PGSIZE) {
pte_t *pte = pgdir_walk(kern_pgdir, (void *) begin, 1); //create
if (!pte) panic("boot_map_region panic, out of memory");
if (*pte & PTE_P) {
cprintf("page %x with ", begin);
pprint(pte);
} else cprintf("page not exist: %x\n", begin);
}
return 0;
}
运行 make qemu
K> showmappings
Usage: showmappings 0xbegin_addr 0xend_addr
K> showmappings 0xf011a000 0xf012a000
begin: f011a000, end: f012a000
page f011a000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f011b000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f011c000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f011d000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f011e000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f011f000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f0120000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f0121000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f0122000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f0123000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f0124000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f0125000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f0126000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f0127000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f0128000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f0129000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
page f012a000 with PTE_P: 1, PTE_W: 2, PTE_U: 0
K> showmappings 0xef000000 0xef010000
begin: ef000000, end: ef010000
page ef000000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef001000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef002000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef003000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef004000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef005000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef006000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef007000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef008000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef009000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef00a000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef00b000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef00c000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef00d000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef00e000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef00f000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
page ef010000 with PTE_P: 1, PTE_W: 0, PTE_U: 4
K>