Part1:Physical Page Management
内存管理两个组件:
- 物理内存分配器:pages(4096B)
- 虚拟内存:将内核和用户软件使用的虚拟地址映射到物理内存中的地址
/*
* 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.
*/
boot_alloc(uint32_t n)
:在物理地址上,从空闲位置开始分配n个字节(需要与PGSIZE对齐),不进行初始化。
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; // virtual address of next byte of free memory
char *result;
if (!nextfree)
{
extern char end[];
nextfree = ROUNDUP((char *)end, PGSIZE);
}
// LAB 2: Your code here.
if (n == 0)
return nextfree;
result = nextfree;
nextfree = ROUNDUP(nextfree + n, PGSIZE);
if ((uint32_t)nextfree - KERNBASE > (npages * PGSIZE))
panic("Out of memory!\n");
return result;
}
mem_init(void)
函数:
- 首先调用
i386_detect_memory
函数,其功能是检测现在系统中有多少可用的内存空间。 kern_pgdir = (pde_t *) boot_alloc(PGSIZE)
memset(kern_pgdir, 0, PGSIZE)
分配一个页,作为内核的页目录表,kern_pgdir
是虚拟地址,并将此页初始化为0。kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P
UVPT
是虚拟地址,存放的是内核页表,因此,需要在此位置上添加一个映射:将kern_pgdir
这个虚拟地址映射到实际的物理地址上,并且添加权限:内核与用户只读。
执行以上代码后的物理内存布局如下:// 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) ----------/ // // The PDX, PTX, PGOFF, and PGNUM macros decompose linear addresses as shown. // To construct a linear address la from PDX(la), PTX(la), and PGOFF(la), // use PGADDR(PDX(la), PTX(la), PGOFF(la)).
pages = (struct PageInfo*)boot_alloc(sizeof(struct PageInfo)*npages)
memset(pages, 0, sizeof(struct PageInfo)*npages)
npages
表示可以分配多少页,每一页都需要使用struct PageInfo
结构进行记录(包括了指向下一页的指针和当前页引用次数)。因此,需要分配足够的内存空间保存pages数组
,pages数组
的每一项是一个PageInfo
结构,对应一个物理页的信息。
执行以上代码后的物理内存布局如下:page_init()
初始化pages数组,初始化page_free_list
,但是某些页不能被使用,需要修改其状态。
下图为物理地址page使用的分布情况:
//根据上面的图以及源代码中的提示,很容易就写出来了
void page_init(void)
{
size_t i;
pages[0].pp_ref = 1;
pages[0].pp_link = NULL;
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 io_hole_end = EXTPHYSMEM / PGSIZE;
for (; i < io_hole_end; ++i)
{
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
//nextfree就是下一个free page
//调用boot_alloc(0),可返回其虚拟指针
size_t next_free_page = PADDR(boot_alloc(0)) / PGSIZE;
for (; i < next_free_page; ++i)
{
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
for (; i < npages; ++i)
{
pages[i].pp_ref = 0;
//头插法
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
mem_init(void)
函数之后的流程请看part2
page_alloc(int alloc_flags)
和page_free(struct PageInfo *pp)
比较简单,按照提示即可完成。
总结一下
part1总共写了五个函,从名字来说可以分三类,boot,memory和page层面。
- boot,启动层面,
boot_alloc()
在启动时可分配N个byte空间。通过调用这个函数,可以将物理内存分割成多个页。 - memory,内存层面,
mem_init()
通过调用boot_alloc()
获得npage*sizeof(Page_Info)个byte的页面,也就是npages个页面。并使用数据结构PageInfo
来记录所有的页。 - page,页面层面,
page_init()
是页面的初始化,初始化做的就是标记页表中哪些可以用,哪些不能用,把能用的链接起来。page_alloc( )
就是分配这些能用的页面,page_free()
就是释放掉页面。
所以,part1基本就完成了内存层面初始化页面以及页面的基本操作(包括删除和分配)。
Part2:Virtual Memory
虚拟地址、逻辑地址、线性地址、物理地址的区别:
段⻚式内存管理实现的⽅式:
先将程序划分为多个有逻辑意义的段,也就是前⾯提到的分段机制;
接着再把每个段划分为多个⻚,也就是对分段划分出来的连续空间,再划分固定⼤⼩的⻚;
这样,地址结构就由段号、段内⻚号和⻚内位移三部分组成
由此,逻辑地址是由段+偏移量组成,逻辑地址与虚拟地址没有明确分别,虚拟地址(逻辑地址)从段号到页号的翻译成为线性地址,然后线性地址通过页号+页内位移翻译成物理地址
名词 | 说明 |
---|---|
页目录表 | 存放各个页目录项的表,页目录常驻内存,页目录表的物理地址存在寄存器CR3中 |
页目录项 | 存放各个二级页表起始物理地址,即页表页 |
二级页表 | 存放页表项 |
页表项 | 页表项的高20位存放各页的对应的物理地址的高20位 |
pgdir_walk(pde_t *pgdir, const void *va, int create)
:
给定一个页目录表指针 pgdir ,该函数应该返回线性地址va所对应的页表项指针(page table entry,PTE),不存在则根据create参数决定是否创建
// 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) ----------/
//
// The PDX, PTX, PGOFF, and PGNUM macros decompose linear addresses as shown.
// To construct a linear address la from PDX(la), PTX(la), and PGOFF(la),
// use PGADDR(PDX(la), PTX(la), PGOFF(la)).
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
pde_t *va_dir_entry = &pgdir[PDX(va)];//获得页目录项物理地址
if (!(*va_dir_entry & PTE_P))//如果不存在
{
if (!create)//不需要创建
return NULL;
struct PageInfo *pp = page_alloc(ALLOC_ZERO);//需要创建,则分配一页作为页表
if (pp == NULL)
return NULL;
pp->pp_ref++;
*va_dir_entry = page2pa(pp) | PTE_U | PTE_P | PTE_W;//将页表的物理地址和权限写入页目录项指针
} // 转化为虚拟地址(获得PPN) + 页表偏移量=va对应的页表项指针
return (pte_t *)KADDR(PTE_ADDR(*va_dir_entry)) + PTX(va);
}
代码流程图:
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
:
把虚拟地址空间范围[va, va+size)映射到物理空间[pa, pa+size)的映射关系加入到页目录表pgdir中
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 *pg_table_entry = pgdir_walk(pgdir, (void *)va, 1);//获得va对应的页表项指针
if (pg_table_entry == NULL)
{
panic("no enough memory to allocate for page!");
return;
}
*pg_table_entry = pa | perm | PTE_P;//将页表项指针指向物理地址pa并赋予权限。完成映射!
pa += PGSIZE;//映射下一页
va += PGSIZE;
}
}
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
:
把一个物理页pp与虚拟地址va建立映射关系
int page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t *pg_table_entry = pgdir_walk(pgdir, va, 1);
if (pg_table_entry == NULL)
return -E_NO_MEM;
if (*pg_table_entry & PTE_P)//此页表项是否已被映射
{
if (PTE_ADDR(*pg_table_entry) == page2pa(pp))//映射的是否就是pp
{
*pg_table_entry = page2pa(pp) | perm | PTE_P;//如果是,只需修改权限
return 0;
}
page_remove(pgdir, va);//如果不是,则需要先移除此映射
}
pp->pp_ref++;
*pg_table_entry = page2pa(pp) | PTE_P | perm;
pgdir[PDX(va)] |= perm;//更新页目录项的权限
return 0;
}
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
:
返回虚拟地址va所映射的物理页的PageInfo结构体的指针,如果pte_store参数不为0,则把这个物理页的页表项地址存放在pte_store中
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
pte_t *pg_table_entry = pgdir_walk(pgdir, va, 0);//查找对应的页表项
// 没有被映射 或 映射地址无效
if (pg_table_entry == NULL || !(*pg_table_entry & PTE_P))
return NULL;
if (pte_store != NULL)
*pte_store = pg_table_entry;
return pa2page(PTE_ADDR(*pg_table_entry));返回页表项的PPN
}
page_remove(pde_t *pgdir, void *va)
:
把虚拟地址va和物理页的映射关系删除
void page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
pte_t *pg_table_entry = NULL;
struct PageInfo *pp = page_lookup(pgdir, va, &pg_table_entry);
if (pp == NULL)//如果是NULL,说明已经没有被映射了或者映射无效了,可直接返回
return;
page_decref(pp);//释放pp物理页
tlb_invalidate(pgdir, va);//刷新TLB
*va_table_entry = 0;
}
TLB:缓存常访问的页表或页目录
总结一下
part2主要工作就是建立虚拟内存与物理内存的映射,包括了查找虚拟地址对应的页表项
Part3:Kernel Address Space
ULIM
以上的地址空间只能被内核读写,用户不可读写
[UTOP,ULIM]
之间的空间可以同时被内核和用户读取,但都不能修改,主要包括了页目录表、pages数组、envs数组(下一个lab)
UTOP
以上为内核空间,其以下的空间给用户进程使用,用户可以读写
继续mem_init(void)函数:
6. boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U)
通过kern_pgdir将物理地址上的pages数组映射到虚拟地址UPAGES处,并设置权限:用户只读
7. boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W)
把由bootstack变量所标记的物理地址范围映射给内核的堆栈,bootstack
定义在/kernel/entry.S中,但是还是不太明白bootstack所指向的物理地址在哪,没找到。
8. boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W)
:
最后将KERNBASE以上的虚拟内存全部映射,虚拟地址范围是[KERNBASE, 232],物理地址范围是[0,232 - KERNBASE];物理地址可能没有2^32 - KERNBASE
那么多,但依然映射
9. lcr3(PADDR(kern_pgdir))
:
将kern_pgdir加载进CR3寄存器
执行完mem_init()后,虚拟地址到物理地址的映射图:
参考资料
MIT 6.828 JOS学习笔记15. Lab 2.1 - fatsheep9146 - 博客园 (cnblogs.com)
MIT-6.828-JOS-lab2:Memory management - gatsby123 - 博客园 (cnblogs.com)