操作系统-内存管理

一个系统中的进程是与其他进程共享CPU与主存资源的。但是如果太多的进程需要太多的内存,那么他们中的一些就根本无法进行。如果当某个进程不小心写进另一个进程使用的内存,它就可能以某种完全和程序逻辑无关了令人迷惑的方式失败。

地址空间

地址空间是一个非负整数地址的有序集合:如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。在一个带虚拟内存的系统中,CPU从一个有N=2^n 个地址的地址空间中生成虚拟地址,这个地址空间叫做虚拟地址空间。n位地址空间->现在操作系统通常支持32/64位虚拟地址空间。

举个例子:一个进程的地址空间包含运行的程序的所有内存状态。比如:程序的代码(code,指令)
必须在内存中,因此它们在地址空间里。当程序在运行的时候,利用栈(stack)来保存当前的函数调用信息,分配空间给局部变量,传递参数和函数返回值。最后,堆(heap)用于管理动态分配的、用户管理的内存,类似Java中new获得内存。(当然还有其他的东西)->假设这3个部分:代码、栈、堆。
操作系统-内存管理_第1张图片
程序代码位于地址空间的顶部(在本例中从 0 开始,并且装入到地址空间的前 1KB)。代码是静态的(因此很容易放在内存中),所以可以将它放在地址空间的顶部,我们知道程序运行时不再需要新的空间。接下来,在程序运行时,地址空间有两个区域可能增长(或者收缩)。它们就是堆(在顶部)和栈(在底部)。
程序不在物理地址 0~16KB 的内存中,而是加载在任意的物理地址。当多个线程(threads)在地址空间中共存时,就没有像这样分配空间的好办法了。程序不在物理地址 0~16KB 的内存中,而是加载在任意的物理地址。 左侧图 :可以看到每个进程如何加载到内存中的不同地址。

物理和虚拟地址

计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。CPU访问内存的最自然的方式就是使用物理地址->物理寻址。
虚拟寻址: CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到内存之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做 地址翻译。

虚拟内存(VM)系统的一个主要目标是透明 操作系统实现虚拟内存的方式,应该让运行的程序看不见。因此,程序不应该感知到内存被虚拟化的事实,相反,程序的行为就好像它拥有自己的私有物理内存。在幕后,操作系统(和硬件)完成了所有的工作,让不同的工作复用内存,从而实现这个假象。

地址转换

LDE 受限直接访问 :让程序运行的大部分指令直接访问硬件,只在一些关键点(如进程发起系统调用或发生时钟中断)由操作系统介入来确保“在正确时间,正确的地点,做正确的事”。为了实现高效的虚拟化,操作系统应该尽量让程序自己运行,同时通过在关键点的及时介入,来保持对硬件的控制。高效和控制是现代操作系统的两个主要目标。

地址转换,扩展了受限直接访问的概念。利用地址转换,操作系统可以控制进程的所有内存访问,确保访问在地址空间的界限内。关键是硬件支持,硬件快速地将所有内存访问操作中的虚拟地址(进程自己看到的内存位置)转换为物理地址(实际位置)。所有的这一切对进程来说都是透明的,进程并不知道自己使用的内存引用已经被重定位,制造了美妙的假象。

一种特殊的虚拟化方式,称为基址加界限的动态重定位。基址加界限的虚拟化方式非常高效,因为只需要很少的硬件逻辑,就可以将虚拟地址和基址寄存器加起来,并检查进程产生的地址没有越界。基址加界限也提供了保护,操作系统和硬件的协作,确保没有进程能够访问其地址空间之外的内容。保护肯定是操作系统最重要的目标之一。


MMU :内存管理单元 负责地址转换
空闲列表:表示那块内存没有使用
动态重定位 : 基址 + 界限 虚拟地址->物理地址
问题(内部碎片):重定位的进程使用了从 32KB 到 48KB 的物理内存,但由于该进程的栈区和堆区并不很大,导致这块内存区域中大量的空间被浪费。这种浪费通常称为内部碎片指的是已经分配的内存单元内部有未使用的空间(即碎片),造成了浪费。

分段

分段:泛化基址 / 界限
在 MMU 中引入不止一个基址和界限寄存器对,而是给地址空间内的每个逻辑段一对。一个段只是地址空间里的一个连续定长的区域,在典型的地址空间里有 3 个逻辑不同的段:代码、栈和堆。分段的机制使得操作系统能够将不同的段放到不同的物理内存区域,从而避免了虚拟地址空间中的未使用部分占用物理内存。
操作系统-内存管理_第2张图片

