内存管理是操作系统中经典的话题。小型嵌入式系统一次只需要执行一个任务,对内存管理没有要求。现代的操作系统通常要同时执行多个进程,多个进程所占用的内存之和通常超出物理内存的容量大小。即便内存容量也在不断的增长,但始终跟不上软件体积膨胀的速度。甚至有些庞大的程序所需要的内存就足以塞满整个物理内存空间。所以,现代操作系统的设计者就要想办法来调和系统的多任务同时运行、软件体积膨胀和有限的物理内存容量之间的冲突,想尽办法做到鱼和熊掌兼得。这就是本文所介绍的操作系统的内存管理。本文所介绍的主要是:
回答这些问题,期间会涉及到以下这些概念,读完本文会对这些概念有一定了解:
把物理地址暴露给进程会带来2个严重问题:
总之,在系统中没有对物理内存的抽象,很难实现上述场景。解决办法是使用地址空间。
地址空间是一个进程可用于寻址内存的一套地址集合。为程序创建了一种抽象的内存。每个进程都有自己的地址空间。地址空间是对物理内存的抽象,就像进程抽象了CPU。 有了地址空间的概念,每个程序都有一个独立的地址空间,使得A程序的地址28和B程序的地址28所对应的物理地址不同。所以,操作系统需要保证2个进程的地址空间上相同的地址对应不同的物理地址,曾经实现这一能力的办法是动态重定位。
现代计算中,计算机通常会同时运行多个程序,即多个进程同时存在于内存中。所有进程所需要的RAM(random access memory)总和通常会超出存储器(内存)所能支持的范围(比如8G的内存)。甚至有时候一个大型的程序自己所需要的RAM就会超过内存的最大容量。有两种应对内存超载的方法(本节只介绍交换技术):
因为很多程序设计语言都允许从堆中动态的分配内存,所以,进程的数据段可以增长。那么问题来了?进程被创建或通过交换技术被换入时,应该给它分配多大的内存呢?
解决办法是可以为进程额外分配一些内存。但当进程换出到磁盘时,只需交换进程实际上使用的内存中的内容。无需交换额外的未使用的内存,如下图3-5a。
如下图3-5b,代码段(程序)的长度是固定的。进程有2个可增长的段:数据段、堆栈段。数据段可以动态分配和释放堆变量。堆栈段可以存放普通的局部变量和返回值。堆栈段和数据段相向增长,如果在两者之间的内存用完了,进程必须移动到足够大的空闲区。
上面介绍了应该给进程分配多大的内存。在动态分配内存时,操作系统必须对其进行管理,操作系统需要知道哪些内存在使用,哪些内存未使用(可以再次被分配)。一般而言,有2种方法跟踪内存使用情况:
如下图3-6a、3-6b和3-6c分别描述了位图和链表进行存储管理的原理。
虚拟内存出现的出现的背景主要包括以下3点:
每个程序拥有自己的地址空间(上面已经介绍过了地址空间的概念)。地址空间被分割成多个块,每一块称作一页(page)或一个页面。进行划分这些页的技术叫做分页(paging)。
从某个角度讲,虚拟内存是对基址寄存器和界限寄存器的一种综合。虚拟内存使得整个地址空间可以用相对较小的单元(页)映射到物理内存,而不是为正文段和数据段分别进行重定位(基址寄存器和界限寄存器的动态重定位)。
虚拟内存很适合在多道程序设计系统中使用。 许多程序的片段同时保存在内存中,当一个程序等待IO时,可以把CPU交给另一个进程运行。
我们已经知道MMU通常是作为一个单独的芯片,其作用是把虚拟地址映射为物理内存地址。这里简单介绍下MMU把虚拟地址映射为物理地址的内部原理。假设在16个4KB页面下,MMU内部操作可见下图:
页表的目的是把虚拟页面映射为页框。从数学角度,页表是一个函数,参数是虚拟页号(高位部分,假设是高4位),结果是物理页框号。通过这个函数可以把虚拟地址中的虚拟页面域(高位部分,假设是高4位)替换为页框域,从而形成物理地址。这也是MMU的作用。
通过上述MMU内部原理,页表的一种最简单的实现,虚拟地址到物理地址的映射可以概括为:虚拟地址被分成虚拟页号(高位部分)和偏移量(低位部分)两部分。例如,对于16位地址和4KB的页面大小,高4位可以指定最多16个虚拟页面中的一页,而低12位确定了所选页面中的字节偏移量(0~4095)。但使用3或5或其他位数才分虚拟地址也开始可行的,不同的划分方案对应不同大小的页面。
构成页表的每一项被称为页表项。页表项主要由以下几个域组成:
我们已经了解了虚拟内存和分页。任何分页系统的实现都要老驴2个问题:
综上两条,对大而快速的页映射的需求成为构建计算机的重要约束。多年以来,计算机的设计者已经意识到这个问题并找到了一个解决方案。这种方案基于这样一种观察:大多数程序总是对少量的页面进行多次的访问,而不是相反。因此,只有少量的页面会被反复读取,其他的页表项很少被访问。
基于上面的的观察结论,可以为计算机设置一个小型的硬件设备,不需访问页表就可将虚拟地址直接映射为物理地址。这种设备称为TLB(Translation Lookside Buffer,转换检测缓冲区)又称为相联存储器或快表。TLB通常在MMU中,包含少量的表项。下图中的表项为8个,实际中很少超过256个。每个表项记录了一个页面的相关信息,包括:
TLB的工作流程是:将一个虚拟地址放入MMU中进行转换时,硬件首先通过将该虚拟页号与TLB中所有表项同时进行(并行)匹配,判断虚拟页面是否存在TLB中。如果发现一个有效的匹配并且要进行的访问操作并不违反保护位,则将页框号直接从TLB中取出而不必再访问页表。如果虚拟页号不再TLB中,就会进行正常的页表查询,然后从TLB中淘汰一个表项,并用页表中找到的表项代替TLB中淘汰的表项。这样,如果这个页面很快被再次访问,第二次访问TLB时就会命中,而不必再访问内存中的页表。当一个表项被清除出TLB时,将被清除表项的修改位(M位)复制到内存中对应的页表项中。
以上介绍的是硬件的方式实现TLB,即对TLB的管理和TLB的失效都是由MMU硬件来实现。但现代的机器中几乎所有的页面管理都是在软件中实现的。TLB表项被操作系统显示的装载。当发生TLB访问失效时,不再由MMU到内存页表中查找并取出需要的页表项,而是生成一个TLB失效并抛给操作系统。系统会先从内存页表中找到该页面,然后从TLB中删除一项,并把找到的页面的表项装载到TLB中。以上这一切都要在有限的几条指令中完成,因为TLB失效比缺页中断发生的更频繁,毕竟TLB的容量并没有那么大。
当使用软件TLB管理时,一个进本的要求是要理解两种不同的TLB失效(软失效、硬失效)的区别:
= 该算法将每个页面和一个软件计数器相关联,计数器初始值为0。每次时钟中断时,由操作系统扫描所有的页面,然后把页面的R位(0或1)加到它的计数器上。每个计数器反应了对应页面的访问频次。发生缺页中断时,淘汰计数最小的页面。即访问频次最低的页面。
最好的两种算法是老化算法和工作集时钟算法,他们分别基于LRU和工作集,二者都具有良好的页面调度性能,可以有效的实现。在实际应用中,这两种算法可能是最重要的。 很多程序为了提升其启动速度,会对其可执行文件的二进制代码进行重新排布,称为二进制重排。其原理就是将启动时的指令符号尽量的排布在相邻的几个页面中,尽量减少程序启动时的缺页中断率。
分段的好处:
易于编程、模块化、保护、共享
分段的实现分为两类:
棋盘形碎片(外部碎片):在进程运行一段时间后,因为段的转入转出,进程的地址空间内存被划分成许多快,一些块包含段,一些则称为空闲区,空闲区的碎片导致了大量的内存浪费。当然这种问题可以通过内存紧缩来解决。
我们已经了解了分段的优点,也知道了分页的优点:
纯分段的缺点也不难想象:
所以有必要对段进行分页,这样段也可以实现“按需加载”,只有那些真正需要的页面才会调入内存,不必把段的全部内容装入内存。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。