外部碎片
当应用程序启动时,由操作系统负责给该应用程序分配其内存空间,假设此时启动了三个应用程序,操作系统分别给其分配了100m,10m和50m的内存,内存情况如下图
此时如果程序B下线,程序A和程序C之间便会空出10m内存,此时如果没有10m以下的应用启动来填充这部分内存,那么这部分内存资源便会被浪费。
这便是内存的外部碎片问题,为了解决内存的外部碎片问题,操作系统使用页式内存管理方式,尽可能减少外部碎片的大小(无法完全清除),通过将内存分为大小一致的内存页,将一个应用程序拆开,存储在不同页中,当下电脑64位和32位就是根据分页大小不同定义的(分别时8k和4k)。
页式存储
操作系统将内存分配8k或4k大小的内存页,我们这里称其位物理内存页,当一个应用程序启动时,操作系统将不会在刻意的给其分配连续的内存空间,而是随机分配给应用程序不一定是连续的内存页。
通过这样操作,外部碎片的问题是解决了,不过散落在内存各处的应用程序碎片,cpu执行时该如何寻找呢?操作系统提供了页表结构。
首先,应用程序内部使用一种虚拟地址。虚拟地址是连续的,其中高位表示当前物理内存页的页号,而低位则表示当前物理内存也中,当前地址距离当前物理内存页的物理页框号(物理内存页的第一行的真实物理地址)的偏移量。而页表内部则是记录页号和物理页框号的位置。
当cpu执行当前应用程序的机器码时,首先它会根据连续的虚拟地址去页表中查找真实的物理内存页,再通过低位去找到物理内存页的对应偏移量的真实物理地址。
通过这种方式,将应用程序所需内存拆分为8k大小,分配给内存中8k大小的页,然后通过页表记录虚拟地址和真实物理地址之见的映射关系,可以很好的解决外部碎片问题,外部碎片最大不会超过8k。不过内存可以分为大量8k大小的物理内存页,通过页表记录这些大量的内存页,也需要大量的内存控制,这些内存空间也注定会超过8k,所以即使是页表,也需要进行分页,这就是多级页表。
多级页表将页表拆分为多个8k大小,分配8k物理内存页,在由另一个页表记录当前页表的虚拟地址和真实物理地址的关系,通过一个操作系统,需要三四级页表,才能记录全部页表信息。
动态内存分配及页表生成
操作系统会分配给一个应用程序固定的内存大小,即使是页式内存管理下也不例外。但一个应用程序通常不会完全使用操作系统分配的内存,当操作系统为应用程序没有使用的内存分配物理内存页时,便会造成内存浪费。
为了解决这个问题,操作系统采用不给应用程序未使用的空间分配物理内存页,甚至页表的初始状态只有第一级的页表,页表内部也只有虚拟地址没有与其映射的页框地址,以及程序的第一行物理地址等,其余大部分页表信息都是在访问程序时才对页表进行填充。
当cpu需要执行应用程序的机器码时,需要通过页表查询真实物理地址,第一次访问时页表并没有记录相关信息,此时会触发页面错误。操作系统在捕捉到这个错误后,会跳转到应用程序的内核态,获取对应的页框填充页表信息,也就是说只有当页面虚拟地址被访问时,其页表中的虚拟地址映射的页框才会被填充。
其中空闲区域的虚拟地址也会在页表中体现,不过多级页表中,记录虚拟地址的页表也是动态生成的,所以连续的空闲区域的虚拟地址实际上并不会访问,也就不会生成对应页表,对应页表的内存资源也会被节省。
TLB缓存
一个应用程序所占使用的物理地址极多,每次执行对应地址的机器码,都需要区页表中查找,页表保存在内存中,虽然内存的访问速度较快,不过在执行程序过程中,需要大量的访问页表,内存的访问速度也无法保证程序的快速运行,所以我们需要一个IO速度更快的存储单位。
TLB缓存是CPU上的硬件单元,TLB缓存的IO速度极快,是内存的几十倍到一百倍,访问速度接进于寄存器,不过其存储空间仍然有限,不足以存下整个页表,所以他只能存储部分页表,当CPU访问页表时,会首先访问TLB缓存,如果存在虚拟地址和页框的对应关系,则直接使用,如果没有则取访问页表,并将本次访问的虚拟地址和页框关系记录在TLB中,替换掉旧的记录。
所以我们在日常写代码应该尽量连续访问连续内存数据,这样可以尽量使两个机器码在同一物理内存页中,提高TLB的命中率,进而提高程序性能。