Physical Page Management
JOS内核以页为最小粒度管理内存,从而实时记录哪些内存区域空闲,哪些内存区域占用,这个信息被记录在一条结构体PageInfo的链表中,链表的每个结点对应一个物理页。
内核开始,会调用mem_init()函数对整个操作系统的内存管理系统进行一些初始化的设置。
mem_init()
首先调用i386_detect_memory()子函数, 检测现在系统中有多少可用的内存空间。
然后调用boot_alloc()函数得到页目录表,kern_pgdir指针指向这个表。页目录表占用一个物理页的内存。
kern_pgdir = (pde_t *)boot_alloc(PGSIZE)
memset(kern_pgdir, 0, PGSIZE)
boot_alloc()是暂时的页分配器,后面都用page_alloc()做页分配器。页分配器维护一个静态变量nextfree,指向下一个可使用的空闲内存空间的虚拟地址。它的核心代码如下:
result = nextfree;
nextfree = ROUNDUP(nextfree+n, PGSIZE);
if((uint32_t)nextfree - KERNBASE > (npages*PGSIZE))
panic("Out of memory!\n");
return result;
紧接着, 为页目录表添加第一个表项,也就是页目录表所在页的虚拟地址和物理地址的映射。
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
宏定义UVPT是一段虚拟地址的起始地址,PADDR(kern_pgdir)计算了kern_pgdir的真实物理地址。
下一步, 分配一块内存给PageInfo结构体数组。每个PageInfo结构体存储着一个物理内存页的信息。内核通过该数组实时记录所有内存页使用情况。
pages = (struct PageInfo *)boot_alloc(npages * sizeof(struct PageInfo));
memset(pages, 0, npages * sizeof(struct PageInfo));
下一步, 调用page_init()函数, 初始化pages数组,初始化pages_free_list链表。page_init()核心代码如下:
size_t i;
page_free_list = NULL;
int num_alloc = ((uint32_t)boot_alloc(0) - KERNBASE) / PGSIZE;
int num_iohole = 96;
for (int i = 0; i < npages; i++)
{
if (i == 0)
{
pages[i].pp_ref = 1;
}else if (i >= npages_basemem && i < npages_basemem + num_iohole + num_alloc)
{
page[i].pp_ref = 1;
}else {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
下一步, 调用check_page_free_list(1)子函数,检查page_free_list()链表的所谓空闲页是否都合法都空闲。
调用check_page_alloc()检查page_alloc()和page_free()能否正常运行。
page_alloc()
分配一个物理页,并返回这个物理页所对应的PgaeInfo结构体。
struct PageInfo *page_alloc(int alloc_flags)
{
struct PageInfo *res;
if (page_free_list == NULL)
return NULL;
res = page_free_list;
page_free_list = res->pp_link;
res->pp_link = NULL;
if (alloc_flags & ALLOC_ZERO)
memset(page2kva(res), 0, PGSIZE);
return res;
}
page_free()
把所给页的PageInfo结构体返回给page_free_list空闲页链表。
void page_free(struct PageInfo *pp)
{
assert(pp->pp_ref == 0);
assert(pp->pp_link == NULL);
pp->pp_link = page_free_list;
page_free_list = pp;
}
Virtual Memory
在x86系CPU中,虚拟地址由segment selector和segment offset组成。虚拟地址通过段地址转换机构转换后得到的地址成为线性地址。线性地址通过分页地址转换机构把线性地址进行转换后得到的真实的RAM地址称为物理地址,通过总线将它送到内存芯片上即可寻址。我们平时写的C程序中指针的值就是虚拟地址中的segment offset。
在JOS内核源代码中, KADDR(pa)提供了物理地址->虚拟地址的转换,PADDR(va)提供了虚拟地址->物理地址的转换。
PageInfo结构体中的整数型变量pp_ref记录了该物理页被多少个不同虚拟地址引用。pp_ref值等于该页被页表项中位于虚拟地址UTOP之下的虚拟页所映射的次数。当物理页的pp_ref为0时可以被释放。一般在page_alloc()后给pp_ref加一。
JOS虚拟内存空间概览:
/*
* 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.
*/
Kernel Space Address
下面说的地址默认指的是虚拟内存地址。
内核将它管理的虚拟地址空间划分为内核空间(高位)和用户空间(低位)两个区域,在JOS实验中,分界线是inc/memlayout.h中的宏定义ULIM。
/*
* 高位
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* | | \
* | | |
* User can't R/W | | |
* | | |
* | | |
* UTOP ---------> -------------------------------- > Kernel Zone
* | | |
* User only Read | | |
* | | |
* | | /
* ULIM ---------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* | | \
* | | |
* User can R/W | | > User Zone
* | | |
* | | /
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* 低位
*/
mem_init()函数中,前面我们已经把页目录表建起来了,也把kern_pgdir的页面映射作为一个页目录表项加到表里了,接下来还要添加这几项到表中:
- PageInfo结构体数组到线性地址UPAGES,大小为一个PTSIZE
- 堆栈区域,由bootstack变量标记的物理地址范围映射给内核的堆栈,内核堆栈的虚拟地址范围是[KSTACKTOP-PTSIZE,KSTACKTOP]。其中[KSTACKTOP-KSTKSIZE,KSTACKTOP]段加入到表中。
- 整个操作系统内核,虚拟地址范围[KERNBASE, 2^32],物理地址[0, 2^32-KERNBASE]。
对应代码实现为:
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W)
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
Questions
- 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 | ? | ? | |
. | ? | ? | |
4 | 0x01600000 | 3C0-3FF | kernel |
3 | 0x01200000 | 3BF | bootstack |
2 | 0x00800000 | 3BC | pages数组 |
1 | 0x00400000 | 3BD | kern_pgdir |
0 | 0x00000000 | [see next question] |
- 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?
user program无权修改内核区内存,否则会破坏内核,操作系统崩溃。
常见操作系统借助段机制和分页机制这两个部件实现对内核地址的保护,分页机制把页表项中的Supervisor/User位设置为0,那么user program就无法访问这个内存页。
- What is the maximum amount of physical memory that this operating system can support? Why?
JOS用一段4096KB的内存空间来存放页目录表,每个PageInfo结构体大小为8B,所以一共可以存放512K个PageInfo结构体,对应512个内存页,每个页大小4096B,对应着2GB的物理内存。
- 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?
PageInfo结构体数组占用4096KB的内存空间,页目录表4KB,当前页表4096KB,总开销6MB+4KB。
- 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?
kern/entry.S文件中有一个指令jmp *%eax,它会将EIP的值设置为寄存器eax中的值,这个值大于KERNBASE。
在entry_pgdir页表中,有把虚拟地址空间[0,4MB)映射到物理地址空间[0,4MB)上,当访问位于[0,4MB)区间内时,可将其转换为物理地址。