Part A 物理页管理
Exercise1
补全在kern/pmap.c下的几个函数。
boot_alloc()
mem_init() (only up to the call to check_page_free_list(1))
page_init()
page_alloc()
page_free()
boot_alloc()
在JOS中,一开始的物理内存布局如下图所示
虚拟内存布局
在代码中,所有的变量的地址都是虚拟地址,JOS中虚拟地址到物理地址的转换很简单:
虚拟地址 = 物理地址 + KERNBASE(0xF0000000)
boot_alloc()中的end代表的就是内核代码的最上端,即内核代码的末尾,此位置往上的物理内存都可以分配。当申请n字节大小空间的内存时,将当前nextfree保存在result当做函数返回值,然后将其向后移动ROUNDUP(n, PGSIZE),此时[result, nextfree)的空间就分配出来了。因为是按页管理内存,所以分配的内存大小需要页对齐。
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; // virtual address of next byte of free memory
char *result;
// end: bss段的末尾,正好是kernel的末尾的指针,第一个未使用的虚拟地址
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
// cprintf("nextfree: %x\n", nextfree);
// LAB 2: Your code here.
// 返回的是上一次的地址,然后再将nextfree往后移动,相当于分配空间
result = nextfree;
if(n != 0) {
nextfree = ROUNDUP(nextfree + n, PGSIZE);
}
return result;
}
mem_init() 只需要完成到调用check_page_free_list(1)之前
在内核代码中每个物理页都由一个PageInfo的数据结构来标识,一共有npages个物理页。所有的PageInfo组成一个pages数组。所以在mem_init需要先对pages结构进行物理内存分配。
之后所出现的物理页其实是指PageInfo,代码中对物理页的操作其实都是操作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;
};
void
mem_init(void)
{
...
// 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:
// npages: 还有多少页物理内存,每个页都要有一个PageInfo
pages = (struct PageInfo*)boot_alloc(sizeof(struct PageInfo) * npages);
// cprintf("npages: %d\n", npages);
memset(pages, 0, npages * sizeof(struct PageInfo));
//////////////////////////////////////////////////////////////////////
...
}
page_init()
分配完内存后自然就要对数据结构进行初始化,即将物理内存中的每一页都与pageInfo关联,其中分为可用页和不可用页。物理内存页到pages数组下标的映射关系为: 地址/PGSIZE(4k)。根据提示可知,一共有两大块空闲物理内存块。[1, npages_basemem)。第二块就是从内核代码往后,这个地址可以用boot_alloc(0)取到,即分配了pages内存之后的地址。但这个地址在代码中是虚拟地址,所以需要将其转换成物理地址,可以用PADDR()宏来转换。所以第二块范围就是[PADDR(boot_alloc(0)/PGSIZE),npages)。找出这些空闲页后需要用page_free_list链表串起来。方便后续内存分配。
void
page_init(void)
{
// 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!
size_t i;
for (i = 1; i < npages_basemem; i++) {
pages[i].pp_ref = 0;
// 构成一个链表
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
size_t first_free_page = (size_t)PADDR(boot_alloc(0))/PGSIZE;
// cprintf("npages_basemem: %d\n first_free_page: %d\n", npages_basemem, first_free_page);
for (i = first_free_page; i < npages; i++) {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
pages[0].pp_ref = 1;
pages[0].pp_link = NULL;
for(i = npages_basemem; i < first_free_page; i++) {
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
}
page_alloc()
空闲物理页的分配。在空闲物理页链表中取出一个物理页即可。返回的是PageInfo*,这个怎么与物理内存中的物理页对应呢?
- 注意: 两个指针相减,结果并不是两个指针数值上的差,而是把这个差除以指针指向类型的大小的结果。
可以用page2pa(PageInfo*)的宏,因为pages数组是连续的物理内存,所以直接将PageInfo* pp 的地址减去pages就可以知道在数组中的下标是多少。在乘以4K就可以得到物理地址了: (pp-pages) << PGSHIFT。PGSHIFT = 1<<12 = 4096 = 4k。
struct PageInfo *
page_alloc(int alloc_flags)
{
// Fill this function in
if (page_free_list == NULL) {
return NULL;
}
struct PageInfo* pageInfo = page_free_list;
page_free_list = page_free_list->pp_link;
pageInfo->pp_link = NULL;
if (alloc_flags & ALLOC_ZERO) {
// 内核虚拟地址空间映射到物理地址空间,直接减去kernbase
memset(page2kva(pageInfo), 0, PGSIZE);
// cprintf("page2kva(pageInfo): %x %x %d\n", pageInfo, page2kva(pageInfo), PGSIZE);
}
return pageInfo;
}
page_free()
这个比较简单。页面释放,将物理页重新插入到page_free_list中。前提是要保证该页面没有被引用,并且也不在空闲链表中。
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.
// cprintf("pp->pp_ref: %d pp->pp_link: %d\n", pp->pp_ref, pp->pp_link == NULL);
assert(pp->pp_ref == 0 && pp->pp_link == NULL);
pp->pp_link = page_free_list;
page_free_list = pp;
}
Part B 虚拟内存
Exercise2
阅读Intel 80386 Reference Manual的第5第6章。
在x86结构下,使用的是分段分页机制,虚拟地址转换为物理地址需要中间还需要经历线性地址(分段的过程)。
在JOS中,虚拟地址=线性地址,为什么呢?因为在boot/boot.S中把所有的段地址都设置成了0 到0xffffffff,即段基址都等于0,相当于0+offset,所以就没有分段的效果了。这样我们就可以专注于实现分页机制了。
Exercise3
使用qemu-debug下的xp命令查看物理地址的内容。因为gdb只能获取到虚拟地址,所以需要使用qemu的下的debug模式才能查看物理地址。
我觉得直接在程序中用cprintf也可以查看。
Exercise4
补全kern/pmap.c下的这些函数,实现页表管理。
pgdir_walk()
boot_map_region()
page_lookup()
page_remove()
page_insert()
在补全这些函数之前,需要先明白一个图的含义。JOS采用的是二级页表机制,主要由五个元素组成,页目录表-页目录项(PDE, page diretory entry),页表-页表项(PTE, page table entry),物理页。PDE和PTE存储的都是地址。
其中一个页目录项对应一个页表,一个页表项对应一个物理页。页目录表的地址存储在CR3寄存器中。
pgdir_walk()
根据(页目录表,虚拟地址,创建标志)找到该虚拟地址所对应的物理页的虚拟地址。
通过PDX获得va的页目录项在页目录表中的偏移取得PDE,如果该PDE所指向的PT是空的话且create == 1,那就创建一个页目录表,即申请一页的物理内存。并设置为用户可读可写。然后再根据PTX获得va在页表项在页表中的偏移获取PTE,返回此PTE的地址。
PTE_ADDR(*pde)的作用是去掉后面的权限位。
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
int pde_index = PDX(va);
int pte_index = PTX(va);
pde_t *pde = &pgdir[pde_index];
if (!(*pde & PTE_P)) {
if (create) {
struct PageInfo *page = page_alloc(ALLOC_ZERO);
if (!page) return NULL;
page->pp_ref++;
*pde = page2pa(page) | PTE_P | PTE_U | PTE_W;
} else {
return NULL;
}
}
pte_t *p = (pte_t *) KADDR(PTE_ADDR(*pde));
return &p[pte_index];
}
boot_map_region()
之前的pgdir_walk是取到页表项,但页表项还未真正的映射到物理页上,此函数将从va开始的大小为size的地址按页从物理地址pa开始映射。相当于对页表项赋值。
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
// 页表项映射到物理地址, 页表本身需要物理地址存储,pgdir_walk得到页表存储的虚拟地址
size_t i;
for (i = 0; i < size; va += PGSIZE, pa += PGSIZE, i += PGSIZE) {
pte_t* pte = pgdir_walk(pgdir, (void*)va, 1);
// 不需要 *pte & PTE_P
if (pte == NULL) {
panic("error");
}
*pte = pa | perm | PTE_P;
}
}
page_lookup()
返回页表项所对应的物理页的虚拟地址,并把页表项存储在pte_store中。**pte_store二级指针相当于传入指针的引用。
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 == NULL || !(*pte & PTE_P)) {
return NULL;
}
if (pte_store) {
*pte_store = pte;
}
return (struct PageInfo*)pa2page(PTE_ADDR(*pte));
}
page_remove()
清空页表项对应的物理页,并把物理页引用减减。
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
pte_t* pte_store;
struct PageInfo* pp = page_lookup(pgdir, va, &pte_store);
if(pp == NULL || !(*pte_store & PTE_P))
return;
page_decref(pp);
*pte_store = 0;
tlb_invalidate(pgdir, va);
}
page_insert()
给页表项赋值一个物理页。
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 == NULL) {
return -E_NO_MEM;
}
pp->pp_ref++;
if (*pte & PTE_P) {
page_remove(pgdir, va);
}
*pte = page2pa(pp) | perm | PTE_P;
// cprintf("page_insert: %x\n", *pte);
return 0;
}
Part 3 内核地址空间
JOS的内核空间为[UTOP, KERNBASE),一共为256MB。
填充完整mem_init(),将虚拟内核地址空间映射到物理地址上。
- [UPAGES, UPAGES+PTSIZE)这段空间是pages数组的空间,将其映射到PADDR(pages)上。
- [KSTACKTOP-KSTKSIZE, KSTACKTOP)是内核栈的空间,将其映射到PADDR(bootstack)
- [KERNBASE, 2^32-1),其中32位系统无法计算 2^32,但 2^32-1 == -KERNBASE。这段地址从物理地址0开始映射。
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
PTE_U);
boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
boot_map_region(kern_pgdir, KERNBASE, -KERNBASE, 0, PTE_W);