【mit6.828】Lab02
前言
在这一节中,会关注操作系统的内存管理,内存管理分为两个部分,一个部分是服务于操作系统的物理地址分配器,这里分配器能够操作4096个比特,称作pages。第二个部分则是虚拟内存,其通过内核和用户软件将虚拟地址映射到物理地址上。在实验前,将代码拉取下来,并以此完成exercise
Part 1: Physical Page Management
Exersise 1:根据要求补全代码,根据内存管理的
原理来进行补全(boot_alloc(); mem_init(); page_init();
page_alloc(); page_free();)
进入pmap.c文件,可以看到几个全局变量的解释
// These variables are set by i386_detect_memory()
size_t npages; // Amount of physical memory (in pages)物理内存页大小
static size_t npages_basemem; // Amount of base memory (in pages) 基内存的页大小?
//上面主要针对的i386的探测内存函数使用的
// These variables are set in mem_init()
pde_t *kern_pgdir; // Kernel's initial page directory (初始化页目录)
struct PageInfo *pages; // Physical page state array 物理页表状态数组
static struct PageInfo *page_free_list; // Free list of physical pages 空置物理页列表
先进入mem_init代码,在进入之前,这里介绍到,对于page table 有两个level,其中kern_pgdir是指根类型的线性虚拟地址。mem_init用来设置部分内核地址空间,用户空间的地址空间的设置后面才会提到
boot_alloc
void
mem_init(void)
{
uint32_t cr0;
size_t n;
// Find out how much memory the machine has (npages & npages_basemem).
// 进行mem_init之前先进行相关的内存探测,看看相关内存够不够
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.、
// 创建页目录,boot_alloc会返回初始化后页目录的内存地址,大小就是PGSIZE指定的大小,在后面操作系统在虚拟地址内存下就会需要页目录转换
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);
进入boot_alloc函数
//这里boot_alloc只是一个临时的内存分配器,
static void *
boot_alloc(uint32_t n)
{
//通过维护一个静态变量nextfree,其中存放的空闲内存空间的虚拟地址
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.
result = nextfree;
nextfree = ROUNDUP(nextfree+n, PGSIZE);
if((uint32_t)nextfree - KERNBASE > (npages*PGSIZE))
panic("Out of memory!\n");
return result
}
当nextfree初始为空的时候,这里使用了extern的方式,链接end。而end指针指向bss段(存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域)
boot_alloc进行处理,获得内存后,进入下一步
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
//进行了页目录的使用之后,需要填写代码初始化page
pages = (struct PageInfo *) boot_alloc(npages * sizeof(struct PageInfo));//PageInfo代表了一页
memset(pages, 0, npages * sizeof(struct PageInfo)); //初始化整个页链表
其中PADDR,是pmap.h头文件中定义的一个宏定义,其功能:获取虚拟地址,返回对应的物理地址
PDX:page directory index
UVPT:User read-only virtual page table (一个只读虚拟page table的首地址)
通过上述操作,使得kern_pgdir指向了真实物理地址
page_init():对所有页链表下的页进行初始化检测
进入函数后有说明:根据页的当前状态来修改结构体的状态,如果页被占用,就把PageInfo结构体中pp_ref属性置1;如果是空闲页,则把该页送入pages_free_list链表中。(0被占用,io 部分被占用,extmem部分被占用),然后就是遍历标记空闲页链表
size_t i;
page_free_list = NULL;
int num_alloc = ((uint32_t)boot_alloc(0) - KERNBASE) / PGSIZE;
int num_iohole = 96;
for(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)
{
pages[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这两个子函数是否可用
check_page();//检测page_insert, page_remove是否可用
在check_page_alloc()就有我们需要实现的page_alloc(),page_free()
page_alloc()
page_alloc()获取page_free_list中空闲pageinfo,修改表头信息,并进行初始化操作
struct PageInfo *
page_alloc(int alloc_flags)
{
struct PageInfo *result;
if (page_free_list == NULL)
return NULL;
result= page_free_list;
page_free_list = result->pp_link;
result->pp_link = NULL;
if (alloc_flags & ALLOC_ZERO)
memset(page2kva(result), 0, PGSIZE);
//其中KADDR:表示获取一个物理地址,返回一段虚拟地址
return result;
}
page_free()
要进行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.
assert(pp->pp_ref == 0);
assert(pp->pp_link == NULL);
pp->pp_link = page_free_list;
page_free_list = pp;
}
part 1 小结
- 这部分对内存管理的相关知识进行初识,在BIOS引动OS时,需要设计一个临时的MMU,以实现引导OS时虚拟地址和实际物理地址之间的转换。当OS被成功引导后,服务于OS的整个功能,就需要一个性能更好的MMU。
- 本节中,了解了MMU在启动时的做的一些准备工作
- 启动时对内存进行探测
- 对页目录进行初始化
- 获得页目录地址后,相应的根据npage的大小,来设置页链表page_init
- 进行这些简单初始化后,需要的对page的相关功能进行检测page_alloc()、page_free()
- page_alloc()、page_free(),对page的处理,就是对page_info修改,从page_free_list中获取并分配时就开辟内存。释放时,同样对page_info修改,然后将其放入page_free_list中去
Part 2: Virtual Memory
在x86体系下,软件提供的虚拟地址由两部分组成【段基地址+段内偏移】,经过段机制将虚拟地址转换为线性地址,线性地址再由页机制转换物理地址,以实现在RAM寻址。在boot.S中的初始化中,将段基地址都初始化为了0,使得offset就等于了虚拟地址。
在lab1中,我们使用了一个简单的page table,实现内核能够运行在0xf0100000,其真实物理地址在0x00100000,但其page table的映射范围却是有限的。本lab 下,则要将page table的范围进行扩展,使其映射扩展到物理内存的头256MB之上,并使虚拟地址实现从0xf0000000 的映射。
在保护模式下(进入boot.S),我们的地址都是需要使用MMU进行转换的虚拟地址。所有C指针都是指向的虚拟地址。在JOS内核中,使用了两种形式来操纵数值,physaddr_t 类型存放物理地址,uintptr_t 类型存放虚拟地址,其类型都是32位整数。但是如果相对这两个类型的数进行deference(解引用),是会出现报错的。
在JOS中uintptr_t通过强制类型转换的方式来实现解引用。但是如果physaddr_t 以同样方式解引用,可能最后得到的地址并不是想要的那个地址。(内核不能轻易解引用一个物理地址,因为都由MMU来转换所有内存引用)
问题:如果下列代码正确,变量x是uintptr_t还是physaddr_t
mystery_t x;
char* value = return_a_pointer();
*value = 10;
x = (mystery_t) value;
答:
value使用了 * 进行了地址解析,所以x应该是uintptr_t类型
JOS内核中有时只知道物理地址,要实现来阅读或修改内存。例如在上面我们阅读的pmap.c代码中,需要加载一个新的页表项,其需要分配一块物理内存来存放,在实现时,jos不能绕开虚拟地址转换,所以在代码中提供了KADDR实现物理地址到虚拟地址的映射,而PADDR提供虚拟地址到物理地址的映射。
Reference counting
在后面的实验中,会遇到一个物理page被多个虚拟地址同时映射。这种情况,会对每片内存设置一个引用计数(前面我们在加载页目录项时,是获取的真实物理地址开辟的),其PageInfo设置了一个pp_ref,当pp_ref为0时,才能对这篇物理页释放。所以在page_alloc时,需要对pp_ref马上加1
Page Table Management
Exercise 4:
对 pgdir_walk()
boot_map_region()
page_lookup()
page_remove()
page_insert()
进行实现,check_page会对这些函数进行检测
回到mem_init中,开始进入check_page中,依次发现需要填写的代码
page_lookup()
这个函数用来返回虚拟地址va对应的物理页的pageinfo的结构体指针。解释中提到,需要使用pgdir_walk的函数。(先实现 pgdir_walk()和pa2page())
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
pte_t *entry = NULL;
struct PageInfo *ret = NULL
entry = pgdir_walk(pgdir,va,0);
if(entry == NULL)
return NULL;
if(!(*entry & PTE_R))
return NULL;
// 通过entry 页表项,来找到其在pagex下的对应位置,并返回一个page(pages[PGNUM(pa)];)
ret = pa2page(PTE_ADDR(*entry));
if(pte_store != NULL){
*pte_store = entry;
}
return ret;
}
在实现page_lookup时,使用了pgdir_walk,来返回对应的虚拟地址,而page_lookup给定的只有pgdir和虚拟地址,侧面证明了pgdir_walk可能使用的页式管理,使用虚拟地址【基地址+偏移地址】的方式来找到对应的物理地址。
pgdir_walk()
注释:该函数给定一个页目录指针pgdir,该函数会返回线性地址va所对应的页表项指针(页表项中存在相关信息位,很多操作都是基于其来做的)
1. 判断相关page table是否存在--->不存在则开辟一个新的page table,调用page_alloc,则new page的引用+1,pgdir_walk()会返回一个new page table page的指针
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
unsigned int page_off;
pte_t *page_base = NULL;
struct PageInfo* new_page = NULL;
//page directory index 返回页目录下对应的索引
unsigned int dic_off = PDX(va);
// 计算方式,就是基地址+偏移量
pde_t dic_entry_ptr = pgdir + dic_off;
// 判断是否存在及权限,不存在就重新开辟
if(!(*dic_entry_ptr & PTE_P)){
if(create){
new_page = page_alloc(1);
if(new_page == NULL) return NULL;
new_page->pp_ref++;
*dic_entry_ptr = (page2pa(new_page | PTE_P|PTE_W|PTE_U));
}
else
return NULL;
}
//page table index:返回page_table下的缩影
page_off = PTX(va);
//将获取的物理地址变换为虚拟地址
page_base = KADDR(PTE_ADDR(*dic_entry_ptr));
return &page_base[page_off];
}
在进行地址获取时,有两个指令PDX和PTX。
其中PDX表示返回页目录下的索引,PTX返回page table下的页索引
所以这个函数在实现时,找到va地址,在页目录下的索引,确定是page,得到页地址后,
再计算va在页内的位置,得到页内偏移位置。(整个流程有点像页【页基址+偏移】)
page_insert
则要实现物理地址中PageInfo *pp和虚拟地址va之间建立联系
要求:
1. 如果虚拟地址已经映射过,则删除,重新映射
2. 要保证page table已经插入进了pgdir中
3. 映射后pp-pp_ref应该自增
int page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
pte_t *entry = NULL;
entry = pgdir_walk(pgdir, va, 1); //Get the mapping page of this address va.
if(entry == NULL) return -E_NO_MEM;
pp->pp_ref++;
if((*entry) & PTE_P) //If this virtual address is already mapped.
{
tlb_invalidate(pgdir, va);
page_remove(pgdir, va);
}
//pp是物理页page,page2pa获取的是pp对应的页地址
*entry = (page2pa(pp) | perm | PTE_P);
pgdir[PDX(va)] |= perm; //Remember this step!
return 0;
}
其实现的流程,即通过va,获得其在页表中的实体,然后进行相关判断,判断完成后entry = page2pa(pp),来存储相关信息,其中entry存储了pp相对page(表头)之间的差值。其建立映射的方式,va-->entry--->pp(记录其与page 的差值,缩小存储空间)
通过这里对pgdir_walk的使用,其使用的一般流程都是获得一个pgdir的基地址,然后通过获得线性地址va来进行寻址,并返回的一个实体pte_t(pte_t:代表一个页表项)
page_remove
void
page_remove(pde_t *pgdir, void *va)
{
pte_t *pte = NULL;
struct PageInfo *page = page_lookup(pgdir, va, &pte);
if(page == NULL) return ;
page_decref(page);
tlb_invalidate(pgdir, va);
*pte = 0;
}
page_remove 的使用相对简单,同样根据pgdir,va利用page_lookup,找到page,然后进行修改page_free
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)
{
int nadd;
pte_t *entry = NULL;
for(nadd = 0; nadd < size; nadd += PGSIZE)
{
entry = pgdir_walk(pgdir,(void *)va, 1); //Get the table entry of this page.
*entry = (pa | perm | PTE_P);
pa += PGSIZE;
va += PGSIZE;
}
}
实现虚拟地址和物理地址之间的转换,通过一个循环来实现物理地址的分配,通过boot_map_region可以发现,其实现原理,则是通过va:虚拟地址,通过页目录找到对应的页表项,然后对页表项进行操作,页表项的第一个项存储pa:物理地址,进行一轮存储后,然后va和pa同时向前走一个pagesize
小结
在机器启动之时,BIOS对机器的相关控制端口进行初始化,随后BIOS将启动bootloader代码,由bootloader引导OS内核进入内存。但是在此之前BIOS运作,都是基于原始的物理地址进行运行,但OS内核是通过虚拟地址的方式来进行访问。所以为了让OS内核代码能够顺利引入到内存中,bootloader会引入一个简单的MMU代码实现4MB的映射,满足引入OS的基本需求。
内核引入后,操作系统自身会设计一套MMU,例如,页式和段式管理的方式。part1 和 part2 的内容则是实现mmu的一些细节,mem_init()中。调用mem_init后--->调用i386_detect_memory(),来对machine中的内存进行探测,探测有效可用空间--->调用boot_alloc,通过外部链接的方式获取段中的空闲地址,来对页目录进行初始化,后面虚拟线性地址与物理页之间的对应关系都需要页目录表来作为中介。实现对页目录进行初始化后,继续调用boot_alloc来获取空闲地址,对pages(Page Info 存储每个page的状态)进行初始化。--->调用page_init,来对pages中页面进行初始化。--->调用check_page_free_list(1)、check_page_alloc()、check_page()来对pages中的一些内存管理接口进行探测。
其中pages数组(空闲page以链表方式链接)会对所有的页面进行一个管理,其中保存了一个物理page页对应的物理地址和权限信息,在page的相关接口,都是需要一个页目录地址和虚拟线性地址作为输入,从而找到虚拟地址对应的物理page,再实现相关的操作。(虚拟地址和物理地址初始化时的对应关系建立由boot_map_region)
从最开始的的分布图,可以发现其地址转换流程,通过虚拟地址会在段式管理机制中处理,得到线性地址,而上面讲述的则是页式管理机制,其中va应该是对应段-->页中间的线性地址。
Part 3: Kernel Address Space
JOS将处理器的32位地址划分成了两部分,分为用户环境和内核环境,其中用户环境占据低地址空间,内核环境占据高地址空间,这部分划分信息在inc/memlayout.h可以查看,其中ULIM作为分界线,内核会保留256MB的虚拟地址空间。所以在Lab1时,mmu将os映射的高地址空间,这样做是为了保证用户环境的地址空间足够。同时用户环境下和内核环境下的进程只能访问各自的地址空间,在页表中会设置相关的权限位
用户环境下的进程是没有权限去访问ULIM之上的地址空间,而内核可以读写。对于【UTOP,ULIM】内核和用户环境都是同样的权限,只能读不能写,这部分地址空间将一些只读的数据结构提供给用户环境。UTOP之下的地址范围都是用户进程使用,其拥有读写权力。
下面我们将要设置UTOP之上的地址空间,即内核地址空间,inc/memlayout.h会展现这部分地址空间布局。
Exercise 5:
完善mem_init中check_page之后的代码,使程序通过 check_kern_pgdir() 和 check_page_installed_pgdir()的检测。
首先阅读memlayout的代码,可以看到内存布局图
/*
* 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.
*/
而我们下一步要做的就是完善页目录管理项,将一些重要的地址范围映射到kern_pgdir中,这里就需要使用到boot_map_region函数接口,其中perm表示了内核空间和用户空间的权限。
对linear address UPAGES进行映射
UPAGES在内存布局中可以看到的一个标记地址,同样将虚拟地址和物理页对应关系方式进行调用,同时在lab中说明了相关权限
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U); //这里就将其映射到了pages的物理地址上
映射内核的堆栈区域
在物理内存中bootstack指的就是kernel stack。kernel stack在虚拟地址中向下生长,这里我们考虑[KSTACKTOP-PTSIZE, KSTACKTOP) 的映射,但是这部分的内存段是被划分了的,如下:
[KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory
[KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed;
所以只对部分内存进行映射
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W)
上面对特定的一些地址位进行映射后,则映射整个操作系统内核
// Map all of physical memory at KERNBASE.
// Ie. the VA range [KERNBASE, 2^32) should map to
// the PA range [0, 2^32 - KERNBASE)
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
问题2:
问题3:内核空间和用户空间都是在同一个地址空间下进行映射的,为什么用户空间不能访问内核空间?又如何实现访问保护?
答:
因为,内核空间是操作系统工作的地方,是整个系统运行的根本,所以是不允许进入的,如果用户空间的进程修改相关数据,可能引发系统崩溃。保护机制,则是在jos中,通过在页表项中设置权限位
问题4:JOS中能支持的最大物理内存是多大?
答:
找到JOS中存放了多少个PageInfo,其中一个PageInfo都对应了一个结构体,然后个数乘以页面大小(4KB),从而得到可管理的内存大小.
问题5:物理内存页数达到最大,需要多大的内存开销
答:
存放pageinfo的所有空间,加上页目录所占用的空间
以上对内存布局的讨论,只针对实验中JOS中的设计。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)