未完待续…
学前疑问:
- 什么是虚拟内存,为什么需要虚拟内存?
- 虚拟内存与我们所熟悉的物理内存(主存, 高速缓存)有什么关系?
- 一个虚拟内存地址是如何访问物理内存并获取所需的内存字节?
- 程序的内存分布状态,为什么需要如此分布?
- 什么是内存映射,内存映射能为我们带来什么好处(利用内存映射能做什么)?
- 堆内存(动态内存)的的分配、分配所带来的问题和如何有效的利用动态内存?
为什么需要虚拟内为什么需要虚拟内存?
为什么内存要分页?
假设内存是连续分配的(也就是程序在物理内存上是连续的)
- 进程A进来,向os申请了200的内存空间,于是os把0~199分配给A
- 进程B进来,向os申请了5的内存空间,os把200~204分配给它
- 进程C进来,向os申请了100的内存空间,os把205~304分配给它
- 这个时候进程B运行完了,把200~204还给os
但是很长时间以后,只要系统中的出现的进程的大小>5的话,200~204这段空间都不会被分配出去(只要A和C不退出)。
过了一段更长的时间,内存中就会出现许许多多200~204这样不能被利用的碎片……
而分页机制让程序可以在逻辑上连续、物理上离散。也就是说在一段连续的物理内存上,可能0~4(这个值取决于页面的大小)属于A,而5~9属于B,10~14属于C,从而保证任何一个“内存片段”都可以被分配出去。
虚拟内存、虚拟页、虚拟地址
虚拟内存大小要看计算机系统的位数。
虚拟页大小是可以设置的,在Unix上一般大小为 4kb。
虚拟地址的数量是根据虚拟页大小和虚拟内存大小确定的,因为一个虚拟地址对应一个虚拟页,虚拟地址分为两部分 VPN + VPO ,虚拟页号 + 虚拟页偏移量,虚拟页号也就是每个页特有的编号,就像一个背包的商品编号9527一样; 虚拟页偏移量也就是页大小, 标识这这个页到底装了多少“有效数据”,就像这个9527的背包能装多少重量的物品一样。
内存映射
Linux 通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping).
所以内存映射也叫做内存映射文件。
虚拟内存区域可以映射到两种类型的对象中的一种:
- Linux 文件系统中的普通文件
- 匿名文件
区别:
普通文件是磁盘上的一个文件,被分成页大小的片,每一片包含一个虚拟内存页面的初始内容,这些页面是按需调度到物理内存当中的。
匿名文件由内核创建,存在于物理内存当中,并不存在于磁盘。
用户函数 mmap 使用内存映射
mmap/umap 是创建/删除虚拟内存区域的函数。例如 .bbs、栈、堆 等等就是不同的虚拟内存区域。1
2
内存映射的好处
页命中和缺页
同任何缓存一样,虚拟内存系统必须有某种方法判断一个虚拟页是否缓存在DRAM中的某个地方。如果是,系统必须确定这个虚拟页放在哪个物理页中。如果不命中,系统必须判断这个虚拟页放在磁盘哪个位置上,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到 DRAM 中,替换这个牺牲页。
页表(PTE):上面这些判断缓存的功能是由软硬联合提供的,包括操作系统软件,MMU(内存管理单元)中的地址复议硬件和一个存放在物理内存中叫做页面的数据结构,页表将许你夜映射到物理页。
每次地址翻译硬件将一个虚拟地址转换成物理地址时,就会读取页表。操作系统负责维护页表的内存,以及磁盘与DRAM之间来回传输页。
页命中:
缺页:DRAM 缓存不命中
上图流程中如果 VP4 已经被修改,那么内核将会将它复制回磁盘。
地址翻译
地址翻译概念流程:
图中可以看出一项 PTE 是一个 有效位+物理页号/磁盘地址 的结构。
假设页命中:虚拟地址通过 PTE 得知有效位为1,得到了物理页号PPN,物理页偏移量 == 虚拟页偏移量,通过 物理页号+虚拟地址偏移量 = 物理地址了,通过物理地址可以直接到物理内存中(DRAM)读取字节块。
假设页缺失:虚拟地址通过 PTE 得知有效位为0,得到了磁盘地址,然后触发页缺失异常,系统通过磁盘地址到磁盘中读取字节块,然后把字节块分页加载到物理内存当中,需要的时候选取物理内存中牺牲页交换,完成后,再次通知系统重新走虚拟地址获取物理页号的流程(跟上面页命中一样了)。
PS:页表中页命中的PTE带着物理页号,缺页的PTE带:着磁盘地址。
VA:虚拟地址 (virtual address)。
PTEA:页表条目地址(page table entry address)。
PTE:页表条目(page table entry)。
PA:物理地址(physics address)。
一个进程的虚拟内存布局
虚拟内存中的区域划分
进程内存区域结构体:
动态内存分配器
动态内存分配器 malloc 函数是维护着一个进程的虚拟内存区域(堆)的内存分配。
动态内存分配器有两种基本风格:
- 显式内存分期器,手动分配/释放内存,例如C语言标准库 malloc/free 函数。
- 隐式分配器,分配器主动检测已分配块何时不再被程序使用并进行释放,例如 Java 的
PS: 动态内存分配(Dynamic memory allocation)又称为堆内存分配
堆的内存碎片
导致堆利用率很低的主要原因是一种称为碎片(fragmentation)的现象。
碎片分为两种:
- 内部碎片
- 外部碎片
内部碎片:这个跟分配器的实现有关,例如分配器可能要增加块的大小来满足内存对齐约束条件,就会产生额外的块。(这个额外的块就是碎片)
外部碎片:这种情况发生在堆中没有一个独立的空闲块足够大来满足一个分配请求的时候发生,程序主动向内核请求额外的虚拟内存来满足这个请求。这样导致了许多分散的空闲块无法分配。(这些空闲块就是碎片)
如何提高内存利用率(减少内存碎片)
- 空闲块的处理:如何记录空闲块
- 放置: 如何选择一个合适的空闲块放置一个新分配的块
- 分割:将一个新分配的块放置到某个空闲块之后,如何处理空闲块中的剩余部分
- 合并:如何处理一个刚刚释放的块
空闲块的处理
使用隐式空闲链表这种数据结构。
放置
- 首次适配
- 下一次适配
- 最佳适配
分页跟分块的
分页:指的是内存分页,例如虚拟内存被分割成虚拟页、物理内存被分割成物理页,磁盘内存被分割成磁盘页。目的是为了减少物理内存的碎片产生,让程序逻辑上是连续的(虚拟内存),物理上是离散的(物理内存)。
分块:
堆块的格式
内存抖动
当分配器释放一个已分配的块时,分配器使用了空闲块的立即合并策略,也就是合并所有相邻的空闲块。这种模式导致了一个问题:如果有大量这些小内存块申请的请求和释放,块就会反复合并,然后马上分割,这样就产生了一种形式的抖动。