内存管理之伙伴系统与SLAB

一、内存碎片

内存在进行申请和释放内存的情况下,难免会产生碎片。Linux采用伙伴系统解决外部碎片的问题,采用slab解决内部碎片的问题。

内存碎片是由内存的申请和释放产生的,通常分为内部碎片和外部碎片。

  1. 内部碎片是由于采用固定大小的内存分区,即以固定的大小块为单位来分配,采用这种方法,进程所分配的内存可能会比所需要的大,这多余的部分便是内部碎片。
  2. 外部碎片是由于未分配的连续内存区域太小,以至于不能满足任意进程所需要的内存分配请求,这些小片段且不连续的内存空间被称为外部碎片。

二、伙伴系统

伙伴系统从物理连续的大小固定的段上进行分配,保证内核在申请一小块内存的情况下,不会从大块的连续空间内截取一小段,从而保证了大块内存的连续性和完整性。满足请求分配单元的大小为 2 的幂(4KB、 8KB、16KB 等)。请求单元的大小如不适当,就调整到下一个更大的 2 的幂。例如请求大小为 11KB,则按 16KB 的段来请求。
假设内存段的大小最初为 256KB,内核请求 21KB 的内存。最初,这个段分为两个伙伴,称为 AL 和 AR,每个的大小都为 128KB;这两个伙伴之一进一步分成两个 64KB 的伙伴,即 BL 和 BR。然而,从 21KB 开始的下一个大的 2 的幂是 32KB,因此 BL 或 BR 再次划分为两个 32KB 的伙伴 CL 和 CR。其中一个 32KB 的段可用于满足 21KB 请求。这种方案如图所示,其中 CL 段是分配给 21KB 请求的。

伙伴系统的一个优点是,通过称为合并的技术,可以将相邻伙伴快速组合以形成更大分段。例如当内核释放已被分配的 CL 时,系统可以将 CL 和 CR 合并成 64KB 的段。段 BL 继而可以与伙伴 BR 合并,以形成 128KB 段。最终可以得到原来的 256KB 段。

在释放内存的操作中,内核将大小为b的空闲伙伴块对合并为大小为2b的单个块,但是成为伙伴块对还需要满足下面两个条件:

  • 位于连续的物理地址中。
  • 第一个块的第一个页帧的物理地址是$2b2^{12}$的倍数。

该算法是迭代的,如果成功合并了已释放的块,则块大小翻倍,然后再次检测是否能合并成更大的块。

伙伴系统的明显缺点是,由调圆整到下一个 2 的幂,很可能造成分配段内的碎片。例如,33KB 的内存请求只能使用 64KB 段来满足。

在系统长期运行过程中,还可能出现这样的情况:系统中虽有较多空闲块,但这些空闲块都不连续即都不是彼此的伙伴,根据伙伴系统算法,这些空闲块无法合并成一个更大的空闲块而形成了内部碎片。在内核版本2.6.24之后,增加了可移动的伙伴分配器来防止这种碎片。为了预防内存碎片,内核根据可移动性把物理内存分为3类:

  1. 不可移动页:位置必须固定,不能移动到其他位置,直接映射到内核虚拟地址空间的页属于这一类。
  2. 可移动页:使用页表映射的页属于这一类,可以移动到其他位置,然后通过修改页表来映射。
  3. 可回收页:不能直接移动,但可以回收,其内容可以从数据源重新获取。

三、SLAB

按照伙伴系统分配内存,每次分配4KB的完整页面,这个单位太大了,需要把页拆分成更小的单位,可以容纳大量小对象。所以引入slab分配器。slab是Linux操作系统的一种内存分配机制,针对一些经常分配并释放的对象,如进程描述符等,这些对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,不仅会造成大量的内存碎片,而且处理速度也太慢。slab分配器基于对象进行管理,相同类型的对象归为一类(如进程描述符就是一类),每当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去Cache 上会有已分配好的并标记为空闲的相应对象比如struct task_struct 来满足请求;当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免这些内碎片。slab分配器并不丢弃已分配的对象,而是释放并把它们保存在内存中。当以后又要请求新的对象时,就可以从内存直接获取而不用重复初始化。slab分配算法采用cache 存储内核对象。slab 缓存、从缓存中分配和释放对象然后销毁缓存的过程要定义一个 kmem_cache 对象,然后对其进行初始化。

slab 层把不同的对象划分为高速缓存组,其中每个高速缓存组都存放不同类型的对象。每种对象类型对应一个高速缓存。例如,一个高速缓存用于存放进程描述符(task_struct结构的一个空闲链表),而另一个高速缓存存放索引节点对象(struct inode)。kmalloc()接口建立在slab层之上,使用了一组通用高速缓存。这些高速缓存又被划分为slab。slab由一个或多个物理上连续的页组成。一般情况下slab仅由一页组成。每个高速缓存可以由多个slab组成。
每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构。每个slab 处于三种状态之一:满、部分满或空。一个满的 slab没有空闲的对象(slab中的所有对象都已被分配)。一个空的slab没有分配出任何对象(slab中的所有对象都是空闲的)。一个部分满的slab有一些对象已分配出去,有些对象还空闲着。当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。如果没有部分满的 slab,就从空的slab中进行分配。如果没有空的slab,就要创建一个slab了。这种策略能减少碎片。
通过建立slab缓存,内核能够储备一些对象供之后使用。内核每创建一个进程都会给进程分配一个mm_struct,考虑到系统中进程的数量,内核需要维护大量的mm_struct,而且该结构体在内核中初始化、分配和释放的频率相当大,为了应付这种场景,slab为这样的对象创建缓存,slab分配器将释放的内存块保存在内部列表中,并不马上返回给伙伴系统,在下一次请求该类型对象实例时,会使用最近释放的内存块。通过这种方式,内核会减少使用伙伴系统的次数,减少处理时间;也可以使内存块驻留在CPU高速缓存的概率增大。inode结构,是磁盘索引节点在内存中的体现。这些数据结构会频繁地创建和释放。struct inode由inode_cachep高速缓存进行分配。这种高速缓存由一个或多个slab组成---由多个slab组成的可能性大一些,因为这样的对象数量很大。每个slab包含尽可能多的struct inode对象。当内核请求分配一个新的inode结构时,内核就从部分满的slab或空的slab(如果没有部分满的slab)返回一个指向已分配但未使用的结构的指针。当内核用完inode对象后,slab分配器就把该对象标记为空闲。下图显示了高速缓存、slab 及对象之间的关系。

 

与传统的内存管理模式相比, slab供了很多优点。

1、内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。

2、slab 缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。

3、slab 分配器还支持通用对象的初始化,从而避免了为同一目的而对一个对象重复进行初始化。

4、slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。

slab分配器对大多数系统都工作良好,但是在另一些情况中,它无法提供最优性能,为此在内核版本2.6后增加了两个slab分配器的替代品:

  • slob分配器为了配合某些微小嵌入式系统,减少了代码量。围绕一个简单的内存块链展开,在分配内存时使用了同样简单的最先适配算法。
  • slub分配器为了配合大规模并行系统,通过将页帧打包为组,并用struct page中未使用的字段来管理这些组,试图最小化所需要的内存开销。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值