MIT6.828 - lab2 内存管理

前言

  谈到内存分配,第一反应就是C语言里的malloc(),以及高级语言C++/Java里的new关键字。可我们现在要写的是系统内核,还没有malloc()库函数(开发内核时是不能随便引用标准库的),和new。内存分配时到底在说什么?对于内核来说,内存分配就是“随意”地返回一个地址给调用方使用,只要保证这个地址不被其他人使用,那就是一次成功的内存分配了。所以,我们一般说的不管是new也好还是malloc也好,内存分配和释放的消耗其实都是内存管理器复杂管理的代价。
 

任务

        在JOS操作系统中实现分页内存管理,包括:物理页面管理(对机器拥有的物理内存的管理,包括建立对应的数据结构、处理分配和回收动作等);虚拟内存管理(将内核和用户软件使用的虚拟地址映射到物理地址)。
        在lab2开始之前,JOS的内存布局如下图所示:

1554707118743

一.物理页面管理

        在物理页面管理部分,我们为页表目录分配内存,完成页表目录的初始化,完成页面数据结构和空闲页面链表的初始化。页面数据结构是一个管理物理内存分割成的所有页面的数组,其中每一项包括指向下一空闲页面的指针和页面的引用计数,在物理页面管理过程中,将找出物理内存中不可使用的页面将其引用计数记为1,其他页放入空闲页面链表中。

1.可分配物理内存大小

        kern/pmap.c,其中最重要的函数就是mem_init(),先调用i386_detect_memory()获取物理内存的实际大小,该函数有三个参数,其中npages记录整个内存的页数,npages_basemem记录basemem的页数,npages_extmem记录extmem的页数。
        在32位i386上,页面大小固定为4KB,因此页面总数(npage)= 物理内存大小>>12(除以4K) 

2.页表目录初始化

        首先调用 boot_alloc()为页表目录分配一页的内存,并且这个页就是紧跟着操作系统内核之后,函数返回一个kern_pgdir 为指向操作系统页表目录的指针;然后调用memset()将该页内存初始化为0。
        操作系统之后在保护模式下工作,通过该页表目录查找页表进行地址转换。boot_alloc()只是被暂时用作页分配器,之后使用的真正页分配器是page_alloc()函数。

kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);

  boot_alloc()这个函数的核心思想就是维护一个静态变量nextfree,里面存放着下一个可以使用的空闲内存空间的虚拟地址,所以每次当我们想要分配n个字节的内存时,我们都需要修改这个变量的值。
       下一跳指令kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;这个指令也是将UVPT处存放一个页表kern_pgdir,并且通过PADDR将页表的真实物理地址映射起来。

3.初始化页面数据结构

        将物理内存以页为单位记录到一个数组中。维护页面的数据结构为 struct PageInfo *pages,它将物理内存分为npages页,数组中每一项PageInfo代表内存中的一页,操作系统通过这个数组追踪所有内存页的使用情况。首先为pages分配能够存放所有PageInfo项目的内存,然后将这段内存初始化为0。

pages = (pde_t *) boot_alloc(npages*sizeof(struct PageInfo));
memset(pages, 0, npages*sizeof(struct PageInfo));

        然后调用page_init()函数,主要功能包括初始化pages数组中的每一项、初始化pages_free_list链表。存放着所有的空闲页的信息。
        如果页已被占用,其 pp_ref (引用计数)被置1;若页空闲,则其 pp_ref 置0,并送入page_free_list链表中;根据注解部分的提示,第0页(第0页为物理地址的前4k,存放real-mode IDT和BIOS structures)、IO hole(为0x0A0000-0x100000的区域,被硬件保留用于特殊用途),内存extended部分(如kernel以及刚刚分配的页表目录)被占用。

4.页面的分配和释放

  初始化完后进入check_page_free_list(1)子函数检查page_free_list链表的空闲页是否合法,接着再运行check_page_alloc()这是为了检查page_alloc()(分配一个物理页,函数的返回值就是这个物理页所对应的PageInfo结构体),page_free()(将一个页的Pageinfo结构体返回给page_free_list空闲页链表)两个子函数是否能正确运行。
        当需要alloc一个页面时,从page_free_list处取出一个空闲页面,page_free_list指向下一个空闲页面;当free一个页面时,将该页面的pp_link(下一个页表地址)指向当前page_free_list(头节点),page_free_list指向free的页面。alloc和free(分配和释放)操作的过程中不修改任何页面的引用计数。

二.虚拟内存管理

        虚拟内存管理主要是对页表进行管理,包括插入和删除线性地址到物理地址的映射关系,以及创建页表等操作(之前只是创建了页表相关的数据结构,整理出可用的空闲页表,但并未填写页表目录和创建出真正的二级页表)。

1.前言

二级页表与页表项

15542765232241554277525097

 二级页表:

  • 第一级页表为页表目录,其页表项指明第二级页表中各页表的地址
    • 页目录表页面的物理地址存放在CR3中,仅高20位有效,低12位必须设置为0(因为页面4K对齐)
  • 第二级页表的页表项将线性地址(虚拟地址)映射到物理地址(实地址)
页表项共32位,其中高20位是页面基址,在页表中表示实页号,在页目录表中页面基址*2^12=相应页表的首地址(2^ 12:一个页面尺寸为4KB(2^ 12),和GDT表相同,页目录表的索引给出的是第二级页表的序号);低12位说明页(页表)的控制状态信息;两级页表中的页表项作用不同,但格式完全一样

