首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

Linux SLUB 分配器详解(2)SLUB 分配器的设计原理

Linux SLUB 分配器详解(2)SLUB 分配器的设计原理

SLUB 分配器的设计原理SLAB 分配器多年以来一直位于 Linux 内核的内存管理部分的核心地带,内核黑客们一般不愿意主动去更改它的代码,因为它实在是非常复杂,而且在大多数情况下,它的工作完成的相当不错。但是,随着大规模多处理器系统和 NUMA系统的广泛应用,SLAB 分配器逐渐暴露出自身的严重不足:
  • 较多复杂的队列管理。在 SLAB 分配器中存在众多的队列,例如针对处理器的本地对象缓存队列,slab 中空闲对象队列,每个 slab 处于一个特定状态的队列中,甚至缓冲区控制结构也处于一个队列之中。有效地管理这些不同的队列是一件费力且复杂的工作。
  • slab 管理数据和队列的存储开销比较大。每个 slab 需要一个 struct slab 数据结构和一个管理所有空闲对象的 kmem_bufctl_t(4 字节的无符号整数)的数组。当对象体积较少时,kmem_bufctl_t 数组将造成较大的开销(比如对象大小为32字节时,将浪费 1/8 的空间)。为了使得对象在硬件高速缓存中对齐和使用着色策略,还必须浪费额外的内存。同时,缓冲区针对节点和处理器的队列也会浪费不少内存。测试表明在一个 1000 节点/处理器的大规模 NUMA 系统中,数 GB 内存被用来维护队列和对象的引用。
  • 缓冲区内存回收比较复杂。
  • 对 NUMA 的支持非常复杂。SLAB 对 NUMA 的支持基于物理页框分配器,无法细粒度地使用对象,因此不能保证处理器级缓存的对象来自同一节点。
  • 冗余的 Partial 队列。SLAB 分配器针对每个节点都有一个 Partial 队列,随着时间流逝,将有大量的 Partial slab 产生,不利于内存的合理使用。
  • 性能调优比较困难。针对每个 slab 可以调整的参数比较复杂,而且分配处理器本地缓存时,不得不使用自旋锁。
  • 调试功能比较难于使用。