基址 大小
代码 36KB 2KB
38KB 2KB
30KB 2KB

如上的假设:然后引用虚拟地址,MMU将基址值加上偏移量得到实际的物理地址。
分段的机制使得操作系统能够将不同的段放到不同的物理内存区域,从而避免了虚拟地址空间中的未使用部分占用物理内存。

外部碎片:由于段的大小不同,空闲内存被割裂成各种奇怪的大小,因此满足内存分配请求可能会很难。
分段还是不足以支持更一般化的稀疏地址空间。

空闲空间的管理

如果要管理的空闲空间大小由大小不同的单元构成,管理就变得困难。这种情况出现在用户级的内存分配库或者操作系统用分段的方式实现虚拟内存。会出现外部碎片。

空间分割与合并 如何快速并相对轻松地追踪已分配的空间 如何利用空间区域的内部空间维护一个简单的列表,来追踪空闲和已分配的空间


分割与合并

空闲列表包含一组元素,记录了堆中的哪些空间还没有分配。假设30字节的堆 : 0空闲10~使用 20空闲 ~30
这个堆对应的空闲列表会有两个元素,一个描述第一个10字节的空闲区域(字节0~9),一个描述另一个空闲区域(字节 20~29):

此时 任何大于10字节的请求都会失败,因为已经不够了,没有足够的连续可用空间 恰好10字节可以由剩下的两个空闲块任意一个满足

如果请求一个字节的内存:分配程序会执行所谓的分割(splitting)动作:它找到一块可以满足请求的空闲空间,将其分割,第一块返回给用户,第二块留在空闲列表中。----> 所有请求的空间小于某块空闲块,分配程序会进行分割

合并: 分配程序会在释放一块内存时合并可用空间。想法很简单:在归还一块空闲内存时,仔细查看要归还的内存块的地址以及邻它的空闲空间块。如果新归还的空间与一个原有空闲块相邻(或两个,就像这个例子),就将它们合并为一个较大的空闲块。-----> 这个=也是堆的空闲列表最初的样子


追踪已分配空间的大小

计算要释放的空间大小(即头块的大小加区域长度)。实际释放的是头块大小加上分配给用户的空间的大小。因此,如果用户请求 N 字节的内存,库不是寻找大小为 N 的空闲块,而是寻找N 加上头块大小的空闲块。


嵌入空闲列表

他就是一个列表 描述了堆中的空闲内存块。

假设例子:让你管理4096字节的内存块(4KB),首先初始化这个列表,开始的时候只有一个条码 记录了4096的空间 (头部信息需要占用字节的)

// mmap() returns a pointer to a chunk of free space 
node_t *head = mmap(NULL, 4096, PROT_READ|PROT_WRITE, 
 MAP_ANON|MAP_PRIVATE, -1, 0); 
head->size = 4096 - sizeof(node_t); 
head->next = NULL;

执行这段代码之后,列表的状态是它只有一个条目,记录大小为 4088。head 指针指向这块区域的起始地址,假设是 16KB.假设有一个 100 字节的内存请求。为了满足这个请求,库首先要找到一个足够大小的块。因为只有一个 4088 字节的块,所以选中这个块。然后,这个块被分割(split)为两块:一块足够满足请求(以及头块,如前所述),一块是剩余的空闲块。假设记录头块为 8 个字节(一个整数记录大小,一个整数记录幻数)对于 100 字节的请求,库从原有的一个空闲块中分配了 108 字节,返回指向它的一个指针.并在其之前连续的 8 字节中记录头块信息,供未来的free()函数使用。同时将列表中的空闲节点缩小为 3980 字节-----> 后续在来两个100进行分配 (已经有3个被分配了)—> 然后中间的释放掉 ——>库马上弄清楚了这块要释放空间的大小,并将空闲块加回空闲列表。

现在的空闲列表包括一个小空闲块(100 字节,由列表的头指向)和一个大空闲块(3764字节)。我们的列表终于有不止一个元素了!是的,空闲空间被分割成了两段,但很常见。

如果剩下的全都释放了 但是还没有合并的话 那么此时的整个内存空间虽然是空闲的 但是却被分成了小段 因此形成了碎片化的内存空间。

