当前位置:嗨网首页>书籍在线阅读

11-匿名内存映射

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

9.4 匿名内存映射

glibc的内存分配使用了数据段和内存映射。实现malloc()最经典的方法就是将数据段切分为一系列2的整数幂大小的块,请求会返回符合要求的最小的那个块。内存释放则只是简单地将这块区域标记为“未使用”。如果相邻的分区都是空闲的,它们会被合成一个更大的分区。如果堆的最顶端是空的,系统可以用brk()来降低断点的地址值,缩小堆占用的空间,将内存返还给系统。

这个算法称为“伙伴内存分配算法(buddy memory allocation scheme)”。它的优点是高速简单,缺点则是会产生两种类型的碎片。当使用的内存块大于请求的大小时则产生“内部碎片(Internal fragmentation)”。内部碎片会降低可用内存的使用率。“外部碎片(External fragmentation)”是指有足够的内存可以满足请求,但是这些内存被划分为两个或多个不相邻的块。外部碎片同样也会导致内存利用不足(因为可能会分配一个更大却并不合适的块)或是直接导致内存分配失败(如果已经没有其他可选的块了)。

另外,这个算法会使一块内存分配“栓住”了另一块内存,导致传统C库无法将释放的内存返回给系统。假设已分配了两个内存块,块A和块B。块A正好处在中断点的位置,块B刚好在块A的下面。当程序释放了块B,但在块A被释放前,C库也无法相应的调整中断点位置。在这种情况下,一个长时间存在的内存分配就可能把内存中所有其他空闲空间都“栓住”了。

不过,一般来说不会有问题,因为C库并没有严格地把释放的空闲空间返还给系统。通常而言,在每次内存释放后,堆所占用的空间并不会缩小。相反,malloc()实现会维护释放的内存,用户后续的内存内分配。只有当堆的大小远远大于已分配的内存大小时,malloc()才会减少数据段的大小。但是,如果分配的内存大,就不会减少。

因此,对于较大的内存分配,glibc并不使用堆,而是创建一个匿名内存映射(anonymous memory mapping)来满足分配请求。匿名内存映射和在第4章讨论的基于文件的映射很相似,但是它并不是基于文件——因此,称之为“匿名”。实际上,匿名内存映射只是一块已经用0初始化的大的内存块,以供用户使用。可以把它想象成单独为某次分配而使用的堆。由于这些映射并不是基于堆,所以不会造成数据段碎片。

使用匿名映射来分配内存有下列好处:

  • 无需关心碎片。当程序不再需要这块内存的时候,只要撤销映射,这块内存就会立即归还给系统了。
  • 匿名内存映射的大小的是可调整的,可以设置权限,还能像普通映射一样接收参数(看第4章)。
  • 每次分配都存在于独立的内存映射中,不需要管理一个全局的堆。

和堆相比,使用匿名内存映射有两个缺点:

  • 每个内存映射都是页大小的整数倍。所以,如果不是页大小整数倍的分配会浪费大量的空间。对于较小的分配来说,空间的浪费更加显著,因为和分配的空间相比,浪费的空间更大。
  • 创建一个新的内存映射比从堆中返回内存的代价要高,因为使用堆几乎不涉及任何内核操作。分配的空间越小,这些代价带来的损失越大。

权衡这些优缺点,glibc的malloc()函数通过数据段来满足小空间的分配,而使用匿名内存映射来满足大的分配。这两者之间的临界值是可配置的(请参阅9.5节“高级内存分配”部分),并且会随着glibc版本的不同而有所变化。目前,这个临界值一般是128KB:分配小于等于128KB的空间是由堆实现,而对于更大空间的内存空间则由匿名内存映射来实现。