本文共 9936 字,大约阅读时间需要 33 分钟。
JOS内核以页为最小粒度管理内存,从而实时记录哪些内存区域空闲,哪些内存区域占用,这个信息被记录在一条结构体PageInfo的链表中,链表的每个结点对应一个物理页。
内核开始,会调用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()能否正常运行。分配一个物理页,并返回这个物理页所对应的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;}
把所给页的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;}
在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. */
下面说的地址默认指的是虚拟内存地址。
内核将它管理的虚拟地址空间划分为内核空间(高位)和用户空间(低位)两个区域,在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的页面映射作为一个页目录表项加到表里了,接下来还要添加这几项到表中:
对应代码实现为:
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);
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] |
user program无权修改内核区内存,否则会破坏内核,操作系统崩溃。
常见操作系统借助段机制和分页机制这两个部件实现对内核地址的保护,分页机制把页表项中的Supervisor/User位设置为0,那么user program就无法访问这个内存页。
JOS用一段4096KB的内存空间来存放页目录表,每个PageInfo结构体大小为8B,所以一共可以存放512K个PageInfo结构体,对应512个内存页,每个页大小4096B,对应着2GB的物理内存。
PageInfo结构体数组占用4096KB的内存空间,页目录表4KB,当前页表4096KB,总开销6MB+4KB。
kern/entry.S文件中有一个指令jmp *%eax,它会将EIP的值设置为寄存器eax中的值,这个值大于KERNBASE。
在entry_pgdir页表中,有把虚拟地址空间[0,4MB)映射到物理地址空间[0,4MB)上,当访问位于[0,4MB)区间内时,可将其转换为物理地址。
转载地址:http://tybuz.baihongyu.com/