为了解决以上 SLAB 分配器的不足之处,内核开发人员 Christoph Lameter 在 Linux 内核 2.6.22 版本中引入一种新的解决方案:SLUB 分配器。SLUB 分配器特点是简化设计理念,同时保留 SLAB 分配器的基本思想:每个缓冲区由多个小的 slab 组成,每个 slab 包含固定数目的对象。SLUB 分配器简化了kmem_cache,slab 等相关的管理数据结构,摒弃了SLAB 分配器中众多的队列概念,并针对多处理器、NUMA 系统进行优化,从而提高了性能和可扩展性并降低了内存的浪费。为了保证内核其它模块能够无缝迁移到 SLUB 分配器,SLUB 还保留了原有 SLAB 分配器所有的接口 API 函数。
本文所列的数据结构和源代码均摘自 Linux 内核 2.6.25 版本。
每个内核对象缓冲区都是由 kmem_cache 类型的数据结构来描述的,表 1 列出了它的字段(省略了统计和调试相关的字段)。
表 1. kmem_cache 数据结构类型名称描述unsigned longflags描述缓冲区属性的一组标志intsize分配给对象的内存大小(可能大于对象的实际大小)intobjsize对象的实际大小intoffset存放空闲对象指针的位移intorder表示一个 slab 需要 2^order 个物理页框kmem_cache_nodelocal_node创建缓冲区的节点的 slab 信息intobjects一个 slab 中的对象总个数gfp_tallocflags创建一个 slab 时使用的一组额外标志intrefcount缓冲区计数器。当用户请求创建新的缓冲区时,SLUB 分配器重用已创建的相似大小的缓冲区,从而减少缓冲区的个数。void (*)(…)ctor创建 slab 时用于初始化每个对象的构造函数intinuse元数据的位移intalign对齐const char *name缓冲区名字struct list_headlist包含所有缓冲区描述结构的双向循环队列,队列头为 slab_cachesintremote_node_defrag_ratio该值越小,越倾向从本节点中分配对象struct kmem_cache_node * []node为每个节点创建的 slab 信息的数据结构(创建缓冲区的节点除外,使用 local_node 字段)struct kmem_cache_cpu * []cpu_slab为每个处理器创建的 slab 信息的数据结构
我们可以看到,SLUB 分配器的 kmem_cache 结构相对 SLAB 而言简化了不少,而且没有了队列的相关字段。值得注意的是 SLUB 分配器具有缓冲区合并的功能:当内核执行绪请求创建新的缓冲区 C2 时,SLUB 分配器会先搜索已创建的缓冲区,如果发现某缓冲区 C1 的对象大小略大于 C2,则重用 C1。测试表明,这项功能减少了大约 50% 的缓冲区数目,从而减少了 slab 碎片并提高了内存利用率。
在 SLUB 分配器中,一个 slab 就是一组连续的物理内存页框,被划分成了固定数目的对象。slab 没有额外的空闲对象队列(这与 SLAB 不同),而是重用了空闲对象自身的空间。slab 也没有额外的描述结构,因为 SLUB 分配器在代表物理页框的 page 结构中加入 freelist,inuse 和 slab 的 union 字段,分别代表第一个空闲对象的指针,已分配对象的数目和缓冲区 kmem_cache 结构的指针,所以 slab 的第一个物理页框的 page 结构就可以描述自己。
每个处理器都有一个本地的活动 slab,由 kmem_cache_cpu 结构描述。表 2 列出它的字段(省略了统计相关的字段)。
表 2. kmem_cache_cpu 数据结构类型名称描述void **freelist空闲对象队列的指针,即第一个空闲对象的指针struct page *pageslab 的第一个物理页框描述符intnode处理器所在 NUMA 节点号,值 -1 用于调试unsigned intoffset用于存放下一个空闲对象指针的位移,以字(word)为单位unsigned intobjsize对象实际大小,与 kmem_cache 结构 objsize 字段一致
在 SLUB 中,没有单独的 Empty slab 队列。每个 NUMA 节点使用 kmem_cache_node 结构维护一个处于 Partial 状态的 slab 队列。表 3 列出它的字段(省略了调试相关的字段)。
表 3. kmem_cache_node 数据结构类型名称描述spinlock_tlist_lock保护 nr_partial 和 partial 字段的自旋锁unsigned longnr_partial本节点 Partial slab 的数目atomic_long_tnr_slabs本节点 slab 的总数struct list_headpartialPartial slab 的双向循环队列
创建处理器活动 slab时,第一个空闲对象的指针被复制到 kmem_cache_cpu 结构的 freelist 字段中。虽然对象分配和释放的操作只针对处理器本地的活动 slab,但是在某些特殊的情况下会为当前处理器创建新的活动 slab 并把原先未用完的活动 slab 加到 NUMA 节点 的Partial 队列中(例如,在处理器 A 上运行的某内核执行绪申请对象,但是 A 的活动 slab 中已经没有空闲对象,因此必须创建新的 slab。但是创建 slab 的操作可能导致睡眠,所以当创建操作完成后该执行绪可能被调度到处理器 B 上,这将停止使用 B 原有的活动 slab,并将其加入 B 所在节点的 Partial 队列中)。相较 SLAB 而言,处于Partial 状态的 slab 的数目比较少,因此合理有效地利用了内存。当本地 slab 没有空闲对象时,SLUB 分配器优先从处理器所在节点的 Partial 队列中分配一个 slab 作为新的本地活动 slab,其次从其它节点中分配 slab。
内核执行绪申请对象时,直接从所在处理器的kmem_cache_cpu 结构的 freelist 字段获得第一个空闲对象的地址,然后更新 freelist 字段,使其指向下一个空闲对象。释放对象时,如果对象属于所在处理器的活动 slab 中,直接将其添加到空闲对象队列的队首,并更新 freelist 字段;否则的话,对象一定属于某 Partial slab 中。如果释放操作使得该 Partial slab 转变成 Empty 状态,则释放该 slab。可见 SLUB 分配器不需要复杂的缓冲区内存回收机制。
SLUB 的调试代码总是可用,一旦激活“slab_debug”选项,用户就可以很方便地选择单个或一组指定的缓冲区进行动态调试。
内核函数常常需要临时分配一块任意大小的物理地址连续的内存空间,如果请求不频繁的话,则没有必要创建单独的缓冲区。Linux 内核为这种请求准备了一组特定大小的通用对象缓冲区。调用 kmalloc 函数就可以得到符合请求大小的内存空间,调用 kfree 则释放该内存空间。kmalloc 工作于 SLUB 分配器之上。内核初始化时,创建一组共 13 个通用对象的缓冲区。kmalloc_caches 数组存放了这些缓冲区的 kmem_cache 数据结构。由于 kmem_cache 数据结构是通过 kmalloc 来分配的,故而只能用静态分配的 kmem_cache 结构数组来描述通用对象的缓冲区。其中 kmalloc_caches[0] 代表的缓冲区专门分配 kmem_cache_node 结构。kmalloc_caches[1] 缓冲区对象大小为64,kmalloc_caches[2] 缓冲区对象大小为192,其余第 i(3-12)号缓冲区对象大小为 2^i。如果请求分配超过物理页面大小的对象,直接调用页框分配器。为了满足老式 ISA 设备的需要,内核还使用 DMA 内存创建了 13 个通用对象的缓冲区,用 kmalloc_caches_dma 数组存放相应的 kmem_cache 结构。
返回列表