实验2正篇——内存管理
Posted yiye_01
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实验2正篇——内存管理相关的知识,希望对你有一定的参考价值。
经过这么久的准备终于进入正题了。本实验主要介绍的是我们实现的操作系统中的内存管理。实验将以两部分内容进行讲解——内核与操作系统我们将互用,请根据实际情况理解:第一部分是内核的物理内存分配器,内核能够分配内存并且能够释放之。分配的内存单元为4k,被称之为内存页。 我们需要实现的任务是维护数据结构用于记录那些内存被使用(分配或者释放),在此基础上实现一个内存分配器。虚拟内存,内核与用户软件映射虚拟地址到物理地址。
第二部分是内核映像。
实验前准备——更新代码
Git更新软件到实验2:
1.更新最新代码:git pull
2.创建分支2:git checkout -b lab2 origin/lab2
更新的主要文件为:
inc/memlayout.h--->包含内存管理的数据结构
kern/pmap.c--->操作系统内存管理代码
kern/pmap.h
kern/clock.c--->读取COMS寄存器的代码——该部分代码被内存检测代码替换
kern/clock.h
在操作系统的内存管理实现过程中会反复用到虚拟地址与物理地址的转换,所以我们首先详细介绍看一张完整的概念关系图:
如上图所示虚线有三个区域:物理内存管理区(1),虚拟地址与物理地址对应区(2),地址转换区(3)。先看1区,物理内存可以分解为若干连续的4KB为单元的物理页——分配与释放的逻辑单元,为了追踪与记录物理页的使用,我们定义了物理页信息来抽象物理页,定义一个数组与物理内存大小一一对应,而每个元素即物理页信息与每4K的物理页一一对应;而2区主要是介绍虚拟地址通过地址转换为物理页(实际不用考虑),然后通过地址中的偏移量(L12),转换为物理地址,而且虚拟地址可以根据x86的内存管理机制分为3部分——高10位,中10位,低12位,同时在程序使用过程中可以将虚拟地址分为两种——其一为程序虚拟地址,其二为内核虚拟地址,对于内核实现过程中会有一个特殊的映射关系——将内核地址与物理地址建立一一对应的关系;最后3区是描述了地址转换的详细过程。
对于如上的理解,为了方便管理物理内存,我们需要代码实现两个部分——物理内存管理与虚拟地址与物理地址对应。针对这两个部分,我们将以面向对象的方式来描述他们,所以我们抽象了两个类,其一为页管理类,地址转换类,它们分别描述了物理内存管理与地址转换过程。
一)物理内存管理
本节我将以面向对象的方式来描述内存管理代码,它的主要实现在kern/pmap.c中实现。内存的基本管理策略是记录与追踪内存的分配与使用。首先,我们应该明白内存的基本管理单元为页——大小为4KB,所以我们分配与释放的基本大小为1页,同时为了共享已经分配的内存页,需要对内存页进行计数。
针对如上描述我们以页管理器为对象,用类的方式描述核心的数据结构如下:
属性:
struct PageInfo //页管理器的核心数据结构,对每个物理页的抽象——页信息
struct PageInfo *pp_link;//指向下一个没有使用的物理页,由此链接成链表用于记录没有使用的物理页。
uint16_t pp_ref;//物理页的引用计数。
;
方法:
voidpage_init(void);//页管理器的初始化,初始化页链表,所有的页信息添加到页空闲列表中
输入:alloc_flags表示是否将分配的物理页清0.(alloc_flags=1)
输出:分配的物理页信息
struct PageInfo *page_alloc(int alloc_flags);//为了分配一个物理页,需要将其对应的页信息从页空闲表中移除,然后被程序使用。
输入:需要被释放的物理页信息
voidpage_free(struct PageInfo *pp);//将需要释放的物理页信息,归还给页空闲表
输入:需要修改的物理页信息
voidpage_decref(struct PageInfo *pp);//减少页信息引用,当引用为0时,释放页
输入:物理页信息
输出:物理地址
physaddr_t page2pa(struct PageInfo *pp);//将物理页信息映射为物理地址
输入:物理地址
输出:物理页信息
struct PageInfo* pa2page(physaddr_t pa);//将物理地址映射为物理页信息
输入:物理页信息
输出:内核虚拟地址
void* page2kva(struct PageInfo *pp);//将物理页信息转换为内核虚拟地址
实例化:
extern struct PageInfo *pages;//页信息数组指针,用于建立与物理页一一对应的关系,在mem_init()中初始化,分配npages长度的数组。
struct PageInfo *page_free_list;//页空闲列表指针,用于指向执行未使用的物理页信息。
二)虚拟内存
根据x86的内存管理机制——分段与分页,我们需要对页目录与页表进行操作,为此,我们也以面向对象的方式来描述之。分页机制主要操作的是页目录与页表——根据x86的32位分页模式,可知页目录其实为4K大小的4K对齐的数组,每个数组元素为32位。因为页目录项是指向页表基地址,所以操作的主要对象为页目录,其中穿插了页表。
针对如上描述我们以地址转换过程为对象,用类的方式描述核心的数据结构如下:
属性:
typedef uint32_t pde_t;//定义页目录项。页表项可以分为两部分——映射的物理地址,对应的访问权限
pde_t *kern_pgdir;//以页目录项的数组,表示内核初始化的页目录
方法:
输入:pgdir为操作的页目录
pp为映射到物理页信息
va为需要映射的虚拟地址
perm为映射的权限
输出:操作状态——0为成功,负数为失败的原因
intpage_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm);//实现将给的虚拟地址va以perm的权限映射到物理页信息pp,映射表是建立在pgdir的页目录中。
输入:pgdir为操作的页目录
va为需要释放的虚拟地址
voidpage_remove(pde_t *pgdir, void *va);//释放虚拟地址va对应的页表项。
输入:pgdir为操作的页目录
va为用于查询的虚拟地址
pte_store为查找到的页表项
输出:查找到的物理页信息
struct PageInfo *page_lookup(pde_t *pgdir, void *va, pte_t **pte_store);//通过虚拟地址va查找所在的对应的物理页信息以及页表。
输入:pgdir为操作的页目录
va为需要释放的虚拟地址
voidtlb_invalidate(pde_t *pgdir, void *va);//使虚拟地址va对应的TLB(传输后备缓冲器)表无效,当修改了va所对应的页表项被修改之后,需要做此操作。
输入:pgdir为操作的页目录
va为用于查询的虚拟地址
create为是否创建新的页表项
输出:查询到的页表项
pte_t *pgdir_walk(pde_t *pgdir, const void *va, int create);//通过虚拟地址va查找所在的页表项。
三)地址映射关系
根据如上描述,我们还需要对地址映射的关系做一些总结,方便我们理解与代码实现。地址与代码实现表,这样做可以很好的用于地址区分与代码理解,在inc/types.h中定义:
C类型 | 地址类型 |
T* | 虚拟地址 |
uintptr_t | 虚拟地址 |
physaddr_t | 物理地址 |
物理地址与内核虚拟地址的对应关系如下代码所示,在kern/pmap.h中定义,基于如下关系——内核虚拟地址-内核基地址=物理地址:
<span style="font-size:12px;">#define PADDR(kva) _paddr(__FILE__, __LINE__, kva) static inline physaddr_t _paddr(const char *file, int line, void *kva) if ((uint32_t)kva < KERNBASE) _panic(file, line, "PADDR called with invalid kva %08lx", kva); return (physaddr_t)kva - KERNBASE; /* This macro takes a physical address and returns the corresponding kernel * virtual address. It panics if you pass an invalid physical address. */ #define KADDR(pa) _kaddr(__FILE__, __LINE__, pa) static inline void* _kaddr(const char *file, int line, physaddr_t pa) if (PGNUM(pa) >= npages) _panic(file, line, "KADDR called with invalid pa %08lx", pa); return (void *)(pa + KERNBASE);// </span>
建立如上关系是在函数mem_init()中实现:
boot_map_region(kern_pgdir,(uintptr_t)KERNBASE,npages* PGSIZE ,0,PTE_W);
四)内核映像——地址空间
内核为了对内存进行有效的管理,需要对整个地址空间(4G)进行合理规划,这样才能有效的进行数据保护以及程序的进程切换。为此必需在整个地址空间创建内核映像来描述之,详细请参考inc/memlayout.h。
将上图从左往右看——定义宏表示对应的地址,中间部分为对应的地址区域,右边部分为对应地址与权限。从下往上看,我们先将整个地址空间(进程空间)分为两部分一部分为内核空间与用户空间,内核空间代码为操作系统所使用的地址空间,而用户空间为用户进程所使用的地址空间。针对如上空间分布知道,我们在每个进程运行都要创建如上的地址空间,而对于内核运行时需要创建内核的地址空间。
(一)内核空间
如上图所示,内核空间与地址空间的分割点为ULIM,对于ULIM只上的地址空间用户进程是没有权限访问的。通过简单的计算内核空间用了264M的地址——内核直接映射区为256M,4M内核堆栈区(每个处理器单独堆栈),4Mio映射区。对于该区域,我们只需要详细介绍内核直接映射区。
内核直接映射区——为了简化系统实现,操作系统使用最高的256M地址空间作为内核直接访问的地址区域,该区域为内核代码所直接引用的区域——以宏KERNBASE指向的地址为界。而内核直接将该区映射到了最低256M——[KERNBASE,4G)--->[0,256M).所以对内存的物理地址访问就只需要将内核虚拟地址减去KERNBASE即可。当然对于高于256M的地址空间,我们需要做二次映射或者其他方式来访问,目前我们没有涉及,所以不做多的介绍。如下我们将更详细的介绍这部分空间的使用这样方便我们进一步理解操作系统的细节,我们先看看如下图:
如上图为在函数mem_init()下建立的内核空间:从下往上,最下的低1M为物理内存的低1M留给8088的bios映像空间;在其上的为内核执行空间——又可以分为代码段与数据段,其中堆栈在data区的4K空间(详细的描述,请参考代码kernel.ld与entry.S);后续为两给配置的地址空间段——内核页目录,页信息数组,其他数据空间。
(二)用户空间
如内核映像所示,在UTOP之下的部分为用户空间,在此区域又可以详细分为3个部分介绍:用户进程空间,用户临时空间,用户异常堆栈区。这里介绍这些有些提前了,但是为了介绍的完整性就只好如此了。
A)用户进程空间
根据应用程序的链接脚本user.ld得到运行时的映像,如下:
如上所示调试信息的stab分区从USTABDATA(0x200000)开始的2M空间,第二部分为用户程序空间UTEXT(0x800000)之上的空间,第三部分为用户堆栈区域——为USTACKTOP之下的部分。
A)用户临时空间——位于UTEMP到UTEXT的4M空间,被内核使用,作为临时拷贝数据用。
B)用户异常堆栈区——位于UXSTACKTOP之下4K空间,被内核使用,设置为用户进程的异常堆栈使用。
(三)公共访问空间
在[UTOP,ULIM)地址区间,内核与用户进程都只有读的权限。该区域也分为3部分——从上到下为4M的内核分配的页表区域,4M的页信息数组的映射区域,4M的用户程序映射区域。
一叶说:根据如上分析,可以看出实验2的部分与操作系统实现的内存管理功能,是使用页管理类与地址转换类创建内核映像的过程。从整个内核映像可以看出,内核执行空间为所有进程所共享,为用户进程提供硬件控制的接口。由内核映像可以看出,有3个堆栈空间用于进程运行时使用,它们会在不同的场景中进行切换,保证代码有序运行。从代码运行的角度来看,整个进程运行空间分为两段——一段为独立运行的部分,一段为内核运行的共享部分。而实现的内存管理的代码为内存的访问提供了最基本的操作,保证内核对内存有效的控制。当然内存在使用过程中会出现碎片——内部碎片与外部碎片,需要其他的复杂的算法来保证。为了更有效的使用内存空间,还需要进一步的对内存空间进行有效的规划与使用,这些算法在后续有一些介绍。同样,我们也需要进一步开发更有效的内存管理机制。本实验的介绍只是基础,起了“抛砖引玉”的作用。
以上是关于实验2正篇——内存管理的主要内容,如果未能解决你的问题,请参考以下文章