- 应用程序是面向虚拟内存编写的,而不是面向物理内存编写的。
- 应用程序在运行时只能使用虚拟地址,CPU负责将虚拟地址翻译成物理地址,而操作系统负责设置虚拟地址与物理地址之间的映射。
- 操作系统仅将应用程序实际使用的虚拟地址映射到物理地址,从而提高内存资源的利用率。
- 每个应用程序只能看到自己的虚拟地址空间,从而保证不同应用程序之间所使用的内存之间的隔离。
- 每个应用程序的虚拟地址空间是统一且连续的,从而降低编程的复杂性。
地址翻译相关
- MMU (Memory Management Unit),内存管理单元
- 负责虚拟地址到物理地址的转换。
- TLB (Translation Lookaside Buffer),转址旁路缓存
- TLB 是 MMU 内部的单元,目的是为了加速地址翻译的过程。
- MMU 将虚拟地址翻译成为物理地址的主要机制有两种:分段机制和分页机制。
- 分段机制:将应用程序的虚拟地址空间划分为由若干个不同大小的段组成,比如代码段、数据段等,同时物理内存也会以段为单位进行分配。
- 分页机制:将应用程序的虚拟地址空间划分为连续的、等长的虚拟页(区分于分段机制下不同长度的段),同时物理内存也会被划分为连续的、等长的物理页。
- 分段机制
- 虚拟地址由两部分组成,“段号”+“段内地址”
- MMU 首先通过段表基地址寄存器找到段表的位置,结合待翻译的虚拟地址中的“段号”,在段表中定位到对应段的信息;取出该段的起始地址-物理地址,结合待翻译的虚拟地址中的“段内地址-偏移量”,从而得到最终的物理地址。
- 分页机制
- 虚拟地址由两部分组成,“虚拟页号”+“页内偏移量”
- MMU 首先通过页表基地址寄存器找到对应条目,结合待翻译的虚拟地址中的“虚拟页号”,在页表中定位到对应的条目;取出该条目中存储的物理页号-物理地址,结合待翻译的虚拟地址中的“页内偏移量”,从而得到最终的物理地址。
- 无论MMU在翻译地址过程中,采用的是分段机制还是分页机制,两者都能实现物理内存资源的离散分配(这是因为在虚拟地址空间中相邻的数据所对应的物理内存中的数据可以不相邻,也就是说虚拟地址中任意位置的数据都可以被映射到物理内存中任意位置的数据)。但是分页机制按照固定页大小来分配物理内存,使得物理内存资源易于管理,可以有效避免分段机制中外部碎片的问题。
- 页表:虚拟内存中的核心数据结构是页表。
- 每个页表项中除了记录了物理地址(物理页号)之外,还包括多个属性位 attribute bit
- 比如用于标识虚拟页的访问权限的权限位(该页是否可读、可写、可执行)等。
- 多级页表
- 多级页表允许在整个页表结构中出现空洞,因为在实际使用中,应用程序的虚拟地址空间中绝大部分都是处于未分配状态的。因此多级页表的设计极大减少了页表占用的空间大小;相比于单级页表需要每一页表项都实际存在且不允许部分创建,避免了空间浪费的问题。
- 优点:显著压缩页表的大小,解决了空间上的问题。
- 缺点:导致地址翻译时长增加,带来了时间上的开销。
- TLB
- MMU 引入了 TLB,用于减少地址翻译的访存次数。
- MMU 会先把虚拟页号作为键值去查询 TLB 中的缓存项(TLB缓存了虚拟页号到物理页号的映射关系)。如果找到,则可以直接获得对应的物理页号;否则则需要查询页表。
- 由于 TLB 缓存的是虚拟页号到物理页号的映射关系,因此在进行页表切换时,需要主动刷新TLB。
- 页表切换 - 情景1:进行系统调用,不需要刷新TLB。
- 在AArch64体系结构上,硬件提供了两个不同的页表基地址寄存器 TTBR1_EL1和TTBR0_EL1 分别供操作系统和应用程序来使用,应用程序与操作系统使用的是不同的页表。因此在系统调用过程中并不需要切换页表,从而避免了TLB刷新的开销。
- 在x86-64体系结构上,硬件只提供了一个页表基地址寄存器 CR3,操作系统和应用程序使用的是同一个页表,但操作系统将自己映射到应用程序页表的高地址部分。因此在系统调用过程中也同样不需要切换页表,从而避免了TLB刷新的开销。
- 页表切换 - 情景2:应用程序切换,需要主动刷新TLB。
- 当应用程序切换时是需要切换页表的,此时虚拟地址也会被“切换”。
- 由于TLB使用的是虚拟地址来查询,因此操作系统在进行页表切换的时候都需要主动刷新TLB。
- TLB 刷新
- 背景:如果操作系统在切换应用程序的过程中刷新TLB,也就是说应用程序每次被切换且开始执行的时候总是会发生TLB未命中的情况,进而不可避免地造成性能损失。
- 解决方案:为TLB缓存项打上“标签”。
- AArch64 提供了 ASID,x86-64 提供了 PCID。
- 操作系统为不同的应用程序分配不同的 ASID 作为应用程序的身份标签。
- ① 在页表中:将标签写入应用程序的页表基地址寄存器中的空闲位,比如 TTBR0_EL1 的高16位,从而起到标识该页表所属的应用程序的目的。
- ② 在TLB中:TLB 中的缓存项,也会包含有ASID这个标签,从而使得TLB中属于不同应用程序的缓存项是可以被区分开的。
- 综上,在此基础上,当MMU进行翻译时,无论是查询TLB还是查询页表,都可以根据应用程序的标签找到属于该应用程序对应的数据;此时切换页表的过程中,操作系统不再需要清空TLB缓存项,从而减少TLB刷新的开销。
- 需要注意的是,在修改页表内容时,操作系统仍然需要主动刷新TLB,以保证TLB缓存项与页表项的内容一致。
换页机制 与 缺页异常
- 换页机制:当物理内存的容量不够时,操作系统将若干物理页的内容写到磁盘等更大且更便宜的存储设备中,并且在页表中去掉虚拟页的映射(虚拟页处于“已分配但未映射”的状态),同时记录该物理页被换出到的磁盘等存储设备中对应的位置信息【换出】;然后操作系统就可以回收物理页并继续分配给别的应用程序来使用。
- 缺页异常:当应用程序访问“已分配但未映射”的虚拟页时,就会触发缺页异常。此时CPU会运行操作系统预先设置好的缺页异常处理函数,该函数会找到一个空闲的物理页或者通过换页的方式找到一个空闲的物理页,将之前写入到磁盘等存储设备中的数据重新加载到该空闲物理页中,并且在页表中填写虚拟地址到物理页的映射【换入】。之后,CPU将回到发生缺页异常的地方继续运行。
- 当前的缺页异常是由于“未映射”导致的。--- 换页机制
- 下文的缺页异常是由于“违反权限”导致的。--- 写时拷贝
- 利用换页机制,操作系统可以把物理内存中放不下的数据临时存放到磁盘等存储设备上,等到需要的时候再加载放回物理内存中,从而能够为应用程序提供超过物理内存容量的内存空间。
- 缺点:频繁的缺页异常以及换页过程都会涉及到耗时的磁盘操作,因此操作系统往往会引入预取机制进行优化。
- 预取机制:当发生换入操作时,操作系统会预测还有哪些物理页即将被访问,提前换入到物理内存。
- 目的:减少发生缺页异常的次数,优化换页过程涉及的耗时问题。
- 按需页分配机制:当应用程序申请分配内存时,操作系统选择将新分配的虚拟页标记为“已分配但未映射”状态且不必给这个虚拟页分配到对应的物理页;当应用程序访问这个虚拟页时触发缺页异常,此时操作系统才真正为这个虚拟页分配对应的物理页,且在页表中填入对应的映射。从而使得操作系统能够在应用程序真正需要使用物理内存时再分配物理页。
- 目的:有效地节约物理内存,提高资源利用率。
- 权衡:初次访存时产生的缺页异常会导致访问延迟增加。可以采用预取机制来搭配使用,从而减少发生缺页异常的次数,提高应用程序的性能。
- 虚拟内存区域 VMA(Virtual Memory Area)
- 背景:当一个虚拟内存页处于“已分配但未映射”或者“未分配”状态时,应用程序访问该虚拟页都会触发缺页异常。
- 操作系统如何记录虚拟页的分配状态?Linux 中,应用程序的虚拟地址空间会被划分为由多个虚拟内存区域 VMA 来组成的数据结构。
- 虚拟内存区域:比如数据和代码、栈、堆,分别对应三个互不连续的虚拟内存区域。
- 每个 VMA 都包含该虚拟内存区域的信息,包括起始虚拟地址、结束虚拟地址、访问权限等信息。
- 当应用程序发生缺页异常时,操作系统会通过缺页异常处理函数来判断“该虚拟页是否属于该应用程序的某个虚拟内存区域”从而来区分该虚拟页所处的分配状态。如果属于,则说明该虚拟页处于“已分配但未映射”状态;如果不属于,则说明该虚拟页处于“未分配”状态。
页替换策略
- 因为换入换出都会导致耗时增加,因此页替换策略对性能具有较大影响。
- 换出:当需要分配物理内存,而空闲的物理页已经用完或者小于某个阈值时,操作系统将根据页替换策略来选择一个或者一些物理页,换出到磁盘等存储设备中以便腾出空间。
- 换入:已被换出的内存页再次被访问时,重新从磁盘中换入到物理内存。
- 页替换策略:依据硬件所提供的页访问信息,猜测哪些页应该被换出,从而最小化缺页异常的发生次数,以提升性能。操作系统也需要考量执行策略本身所带来的开销。
- MIN 策略 = OPT 策略,理论最优的页替换策略。作为一个标准,衡量其他页替换策略的优劣。
- 在选择被换出页时,优先选择未来不会再访问的页或最长时间内不会再访问的页。
- 特点:实际上难以实现,因为页访问顺序取决于应用程序,而操作系统无法预知应用程序未来访问页的顺序。
- FIFO 策略
- 操作系统维护一个队列用于记录换入内存的物理页号,优先选择最先换入的页进行换出。
- 特点:简单所以时间开销低,但实际使用中表现不佳(因为页换入顺序与使用是否频繁,通常没有关联)。
- Second Chance 策略
- FIFO的改进版,要求操作系统维护一个先进先出的队列用于记录换入物理内存的物理页号,此外还要为每一个物理页号维护一个访问标志位,考虑了页的访问信息。
- 特点:考虑了页的访问信息,因此Second Chance策略会优于FIFO。
- LRU 策略
- 操作系统维护一个链表,按照内存页的访问顺序将内存页插入链表中。优先选择最久未被访问的内存页进行换出(链头)。
- 特点:由于需要时刻记录CPU访问了哪些物理页,因此开销往往很大。
- MRU 策略
- 与 LRU 相反,优先选择最近访问的内存页进行换出(链头)。
- 特点:与LRU相同,区别只是优先换出的选择不同。
- 时钟算法策略
- 与 Second Chance 相似,不仅记录了换入物理内存的物理页号,同时为每个物理页号维护了访问标志位。
- 区别在于 Second Chance 需要将页号从队头移动到队尾,而 时钟算法不需要,时钟算法只需要移动指针去指向下一个页号即可,所以更高效。
- MIN 策略 = OPT 策略,理论最优的页替换策略。作为一个标准,衡量其他页替换策略的优劣。
- 在选择和实现页替换策略时,操作系统的原则是以最小的开销达到尽可能接近MIN策略的效果。如果选择的页替换策略 与 实际的工作负载不匹配,导致大量的CPU时间被用来处理缺页异常以及等待缓慢的磁盘操作,从而可能导致颠簸现象,而操作系统的调度器可能还会加剧颠簸现象,CPU的利用率会大幅下降,造成严重的性能损失。
- 工作集模型:应该将应用程序的工作集同时保持在物理内存中,意味着一个应用程序的工作集要么全部在物理内存中(运行时),要么全部被换出,从而减少应用程序运行时所发生的换页次数。
- 目的:有效避免颠簸现象的发生。
- 工作集的概念指导着操作系统的换页策略,优先将工作集中的页换出。操作系统通过预测工作集,从而灵活地进行页替换。
虚拟内存
- 虚拟内存抽象,使得应用程序能够拥有一个独立而连续的虚拟地址空间,通过页表与硬件的配合,能够在对应用程序透明的前提下自动进行虚拟地址到物理地址的翻译。
- 虚拟内存还包括其他的内容
- 利用虚拟内存实现共享内存
- 写时拷贝
- 利用虚拟内存节约物理内存(内存去重和内存压缩)
- 大页
1. 利用虚拟内存实现共享内存
- 允许同一个物理页在不同的应用程序之间进行共享,即允许两个不同的虚拟页映射到同一个物理页。
- 目的:让不同的应用程序之间可以互相通信传递数据。
2. 写时拷贝
- 利用了页表中用于表示“是否可写”的访问权限位来实现。
- 写时复制机制的整个过程如下:
- 首先发生在父进程fork子进程的时候,父子进程会共享所有的物理页(通过将物理页映射到每个进程页表形成共享)且将父子进程对应的页表项修改为只读(通过在页表项中清除可写位权限位)。
- 此时,父子进程都以只读的方式共享同一个物理内存。
- 页表项中除了存储物理地址-物理页之外,还包括权限位等信息。
- 一旦某一个进程对该内存区域进行修改,就会触发缺页异常。
- 当前的缺页异常是由于“违反权限”导致的。--- 写时拷贝
- 上文的缺页异常是由于“未映射”导致的。--- 换页机制
- 触发缺页异常后,CPU会将控制流传递给操作系统预先设置的缺页异常处理函数;该函数会从对应的寄存器中获取异常原因是由于进程对只读内存执行了写操作,且可以知道被操作的内存区域被操作系统标记为“写时拷贝”。于是,操作系统会在物理内存中将原来共享的物理页重新拷贝一份,将新拷贝的物理页标记为“可读可写”然后重新映射给触发异常的进程(建立新的物理页的页表映射关系),然后再恢复该进程的执行。该操作并不会影响另一方进程,两个进程此后对这块共享的物理页的访问就分道扬镳了。
- 首先发生在父进程fork子进程的时候,父子进程会共享所有的物理页(通过将物理页映射到每个进程页表形成共享)且将父子进程对应的页表项修改为只读(通过在页表项中清除可写位权限位)。
- 特点:节约物理内存资源,让父子进程以只读的方式共享全部内存数据,避免内存拷贝所带来的时间和空间的开销。
3. 利用虚拟内存节约物理内存
① 内存去重
- 操作系统定时在内存中扫描具有相同内容的物理页,并找到映射到这些物理页的虚拟页;然后只保留其中一个物理页,将具有相同内容的其他虚拟页都用写时拷贝的方式映射到该保留的物理页,然后释放其他的物理页以供将来使用。【去掉的是物理内存】
- 该功能是由操作系统发起使用,且对于用户态的应用程序完全透明。Linux 操作系统实现了该功能,称为 KSM(Kernel Same-page Merging)。
- 优点:节约物理内存资源。
- 缺点1:会增加应用程序访存时延。
- 当应用程序写一个被去重的内存页时,会触发缺页异常从而触发内存拷贝,导致性能下降。
- 缺点2:安全性问题。
- 攻击者在内存中通过穷举的方式不断构造数据,然后等待操作系统去重,再通过访问延迟来确认是否发生了内存去重;通过这种猜测的方式去确认系统中是否存在某些敏感数据。
- 防御方式:操作系统仅在同一用户的应用程序内存之间进行内存去重,使得攻击者无法猜测别的用户的应用程序中的数据。
② 内存压缩
- 当内存资源不充足时,操作系统会选择一些“最近不太会使用”的内存页并压缩其中的数据,从而释放更多空闲的内存。当应用程序访问被压缩的数据时,操作系统会将其进行解压,所有的操作都是在内存中完成。【被压缩的是物理页-物理内存】
- 区别于换页机制。
- 换页机制旨在,操作系统可以把物理内存中放不下的数据临时存放到磁盘等存储设备上,通过释放物理页,从而能够为应用程序提供超过物理内存容量的内存空间。
- 内存压缩旨在,操作系统选择一些内存页进行压缩,通过释放物理页,从而能够节约物理内存资源。
- 换页机制的操作会涉及磁盘操作,耗时。而内存压缩的所有操作都在内存中完成,快速腾出空闲的内存空间且快速恢复被压缩的数据。
- 优点:通过在换出物理页之前引入了内存压缩,可以延迟将内存数据写出到磁盘设备的操作,写出/读入的数据量由于经过内存压缩会明显变小,从而更加高效的进行磁盘批量IO,甚至可能避免磁盘IO。
- 换出过程优化:先将数据压缩到swap区域,然后批量换出到磁盘设备。
- 换入过程优化:先将数据换出到swap区域,然后解压数据。
4. 大页
- 背景:MMU中引入TLB ,是为了减少地址翻译的访存次数。而高TLB命中率可以有效降低地址翻译的性能开销。有限的 TLB 缓存项,在内存容量需求变大的场景下,很难保证高TLB命中率。
- 大页机制能够有效缓解TLB缓存项不够用的问题。
- ① 大页的大小可以是2MB甚至1GB,大幅减少TLB的占用量。
- 原本在内存页大小为4KB的情况下,访问2MB的内存需要512个TLB缓存项。
- 使用大页,访问2MB的内存需要1个TLB缓存项。
- ② 大页的引入可以提升查询页表的效率。以 AArch64 体系结构为例,在L2页表项中存在一个特殊位-第1位,用于标识这个页表项中存储的物理页号(物理地址)是指向L3页表项还是指向一个2MB的物理页,分别对应1和0。同理,如果L1页表项的第1位为0,则表示该页表项指向的是一个大小为1GB的大页。
- 如果L2页表项的第1位为0,表示该页表项指向一个大小为2MB的大页-物理页,则会直接返回该物理地址。
- 如果L2页表项的第1位为1,表示该页表项指向一个L3的页表项,则会继续进行页表查询的流程。
- ① 大页的大小可以是2MB甚至1GB,大幅减少TLB的占用量。
- 操作系统可以利用硬件提供的大页支持,在虚拟内存中以2MB甚至1GB的大页来进行内存地址映射。
- 优点1:减少TLB缓存项的使用。
- 优点2:减少多级页表中的页表级数,从而提升查询页表的效率。
- 缺点1:过度的大页使用,可能会造成物理内存的资源浪费,因为应用程序可能未使用整个大页而只是使用了小部分内存空间。
- 缺点2:会增加操作系统管理内存的负责度。
物理内存
- 物理内存分配器会关注两个方面,一个是内存资源利用率,另一个是性能。
- 更高的内存资源利用率。尽可能减少内存资源的浪费,即减少内存碎片。
- 更好的性能表现。尽可能降低内存分配延迟和节约CPU资源。
- 通过算法来解决碎片问题能够有效提高内存资源的利用率,但是过于复杂的算法可能带来高昂的性能开销,比如会增加分配器完成内存分配请求的时间,或者由于过多的后台处理导致占用更多的CPU资源。
- 综上,一个优秀的物理内存分配器,需要兼顾内存资源利用率和性能。
内存碎片
- 内存碎片指的是无法被利用的内存,内存碎片会直接导致内存资源的利用率下降。
- 内存碎片又分为外部碎片和内部碎片。
- 外部碎片:每个未分配的空闲内存都小于请求分配的内存大小,但是加起来却足够。也就是说,系统中存在足够的空闲内存,但是无法满足该内存分配的请求。此时,这些无法使用的空闲物理内存就被称为外部碎片。
- 通常会在多次内存分配和内存回收之后产生,物理内存在多次内存分配和回收之后,空闲的部分会处于离散分布的状态。
- 解决方式:将物理内存以固定大小划分为若干块,每次内存分配请求都使用一个块来处理。由此,外部碎片的问题解决了,但是会导致内部碎片的问题出现。
- 内部碎片:即已分配但未被使用的内存;当分配的内存大于实际使用的内存,就会导致内存的浪费,即产生了内部碎片。
1. 物理内存分配器 - 伙伴系统分配器
- 将物理内存划分为连续的块,以块为基本单位进行内存分配。每个块都由一个或者多个连续的物理页组成,每个块中包含的物理页的数量必须是2的n次幂,而最小的分配单位是一个物理页(4KB)。当一个请求需要分配m个物理页时,伙伴系统将寻找一个大小合适的块,该块包含2^n个物理页,且满足 2^(n-1) < m ≤ 2^(n)。
- 实现:使用空闲链表数组来实现伙伴系统。
- 全局会有一个有序数组;数组中的每一项都会指向一个空闲链表,每个链表代表着不同的内存块大小。
- 每个链表会将其所对应的“内存块的大小”的空闲块给连接起来。也就是说,一个链表中的空闲块大小相同。
- 实现1:内存分配
- 伙伴系统分配器首先计算出应该分配多大的空闲块,即合适大小的内存块。然后查找对应的空闲链表。
- 如果链表不为空,则直接从链表头部取出空闲块来进行内存分配。
- 如果链表为空,则分配器会依次查找更大内存块的链表,并从该链表头取出空闲块来进行分裂操作,从而获得两个小一号的内存块;此时将使用其中一个来服务内存分配的请求,而另一个内存块将作为空闲块插入对应的内存大小的链表中。
- 实现2:内存回收
- 伙伴系统分配器首先找到“被释放的内存块”的伙伴块。
- 如果伙伴块处于非空闲状态,则将被释放的块直接插入对应的内存大小的空闲链表中,完成内存释放。
- 如果伙伴块处于空闲状态,则将它们进行合并操作,合并后的块将被当成一个完整的块释放,并往上重复此过程。
- 综上,① 在处理内存分配请求时,大的内存块会被分裂成两个小一号的块,这两个块互为伙伴;分裂得到的块可以继续分裂,直到得到一个大小合适的块【分裂】② 在一个内存块被释放后,内存分配器会找到其空闲的伙伴块并将这两个伙伴块进行合并从而形成一个大一号的空闲内存块;合并得到的块可以继续向上合并【合并】。由于分裂操作和合并操作是级联的,因此能够很好地缓解外部碎片的问题。
- 伙伴系统能够有效工作的一个重要原因是如何确定一个块的伙伴块?互为伙伴块的两个块,其物理地址仅仅只有一个位不同 且 该位由块的大小决定。
- 比如块的大小是8KB(2^13)。
- 此时,块A(0-8KB)和块B(8-16KB)互为伙伴块,它们的物理地址分别是0x0000、0x2000,仅有第13位不同。
- 优点:伙伴系统能高效、有效地管理物理内存页。
- 用处:伙伴系统分配器,被广泛用于分配连续的物理内存页。
- 缺点:内部碎片问题,降低了内存资源的利用率。
2. 物理内存分配器 - SLAB分配器
- 背景:伙伴系统分配器的最小分配单位是一个物理页-4KB,但是大多数情况下,内核需要分配的内存大小通常只是几十或者几百个字节,远远小于一个物理页的大小。在此情况下,使用伙伴系统分配器则会导致严重的内部碎片问题,降低了内存资源的利用率。
- 于是,设计了另外一套内存分配机制,用于分配小内存:SLAB 分配器。
- SLAB 分配器遇到的问题:比如维护太多队列、实现日趋复杂、存储开销也由于复杂的设计而增大等。
- SLUB 分配器,是在 SLAB 的基础上设计的,极大简化了 SLAB 的设计和数据结构,在降低复杂度的同时依然能够提供与原来相当甚至更好的性能,同时继承了 SLAB 分配器的接口。
- SLOB 分配器,是 SLAB 家族中最简单的分配器,主要是为了满足内存资源稀缺的场景需求而设计(比如嵌入式设备);具有最小的存储开销,但在碎片问题的处理方面比不上其他两种处理器。
- 在Linux 2.6.23之后,SLUB分配器成为 Linux 内核种默认使用的分配器。
- SLUB 分配器,为了满足操作系统频繁分配小对象的需求,依赖于伙伴系统进行物理页的分配。也就是说,SLUB 是把伙伴系统分配出来的大块内存进一步细分为小块的内存来进行管理。
- SLUB 分配器只分配固定大小的内存块,块的大小通常是 2^n 个字节;可以根据实际使用的需要来设置一些别的大小从而减少内部碎片。对于每一种特定的内存块大小,SLUB 分配器都会使用独立的内存资源池来进行分配。也就是说,每个内存资源池中,slab 所支持的内存块大小不同;通过在 slab 头部加入元数据来指定该 slab 支持的内存块大小。
- 实现:SLUB 分配器会根据开发者的配置或者默认配置,向伙伴系统申请一定大小的物理内存块,即一个或者多个连续的物理页。将获得的物理内存块作为一个 slab(数据结构),slab 内部又会划分为等长的小块内存,其中已分配的小块内存和空闲的小块内存混杂在一起。slab 内部空闲的小块内存会被组织成为一个空闲链表的形式。
- 一个内存资源池,通常还有 current 和 partial 两个指针
- current 指向一个 slab,所有分配请求都会从该指针指向的 slab 中获得空闲内存块。
- partial 指向“由所有拥有空闲块的 slab 所组成的链表”。
- 一个内存资源池,通常还有 current 和 partial 两个指针
- 实现1:内存分配
- 分配器首先定位到能够满足请求大小且内存大小最接近的内存资源池,然后从该内存资源池的 current 指针所指向的 slab 中取出一个空闲内存块返回。
- 如果 current 指针所指向的 slab 在取出一个空闲块之后不再拥有空闲块,即该 slab 中的小块内存已经全部分配完,则会从 partial 指针所指向的链表中取出一个 slab 给 current 指针。如果此时,partial 指针所指向的链表为空,即表示当前内存资源池中所有的内存都分配完毕,则 SLUB 分配器会向伙伴系统申请分配新的物理内存来作为新的 slab。
- 实现2:内存回收
- 根据释放块的大小找到对应的内存资源池,将释放的内存块放入对应的 slab 的空闲链表中。
- 如果该 slab 原本已经没有空闲块,即在将释放的内存块插入之前该 slab 已经全部分配完,则将其重新移动到 partial 指针所指向的链表中。
- 如果将释放的内存块插入链表后,该 slab 中的全部内存块都是空闲内存块,那么就将该 slab 释放并还给伙伴系统。
- 优点1:分配速度快速,因为是直接从 current 指针所指向的 slab 取出第一个空闲块来进行内存分配。通过合理设置不同大小的内存资源池,可以尽可能减少内部碎片导致的开销。
- 优点2:有效避免了外部碎片。
3. 物理内存分配器 - 基于空闲链表的内存分配方法
- 在用户态的内存分配器中使用,比如堆内存分配器
① 隐式空闲链表
- 链表中的每个元素都代表了一块内存区域,空闲内存块和已分配内存块混杂在同一个链表中,也就是说链表中存放着所有的内存块。
- 内存块:每个内存块的头部都存储了元数据信息,包括用于表示当前内存块是否空闲、当前内存块的大小等信息。通过内存块的大小,可以找到下一个内存块的位置(下一个内存块可能是空闲内存块,也可能是已分配的内存块);通过内存块的元数据信息,可以知道当前找到的内存块是否是一个空闲内存块。
- 内存分配
- 分配器在链表中依次查询,找到第一块大小足够的空闲内存块即返回。
- 如果找到的第一个空闲内存块的大小不仅能满足分配请求的大小,还有足量剩余,则将该内存块进行分裂,一部分用于服务内存分配请求,另一部分留作新的空闲块,从而缓解内部碎片的问题。
- 内存回收
- 分配器会根据内存块的地址,优先检查该内存块紧邻的前后内存块是否空闲?如果有空闲内存块存在,则进行合并从而产生更大的空闲内存块,从而有效避免外部碎片的问题。
② 显示空闲链表
- 区别于隐式空闲链表:显示空闲链表仅把空闲内存块放在链表中(而隐式空闲链表是将所有内存块放在链表中)。
- 链表中的每个元素-每个空闲内存块,大小不一。
- 内存块:由于下一个空闲内存块可能出现在内存中的任何位置,因此不能再依靠内存块的大小来寻找下一个空闲内存块的位置。空闲内存块需要额外维护两个指针分别指向前后的空闲内存块(prev 和 next)。
- 分配器只需要在空闲内存块中维护指针,内存块的数据部分可以复用。
- 内存分配 与 内存回收:与隐式空闲链表相同。
- 优点:内存分配的速度会更快。
- 因为显示空闲链表进行内存分配所使用的时间,仅与空闲内存块的数量成正相关。
- 而隐式空闲链表进行内存分配所使用的时间与所有内存块的数量成正相关(包括空闲内存块和已分配内存块)。
- 该优势在内存使用率高的情况下更加明显,因为空闲内存块的数量更少,而已分配内存块的数量更多。
③ 分离空闲链表
- 在显示空闲链表的基础上构建的。
- 通过维护多个不同的显示空闲链表,每个链表对应不同的内存大小,即每个链表服务固定范围大小的内存分配请求。
- 内存分配
- 首先找到内存块大小所对应的显示空闲链表。
- 如果在内存块大小对应的显示空闲链表中找到合适的空闲内存块,则从该链表中取出一个空闲内存块;如果该内存块不仅能够满足内存分配请求的大小,而且还有剩余,则将剩余部分插入到对应内存大小的空闲链表中。
- 如果在内存块大小对应的显示空闲链表中找不到合适的空闲内存块,则依次去更大的内存块大小所对应的显示空闲链表中寻找。
- 内存回收
- 首先采用单个显示空闲链表的合并策略,然后将合并产生的空闲内存块插入到内存块大小所对应的空闲链表中。
- 优点:相比于基于单个显示空闲链表的普通显示空闲链表设计,基于多个显示空闲链表的分离显示链表设计的优势在于能够获得更好的性能。
- ① 分配内存速度更快。
- ② 多个显示空闲链表可以更好地支持并发操作。
物理内存 与 CPU缓存
- ① CPU 引入CPU缓存,通过缓存来间接访问物理内存中的数据。
- 因为访问缓存比访问物理内存要快很多,从而能够极大提升计算机系统的性能。
- 然而相比于物理内存,缓存要小很多。
- ② 物理内存中的数据会根据物理地址,以缓存行 cache line 为粒度放入到 CPU缓存中。如果缓存已满或者存在缓存冲突,则会根据预设的替换策略来替换掉整个缓存行。
- ③ 操作系统在给应用程序分配物理页时,如果能够分配尽量不会造成缓存冲突的物理页,则可以使得尽可能多的应用程序数据存放到CPU缓存中,从而充分利用缓存大小来提升应用访存性能。
管理CPU缓存资源
① 软件方案:染色机制
- 将能够被存放到缓存中不造成缓存冲突的物理页(即不同位置的物理页),标记上不同的颜色;在为连续虚拟内存页分配物理页时,优先选择不同颜色的物理页(即不同位置的物理页)来进行内存分配。
- 由于连续的虚拟内存页通常可能在短时间内被相继访问,因此为连续虚拟内存页分配不同颜色的物理页,可以使得被访问的数据会同时处于缓存中且不引起缓存冲突,从而避免了缓存未命中带来的开销。
② 硬件方案:Intel CAT
- Intel缓存分配技术:Intel Cache Allocation Technology
- 背景:一般来说,CPU 的最末级缓存会被多个CPU核心共享。由于每个CPU核心可以同时运行不同的应用程序,这些应用程序都会争用最末级缓存的资源,因此可能会导致性能抖动甚至系统的整体性能下降。
- 操作系统设置应用程序所能使用的最末级缓存的大小和区域,从而实现最末级缓存资源在不同应用程序之间的隔离。
③ 硬件方案:ARMv8-A MMAP
- 在 AArch64 处理器中也有 MPAM(Memory System Resource Partitioning and Monitoring) 技术;支持配置多个分区ID-Partition ID,并且限制每个分区能够使用的缓存资源。
- 操作系统可以把应用程序划分到某个分区 Partition,从而限制应用程序能够使用的缓存资源。