解决方案很简单:遍历列表,合并(merge)相邻块。完成之后,堆又成了一个整体。


基本策略

最优匹配 : 策略非常简单:首先遍历整个空闲列表,找到和请求大小一样或更大的空闲块,然后返回这组候选者中最小的一块。这就是所谓的最优匹配(也可以称为最小匹配)。只需要遍历一次空闲列表,就足以找到正确的块并返回。最优匹配背后的想法很简单:选择最接它用户请求大小的块,从而尽量避免空间浪费。然而,这有代价。简单的实现在遍历查找正确的空闲块时,要付出较高的性能代价。

最差匹配 : 方法与最优匹配相反,它尝试找最大的空闲块,分割并满足用户需求后,将剩余的块(很大)加入空闲列表。最差匹配尝试在空闲列表中保留较大的块,而不是向最优匹配那样可能剩下很多难以利用的小块。但是,最差匹配同样需要遍历整个空闲列表。更糟糕的是,大多数研究表明它的表现非常差,导致过量的碎片,同时还有很高的开销。

首次匹配 : 策略就是找到第一个足够大的块,将请求的空间返回给用户。同样,剩余的空闲空间留给后续请求。首次匹配有速度优势(不需要遍历所有空闲块),但有时会让空闲列表开头的部分有很多小块。因此,分配程序如何管理空闲列表的顺序就变得很重要。一种方式是基于地址排序。通过保持空闲块按内存地址有序,合并操作会很容易,从而减少了内存碎片。

下次匹配 : 不同于首次匹配每次都从列表的开始查找,下次匹配算法多维护一个指针,指向上一次查找结束的位置。其想法是将对空闲空间的查找操作扩散到整个列表中去,避免对列表开头频繁的分割。这种策略的性能与首次匹配很接它,同样避免了遍历查找。

其他方式

分离空闲列表:如果某个应用程序经常申请一种(或几种)大小的内存空间,那就用一个独立的列表,只管理这样大小的对象。其他大小的请求都一给更通用的内存分配程序。

伙伴系统:这种分配策略只允许分配 2 的整数次幂大小的空闲块,因此会有内部碎片的麻烦。伙伴系统的漂亮之处在于块被释放时。如果将这个 8KB 的块归还给空闲列表,分配程序会检查 “伙伴” 8KB 是否空闲。如果是,就合二为一,变成 16KB 的块。

分页

第一种是将空间分割成不同长度的分片,就像虚拟内存管理中的分段。遗憾的是,这个解决方法存在固
有的问题。具体来说,将空间切成不同长度的分片以后,空间本身会碎片化。
第二种方法:将空间分割成固定长度的分片。在虚拟内存中,我们称这种思想为分页,分页不是将一个进程的地址空间分割成几个不同长度的逻辑段(即代码、堆、段),而是分割成固定大小的单元,每个单元称为一页。相应地,我们把物理内存看成是定长槽块的阵列,叫作页帧。每个这样的页帧包含一个虚拟内存页。

操作系统-内存管理_第3张图片
为了记录地址空间的每个虚拟页放在物理内存中的位置,操作系统通常为每个进程保存一个数据结构,称为页表。页表的主要作用是为地址空间的每个虚拟页面保存地址转换,从而让我们知道每个页在物理内存中的位置。
页面大小为16字节,位于64字节的地址空间。因此我们需要能够选择4个页,地址的前2位就是做这件事的。 当进程生成虚拟地址时,操作系统和硬件必须协作,将它转换为有意义的物理地址。


页表中究竟有什么? PTE 表项 VPN虚拟页号 PFN 物理帧号
页表的组织:页表就是一种数据结构,用于将虚拟地址(或者实际上,是虚拟页号)映射到物理地址(物理帧号)。
因此,任何数据结构都可以采用。最简单的形式称为线性页表(linear page table),就是一个数组。操作系统通过虚拟页号(VPN)检索该数组,并在该索引处查找页表项(PTE),以便找到期望的物理帧号(PFN)。
X86架构的示例页表项 : 它包含一个存在位(P),确定是否允许写入该页面的读/写位(R/W) 确定用户模式进程是否可以访问该页面的用户/超级用户位,有几位(PWT、PCD、PAT 和 G)确定硬件缓存如何为这些页面工作,一个访问位(A)和一个脏位(D),最后是页帧号(PFN)本身。
操作系统-内存管理_第4张图片分页也很慢:因为它可能太大了,也会让速度变慢 对于每一个内存引用,分页都需要我们执行一个额外的内存引用,以便首先从页表中获取地址转换。