物理地址与虚拟地址转换
        在x86体系中,一个虚拟地址(Virtual Address)是由两部分组成,一个是段选择子(segment selector),另一个是段内偏移(segment offset)。一个线性地址(Linear Address)指的是通过段地址转换机构把虚拟地址进行转换之后得到的地址。一个物理地址(Physical Addresses)是分页地址转换机构把线性地址进行转换之后得到的真实的内存地址,这个地址将会最终送到你的内存芯片的地址总线上。根据上一个lab的知识可以知道一旦进入保护模式,我们就不能直接使用线性地址或者物理地址了。所有代码中的地址引用都是虚拟地址的形式,然后被MMU(内存管理单元)系统所转换,所以C语言中的指针其实都是虚拟地址。
        在需要读写内存时,JOS内核有时候可能只知道内存的物理地址,例如新加入一个页表项时需要为他分配一块物理内存来存放它并初始化这块内存。此时可以使用KADDR(pa)将物理地址转化为虚拟地址,然后对虚拟地址进行操作。如果想通过虚拟地址的值求得物理地址的值,我们可以采用PADDR(va)指令。

引用计数
        在实际应用中经常会有多个不同的虚拟地址页被同时映射到同一物理页的情况。struct PageInfo结构中使用pp_ref字段保存物理页的引用计数。当引用计数为0,代表页面没有被使用,这个物理页才能被释放。
        在之前的实现中可以发现alloc和free操作的过程中不修改任何页表的引用计数,因此当调用page_alloc()得到页面后,总会得到一个引用计数为0的页面,需要将它的引用计数+1

2.具体实现 - 页表管理

        先完成pgdir_walk函数,这个函数的主要功能是根据给定的页目录表指针,返回va所对应的页表项指针。这里通过页目录表索引页目录项,查找页目录项中的二级页表是否存在,不存在就看create标志位是否为true如果为true就创建一个新的页表,不为true就返回NULL,最后返回页表项的虚拟地址。
  接着完成boot_map_region函数,主要负责把把虚拟地址空间范围[va, va+size)映射到物理空间[pa, pa+size)的映射关系加入到页表pgdir中。这个函数利用之前编写的pgdir_walk(),对指定的虚拟地址,完成了其页表目录和二级页表的建立,从而完成映射。pgdir_walk()的返回值是页表项的地址(虚拟地址),对于得到的页表项,填写其高20位为物理页面地址(页面基址),设置其权限为perm,存在位为1。
        再完成page_insert()主要功能是把物理内存页pp与虚拟地址va建立映射关系。思路是通过pgdir_walk查看va对应的页表项,如果va已经被映射则删除映射,再将va和pp之间的映射关系加入到页表项中。
  之后完成page_lookup函数(返回映射到虚拟地址“va”的页面。如果pte_store不为零,则将该页的pte地址存储在其中,可用于验证系统调用参数的页面权限,但不应被大多数调用者使用。),功能是返回虚拟地址va映射的物理页pageinfo结构体的指针,如果pte_store参数不为0则把物理页的页表项地址存放在pte_store中。
  最后完成page_remove函数,这个功能是删除虚拟地址va和物理页的映射关系pp_ref值要减一, 如果pp_ref减为0,要把这个页回收, 这个页对应的页表项应该被置0。

三.内核地址空间

  在这一部分就是要完善mem_init()函数,把操作系统的一些地址范围映射到新页目录kern_pgdir上,其次映射内核的堆栈区域,最后映射整个操作系统内核我们完成后整个操作系统支持的物理内存有2GB。
        (由于这个操作系统利用一个大小为4MB的空间UPAGES来存放所有的页的PageInfo结构体信息,每个结构体的大小为8B,所以一共可以存放512K个PageInfo结构体,所以一共可以出现512K个物理页,每个物理页大小为4KB,自然总的物理内存占2GB)
  JOS把32位线性地址虚拟空间划分成两个部分。其中用户环境(进程运行环境)通常占据低地址的那部分,叫用户地址空间。而操作系统内核总是占据高地址的部分,叫内核地址空间。这两个部分的分界线是定义在memlayout.h文件中的一个宏 ULIM。JOS为内核保留了接近256MB的虚拟地址空间。这就可以理解了,为什么在实验1中要给操作系统设计一个高地址的地址空间。如果不这样做,用户环境的地址空间就不够了。由于内核和用户进程只能访问各自的地址空间,所以我们必须在x86页表中使用访问权限位(Permission Bits)来使用户进程的代码只能访问用户地址空间,而不是内核地址空间。否则用户代码中的一些错误可能会覆写内核中的数据,最终导致内核的崩溃。处在用户地址空间中的代码不能访问高于ULIM的地址空间,但是内核可以读写这部分空间。而内核和用户对于地址范围[UTOP, ULIM]有着相同的访问权限,那就是可以读取但是不可以写入。这一个部分的地址空间通常被用于把一些只读的内核数据结构暴露给用户地址空间的代码。在UTOP之下的地址范围是给用户进程使用的,用户进程可以访问,修改这部分地址空间的内容。
        在所有的映射完成后,页表目录kern_pgdir和二级页表根据当前使用的内存进行了适当的填写,此时修改寄存器cr3的值为当前的页表目录的物理地址lcr3(PADDR(kern_pgdir));

资料来源: MIT6.828 Lab2_饮水小思源的博客-CSDN博客MIT-JOS系列4:内存管理_io hole_sssaltyfish的博客-CSDN博客

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值