Lab2:物理内存管理
- 进程的段地址空间由多个段组成,比如代码段、堆栈段和符号表段等等
- 段对应一个连续的内存“块”
- 不同段在物理内存中是分散的二维结构
段访问
- 首先由CPU读取逻辑地址,逻辑地址由段号和段内偏移组成
- 通过段寄存器找到相应的段描述符获得段基址
- 然后由MMU判断长度是否符合,否则就引发内存异常
- 最后通过段基址和段内偏移找到真实的物理内存
页机制
页机制把真实的物理内存分为大小相同的基本分配单位,叫做页帧,再接着把逻辑地址空间也划分为大小相同的基本分配单位,页和帧的大小必须是一致的,最后再完成从页面到页帧的映射,也就是逻辑地址到物理地址的转换
页和页帧
页帧和段的寻址方式有点像,也就是当前帧的基地址再加上帧内偏移,当前帧的基地址也是由当前的帧号和每一帧的大小确定的
页的寻址也是相同,但是页号不一定等于帧号,所以也就是在页和帧之间还有一层地址映射
页表
页表就是保存了逻辑地址到物理地址之间的映射关系,逻辑地址中保存着页号和页内偏移,到页表中查询页帧最后得到真实的物理地址
每一个页面都会对应一个页表项,页表会随着进程的变化而变化,页表项不仅记录对应的页帧还保存了一些标志:存在位、修改位和引用位
页机制的性能问题
页机制存储管理机制在访问性能方面可能因为每一次的访问地址需要两次访问而造成低效,并且如果页表过大也会造成性能问题
快表
快表就是类似一个缓存机制,将最近访问的页表项缓存到TLB中,这样每次访问都可以先到快表中查询,如果TLB命中就会节省非常大的时间
多级页表
多级页表就是将页表分为树状结构,通过级级查询找到最后的结果,这样就可以减少每级页表的长度
段页式存储管理
段页式顾名思义也就是结合了段机制和页机制,这样就可以结合段机制在内存保护方面的优势和页机制在内存利用率上面的优势
段页式其实也就是在中间多加了一层映射,由段机制产生的线性地址到页表的映射,这样在逻辑地址中存储着段表和页表的偏移地址,也就是段号和页号,然后通过段表查询到页表的地址,再通过页号查询 到真实的物理内存地址
启用分页机制
kern_entry: # load pa of boot pgdir movl $REALLOC(__boot_pgdir), %eax movl %eax, %cr3 # enable paging movl %cr0, %eax orl $(CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_TS | CR0_EM | CR0_MP), %eax andl $~(CR0_TS | CR0_EM), %eax movl %eax, %cr0 # update eip # now, eip = 0x1..... leal next, %eax # set eip = KERNBASE + 0x1..... jmp *%eax next: # unmap va 0 ~ 4M, it's temporary mapping xorl %eax, %eax movl %eax, __boot_pgdir # set ebp, esp movl $0x0, %ebp # the kernel stack region is from bootstack -- bootstacktop, # the kernel stack size is KSTACKSIZE (8KB)defined in memlayout.h movl $bootstacktop, %esp # now kernel stack is ready , call the first C function call kern_init
首先要启动分页模式,cr3寄存器就应该存放着一级页表,然后进行分页模式的使能,再做一些准备工作然后跳入kern_init内核初始化
void pmm_init(void) { boot_cr3 = PADDR(boot_pgdir); init_pmm_manager(); page_init(); check_alloc_page(); check_pgdir(); static_assert(KERNBASE % PTSIZE == 0 && KERNTOP % PTSIZE == 0); boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W; boot_map_segment(boot_pgdir, KERNBASE, KMEMSIZE, 0, PTE_W); gdt_init(); check_boot_pgdir(); print_pgdir(); }
- 先定义物理内存管理的各个函数
- 检测物理内存空间,保留已使用的内存,然后使用pmm-> init_memmap创建空闲页面列表
- 映射物理内存地址到线性地址
- 重新加载全局描述符表
页机制的代码实现
以页为单位来进行内存的分配
struct Page { int ref; uint32_t flags; unsigned int property; list_entry_t page_link; };
ref: ref作为页的引用次数
flags: 标识当前页的使用状态,比如如果这个页被内核使用那么它就是保留状态的
property: 标识当前可用的连续块的大小,这个时候这个页是作为这些空闲块的开始地址的
page_link: 空闲块的链表结构
static void default_init_memmap(struct Page *base, size_t n) { assert(n > 0); struct Page *p = base; for (;p != base + n; p ++) { assert(PageReserved(p)); p->flags = 0; SetPageProperty(p); p->property = 0; set_page_ref(p, 0); list_add_before(&free_list, &(p->page_link)); } nr_free += n; base->property = n; }
init_memmap是进行内存的初始化工作,主要就是设置标志位和引用数和加入空闲队列中
static struct Page * default_alloc_pages(size_t n) { assert(n > 0); if (n > nr_free) { return NULL; } list_entry_t *le, *len; le = &free_list; while ((le=list_next(le)) != &free_list) { struct Page *p = le2page(le, page_link); if (p->property >= n) { int i; for (i = 0; i property > n){ (le2page(le, page_link))->property = p->property - n;//如果选中的第一个连续的块大于n,只取其中的大小为n的块 } ClearPageProperty(p); SetPageReserved(p); nr_free -= n;//当前空闲页的数目减n return p; } } return NULL; }
alloc_pages是进行以页为单位进行内存的分配,用的是最简单的First-fit算法,也就是从空闲队列中找到第一个可用的块就直接进行分配,然后如果这个块大于需要分配的内存大小,那就进行切割
static void default_free_pages(struct Page *base, size_t n) { assert(n > 0); struct Page *p = base; for (; p != base + n; p ++) { assert(!PageReserved(p) && !PageProperty(p)); p->flags = 0; set_page_ref(p, 0); } base->property = n; SetPageProperty(base); list_entry_t *le = list_next(&free_list); while (le != &free_list) { p = le2page(le, page_link); le = list_next(le); // TODO: optimize if (base + base->property == p) { base->property += p->property; ClearPageProperty(p); list_del(&(p->page_link)); } else if (p + p->property == base) { p->property += base->property; ClearPageProperty(base); base = p; list_del(&(p->page_link)); } } nr_free += n; le = list_next(&free_list); while (le != &free_list) { p = le2page(le, page_link); if (base + base->property property != p); break; } le = list_next(le); } list_add_before(le, &(base->page_link)); }
free_pages是alloc_page的逆过程,一样是先设置一些标志位,然后再遍历空闲队列进行空闲块的合并,再重新入队