分页的优点:它不会导致外部碎片,因为分页将内存划分为固定大小的单元。其次,它非常灵活,支持稀疏虚拟地址空间。


快速地址转换(TLB 地址转换旁路缓冲存储器) :他就是频繁发生的虚拟到物理地址转换的硬件缓存 -> 地址转换缓存 (对每次内存访问,硬件先检查TLB,看看是否有期望的转换映射,如果有就完成转换,不用访问页表)

TLB基本算法:假定使用简单的线性页表(即页表是一个数组)和硬件管理的 TLB。

假设一个例子:一个数组 int[] = new int[10], 如果没有TLB的情况下 它需要10次的 分页的内存访问 如果使用TLB呢? 假设一页16字节可以保存4个 那么需要3页,第一次的时候TLB肯定是没有命中的 但是第二次的时候访问 index =1 的时候 TLB命中… 所以10次中3次没有命中 7次命中 命中率 70% 它是提高了性能的,那么如果 一页是 32 或者 64呢? 典型的页大小为 4KB (4096字节)。

如果在此次访问数组结束后,再次访问该数组? 假设TLB足够大,能缓存锁续的转换映射:命中命中… 由于时间局部性即在短时间内对内存项再次引用,所以 TLB 的命中率会很高。类似其他缓存,TLB 的成功依赖于空间和时间局部性。

时间局部性:最近访问过的 可能会很快被再次访问
空间局部性:访问内存地址X 下一次是 x的邻居

TLB替换策略: 一种常见的策略是替换最近最少使用(LRU)的项。LRU尝试利用内存引用流中的局部性,假定最近没有用过的项,可能是好的换出候选项。另一种典型策略就是随机(random)策略,即随机选择一项换出去。


分页,较小的表:页表太大,因此消耗的内存太多 线性表:线性页表变得相当大。假设一个 32 位地址空间(232 字节),4KB(212 字节)的页和一个 4 字节的页表项。一个地址空间中大约有一百万个虚拟页面(232/212)。乘以页表项的大小,你会发现页表大小为 4MB。

通常每个进程一个页表,那么一百个活动进程的话 400M (虽然不常见)

简单解决方案: 更大的表。再以 32 位地址空间为例,但这次假设用 16KB 的页。因此,会有 18 位的 VPN 加上 14 位的偏移量。假设每个页表项(4字节)的大小相同,现在线性页表中有 218 个项,因此每个页表的总大小为 1MB,页表缩到四分之一。然而 大内存页会导致每页内的浪费,这被称为内部碎片。

大多数系统在常见情况下使用相对较小的页大小: 4KB 8KB


混合方式- 分段 + 分页 减少页表的内存开销 分段:基址 + 界限/限制寄存器(知道该段多大)
组合的情况下 基址保存 该段的页表的物理地址。界限寄存器用于指示页表的结尾(他又多少有效页)
假设有 3 个基本/界限对,代码、堆和栈各一个。当进程正在运行时,每个段的基址寄存器都包含该段的线性页表的物理地址。因此,系统中的每个进程现在都有 3 个与其关联的页表。在上下文切换时,必须更改这些寄存器,以反映新运行进程的页表的位置。

​ 区别在于:每个分段都有界限寄存器,每个界限寄存器保存了段中最大有效页的值。例如,如果代码段使用它的前 3 个页(0、1 和 2),则代码段页表将只有 3个项分配给它,并且界限寄存器将被设置为 3。内存访问超出段的末尾将产生一个异常,并可能导致进程终止。以这种方式,与线性页表相比,组合方法实现了显著的内存节省。


多级页表: 将线性表变成了类似树 的结构 将页表分成页大小的单元 它只是让线性页表的一部分消失(释放这些帧用于其他用途),并用页目录来记录页表的哪些页被分配。
操作系统-内存管理_第5张图片
多级页表分配的页表空间,与你正在使用的地址空间内存量成比例。因此它通常很紧凑,并且支持稀疏的地址空间。

多级页表是有成本的。在 TLB 未命中时,需要从内存加载两次,才能从页表中获取正确的地址转换信息(一次用于页目录,另一次用于 PTE 本身),而用线性页表只需要一次加载。因此,多级表是一个时间—空间折中的小例子。


页表交换到磁盘: 将这些页表中的一部分交换到磁盘去。

超过物理内存

以上是假设地址空间很小,能放入内存中。那么如果不能呢?
为了支持更大的地址空间,操作系统需要把当前没有在用的那部分地址空间找个地方存储起来。那就
是比内存有更大的容量。 硬盘


交换空间 : 在硬盘上开辟一部分空间用于物理页的移入和移出 ,将内存的页交换到其中,并在需要的时候又交换回去,因此会假设操作系统能够以页大小为单元读取或者写入交换空间,为了达到这个目的,操作系统需要记住给的页的硬盘地址。

​ 交换空间的大小决定了系统在某一时刻能够使用的最大内存页数

​ 存在位:如果存在未设置为1 表示该页存在于物理内存中 0的话表示在磁盘上 访问不在物理内存中的页通常称为页错误。在页错误的时候,操作系统被唤起处理错误。一段 也错误的处理程序(当错做系统接受到之后,在PTE中查找地址,并将请求发送到硬盘,将页读取到内存中,硬盘IO完成时,操作系统会更新页表)

​ 交换何时发生:为了保证有少量的空闲内存,大多数操作系统会设置高水位和低水位,来帮助决定何时从内存中清除页。
原理是这样:当操作系统发现有少于 LW 个页可用时,后台负责释放内存的线程会开始运行,直到有 HW 个可用的物理页。这个后台线程有时称为交换守护进程或页守护进程,它然后会很开心地进入休眠状态,因为它毕竟为操作系统释放了一些内存。

​ 内存满了怎么半:内存交换策略 希望先交换出一个或者多个页

超出物理内存大小时的一些概念:在页表结构中需要添加额外信息,比如增加一个存在位,告诉我们页是不是在内存中。如果不存在,则操作系统页错误处理程序会运行以处理页错误,从而将需要的页从硬盘读取到内存,可能还需要先换出内存中的一些页,为即将换入的页腾出空间。


缓存管理 目标是让缓存未命中的最少,让从磁盘获取页的次数最少 公式:AMAT = (PHit·TM) + (PMiss·TD) 其中 TM表示访问内存的成本,TD表示访问磁盘的成本,PHit表示在缓存中找到数据的概率(命中),PMiss表示在缓存中找不到数据的概率(未命中)。PHit和 PMiss从 0.0 变化到 1.0,并且 PMiss + PHit = 1.0

最优替换策略 MIN 达到总体未命中数量最少 替换内存中在最远将来才会被访问到的页,可以达到缓存未命中率最低。 但是未来的访问是无法知道的!!!
还有 FIFO LRU RANDOM等

近似LRU : 需要硬件增加一个使用位 有时称为引用位, 系统的每个页有一个使用位,然后这些使用位存储在某个地方。每当页被引用(即读或写)时,硬件将使用位设置为 1。但是,硬件不会清除该位(即将其设置为 0),这由操作系统负责。

​ 时钟算法:系统中所有的页都放在一个循环列表中。时钟指针开始时,执行某个特定的页(哪个页不重要) 当必须进行页替换时,操作系统检查当前指向的页 P 的使用位是 1 还是 0。如果是 1,则意味着页面 P 最近被使用,因此不适合被替换。然后,P 的使用位设置为 0,时钟指针递增到下一页(P + 1)。该算法一直持续到找到一个使用位为 0 的页,使用位为 0 意味着这个页最近没有被使用过。

考虑脏页 如果页已被修改并因此变脏,则踢出它就必须将它写回磁盘,这很昂贵。如果它没有被修改,踢出就没成本。物理帧可以简单地重用于其他目的而无须额外的 I/O。因此,一些虚拟机系统更倾向于踢出干净页,而不是脏页。
为了支持这种行为:硬件应该包括一个修改位(脏位)每次写入页时都会设置此位,因此可以将其合并到页面替换算法中。

页选择策略 操作系统还必须决定何时将页载入内存。 按需: 操作系统在页被访问时将页载入内存中。预取 操作系统可能会猜测一个页面即将被使用,从而提前载入。 聚集写入分组写入 收集一些待完成写入 而不是一个一个

抖动: 当内存就是被超额请求时,这组正在运行的进程的内存需求是否超出了可用物理内存?在这种情况下,系统将不断地进行换页。

你可能感兴趣的:(操作系统,操作系统,内存管理)