找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4688

积分

0

好友

612

主题
发表于 昨天 18:53 | 查看: 13| 回复: 0

在 Linux 内核运行过程中,大量对象都会被频繁创建和销毁,例如进程描述符 task_struct、文件对象 struct file、网络数据结构以及各种驱动设备对象等。这些对象通常并不需要完整的物理页空间,如果每一次申请都直接通过伙伴系统获取页面,不仅效率低下,还会造成严重的内存浪费。

因此,Linux 内核引入了专门针对小对象分配优化的 slab 分配器,而 SLUB 作为现代 Linux 内核默认采用的对象分配机制,承担着大量内核动态内存管理任务。

理解 SLUB,不仅能够帮助我们掌握 kmalloc() 背后的真实工作机制,还能够进一步理解 Linux 内核如何解决高频内存分配、多 CPU 并发以及内存碎片等问题。同时,在进行驱动开发或者内核问题定位时,也能够更加准确地分析内存泄漏、Use After Free 等异常问题。

那些找不到对象的程序猿,SLUB帮你安排得明明白白!

一、Linux 内核内存管理体系:SLUB 所处的位置

1.1 Linux 内存分配整体架构:从物理页到内核对象

Linux 内核中的内存管理并不是依靠某一个单独的分配器来完成的,而是由多个层次相互配合而构成的一套完整体系。不同的内存分配机制分别负责不同粒度以及不同使用场景的内存资源,从最底层真实存在的物理页面开始,一直到内核代码最终拿到可以直接使用的对象,中间会经历多个不同的管理层次。只有先把 SLUB 放到整个 Linux 内存体系之中去观察,才能真正理解它为什么会存在,以及它到底解决了什么问题。

从底层来看,Linux 首先需要对机器上真实存在的物理内存进行管理。物理内存并不会直接以"128 字节""256 字节"这样的方式交给上层使用,而是被划分成为一个个固定大小的物理页面。对于常见的体系结构而言,一个页面通常为 4KB,不过具体大小仍然会受到硬件架构以及内核配置的影响。Linux 内核会使用 struct page 对每一个物理页面进行描述,并且通过页分配器对这些页面进行组织以及管理,而 Buddy 伙伴系统就是这一层最为重要的页面分配机制之一。

Buddy System 所擅长处理的是以 Page 为基础的大块物理内存。例如某个内核模块需要连续的 1 页、2 页、4 页甚至更多页面,那么伙伴系统能够按照对应的 order 去寻找、拆分以及合并空闲页面。可是 Linux 内核运行时所需要的内存并不全部都是整页大小。实际上,内核里面存在着数量极为庞大的小型对象,它们可能只有几十字节、几百字节或者一两千字节。如果为一个只有几十字节的对象直接分配一个完整页面,那么绝大多数页面空间都会处于没有被利用的状态。

为了解决这种问题,Linux 在 Buddy 伙伴系统之上又增加了一层专门面向小对象的内存管理机制,也就是 slab allocator。slab allocator 并不是取代 Buddy System,而是建立在 Buddy System 的基础之上。 它首先向伙伴系统申请一组页面,然后再把这些页面划分成为许多个更小的对象,之后上层内核代码需要申请对象的时候,就可以直接从这些已经划分好的空间之中取得,而不需要每一次都重新进入 Buddy System。

SLUB 就是 slab allocator 的一种具体实现。它向下从 Buddy System 获取页面,向上则通过 kmem_cache_alloc、kmalloc 等接口向内核其他子系统提供对象。 整个关系可以表示为:

内核子系统 / 设备驱动
        ↓
kmalloc / kmem_cache_alloc
        ↓
SLUB 对象分配器
        ↓
Buddy 伙伴系统
        ↓
Physical Page
        ↓
物理内存

从这个层次关系当中可以观察到,Buddy System 和 SLUB 所管理的其实并不是同一种粒度的资源。 Buddy System 所看到的是 Page,它关心的是哪些物理页面处于空闲状态、哪些页面能够组合成为更大的连续区域;SLUB 所看到的则是 Object,它关心的是一个 slab 当中还有多少个对象可以分配、当前 CPU 是否存在可用的空闲对象以及什么时候需要重新向 Buddy System 申请新的页面。

因此,SLUB 可以看作是 Linux 页级内存管理和对象级内存使用之间的一座桥梁。 它把 Buddy System 所提供的大块页面资源进一步加工成为适合内核直接使用的小型对象,使得 Linux 内核既能够利用伙伴系统管理物理内存,又能够避免大量小对象频繁直接访问页分配器所产生的性能以及空间方面的问题。

1.2 Buddy 伙伴系统:Linux 页级内存分配的基础

Buddy System 乃是 Linux 内核页级内存管理之中的重要组成部分。 SLUB 最终所使用的内存并不是凭空出现的,当某个 SLUB Cache 没有足够的 slab 可以继续提供对象的时候,仍然需要向底层页面分配器获取新的物理页面。因此,如果不了解 Buddy System 的基本思想,就很难完全看清楚 SLUB 在整个内存分配链路里面所处的位置。

Linux 将物理内存划分成为一个个 Page,并且使用 struct page 来描述这些页面。伙伴系统并不是简单地把所有空闲页面放在同一个链表之中,而是按照连续页面数量的不同,把空闲区域组织成为不同的 order。 order 和页面数量之间存在 2 的幂次关系,例如 order 0 表示 1 个页面,order 1 表示 2 个连续页面,order 2 表示 4 个连续页面,order 3 表示 8 个连续页面。随着 order 不断增加,能够管理的连续物理区域也会越来越大。

例如系统此时存在一个 order 3 的空闲内存块,也就是 8 个连续页面,可是某一次申请只需要 4 个连续页面,那么伙伴系统不会把整个 8 页全部交给调用者,而是会把这个较大的内存块拆分成为两个 order 2 的内存块。调用者取得其中一个 4 页区域之后,另外一个 4 页区域仍然会继续作为空闲内存保留在伙伴系统之中。这个拆分过程可以表示为:

order 3
8 Pages
   ↓
拆分
   ↓
order 2        order 2
4 Pages        4 Pages
   ↓
分配其中一块

Buddy System 之所以被称为"伙伴系统",关键就在于它不仅能够拆分内存,同时还能够在内存释放的时候重新进行合并。 假设前面拆分得到的两个 order 2 内存块后来都重新变成了空闲状态,并且它们在物理地址方面符合伙伴关系,那么内核就能够把它们重新合并成为一个 order 3 的区域。通过这种不断进行拆分以及合并的方式,伙伴系统尽可能维持比较大的连续空闲区域,从而减少外部碎片对于连续内存申请所造成的影响。

在实际的 Linux 内核代码之中,我们经常能够看到 alloc_pages()__alloc_pages() 以及类似的页面申请接口。调用这些接口的时候,传入的 order 就决定了需要多少个连续页面。例如 order 为 0 时只需要一个页面,而 order 为 2 时就需要 4 个连续页面。对于驱动程序或者某些需要 DMA 连续物理内存的场景而言,这一类接口是十分重要的。

可是 Buddy System 的设计目标决定了它更适合处理页面级别的资源,而不是几十字节或者几百字节的小型内存。 假设系统页面大小为 4KB,一个对象只有 128 字节,如果直接通过伙伴系统分配一个 order 0 页面来承载这个对象,那么一个 4096 字节的页面最终只有 128 字节真正被使用,其余空间都会被浪费掉。如果内核里面存在成千上万个这样的对象,那么这种浪费就会变得非常明显。

除此之外,伙伴系统的每一次分配以及释放还需要处理空闲区域查找、order 选择、页面拆分以及页面合并等工作。如果所有小对象都直接经过 Buddy System,那么原本只是申请一个几十字节结构体的操作,也可能需要进入较重的页级内存管理流程。正是由于这些原因,Linux 才需要在 Buddy System 之上再增加一个更加适合小对象管理的分配层,而 SLUB 所承担的正是这样的工作。

1.3 为什么伙伴系统无法高效管理小块内存?

Linux 内核和普通用户程序有着明显不同的地方,其中一个十分重要的特点就是内核运行过程之中存在大量生命周期比较短并且创建频率非常高的数据结构。 进程创建的时候需要建立进程相关的数据对象,打开文件的时候需要生成文件系统相关的结构,网络数据到达的时候又会产生 skb 等网络对象,设备驱动同样会不断申请自身所需要的数据结构。这些对象虽然单个体积不大,但是数量却非常庞大。

如果所有的对象全部直接交给 Buddy System 管理,第一个问题就是内存利用率会非常低。Buddy System 所能够直接分配的最小单位是一个 Page,而内核对象的大小往往远远小于一个页面。 例如某个对象只有 256 字节,而页面大小是 4096 字节,那么直接申请一页就意味着剩余的 3840 字节无法由其他普通小对象继续方便地利用。对象数量越多,这种内部空间浪费就会越明显。

第二个问题就是分配以及释放过程的开销比较大。Buddy System 在申请页面的时候需要根据 order 寻找合适的空闲区域,如果没有当前 order 的可用页面,还需要从更高 order 的内存块进行拆分。当页面释放之后,又需要检查对应伙伴是否同样处于空闲状态,从而决定要不要进行合并。这些操作对于页面管理而言是合理的,但是如果几十字节的小对象每一次创建以及销毁都要经历类似流程,就会使得内存分配本身的开销变得过高。

第三个问题则是对象初始化成本。Linux 内核中的很多对象在被创建之后,并不是只需要得到一块空白内存就可以直接使用,还可能存在一些固定的初始化过程。倘若一个对象被频繁地申请、释放,再申请、再释放,那么每一次都重新构造这些数据内容会产生额外开销。slab allocator 的对象缓存思想正好能够在一定程度上减少这种重复工作, 对于某些专用 kmem_cache,还可以通过对象构造相关机制配合完成初始化管理。

因此,Linux 内核真正需要的并不是让 Buddy System 变得能够直接处理任意大小的对象,而是在它之上增加一个对象级管理层。这个对象级管理层可以一次向 Buddy System 申请多个页面,然后将这些页面按照特定对象大小进行切分。 例如一个 slab 拥有若干个页面,每一个对象大小为 256 字节,那么这组页面里面就能够存放大量 256 字节对象。之后内核申请 256 字节对象的时候,只需要从这些已经切分完成的空间里面取出一个即可。

其基本思想可以表示为:

Buddy System
     ↓
申请若干 Page
     ↓
建立 Slab
     ↓
Object Object Object Object Object ...
     ↓
提供给内核使用

从这个过程就可以观察到,SLUB 并没有改变 Buddy System 的职责。 Buddy System 仍然负责页面资源,而 SLUB 只是把页面资源重新组织成为更细粒度的对象资源。这样既保留了 Linux 原有的页级物理内存管理体系,又能够使得大量小对象不必频繁进入 Buddy System,从而显著降低空间浪费以及分配开销。

1.4 slab 分配器的诞生:从页管理到对象管理

slab allocator 的出现,使 Linux 内核的内存管理从单纯的 Page 管理进一步扩展到了 Object 管理。 它所解决的核心问题并不是"如何找到一块物理内存",因为这件事情 Buddy System 已经能够完成,而是"如何把物理页面更加高效地组织成为内核真正需要的对象"。

slab allocator 最重要的思想之一,就是为经常使用的内核对象建立缓存。 比如文件系统之中会频繁创建 inode,那么就可以建立专门管理 inode 对象的 Cache;系统中需要频繁创建 dentry,就可以建立对应的 dentry Cache。一个 Cache 并不是只保存一个对象,而是负责管理某一类具有相同尺寸以及相同分配属性的对象。在 Cache 下面还会存在多个 slab,每一个 slab 又由一个或者多个 Page 所构成,而真正交给内核其他模块使用的,则是 slab 里面划分出来的 Object。

Cache、Slab 和 Object 这三个概念乃是理解整个 slab allocator 的基础。 Cache 负责某一类对象的总体管理,它知道对象大小、对齐方式以及 slab 相关属性;Slab 是从底层页面资源之上建立起来的对象容器,一个 slab 通常包含若干个可以分配的 Object;Object 则是内核调用者最终真正获得的那块内存。例如某个 Cache 专门用于管理 256 字节对象,那么这个 Cache 下面的每一个 slab 都会把自己的页面空间按照相应布局划分成为多个 256 字节左右的对象区域。

三者之间的关系可以表示为:

kmem_cache
    |
    +---------------- slab
    |                  |
    |                  +---- object
    |                  +---- object
    |                  +---- object
    |
    +---------------- slab
                       |
                       +---- object
                       +---- object
                       +---- object

假设内核需要创建一个新的对象,slab allocator 首先会在对应 Cache 当中寻找可以使用的空闲 Object。如果存在,那么直接取出一个对象即可,不需要重新向 Buddy System 申请页面。只有在现有 slab 无法继续提供空闲对象的时候,分配器才需要进一步获取新的页面并建立新的 slab。这样一来,大量对象的申请操作实际上都会停留在 slab allocator 内部完成,而不会反复进入页分配器。

对象释放的时候也是类似的。一个 Object 被释放之后,并不意味着它所在的物理页面马上就会被归还给 Buddy System。大多数情况下,该 Object 只是重新变成空闲状态,之后下一次同类型对象申请到来的时候还能够继续使用这一块空间。只有当整个 slab 达到一定的空闲条件,并且系统认为这些页面没有继续保留的必要时,相关页面才有可能被重新归还给底层页面分配器。

这种"对象缓存"的思想对于内核而言极为重要。 它就好似一个提前准备好的零件仓库一般,Buddy System 负责提供大块的原材料,而 slab allocator 负责把这些原材料加工成统一规格的零件。内核其他子系统真正需要零件的时候,不必每一次都重新取得原材料再加工,而是直接从已经准备好的仓库中拿取即可。SLAB、SLUB 以及历史上的其他 slab allocator 实现虽然内部结构存在差异,但是其最根本的对象缓存思想都是建立在这个基础之上的。

1.5 用户态 malloc 与内核 SLUB 的本质区别

在学习 Linux 内存管理的时候,很多开发者很容易把用户空间中的 malloc 和内核中的 kmalloc、SLUB 联系到一起,因为从使用结果来看,它们好像都是"传入一个大小,然后得到一块可以使用的内存"。可是从实现层次以及设计目标来看,malloc 和 SLUB 并不是同一个层面的东西,它们所面对的运行环境也存在着很大的差异。

用户态 malloc 一般是由 C 运行库提供的内存分配接口。以常见的 glibc 为例,malloc 本身运行在用户空间,它会对进程自身所拥有的虚拟地址空间进行管理。当现有堆空间无法满足需求的时候,用户态分配器可以通过 brk 或 mmap 等系统调用向内核申请新的虚拟内存区域,之后再在这些区域内部进行更加细粒度的划分。因此,malloc 的主要服务对象是某一个具体用户进程,它所管理的是这个进程能够访问的用户虚拟地址空间。

SLUB 则完全处于 Linux 内核内部。 它所服务的是内核自身以及运行在内核空间之中的各个子系统。内核需要创建 task_struct、inode、dentry 以及网络对象的时候,并不会调用用户空间 malloc,而是通过 kmalloc 或 kmem_cache_alloc 等内核接口取得内存。这些接口背后会进入 slab allocator,也就是在采用 SLUB 的系统之中进入 SLUB 的对象管理流程。

二者在底层资源来源方面也存在不同。malloc 最终看到的是用户进程的虚拟地址空间,它并不直接控制某个具体物理页究竟放在什么地方,具体的虚拟内存映射以及物理页面管理仍然由内核完成。而 SLUB 本身就位于内核内存管理体系之中,它能够直接建立在 Buddy System 提供的页面之上,并且需要考虑 GFP 标志、NUMA Node、CPU 本地缓存以及内核并发访问等问题。

除此之外,SLUB 还具有非常明显的"对象类型"特征。 用户态 malloc 主要按照大小提供一段通用内存,例如 malloc(128) 所关心的是取得不少于 128 字节的可用区域。而 kmem_cache_create() 可以在内核之中为某一种固定对象建立专门的 Cache,之后所有这类对象都可以从对应的 Cache 中进行申请。这样的设计对于大量重复创建固定结构体的内核场景非常适合。

因此,可以把 malloc 理解为用户空间动态内存管理的一部分,而 SLUB 则属于 Linux 内核对象内存管理的一部分。它们虽然最终都体现为"一块内存的申请以及释放",但是从所在层次、管理对象以及底层机制来看,二者之间存在着本质区别。

1.6 Linux 内核常见内存分配接口关系梳理

Linux 内核之中存在着多种内存分配接口,其中 alloc_pages、kmalloc、kmem_cache_alloc 以及 vmalloc 都是非常常见的。它们看起来都是用于申请内存,但是每一种接口所处的层次以及适合的使用场景并不相同。理解这些接口之间的关系,对于后续分析 kmalloc 如何进入 SLUB,以及 SLUB 又如何从 Buddy System 获取页面是十分重要的。

alloc_pages() 属于典型的页级内存分配接口,它直接面向页面分配机制进行工作。 调用者需要指定 GFP 标志以及 order,内核会按照相应要求去申请一个或者多个连续物理页面。例如 order 为 0 时申请一个页面,order 为 1 时申请两个连续页面。由于其直接操作 Page,因此更加适合那些本身就需要页面资源或者需要连续物理内存的内核场景,而不适合用来频繁申请几十字节的小型对象。

kmem_cache_alloc() 则是典型的对象级分配接口。 使用这个接口之前,通常需要存在一个 struct kmem_cache,用于描述某一类对象的缓存。内核里面大量固定类型的数据结构都可以采用这种方式进行管理。当对象被频繁创建以及销毁的时候,专用 kmem_cache 能够让同一类型对象集中管理,并且利用 SLUB 的缓存机制提高申请以及释放效率。

kmalloc() 是驱动以及内核开发当中极为常见的通用小块内存申请接口。 开发者只需要指定需要的字节数以及 GFP 标志,就能够得到一块适合内核使用的内存。可是 kmalloc 并不是独立于 SLUB 之外的另一套对象分配器。在启用 SLUB 的 Linux 系统之中,常见尺寸的 kmalloc 请求最终仍然会映射到相应的 kmalloc Cache,然后通过 slab allocator 完成真正的对象分配。 因此后续我们深入分析 kmalloc 调用链的时候就会看到,kmalloc 和 kmem_cache_alloc 在底层实际上具有十分紧密的联系。

vmalloc() 和前面几种接口又存在明显区别。kmalloc 所返回的内存在常规情况下不仅虚拟地址连续,而且其底层对应的物理内存也满足相应连续性要求,这使它适合多数内核小块内存使用场景。vmalloc() 所强调的则是虚拟地址连续,它能够把多个物理上并不连续的页面重新映射到一段连续的内核虚拟地址区域之中。因此,在需要申请比较大的内存空间,而又不要求底层物理页面连续的时候,vmalloc 往往更加合适。不过由于 vmalloc 涉及页表映射等额外操作,它的分配以及访问特性和 kmalloc 并不完全相同。

从整个 Linux 内核内存管理体系来看,这几个接口大致可以理解为:alloc_pages 更加靠近 Buddy System,负责页面级资源;kmem_cache_alloc 更加靠近 SLUB 对象缓存,负责固定类型对象;kmalloc 面向通用的小块内存请求,但是其底层大量依赖 SLUB 所建立的 kmalloc Cache;vmalloc 则通过虚拟内存映射机制提供虚拟地址连续的大块空间。

其中典型的 kmalloc 内存申请路径可以简化表示为:

内核代码
   ↓
kmalloc()
   ↓
选择对应的 kmalloc Cache
   ↓
SLUB 分配对象
   ↓
Cache 中存在空闲 Object
   ↓
直接返回对象

倘若当前 SLUB Cache 已经没有可以继续使用的 slab,那么才会进一步进入慢速路径:

kmalloc()
   ↓
SLUB
   ↓
当前 slab 无可用 Object
   ↓
申请或者取得新的 slab
   ↓
Buddy System
   ↓
获得新的 Page
   ↓
划分 Object
   ↓
返回内存

从这两个过程就能够观察到,Linux 内核中的各种内存分配机制实际上是相互衔接的。Buddy System 并没有因为 SLUB 的存在而消失,SLUB 也不是另外建立了一套脱离物理页面的内存体系。它们之间形成的是一种明显的上下层关系:Buddy System 管理物理页面,SLUB 利用这些页面建立对象缓存,而 kmalloc、kmem_cache_alloc 等接口则把这些能力提供给内核其他模块使用。

因此,在真正深入 SLUB 源码之前,必须先建立起"Page—Slab—Object"这一条基本认知链路。 Page 是底层物理内存管理的单位,Slab 是 SLUB 在一组页面之上建立的对象容器,而 Object 则是最终交给内核调用者使用的内存单位。后续无论分析 kmem_cache、freelist、per-CPU 数据结构,还是研究 kmalloc 的快速路径以及慢速路径,本质上都是围绕着这条链路展开的。

二、SLUB 分配器演进历程:为什么最终选择 SLUB?

2.1 从 SLOB 到 SLAB:Linux 内核对象分配器的发展演进

在 Linux 内核早期的发展过程中,内核对于小对象内存管理的需求并不像今天这样复杂。随着内核功能不断增加,文件系统、网络协议栈、设备驱动以及进程管理等子系统逐渐引入了大量动态创建的数据结构,这些对象通常具有固定大小、生命周期明确以及频繁申请释放等特点。传统的页面级内存分配方式已经无法很好地满足这种使用需求,因此 Linux 开始引入专门面向对象管理的分配机制。

最初,Linux 主要通过 Buddy System 完成内存分配。Buddy System 以 Page 为基本单位,能够高效管理物理内存区域,但是它并不了解上层内核对象的具体需求。例如,一个 inode 结构体可能只有几百字节,而 Buddy System 却只能按照页面进行分配。在这种情况下,内核虽然能够获得所需要的内存,但同时也会造成大量空间浪费,并且频繁的小对象申请会增加页分配器的压力。

为了解决这一问题,Linux 内核先后出现了 SLOB、SLAB 等不同的小对象分配方案。

SLOB(Simple List Of Blocks)是一种较为简单的内存分配器,它主要针对资源受限的小型系统设计。 其核心思想是通过维护空闲内存块链表,在较小的内存空间中尽可能减少管理开销。由于 SLOB 结构简单,占用额外空间较少,因此在一些嵌入式设备中具有一定优势。

但是,SLOB 的设计目标决定了它并不适合现代多核 Linux 系统。随着处理器核心数量不断增加,内核需要面对越来越复杂的并发内存访问场景。SLOB 采用简单链表管理空闲区域,在大量 CPU 同时进行内存申请和释放时,容易产生明显的性能瓶颈。

因此,Linux 后续引入了更加成熟的 SLAB 分配器。

SLAB 的核心思想就是对象缓存。 它不再直接面向页面,而是在 Buddy System 提供的页面基础之上建立 Cache,将连续页面划分成为大量固定大小的 Object。当内核需要申请某类对象时,可以直接从对应 Cache 中获取,而不需要重新向底层申请页面。

例如:

Buddy System
      ↓
申请 Page
      ↓
建立 inode Cache
      ↓
划分 inode Object
      ↓
提供给文件系统使用

这种方式极大提升了小对象分配效率,同时减少了页面级分配带来的浪费。

SLAB 的出现解决了 Linux 内核对象管理中的核心问题,使内核从"页面管理"进一步发展到了"对象管理"。但是随着 Linux 开始广泛应用于多核服务器以及复杂嵌入式平台,SLAB 逐渐暴露出新的问题。

其中最主要的问题就是结构复杂。

为了提升性能,SLAB 维护了大量状态信息,例如 full slab、partial slab、free slab 等不同链表,并且需要管理 CPU 本地缓存以及全局缓存之间的关系。这些设计虽然能够提升一定性能,但是同时也增加了代码复杂度以及维护难度。

因此,Linux 内核社区开始探索一种更加简单、高效的新型 slab 分配器,这就是后来的 SLUB。

2.2 SLUB 的诞生:简化 SLAB 架构,优化多核性能

SLUB(Simple Unqueued Allocator)由 Linux 内核开发者 Christoph Lameter 提出,其设计目标非常明确:在保持 slab 分配思想的基础之上,简化内部管理结构,提高现代多核系统中的运行效率。

从名称中可以看出,SLUB 强调的是"Simple"。它并不是重新设计一种完全不同的内存管理方式,而是在 SLAB 的基础上进行优化,通过减少复杂的数据结构,使对象分配过程更加直接。

传统 SLAB 最大的问题之一,是维护了大量全局状态。例如,一个 Cache 中可能存在多个 slab,而这些 slab 又分别处于不同状态:

  • 已经完全分配的 full slab;
  • 部分使用的 partial slab;
  • 尚未使用的 free slab。

为了管理这些状态,SLAB 需要维护多个链表。当 CPU 申请对象时,需要根据当前状态在不同链表之间查找合适的 slab。

这种方式在单核时代影响并不明显,但是在现代多核环境中,不同 CPU 同时访问这些共享数据结构,就容易产生锁竞争。

SLUB 对这一部分进行了简化。

它减少了传统 SLAB 中复杂的 slab 链表管理,而更加依赖以下几个关键机制:

  • per-CPU 本地缓存;
  • freelist 空闲对象链表;
  • cmpxchg 原子操作。

其中最重要的变化,就是将对象分配尽可能限制在 CPU 本地范围内。

在 SLUB 中,每一个 CPU 都拥有自己的本地缓存信息。当某个 CPU 需要申请对象时,首先会检查自身 CPU 对应的 freelist。如果其中存在可用对象,那么无需访问全局结构,也无需竞争锁,就可以直接返回对象地址。

整个过程如下:

传统 SLAB:

CPU
↓
访问共享 slab 链表
↓
竞争锁
↓
查找对象
↓
返回

而 SLUB:

CPU
↓
访问本地 freelist
↓
直接获取 Object
↓
返回

由于大量内存申请都能够在 CPU 本地完成,因此 SLUB 大幅减少了多核环境中的同步开销。

除此之外,SLUB 还进一步降低了代码复杂度。传统 SLAB 需要维护大量状态迁移逻辑,而 SLUB 更加关注当前 CPU 是否存在可用对象,以及如何快速完成对象分配。当快速路径无法满足需求时,再进入慢速路径处理 slab 获取以及页面申请。

这种设计理念与现代 CPU 架构非常契合。现代处理器越来越依赖缓存一致性以及局部性原则,而 SLUB 通过 CPU 本地缓存减少共享数据访问,使内存分配过程更加高效。

2.3 SLUB 成为主流的原因:高性能、低开销与易维护性

随着 Linux 应用场景不断扩大,内核内存分配器需要面对越来越复杂的运行环境。从服务器领域的大规模多核处理器,到移动设备以及嵌入式系统,内核不仅要求分配器具备较高性能,同时也需要保证结构简单、稳定可靠。

SLUB 正是在这种背景下逐渐成为 Linux 默认选择的 slab 分配器。

首先,在性能方面,SLUB 最大的优势来自于 per-CPU 缓存机制。 由于大部分对象申请和释放都可以直接在 CPU 本地完成,因此避免了访问共享数据结构产生的锁竞争。在多核环境中,这种设计能够明显提升并发内存分配能力。

其次,在内存管理开销方面,SLUB 相比 SLAB 删除了大量复杂的管理结构。它不需要维护过多 slab 状态链表,而是通过更加直接的方式管理空闲对象。这不仅减少了额外内存占用,同时也降低了代码复杂度。

再次,在维护以及调试方面,SLUB 也提供了更加完善的支持。Linux 内核提供了 CONFIG_SLUB_DEBUG 调试机制,可以通过对象填充、边界检测以及分配追踪等方式发现内存错误。例如:

  • Red Zone 可以检测对象越界访问;
  • Poisoning 可以发现释放后继续访问的问题;
  • User Tracking 可以记录对象申请和释放位置。

这些能力对于内核开发尤其重要,因为内核内存错误通常不会像用户程序那样直接触发异常,而可能表现为系统随机崩溃、设备异常或者长时间运行后的稳定性下降。

因此,SLUB 的优势并不只是"更快",而是在性能、结构复杂度以及调试能力之间取得了更好的平衡。

从 Linux 内核的发展过程来看:

SLOB 解决的是资源受限环境下的小对象分配问题;SLAB 建立了对象缓存这一核心思想;SLUB 则进一步针对现代多核系统优化,使对象分配更加简单、高效。

最终,Linux 在较新的内核版本中默认采用 SLUB 作为主要 slab 分配器。理解 SLUB 的演进过程,也能够帮助我们理解 Linux 内核为什么没有简单地继续扩展 SLAB,而是选择重新设计更加符合现代硬件环境的对象管理机制。

三、SLUB 核心设计思想与关键数据结构

3.1 SLUB 整体架构:Cache、Slab、Object 三层关系

理解 SLUB 分配器,首先需要理解它内部最核心的三个概念,也就是 Cache、Slab 以及 Object。这三个概念共同构成了 SLUB 对象管理的基础模型,也是后续分析 kmem_cache、freelist 以及对象分配流程的关键。

在 Linux 内核中,SLUB 并不是直接管理某一个具体对象,而是按照对象类型建立缓存。例如,内核中的 task_struct、inode、dentry 等结构体,由于会被大量创建,因此通常都会拥有对应的 Cache。Cache 负责描述这一类对象应该如何分配,包括对象大小、对齐方式、包含多少页面以及如何进行释放管理等信息。

Slab 则是 Cache 实际管理的内存区域。它通常由一个或者多个连续物理页面组成,这些页面最初来自 Buddy System。当 SLUB 从伙伴系统获取页面之后,会进一步将这些页面划分成为多个固定大小的 Object。

Object 则是最终提供给内核使用的最小单位。例如驱动程序通过 kmem_cache_alloc() 获取的一块结构体空间,本质上就是某个 Cache 中的一个 Object。

三者之间的关系可以表示为:

kmem_cache
      |
      +------ slab
                  |
                  +------ object
                  +------ object
                  +------ object

例如,假设系统需要大量创建 inode 对象,那么内核会建立 inode Cache。当第一个 inode 创建时,SLUB 会检查该 Cache 是否存在可用 slab。如果存在空闲 Object,则直接返回;如果不存在,则向 Buddy System 申请新的页面,并建立新的 slab。

因此,一次对象申请实际上并不是简单地获取一块内存,而是经过:Cache 查找 → Slab 判断 → Object 获取,这一系列过程。

这种分层设计使 SLUB 能够同时兼顾内存利用率以及分配效率。Buddy System 负责提供连续物理空间,SLUB 负责组织对象,而上层内核模块只需要通过统一接口获取对象即可。

3.2 kmem_cache:对象缓存管理核心

在 SLUB 分配器之中,kmem_cache 乃是最为核心的数据结构之一。 它并不是某一个具体对象,而是用于描述"一类对象应该如何被管理"的缓存控制结构。Linux 内核中的大量固定类型对象,例如 task_struct、inode、dentry 等,都会建立属于自己的 kmem_cache,通过这种方式实现高效的对象分配以及生命周期管理。

相比直接通过 kmalloc 申请一块指定大小的内存,使用 kmem_cache 最大的优势在于它明确知道当前管理的对象类型以及对象特征。例如,一个 inode Cache 会提前知道 inode 对象的大小、内存对齐要求以及如何进行分配和释放。当文件系统需要创建新的 inode 时,内核不需要重新计算这些信息,而是直接从对应 Cache 中获取已经准备好的对象。

在 Linux 内核源码中,kmem_cache 结构体包含了大量用于描述缓存属性的信息。不同内核版本中的字段会有所变化,但是整体设计思想基本保持一致。

例如:

struct kmem_cache {
    unsigned int object_size;
    unsigned int size;
    unsigned int offset;
    unsigned int align;
    unsigned int min_partial;
};

其中:

  • object_size 表示实际对象大小;
  • size 表示经过对齐处理之后对象占用的空间大小;
  • align 表示对象的内存对齐要求;
  • offset 则用于 freelist 等内部管理信息的位置计算。

在实际分配过程中,SLUB 并不会简单按照 object_size 进行切割。由于 CPU Cache、内存访问效率以及内部管理需求的影响,对象通常需要进行一定程度的对齐。例如,一个结构体虽然实际大小为 70 字节,但是为了保证访问效率,SLUB 可能会按照 80 字节甚至更大的粒度进行布局。

这种设计能够避免对象跨越多个 Cache Line,从而提高 CPU 访问效率。

除了描述对象本身之外,kmem_cache 还负责管理 slab 之间的关系。一个 Cache 可以包含多个 slab,而每一个 slab 中又保存大量 Object。当某个 CPU 申请对象时,SLUB 首先会从当前 Cache 对应的空闲区域寻找对象,如果当前区域无法满足需求,再进入更加复杂的分配流程。

整个过程可以理解为:

kmem_cache
      ↓
管理多个 slab
      ↓
每个 slab
      ↓
包含多个 object

因此,kmem_cache 就像一个对象仓库的管理者,它并不直接保存所有对象的数据,而是负责维护这些对象存储在哪里、如何分配以及如何释放。

3.3 slab:连续内存页中的对象集合

如果说 kmem_cache 描述的是"如何管理对象",那么 slab 就代表"对象实际存放在哪里"。

在 SLUB 中,一个 slab 通常由一个或者多个连续物理页面组成。这些页面最初由 Buddy System 分配,然后由 SLUB 进一步划分成为多个大小相同的 Object。

例如,一个 Cache 管理大小为 256 字节的对象,而系统申请了一组 4KB 页面:

4096 Bytes Page
↓
256 Bytes Object
Object  Object  Object  Object
Object  Object  Object  Object
Object  Object  Object  Object

这样,一个 slab 就可以保存大量同类型对象。

当 Cache 中已经存在可用 slab 时,对象申请过程就无需再次进入 Buddy System,而是直接从 slab 中寻找空闲 Object。这也是 SLUB 能够快速完成小对象分配的重要原因。

根据当前使用情况,slab 中的对象通常可以处于不同状态:

  • 已经被分配使用的对象;
  • 当前空闲,可以继续分配的对象;
  • 暂时没有被使用,但是仍然保留给 Cache 的空间。

传统 SLAB 分配器会通过多个链表维护 full、partial、free 等状态,而 SLUB 对这种设计进行了简化。它更加关注当前 CPU 是否存在可用对象,以及如何快速找到下一块空闲空间。

在 SLUB 中,一个 slab 并不是孤立存在的,它会与对应的 kmem_cache 关联。当 slab 中的对象全部被使用时,Cache 会记录这一状态;当大量对象被释放之后,该 slab 又可能重新成为可分配区域。

因此,slab 可以看作是 SLUB 从页面到对象之间建立起来的中间层。 它屏蔽了底层 Page 管理的复杂性,同时向上提供统一 Object 分配能力。

3.4 object:内核实际申请和释放的最小单位

Object 是 SLUB 中最接近使用者的一层,也是内核真正获得的内存单位。

当驱动程序调用:

ptr = kmalloc(size, GFP_KERNEL);

或者:

obj = kmem_cache_alloc(cache, GFP_KERNEL);

最终返回给调用者的,并不是一个 Page,也不是一个 slab,而是其中某一个 Object。

Object 的大小通常由 Cache 创建时决定。例如:

kmem_cache_create(
    "my_cache",
    sizeof(struct my_object),
    ...
);

这里指定的 struct my_object 大小,就决定了后续每一个 Object 的基本尺寸。

由于同一个 Cache 中所有对象大小一致,因此 SLUB 不需要在每次分配时重新计算空间布局。这也是对象缓存相比通用内存分配方式更加高效的重要原因。

另外,Object 在释放之后并不会立即消失。 对于 SLUB 来说,释放 Object 更多意味着:

"这个对象当前不再被使用,可以重新进入空闲状态。"

例如:

Object A
   |
   | kfree()
   ↓
空闲 Object
   |
   | 下一次申请
   ↓
重新返回使用

这种对象复用机制避免了频繁创建和销毁内存区域。

不过,由于 Object 会经历多次申请和释放,因此也容易产生一些典型内存问题。例如:

  • 对象释放之后继续访问;
  • 同一个对象重复释放;
  • 写入超过对象边界。

因此,SLUB 才提供了 Poisoning、Red Zone 等调试机制,用于检测 Object 生命周期中的异常行为。

3.5 freelist:空闲对象链表管理机制

在 SLUB 分配器中,freelist 是管理空闲 Object 的核心机制。

当一个 slab 被创建之后,其中并不是所有对象都会立即被使用。为了能够快速找到下一块可用空间,SLUB 需要维护当前 slab 中哪些 Object 处于空闲状态,而 freelist 正是用于完成这一任务的数据结构。

简单来说,freelist 就是一条连接空闲 Object 的链表。

例如:

slab
Object A  → Object B  → Object C
空闲对象链表

当 CPU 需要申请对象时,SLUB 只需要从 freelist 头部取出一个 Object 即可:

freelist
Object A → Object B → Object C

申请

返回 Object A

新的 freelist:
Object B → Object C

这种方式避免了每次分配时遍历整个 slab 寻找空闲空间。

SLUB 对 freelist 的设计进行了进一步优化。 传统链表通常需要额外保存 next 指针,而 SLUB 为了减少对象额外开销,会利用对象自身空间保存 freelist 指针。 在对象尚未被使用的时候,该区域可以作为链表连接信息;当对象分配出去之后,这部分空间就交还给使用者。

这种设计非常巧妙,因为内核对象数量通常非常巨大,如果每一个对象额外增加管理字段,即使只增加几个字节,也可能造成明显内存浪费。

但是,这种设计也带来了安全问题。如果攻击者或者错误代码能够修改已经释放对象中的 freelist 指针,就可能影响后续分配流程。因此现代 Linux 内核又加入了 freelist 随机化、指针保护等安全机制,提高 SLUB 抵抗内存破坏攻击的能力。

3.6 per-cpu cache:SLUB 高性能的核心秘密

SLUB 能够替代传统 SLAB 成为 Linux 默认分配器,其中最重要的原因之一,就是它充分利用了 per-CPU cache 机制。

在多核 CPU 系统中,如果所有 CPU 都访问同一个全局空闲链表,那么必然会产生锁竞争。例如 CPU0 和 CPU1 同时申请对象,如果它们都需要修改同一个 freelist,就必须通过同步机制保证数据一致。

这种竞争会严重影响高并发环境下的性能。

SLUB 的解决方式是让每一个 CPU 尽可能管理自己的本地对象。

每个 CPU 都拥有自己的缓存区域,当该 CPU 需要申请对象时,首先从本地缓存获取,而不是访问全局结构。

例如:

CPU0
local freelist
Object A

CPU1
local freelist
Object B

两个 CPU 可以同时完成对象分配,而不会互相影响。

只有当 CPU 本地缓存不足时,才需要进入慢速路径,从共享区域获取新的 slab。

这种设计充分利用了现代 CPU 的局部性特点。由于一个 CPU 通常会连续处理相关任务,因此短时间内申请的对象往往具有一定关联性,将对象保留在本地缓存中不仅减少锁竞争,同时也提高 CPU Cache 命中率。

因此,per-CPU cache 是 SLUB 高性能设计中的核心机制之一。

3.7 NUMA 架构下 Node 节点管理机制

随着服务器处理器核心数量不断增加,单一内存控制器结构逐渐无法满足性能需求,因此现代服务器普遍采用 NUMA(Non-Uniform Memory Access)架构。

在 NUMA 系统中,不同 CPU 节点拥有自己的本地内存区域。CPU 访问本地内存速度较快,而访问其他节点内存则需要经过互联总线,因此访问延迟更高。

为了适应 NUMA 架构,SLUB 也引入了 Node 层级管理机制。

简单来说,每一个 NUMA Node 都会维护属于自己的 slab 资源。当某个 CPU 申请对象时,SLUB 会优先尝试从当前 CPU 所属 Node 获取内存。

例如:

CPU0
↓
Node0
↓
Node0 slab
↓
Object

这样能够尽量保证 CPU 使用本地内存,从而减少跨节点访问带来的性能损耗。如果当前 Node 没有足够资源,SLUB 才可能考虑从其他 Node 获取内存。

因此,在 NUMA 环境下,SLUB 的对象分配实际上同时受到 CPU 本地缓存、NUMA Node、Cache 状态等多个因素影响。

这种分层设计使 SLUB 不仅能够适应普通单机系统,也能够满足大型服务器环境中的高并发内存管理需求。

通过对 Cache、Slab、Object、freelist、per-CPU cache 以及 NUMA Node 等核心概念的理解,我们已经建立起了 SLUB 内部运行的基本模型。后续进一步分析 kmalloc 底层流程时,就能够清楚看到一次普通内存申请如何经过这些结构,最终返回给内核调用者。

四、kmalloc 底层原理:一次内存申请到底发生了什么?

4.1 kmalloc 为什么比直接申请页更高效?

在 Linux 内核开发过程中,kmalloc 是最常见的内存申请接口之一。无论是设备驱动、网络子系统,还是各种内核模块,都经常需要通过 kmalloc 获取一块可以直接使用的内核内存。

从调用形式来看,kmalloc 非常简单:

void *ptr;
ptr = kmalloc(size, GFP_KERNEL);

开发者只需要提供申请大小以及 GFP 标志,就能够得到一块内核虚拟地址空间。但是,kmalloc 的内部实现并不像表面看起来那么简单。 它并不会每一次申请内存时都直接向 Buddy System 请求新的物理页面,而是会根据申请大小选择合适的 kmalloc Cache,然后通过 SLUB 分配器快速返回一个已经准备好的 Object。

这也是 kmalloc 能够高效工作的核心原因。

如果 kmalloc 每次都直接调用 alloc_pages():

kmalloc
↓
alloc_pages
↓
Buddy System
↓
申请 Page

那么即使只是申请几十字节的数据,也需要进入页级内存管理流程。这不仅会造成页面空间浪费,同时还会频繁触发页面查找、拆分以及管理操作。

而通过 SLUB:

kmalloc
↓
选择 kmalloc Cache
↓
SLUB 获取 Object
↓
返回地址

大多数情况下,申请过程只需要在已有缓存中找到一个空闲对象即可。

例如,系统中存在一个管理 256 字节对象的 kmalloc-256 Cache。

当驱动程序申请:

kmalloc(200, GFP_KERNEL);

由于 200 字节小于 256 字节,因此内核可以直接从 kmalloc-256 Cache 中获取一个 Object。

实际过程:

kmalloc-256 Cache
Object1  已使用
Object2  空闲
Object3  空闲

申请
↓
返回 Object2

整个过程无需访问 Buddy System。

只有当 Cache 中没有足够空闲对象时,SLUB 才会向底层申请新的页面,并建立新的 slab。

因此,kmalloc 高效的根本原因并不是它本身进行了特殊优化,而是它充分利用了 SLUB 对象缓存机制。

4.2 kmalloc size class:不同大小对象如何匹配缓存?

为了让 kmalloc 能够快速完成对象分配,Linux 内核提前建立了一系列固定大小的 kmalloc Cache。

这些 Cache 被称为 size class。

例如:

kmalloc-8
kmalloc-16
kmalloc-32
kmalloc-64
kmalloc-128
kmalloc-256
kmalloc-512
...

每一个 Cache 负责管理一定范围大小的对象。

假设程序申请:

kmalloc(100, GFP_KERNEL);

Linux 并不会创建一个刚好 100 字节的 Cache,而是寻找能够容纳该对象的最小 Cache。

因此:

申请大小:
100 Bytes

匹配:
kmalloc-128

随后 SLUB 会从 kmalloc-128 Cache 中返回一个 Object。

这种设计有两个明显优势。

首先,避免了动态创建大量不同大小的缓存。如果每一种申请大小都建立一个 Cache,那么系统需要维护大量管理结构,反而会增加开销。

其次,固定大小 Cache 更容易进行对象复用。大量类似大小的内存申请可以共享同一个缓存区域,提高利用率。

但是,这种方式也存在一定内部碎片问题。

例如申请:

100 Bytes

实际分配:

128 Bytes

其中多出的 28 字节无法直接使用。

不过相比直接分配整个页面:

100 Bytes
↓
4096 Bytes

这种浪费已经非常小。

因此,Linux 通过 size class 在内存利用率和分配效率之间取得了平衡。

4.3 kmalloc 调用链完整分析

理解 kmalloc 的底层流程,是理解 SLUB 工作机制的重要入口。

一次典型的 kmalloc 调用链如下:

kmalloc
↓
__kmalloc
↓
kmalloc_slab
↓
kmem_cache_alloc
↓
slub_alloc

当内核代码调用 kmalloc 时,首先进入 kmalloc() 接口。

例如:

ptr = kmalloc(256, GFP_KERNEL);

kmalloc 首先会根据申请大小判断应该使用哪一个 Cache。

如果申请大小属于某个 kmalloc size class,那么系统会找到对应的 kmem_cache。

例如:

256 Bytes
↓
kmalloc-256 Cache

随后进入 __kmalloc()

__kmalloc 负责进一步处理具体分配逻辑,包括:

  • 判断申请大小;
  • 获取对应 Cache;
  • 调用 SLUB 分配接口。

接下来,kmalloc_slab() 会根据大小选择具体的 slab cache。

例如:

cache = kmalloc_slab(size, flags);

返回:

kmalloc-256

之后进入:

kmem_cache_alloc()

这一步正式进入 SLUB 对象分配流程。

最终由:

slub_alloc()

完成对象获取。

因此,一次简单的:

kmalloc(256)

实际上经历了:

申请大小判断
↓
Cache 匹配
↓
进入 SLUB
↓
查找空闲 Object
↓
返回对象地址

这一完整过程。

4.4 SLUB 快速路径:CPU 本地缓存直接返回对象

为了提高执行效率,SLUB 将对象分配过程分为快速路径和慢速路径。

快速路径是最常发生的情况。

当 CPU 当前拥有可用的空闲对象时,SLUB 不需要访问复杂的数据结构,也不需要申请新的页面,而是直接从 CPU 本地 freelist 中获取对象。

例如:

CPU0
freelist
Object A
Object B
Object C

此时 CPU0 执行:

kmalloc(128)

SLUB 只需要:

  1. 查看当前 CPU 的缓存;
  2. 获取 freelist 第一个对象;
  3. 更新 freelist;
  4. 返回地址。

整个过程非常短。

从执行角度来看:

kmalloc
↓
当前 CPU Cache
↓
freelist
↓
Object
↓
return

由于大部分内存申请都可以在快速路径完成,因此 SLUB 能够达到非常高的分配效率。

这也是为什么 Linux 内核中的大量频繁对象创建操作不会造成严重性能压力。

4.5 SLUB 慢速路径:缓存耗尽后的补充流程

虽然快速路径效率很高,但是 CPU 本地缓存中的对象数量并不是无限的。

当当前 CPU 没有可用 Object 时,SLUB 就会进入慢速路径。

慢速路径主要负责:

  • 寻找其他可用 slab;
  • 获取 partial slab;
  • 必要时申请新的页面。

例如:

CPU freelist
为空
↓
查找 partial slab
↓
存在可用对象
↓
重新填充 CPU Cache

如果 partial slab 也无法提供对象,那么 SLUB 会进一步向 Buddy System 请求新的页面:

SLUB
↓
申请 slab
↓
Buddy System
↓
获得 Page
↓
划分 Object
↓
加入 Cache

相比快速路径,慢速路径涉及更多操作,因此执行开销更高。

但是由于它并不是每一次申请都会发生,所以不会影响整体性能。

4.6 从 Buddy System 获取新的 slab 页面

当 SLUB 需要创建新的 slab 时,最终仍然需要依赖 Buddy System 提供物理页面。

整个过程如下:

kmalloc
↓
SLUB Cache 无空闲对象
↓
创建新的 slab
↓
alloc_slab_page()
↓
Buddy System
↓
获得 Page

例如,一个 Cache 管理 128 字节对象。

假设 SLUB 从 Buddy System 获取 4 个页面:

4 Pages
↓
16KB 空间
↓
划分多个 128 Bytes Object

随后,这些 Object 会加入对应 Cache,等待后续申请。

因此,SLUB 并不是替代 Buddy System,而是建立在 Buddy System 之上的对象管理层。

Buddy System 提供原始内存资源,SLUB 负责将这些资源转换成为内核可以快速使用的小对象。

从一次 kmalloc 调用过程可以看到,Linux 内核内存管理实际上存在明显的层次结构:

应用需求
↓
kmalloc
↓
kmalloc Cache
↓
SLUB
↓
Slab
↓
Buddy System
↓
Physical Page

理解这条链路,是后续分析 SLUB 源码中对象创建、分配以及释放流程的基础。

五、SLUB 内存分配完整流程源码级分析

5.1 kmem_cache_create:创建对象缓存流程

在 SLUB 分配器之中,所有对象的管理都是建立在 kmem_cache 之上的。因此,在真正申请对象之前,内核首先需要创建对应的 Cache。对于大量具有固定结构的数据对象而言,建立专用 kmem_cache 能够让内核提前了解对象大小、内存布局以及分配策略,从而避免每一次申请对象时重新进行计算。

Linux 内核中的许多核心子系统都会创建自己的 Cache。例如,进程管理模块需要管理 task_struct,文件系统需要管理 inode 和 dentry,网络子系统需要管理各种网络数据结构。这些对象具有明显的共同特点:数量庞大、大小固定、生命周期频繁变化,因此非常适合使用 SLUB Cache 进行管理。

创建 Cache 的主要接口是:

kmem_cache_create()

其基本使用形式如下:

struct kmem_cache *cache;

cache = kmem_cache_create(
        "my_cache",
        sizeof(struct my_object),
        0,
        SLAB_HWCACHE_ALIGN,
        NULL
);

其中:

第一个参数表示 Cache 名称,用于在系统中标识该对象缓存。

第二个参数表示对象大小,也就是每一个 Object 实际需要占用的空间。

第三个参数表示对象对齐要求。

第四个参数用于指定 Cache 标志,例如:

SLAB_HWCACHE_ALIGN

表示按照硬件 Cache Line 进行对齐,以减少 CPU 访问过程中的缓存冲突。

第五个参数是构造函数,用于对象创建时进行初始化操作。

当 kmem_cache_create() 被调用之后,内核并不会立即申请大量物理页面。因为此时只是创建了对象管理规则,并没有真正需要使用对象。

因此,创建 Cache 和创建 slab 是两个不同阶段。

Cache 创建:

建立管理结构
↓
记录对象大小
↓
等待对象申请

对象真正申请:

kmem_cache_alloc()
↓
发现没有 slab
↓
向 Buddy System 申请页面
↓
建立 slab

这种延迟分配机制能够避免不必要的内存占用。如果某个 Cache 创建之后长期没有使用,那么系统并不会提前浪费大量物理内存。

从设计思想来看,kmem_cache_create() 完成的是"建立对象管理模型",而不是"立即分配对象空间"。

5.2 创建 slab:如何初始化对象空间?

当 Cache 第一次需要提供对象,但是当前又不存在可用 slab 时,SLUB 才会真正向 Buddy System 请求物理页面,并创建新的 slab。

这一过程可以理解为:

kmem_cache
      ↓
没有可用 object
      ↓
申请 Page
      ↓
创建 slab
      ↓
划分 Object

假设某个 Cache 管理大小为 256 字节的对象。

当 SLUB 从 Buddy System 获取连续页面之后:

Page Page Page
↓
连续内存区域
↓
Object Object Object Object

这些 Object 就成为后续分配使用的基本单位。

在 slab 创建过程中,SLUB 需要完成几个重要步骤。

首先,需要确定 slab 需要多少页面。

这个数量并不是固定的,而是根据对象大小、页面大小、Cache 参数、内存压力等综合决定。

如果对象较小,一个 slab 可以容纳更多 Object;如果对象较大,则需要更多页面才能满足管理需求。

其次,需要建立对象之间的管理关系。

由于 SLUB 需要快速找到空闲 Object,因此新创建的 slab 会建立 freelist。

例如:

Object1 → Object2 → Object3 → Object4

这些对象最初全部处于空闲状态。

最后,新的 slab 会被加入对应 Cache 的管理结构之中。

之后,当 CPU 需要申请对象时,就可以直接从该 slab 获取。

5.3 slub_alloc:对象分配核心逻辑拆解

slub_alloc 是 SLUB 中真正完成对象分配的重要路径之一。

虽然不同 Linux 内核版本中的函数名称以及调用关系可能存在变化,但是整体思想基本一致:

首先尝试快速路径;

如果快速路径失败,再进入慢速路径。

一次典型对象申请过程如下:

kmem_cache_alloc()
        ↓
slub_alloc()
        ↓
检查当前 CPU freelist
        ↓
存在 Object?
        ↓
返回
        ↓
不存在
        ↓
进入慢速路径

快速路径的核心思想就是尽可能不访问共享数据。

因为现代 Linux 系统通常运行在多核处理器之上,如果每一次对象申请都需要访问全局 Cache,那么多个 CPU 之间必然产生锁竞争。

因此 SLUB 优先使用当前 CPU 的本地缓存。

例如:

CPU0
freelist
Object A
Object B

CPU0 执行:

kmem_cache_alloc(cache)

只需要取出 Object A→更新 freelist→返回地址。

整个过程无需访问 Buddy System,也无需遍历 slab。

5.4 freelist 指针如何寻找空闲对象?

freelist 是 SLUB 实现快速分配的重要机制。

在传统链表结构中,通常需要额外的数据字段保存 next 指针。但是 SLUB 为了减少对象管理开销,采用了一种更加巧妙的方法:

利用空闲 Object 自身空间保存链表关系。

例如:

Object A
next → Object B

Object B
next → Object C

当这些 Object 处于空闲状态时,它们内部的一部分空间可以被 SLUB 用作 freelist 指针。

当 Object 被分配出去之后,这部分空间就重新交给调用者使用。

这样做最大的优势是不需要额外维护链表节点。

对于内核这种拥有大量对象的系统而言,即使每个对象额外增加几个字节,也可能产生巨大的内存浪费。

但是,这种设计也带来了安全风险。如果已经释放的对象被非法修改,例如:

Object A
next → 恶意地址

那么下一次 SLUB 分配对象时,就可能返回错误地址。

因此现代 Linux 内核针对 freelist 增加了多种保护机制,例如:

  • freelist randomization;
  • pointer encoding;
  • SLUB debug。

这些机制能够降低对象管理结构被破坏带来的影响。

5.5 slab 扩容:什么时候向伙伴系统申请新页?

虽然 SLUB 会尽可能复用已有 Object,但是 Cache 中的空间并不是无限的。

当出现以下情况时:

  • 当前 CPU 没有可用 Object;
  • partial slab 中没有空闲对象;
  • Cache 总体资源不足;

SLUB 就需要创建新的 slab。流程如下:

对象申请
↓
Cache 无空闲 Object
↓
申请新的 slab
↓
Buddy System
↓
返回 Page
↓
建立 Object
↓
加入 Cache

底层页面申请通常会进入:

alloc_slab_page()

最终调用页面分配接口获取物理页面。

例如:

SLUB
↓
alloc_pages()
↓
Buddy System
↓
Physical Page

获得页面之后,SLUB 会根据 Cache 中记录的对象大小进行切割。

例如申请:

16KB 页面空间

对象大小:

256 Bytes

那么:

16KB
↓
64 个 Object

这些 Object 会加入 freelist,之后即可被快速分配。

5.6 内存释放流程:kfree 到 slab_free

与分配过程类似,SLUB 的释放过程也不是简单地把内存立即归还系统。

当内核调用:

kfree(ptr);

实际上会进入:

kfree()
↓
kmem_cache_free()
↓
slab_free()

首先,内核需要确定该地址属于哪个 Cache。

因为 SLUB 管理的是不同类型对象,所以释放时必须找到对应 Cache。

找到 Cache 之后,SLUB 会将该 Object 重新加入空闲链表。

例如释放前:

freelist:
Object B → Object C

释放 Object A:

Object A → Object B → Object C

之后下一次申请时,Object A 又可以被重新使用。

这儿需要注意一个点:Object 释放并不代表物理页面立即释放。

例如一个 slab:

Object1
Object2
Object3

即使 Object1 被释放:

Object1 空闲
Object2 使用
Object3 使用

这个 slab 仍然属于 Cache。只有当整个 slab 长时间没有被使用,并且满足回收条件时,SLUB 才可能将其释放,并最终归还给 Buddy System。

5.7 空闲 slab 回收与内存返还机制

SLUB 并不会简单地认为"对象释放之后,马上把页面还给系统。"

如果这样处理,会导致大量频繁的页面申请和释放,反而降低系统性能。

因此,SLUB 通常会保留一定数量的空闲 slab。

这样,当后续再次申请相同类型对象时,可以直接复用已有空间。

但是,如果系统内存压力较大,或者某些 slab 长时间处于空闲状态,内核会通过回收机制释放这些资源。

回收过程大致如下:

空闲 Object 增多
↓
slab 长时间空闲
↓
SLUB 回收 slab
↓
释放 Page
↓
Buddy System

最终,这些页面重新回到底层物理内存管理体系。

通过分析 SLUB 完整分配流程可以发现,一次简单的对象申请实际上经历了多个层次:

kmalloc
↓
kmem_cache
↓
slub_alloc
↓
freelist
↓
Object
↓
slab
↓
Buddy System

快速路径保证了日常情况下的高性能,而慢速路径则负责资源不足时的新空间获取。

正是这种"缓存优先、页面补充"的设计,使 SLUB 能够在 Linux 内核中同时兼顾分配速度、内存利用率以及系统稳定性。

六、SLUB 高性能实现原理:为什么它这么快?

6.1 Per-CPU 缓存如何避免全局锁竞争?

SLUB 能够在 Linux 内核中取代传统 SLAB 成为默认对象分配器,一个非常重要的原因就是它针对现代多核处理器环境进行了优化。其中最核心的设计之一,就是 per-CPU 缓存机制。

在早期单核系统中,内存分配操作通常不会面临多个执行单元同时访问的问题。但是随着 CPU 核心数量不断增加,多个 CPU 可能在同一时间执行内核代码,并且同时申请或者释放对象。如果所有 CPU 都直接访问同一个全局对象缓存,那么不同 CPU 之间就需要通过锁机制保证数据一致。

例如:

CPU0
申请 Object
        ↓
访问共享 freelist

CPU1
申请 Object
        ↓
同时访问共享 freelist

此时两个 CPU 会产生竞争。

为了避免这种情况,SLUB 将大量对象分配操作放到了 CPU 本地完成。 每一个 CPU 都拥有自己的本地缓存信息,当某个 CPU 需要申请对象时,优先从自身维护的空闲区域获取,而不是访问全局共享结构。

例如:

CPU0
local cache
Object A

CPU1
local cache
Object B

两个 CPU 可以同时完成对象分配,而不需要互相等待。

这种设计充分利用了 CPU 执行的局部性特点。在实际运行过程中,一个 CPU 通常会连续处理某一类任务,因此短时间内申请的对象往往具有较强的关联性。将这些对象保留在当前 CPU 附近,不仅减少了锁竞争,同时也提高了处理器缓存命中率。

只有当当前 CPU 本地缓存不足时,SLUB 才会进入慢速路径,从共享区域或者其他 slab 中获取新的资源。

因此,per-CPU 缓存实际上将大量高频操作转换成为了:

"CPU 自己管理自己的对象"。

这也是 SLUB 能够适应现代多核 Linux 系统的重要原因。

6.2 cmpxchg 原子操作与无锁分配机制

除了 per-CPU 缓存之外,SLUB 另一个重要优化就是减少锁的使用,引入原子操作完成部分并发控制。

在多 CPU 环境中,如果多个执行单元同时修改同一个数据结构,就必须保证操作具有原子性,否则可能导致数据状态错误。

传统方式通常采用锁:

获取锁
↓
修改数据
↓
释放锁

虽然这种方式简单可靠,但是锁本身也会带来额外开销。当多个 CPU 频繁竞争同一个锁时,系统性能会明显下降。

因此 SLUB 大量使用 cmpxchg(Compare And Exchange)等原子操作。

cmpxchg 的核心思想是比较当前值是否符合预期,如果符合,则直接替换为新值。

例如:

cmpxchg(ptr, old, new)

其执行逻辑类似:

当前值 == old ?
      是
      ↓
替换为 new
      否
      ↓
失败

这种机制能够保证修改过程具有原子性,而不需要传统锁。

在 SLUB 中,freelist 更新过程就大量依赖这种思想。

例如多个 CPU 竞争修改某个空闲对象链表时,SLUB 可以通过原子操作保证链表状态正确,同时避免长期持有锁。

这种设计特别适合内核中的高频短操作。

因为对象分配通常只需要修改少量指针,如果为了这种简单操作频繁加锁,会产生明显浪费。

6.3 对象批量分配:减少频繁访问 Buddy System

SLUB 高性能的另一个重要原因,是它并不会每次对象不足时都直接向 Buddy System 请求新的页面,而是采用批量管理方式。

如果每一次对象申请都进入 Buddy System:

申请 Object
↓
Buddy System
↓
申请 Page
↓
返回

那么大量小对象申请会造成严重开销。

因此,SLUB 会一次申请一组页面,然后将这些页面转换成为多个 Object。

例如:

Buddy System
↓
申请 8 Pages
↓
建立 slab
↓
划分
Object1
Object2
Object3
...
ObjectN

之后的大量对象申请,都可以直接从这个 slab 中完成。

这种方式类似于批量采购。

如果每次只购买一个零件,需要不断访问供应商;而一次采购大量零件,则可以长期从库存中获取。

SLUB 中的 slab 就相当于提前准备好的库存。

只有库存不足时,才重新访问 Buddy System。

这种设计减少了两个方面的开销:

第一,减少页面分配次数。

Buddy System 不需要频繁参与小对象申请。

第二,减少对象初始化成本。

同一个 Cache 中的对象布局固定,创建之后可以重复使用。

6.4 Cache Coloring 与内存访问优化

在早期 slab 分配器设计中,为了进一步提高 CPU Cache 利用率,引入了 Cache Coloring(缓存着色)机制。

理解这一机制之前,需要先了解 CPU Cache 的基本工作方式。

现代 CPU 访问内存时,并不是每一次都直接访问物理内存,而是优先从高速缓存 Cache 中读取数据。

CPU Cache 通常按照 Cache Line 作为访问单位。

例如:

CPU
↓
Cache Line
↓
Memory

如果多个对象长期映射到相同 Cache Line 位置,就可能产生 Cache 冲突,降低访问效率。

Cache Coloring 的思想,就是通过调整对象在内存中的起始偏移,让不同对象尽量分布到不同 Cache Line。

例如普通布局:

Object A
Object B
Object C

可能导致多个对象访问集中在相同缓存区域。

而通过着色:

Object A
  偏移
Object B
  偏移
Object C

可以改善缓存利用率。

虽然现代 SLUB 相比传统 SLAB 已经弱化了 Cache Coloring 的作用,但是内存布局优化思想仍然存在。

对象对齐、随机化布局以及 Cache Line 优化,都体现了 Linux 内核对于 CPU 硬件特性的考虑。

6.5 内存对齐机制:提高 CPU Cache 命中率

除了 Cache Coloring,SLUB 还会通过对象对齐提高访问效率。

所谓内存对齐,就是让对象地址按照某种边界进行排列。

例如:

struct object {
    int a;
    char b;
};

虽然结构体实际大小可能不是 CPU 最优访问大小,但是通过对齐,可以保证关键数据位于更加适合 CPU 访问的位置。

在 kmem_cache 创建过程中,可以指定:

SLAB_HWCACHE_ALIGN

表示希望对象按照硬件 Cache Line 进行排列。

这样做的目的主要有两个:

第一,减少一个对象跨越多个 Cache Line 的情况。

第二,降低多个 CPU 同时访问不同对象时产生缓存竞争的概率。

比如两个频繁访问的数据恰好位于同一个 Cache Line,那么一个 CPU 修改其中一个数据时,可能导致另一个 CPU 的缓存失效。

这种现象称为 False Sharing(伪共享)。

通过合理的内存布局,可以减少这种问题。

因此,SLUB 的高性能不仅来自软件层面的缓存管理,也来自对 CPU 硬件访问机制的优化。

6.6 SLUB 如何降低内存碎片?

内存碎片一直是操作系统内存管理中的重要问题。

所谓碎片,主要分为内部碎片和外部碎片两类。

内部碎片指分配出去的空间大于实际需求。例如申请 100 Bytes,实际分配 128 Bytes,多出的空间属于内部碎片。

外部碎片则指,虽然系统存在大量空闲空间,但是这些空间无法组成满足需求的连续区域。

SLUB 主要通过对象缓存机制降低碎片影响。

首先,由于同一个 Cache 中对象大小固定,因此 slab 内部空间利用率较高。

例如 inode Cache:

inode
inode
inode
inode

不会出现不同大小对象混杂导致的空间浪费。

其次,SLUB 通过 slab 管理方式减少了小对象直接占用页面的问题。

如果没有 SLUB:

100 个小对象
↓
100 个 Page

可能造成大量浪费。

而通过 SLUB:

多个小对象
↓
共享 slab

能够充分利用页面空间。

此外,SLUB 还会根据 Cache 使用情况回收长期空闲 slab,使不再需要的页面重新返回 Buddy System。

因此,SLUB 并不是完全消除碎片,而是在对象管理层面减少碎片产生,并通过缓存回收机制保持系统内存利用率。

通过以上机制可以看到,SLUB 的高性能并不是来自某一个单独优化,而是多个设计共同作用的结果。

Per-CPU cache 减少 CPU 之间竞争;cmpxchg 原子操作降低锁开销;批量 slab 分配减少 Buddy System 访问;对象对齐优化 CPU Cache 使用;Cache 管理降低内存碎片。这些机制共同构成了 SLUB 高效、稳定的对象分配体系。

七、SLUB 调试机制:如何定位内核内存问题?

7.1 为什么内核内存问题比用户态更难排查?

在用户态程序开发过程中,内存问题虽然也十分常见,但是由于进程之间存在明确的地址空间隔离,因此很多错误通常会表现得比较直接。例如访问已经释放的内存,可能导致程序立即崩溃;访问非法地址,通常会触发 Segmentation Fault。

但是 Linux 内核中的内存问题往往更加复杂。 内核运行在整个系统的核心位置,所有驱动、文件系统、网络协议栈以及硬件管理模块都共享同一个内核地址空间。因此,一个模块中的错误内存操作,可能影响完全无关的其他模块。

例如,一个驱动程序申请了一个对象:

obj = kmalloc(sizeof(struct device_data), GFP_KERNEL);

之后释放:

kfree(obj);

但是由于代码逻辑错误,后续仍然继续访问:

obj->status = 1;

这种情况就是典型的 Use After Free。

在用户程序中,这类问题可能很快导致崩溃,但是在内核中,被释放的内存可能已经被 SLUB 重新分配给其他对象。因此:

  • 当前访问可能暂时正常;
  • 数据可能被其他模块修改;
  • 错误可能延迟很长时间才暴露。

最终表现出来的现象可能是:

  • 内核随机崩溃;
  • 某个无关驱动异常;
  • 文件系统错误;
  • 系统运行数小时后突然死机。

这种"不确定性"正是内核内存问题最难定位的原因。

此外,Linux 内核中的对象生命周期通常更加复杂。

一个对象可能:

  • 在中断上下文使用;
  • 在多个线程之间共享;
  • 被不同子系统引用。

因此,简单查看代码中的 malloc/free 并不能完全判断问题所在。

为了帮助开发者定位这些隐藏问题,Linux 内核为 SLUB 提供了一套完整的调试机制,也就是:

CONFIG_SLUB_DEBUG。

7.2 CONFIG_SLUB_DEBUG 调试框架

CONFIG_SLUB_DEBUG 是 Linux 内核针对 SLUB 分配器提供的重要调试功能。

在开启该配置之后,SLUB 会在正常对象管理之外,增加额外的检测信息,用于发现对象生命周期以及内存访问过程中的异常。

常见配置方式:

CONFIG_SLUB_DEBUG=y

启用之后,内核可以通过启动参数进一步指定调试级别:

slub_debug=

例如:

slub_debug=FZPU

其中不同字符代表不同检测功能:

  • F:检测基本错误;
  • Z:开启 Red Zone;
  • P:开启 Poisoning;
  • U:记录用户空间分配信息。

通过这些机制,SLUB 能够在对象分配和释放过程中记录更多状态,从而帮助开发者定位问题。

例如普通 SLUB:

Object
数据区域

开启调试之后:

Red Zone
Object
Poison区域
Tracking信息

这样,当对象发生越界写入或者非法访问时,SLUB 就能够通过额外信息发现异常。

需要注意的是,SLUB 调试机制会增加额外开销。

因为为了检测错误,系统需要保存更多元数据、增加边界检查、记录调用信息。

因此,在生产环境中通常不会默认开启全部调试功能,而是在开发、测试以及问题定位阶段使用。

7.3 Memory Poisoning:检测非法访问

Memory Poisoning 是 SLUB 调试中非常重要的一项机制。

它主要用于检测"对象释放之后,是否仍然被错误使用"。也就是常见的 Use After Free。

正常情况下,一个对象释放之后:

kfree(obj);

该对象对应的内存空间会重新回到 SLUB 管理。但是,这块空间中的旧数据可能仍然存在。

例如释放前:

Object
status = 1
count = 100

释放之后:

Object
仍然保存旧数据

如果错误代码再次访问:

obj->status

由于数据仍然存在,错误可能不会立即暴露。

Memory Poisoning 的思想就是,释放对象之后,不保留原来的内容,而是写入特殊标记。

例如:

释放前:
Object
AA BB CC DD

释放后:
6B 6B 6B 6B

当代码再次访问:

obj->status

读取到异常数据后,开发者就能够判断这个对象已经被释放。

Linux 内核中常见的 poison 模式包括:

0x6b
表示释放后的对象

0x5a
表示未初始化区域

通过这些特殊填充值,SLUB 可以帮助开发者快速发现对象生命周期错误。

例如系统日志可能出现:

BUG kmalloc-128:
Object already free

此时就说明某个对象已经释放,但是仍然被访问。

7.4 Red Zone:检测内存越界

除了 Use After Free,内核中另一类非常严重的问题就是 Buffer Overflow。也就是写入超过对象实际大小。

例如:

char *buf;

buf = kmalloc(32, GFP_KERNEL);

memset(buf, 0, 64);

程序申请了 32 字节空间,但是却写入了 64 字节。这种错误会覆盖相邻对象的数据。

由于 SLUB 中大量对象存储在同一个 slab 内,因此一次越界写入可能破坏其他完全无关对象。

例如 正常:

Object A
Object B
Object C

越界之后:

Object A
↓↓↓↓
Object B 被破坏

问题可能直到很久之后才出现。Red Zone 的作用就是在对象边界增加保护区域。

例如 普通对象:

Object

开启 Red Zone:

Red Zone
Object
Red Zone

如果代码越界写入:

Object
↓
覆盖 Red Zone

SLUB 就能够检测到边界被破坏。

例如:

BUG: kmalloc-64:
Redzone overwritten

开发者通过这个信息,就可以判断某个对象发生了越界访问。

7.5 User Tracking:追踪对象申请释放位置

在复杂内核系统中,仅仅知道"哪个对象出错"往往是不够的。

开发者还需要知道这个对象是谁申请的?什么时候释放的?是否存在异常生命周期?

User Tracking 就是用于解决这一问题。开启该功能之后,SLUB 会记录对象相关调用信息。

例如对象申请:

kmalloc()
↓
driver_alloc_buffer()

对象释放:

kfree()
↓
driver_release()

如果之后发现:

Object used after free

那么开发者可以查看创建位置、释放位置、调用路径。从而快速定位问题代码。这对于大型内核工程尤其重要。

因为 Linux 内核包含数千万行代码,一个对象可能经过多个函数、多层调用之后才出现异常。

如果没有调用追踪信息,仅依靠崩溃现场,很难判断真正的问题来源。

7.6 常见问题分析:Use After Free、Double Free、Buffer Overflow、Memory Leak

SLUB 调试机制主要用于解决几类典型内存错误。

1. Use After Free

Use After Free 指对象已经释放,但是代码仍然继续使用。

例如:

obj = kmalloc(...);

kfree(obj);

obj->value = 10;

问题原因通常包括:

  • 生命周期管理错误;
  • 引用计数缺失;
  • 多线程竞争。

SLUB 可以通过 Poisoning 和 Tracking 发现这种问题。

2. Double Free

Double Free 指同一个对象被释放两次。

例如:

kfree(obj);

kfree(obj);

这种错误非常危险。

第一次释放:

Object
↓
free

第二次释放:

再次加入 freelist

可能导致 freelist 链表结构损坏。严重情况下,攻击者甚至可以利用这种漏洞修改内核执行流程。

SLUB 会通过对象状态检测发现该对象已经处于释放状态。

3. Buffer Overflow

Buffer Overflow 指写入超过对象边界。

例如:

char buf[32];

memcpy(buf,data,100);

这种错误可能破坏相邻对象。Red Zone 是检测此类问题的重要工具。

4. Memory Leak

Memory Leak 指对象申请之后没有释放。

例如:

ptr = kmalloc(...);

/*
忘记 kfree
*/

随着系统运行时间增加:

申请
↓
未释放
↓
数量不断增加

最终可能导致内存耗尽。

SLUB 可以通过 /proc/slabinfo、slabtop、kmemleak 等工具辅助分析。

通过这些调试机制可以看到,SLUB 不仅是一个负责"快速分配内存"的工具,同时也是 Linux 内核内存错误定位体系的重要组成部分。

它通过 Poisoning 检测释放后访问,通过 Red Zone 检测越界,通过 Tracking 记录对象生命周期,使开发者能够从复杂的内核运行环境中定位隐藏的内存问题。

八、SLUB 实战:从系统运行状态分析内存问题

8.1 slabtop:查看 SLUB 缓存使用情况

在 Linux 系统中,最常用的 slab 分析工具之一就是 slabtop。

执行 slabtop 之后,系统会显示当前内核中各类 slab cache 的使用情况。

例如:

OBJS ACTIVE USE OBJ SIZE SLABS CACHE SIZE NAME
120000 110000 91% 256 7500 1920000 kmalloc-256
80000 70000 87% 128 2500 320000 inode_cache

其中 NAME 表示 Cache 名称。

例如:

kmalloc-256
inode_cache
dentry

代表不同类型对象缓存。

OBJ SIZE 表示单个 Object 大小。

例如 256  说明每一个对象占用 256 字节。

OBJS 表示当前 Cache 中对象总数量。

ACTIVE 表示当前正在使用的对象数量。

USE 表示使用比例。

通过这些字段,开发者可以快速判断某类对象是否存在异常增长。

例如正常情况下:

inode_cache
对象数量稳定

但是如果长期运行之后:

inode_cache
几百万对象
持续增长

那么可能说明:

  • 文件对象没有释放;
  • 某个模块持续创建 inode 相关结构;
  • 存在引用计数异常。

因此,slabtop 更像是一个实时观察工具,它能够帮助开发者快速发现:

"哪一种对象正在异常增加"。

8.2 /proc/slabinfo 详细解析

除了 slabtop 之外,Linux 还提供了更加底层的接口:

/proc/slabinfo

该文件保存了当前系统中所有 slab cache 的详细信息。

查看:

cat /proc/slabinfo

可能看到:

kmalloc-256  120000 130000 256 16 1 : tunables
inode_cache   80000 90000 512 8 1 : tunables

相比 slabtop,slabinfo 提供的信息更加底层。

其中:

第一个数字表示当前使用对象数量。

第二个数字表示当前分配对象总数量。

第三个数字表示对象大小。

第四个数字表示每个 slab 中包含的对象数量。

例如:

kmalloc-256
object size:
256 Bytes

表示该 Cache 中每一个对象大小为 256 字节。

通过 slabinfo,可以进一步分析:

某个 Cache 是否存在持续增长。

例如第一次观察:

kmalloc-128
50000 objects

几小时之后:

kmalloc-128
500000 objects

如果系统业务没有明显变化,那么这种增长通常值得关注。

8.3 如何发现异常增长的 slab cache?

在实际开发过程中,最常见的问题之一就是:某一个 slab cache 数量不断增长。

但是,仅仅看到对象数量增加,并不能直接说明存在内存泄漏。

因为某些 Cache 本身就具有缓存性质。

例如 dentry cache。

文件系统为了提高访问速度,会主动缓存目录项信息。

因此 dentry 数量增加,并不一定代表错误。判断 slab 是否异常,需要结合多个因素。

首先,需要观察增长趋势。

例如正常:

10:00
100000 objects

12:00
110000 objects

可能只是缓存增长。

异常:

10:00
100000 objects

12:00
800000 objects

14:00
2000000 objects

这种持续增长就非常可疑。

其次,需要结合业务行为。

例如某驱动负责网络数据:

正常:

网络关闭
↓
skb 数量下降

如果:

网络关闭
↓
skb cache 仍持续增长

可能存在释放异常。最后,可以结合 SLUB Debug 或 kmemleak 进一步确认。

8.4 kmalloc 缓存异常问题定位

kmalloc 是最常使用的内存接口,因此 kmalloc Cache 异常也是内核中非常常见的问题。

例如:

kmalloc-512
对象数量持续增长

这意味着某些代码不断申请 512 字节左右的内存。

但是,仅通过 Cache 名称无法知道具体是谁申请的。

因为:

kmalloc(400)
↓
kmalloc-512

多个不同模块可能都会进入同一个 Cache。

因此,进一步定位通常需要结合以下方法。

方法一:开启 SLUB Tracking

通过:

slub_debug=U

记录对象申请位置。

例如发现:

kmalloc-512
增长异常

进一步查看:

driver_alloc_buffer()

那么就可以定位到具体模块。

方法二:结合调用路径分析

内核对象申请通常存在调用链:

driver_write()
↓
allocate_packet()
↓
kmalloc()

真正的问题往往不是 kmalloc 本身,而是调用者没有正确释放。

方法三:检查对象生命周期

例如错误:

buffer = kmalloc(...);

list_add(&buffer->list);

但是:

remove()
{

没有 kfree()

}

导致申请:

Object++

释放:

Object 不变

最终形成泄漏。

8.5 驱动开发中的 SLUB 内存泄漏案例分析

在设备驱动开发过程中,SLUB 内存泄漏非常常见。

下面分析一个简单案例。

假设某个字符设备驱动:

static int device_open(...)
{

    data = kmalloc(sizeof(*data), GFP_KERNEL);

    return 0;
}

驱动打开设备时申请了一块私有数据。

但是在关闭设备时:

static int device_release(...)
{

    return 0;
}

忘记:

kfree(data);

那么每一次打开设备:

open
↓
kmalloc
↓
Object +1

关闭:

release
↓
没有释放

最终:

Object
100
1000
10000
...

不断增长。

如果使用 slabtop 可能看到:

kmalloc-128
对象数量持续增加

但是还不知道具体来源。

进一步开启:

slub_debug=U

可以看到:

device_open()
driver.c:120

最终定位到 open 函数申请,但是 release 没有释放。

实际驱动开发中,这类问题非常典型。

尤其是在字符设备、网络驱动、USB驱动、文件系统模块中,大量对象都依赖动态内存管理。

通过这些工具和案例可以看到,SLUB 不仅提供了高效的对象分配能力,同时也提供了一套完整的问题分析方法。

实际定位内核内存问题时,通常遵循这样的流程:

发现系统内存增长
        ↓
slabtop 查看异常 Cache
        ↓
/proc/slabinfo 分析趋势
        ↓
开启 SLUB Debug
        ↓
定位申请位置
        ↓
检查对象生命周期
        ↓
修复释放逻辑

掌握这套流程之后,开发者就不再只是知道"系统内存不足",而能够进一步回答:

"是哪一种对象占用了内存?"

"是谁申请了这些对象?"

"为什么它们没有被释放?"

这也是深入理解 SLUB 对内核开发最重要的实际价值之一。

九、Linux 内核源码实战:深入 SLUB 核心代码

9.1 kmem_cache 数据结构源码解析

kmem_cache 是 SLUB 管理某一类对象的核心控制结构。它描述了一个 Cache 的基本属性,包括对象大小、对齐方式、内存布局、NUMA 节点信息以及 CPU 本地缓存等内容。

在 Linux 内核源码中:

struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;

    unsigned int object_size;
    unsigned int size;

    unsigned int offset;
    unsigned int inuse;
    unsigned int align;

    unsigned int min_partial;

struct kmem_cache_node *node[MAX_NUMNODES];
};

不同内核版本中的字段会有所变化,但是整体结构基本围绕几个核心功能展开。

其中:

object_size

表示对象真正需要使用的大小。

例如:

struct task_struct

如果大小为 6000 字节,那么对应 Cache 就需要知道每一个 Object 至少需要能够容纳这么大的空间。

size

表示经过内存布局调整之后,一个 Object 实际占用的空间大小。

它可能比 object_size 更大。

原因包括:

  • 内存对齐;
  • freelist 指针存储;
  • 调试信息;
  • Red Zone。

例如:

实际结构体大小:
100 Bytes

SLUB 分配大小:
128 Bytes

多出的空间用于满足内存布局要求。

align

表示对象对齐要求。

例如:

SLAB_HWCACHE_ALIGN

会要求对象按照 CPU Cache Line 进行排列。

这样可以减少缓存访问冲突。

除了对象自身信息之外,kmem_cache 还保存了 CPU 以及 NUMA 相关管理信息。

其中 cpu_slab 它表示每一个 CPU 对应的本地缓存。

结构类似:

kmem_cache
      |
      +---- CPU0 cache
      |
      +---- CPU1 cache
      |
      +---- CPU2 cache

当 CPU 申请对象时,首先访问这里。

另外:

node[MAX_NUMNODES]

用于保存 NUMA 节点相关信息。

在多节点系统中,不同 CPU 可能属于不同 NUMA Node,而 SLUB 需要尽量从本地 Node 获取对象。

因此,从整体来看:

kmem_cache 主要承担三个职责:

第一,描述对象。包括对象大小、对齐方式以及布局信息。

第二,管理对象来源。包括 slab、CPU cache、NUMA node。

第三,协调对象分配和释放。通过 kmem_cache,SLUB 将"某一种对象应该如何管理"这一信息统一保存起来。

后续所有对象申请,实际上都是围绕对应 kmem_cache 展开的。

9.2 struct slab 核心字段分析

如果说 kmem_cache 描述的是整个对象缓存,那么 struct slab 描述的就是实际存放对象的内存区域。

在较新的 Linux 内核中,slab 信息逐渐从传统的 page 结构中独立出来,因此引入了:

struct slab

用于描述一个 slab。

其核心思想是一个 slab 对应一组连续物理页面,而这些页面内部保存大量 Object。

结构关系:

kmem_cache
      |
      slab
      |
      +---- Object
      +---- Object
      +---- Object

一个简化后的 slab 结构:

struct slab {
    unsigned long flags;

struct list_head slab_list;

    unsigned long counters;

    void *freelist;

struct kmem_cache *slab_cache;
};

其中 freelist 是最重要字段之一。

它保存当前 slab 中空闲 Object 的链表入口。

例如:

freelist
   ↓
Object A
   ↓
Object B
   ↓
Object C

当 CPU 需要对象时,SLUB 直接从 freelist 获取。

slab_cache

表示该 slab 属于哪个 kmem_cache。

例如:

inode slab
↓
inode_cache

kmalloc slab
↓
kmalloc-128

通过这个关联关系,释放对象时,内核能够知道这个地址属于哪个 Cache。应该返回哪里。

另外 counters 用于记录 slab 当前状态。

例如:

  • 已使用对象数量;
  • 空闲对象数量。

通过这些信息,SLUB 能够判断当前 slab 是否还有继续分配的能力。

9.3 alloc_slab_page 页面申请流程

当 SLUB 中已经没有可用对象时,就需要创建新的 slab。

而创建 slab 的第一步,就是向 Buddy System 申请页面。

这个过程通常通过 alloc_slab_page() 完成。

整体流程:

对象申请
      ↓
SLUB 无空闲 Object
      ↓
创建 slab
      ↓
alloc_slab_page()
      ↓
Buddy System
      ↓
返回 Page

底层最终会进入:

alloc_pages()

例如:

page = alloc_pages(
        gfp_flags,
        order
);

其中 gfp_flags 决定内存申请行为。

例如 GFP_KERNEL 表示普通内核内存申请。

而 GFP_ATOMIC 表示不能睡眠环境下申请。

order 决定申请多少连续页面。

例如:

order 0
↓
1 Page

order 2
↓
4 Pages

获得页面之后,SLUB 会继续完成页面初始化、建立 slab、划分 Object、建立 freelist。

例如申请:

4KB Page

对象大小:

128 Bytes

那么:

4096 / 128
=
32 Objects

最终形成:

slab
Object1
Object2
...
Object32

之后这些 Object 就能够被 kmem_cache_alloc() 使用。

9.4 ___slab_alloc 核心分配函数解析

在 SLUB 源码中,对象分配的核心逻辑主要集中在:

___slab_alloc()

这个函数负责处理:

  • 当前 CPU 是否存在空闲对象;
  • 是否需要获取新的 slab;
  • 如何更新 freelist。

一次典型流程如下:

kmem_cache_alloc()
        ↓
slab_alloc()
        ↓
___slab_alloc()
        ↓
检查 CPU freelist
        ↓
返回 Object

首先,函数会检查当前 CPU 的 slab 状态。

如果:

freelist != NULL

说明存在空闲对象。

那么直接:

取出 Object
↓
更新 freelist
↓
返回

这是最快路径。

如果:

freelist == NULL

说明当前 CPU 没有可用对象。

此时需要进入下一阶段:

寻找 partial slab。

流程:

CPU cache 空
      ↓
查找 partial slab
      ↓
存在
      ↓
重新填充 CPU cache

如果 partial slab 也不存在:

那么:

申请新的 slab
↓
Buddy System
↓
建立 Object

因此 ___slab_alloc 实际上完成了快速路径和慢速路径之间的连接。

9.5 do_slab_free 释放路径分析

对象释放对应的核心流程是:

kfree()
↓
kmem_cache_free()
↓
do_slab_free()

do_slab_free() 的主要任务就是将释放对象重新加入 SLUB 管理体系。

例如释放之前:

freelist:
Object B
↓
Object C

释放 Object A:

Object A
↓
Object B
↓
Object C

之后 Object A 就重新成为可用对象。

但是释放过程并不是简单修改一个指针。

SLUB 还需要处理:

  • 当前 CPU 是否属于该对象所在节点;
  • 是否需要更新 slab 状态;
  • 是否触发回收。

如果释放之后整个 slab 都为空。

例如:

Object1 free
Object2 free
Object3 free

那么该 slab 可能进入 partial 状态。

如果系统内存压力较大,最终可能被释放回 Buddy System。

9.6 SLUB 核心代码阅读方法

SLUB 源码规模较大,如果直接从头阅读,很容易陷入大量细节。因此,理解 SLUB 代码需要采用正确的方法。

首先,需要围绕核心调用链阅读。

对象申请:

kmalloc
↓
kmem_cache_alloc
↓
slab_alloc
↓
___slab_alloc
↓
new_slab

对象释放:

kfree
↓
kmem_cache_free
↓
slab_free
↓
do_slab_free

其次,需要重点关注几个核心数据结构:

kmem_cache
        ↓
kmem_cache_cpu
        ↓
slab
        ↓
freelist

因为 SLUB 的所有机制,本质都是围绕:"如何快速找到一个空闲 Object"展开。

最后,需要结合实际调试观察源码行为。

三者结合起来,才能真正理解 SLUB 的工作方式。

十、SLUB 与其他内存分配机制对比

10.1 SLUB vs SLAB:设计理念与性能差异

SLUB 和 SLAB 都属于 Linux 内核中的 slab allocator,它们解决的问题本质相同,都是为了更加高效地管理大量固定大小的内核对象。二者都建立在 Buddy System 之上,通过提前申请页面并划分成为 Object,从而避免小对象频繁进入页级分配流程。

但是,二者在内部设计理念上存在明显区别。

传统 SLAB 的设计更加偏向于功能完整性。它为了提高对象管理效率,会维护多个不同状态的 slab 链表,例如:

full slab
partial slab
free slab

其中:

  • full slab 表示所有对象已经被占用;
  • partial slab 表示部分对象已经分配;
  • free slab 表示当前没有被使用。

通过这种分类管理方式,SLAB 可以快速找到符合条件的 slab。

但是,这种设计也带来了较高的管理复杂度。因为每一次对象状态变化,都可能涉及:

  • slab 链表移动;
  • 全局状态更新;
  • 锁竞争。

在单 CPU 或者低并发环境中,这些开销并不明显。

但是随着 Linux 运行环境逐渐转向多核处理器,多个 CPU 同时访问共享 slab 数据结构的问题越来越突出。

SLUB 则针对这些问题进行了重新设计。

它减少了传统 SLAB 中复杂的链表管理,更加强调:

  • CPU 本地缓存;
  • freelist;
  • 简单对象管理。

例如 SLAB:

CPU
↓
共享 slab 链表
↓
查找对象

SLUB:

CPU
↓
本地 freelist
↓
直接获取对象

这种设计使 SLUB 在多核环境中具有更好的扩展能力。

此外,SLUB 的代码结构更加简单。

传统 SLAB 为了维护大量状态,需要大量辅助逻辑。而 SLUB 将很多管理工作转移到更简单的数据结构之中,使代码更容易维护。

因此,从设计理念来看:

SLAB 更像是:"通过复杂管理换取性能。"

而 SLUB 更像是:"通过简化结构配合现代 CPU 特性获得性能。"

这也是 Linux 后续版本逐渐默认采用 SLUB 的重要原因。

10.2 SLUB vs SLOB:服务器与嵌入式场景选择

SLOB 是 Linux 内核中曾经存在的一种简单对象分配器。

它的设计目标与 SLUB 完全不同。

SLOB 主要面向:

  • 内存资源非常有限的嵌入式设备;
  • 小容量 RAM 系统。

它的核心思想是维护一个空闲内存链表。

当需要分配内存时,遍历链表;寻找合适大小的空闲区域。

例如:

Free Block
↓
查找
↓
分配

这种方式最大的优势是实现简单,管理数据结构少。因此额外内存开销非常低。

但是,它的问题也非常明显。

第一,分配速度较慢。因为每一次申请都可能需要遍历空闲链表。

第二,多 CPU 扩展能力较差。因为链表访问容易产生竞争。

第三,不适合大量对象频繁申请释放。

相比之下 SLUB:

Cache
↓
Slab
↓
Object

通过对象缓存机制提高效率。

因此对于大型服务器(多 CPU、大量并发任务、复制内核服务)SLUB 更合适。

对于极低资源嵌入式系统(几十 MB 内存、对性能要求不高)SLOB 可能更加节省空间。

不过随着现代嵌入式设备性能不断提升,大量 ARM Linux 平台同样开始采用 SLUB。

因为即使是嵌入式系统,也越来越需要网络功能、图形系统、多任务处理。

这些场景都更加依赖高性能对象分配。

因此,SLOB 解决的是"如何以最低管理成本运行。"

而 SLUB 解决的是"如何在复杂系统中高效运行。"

10.3 SLUB vs kmalloc:对象缓存与通用分配区别

在 Linux 内核开发中,很多人容易混淆 SLUB 和 kmalloc。

实际上,二者并不是同一层面的概念。

SLUB 是内核内部对象分配机制。

kmalloc 是提供给内核开发者使用的接口。

二者关系:

驱动代码
↓
kmalloc()
↓
kmalloc Cache
↓
SLUB
↓
Object

例如驱动:

buf = kmalloc(128,GFP_KERNEL);

开发者看到的是 kmalloc。

但是内部:

kmalloc-128 Cache
↓
SLUB
↓
Object

因此 kmalloc 是入口。SLUB 是实现。

除此之外,二者使用场景也不同。

kmalloc 适合临时缓冲区、小块内存、不固定类型数据。

例如:

buffer = kmalloc(1024,GFP_KERNEL);

而 kmem_cache_alloc 更适合大量重复对象、固定结构体。

例如:

struct connection {
    ...
};

如果网络模块需要创建几十万个 connection 对象,那么建立专用 Cache:

connection_cache
↓
connection Object

效率会明显高于不断 kmalloc。

因此,kmalloc 关注"申请多少空间。"SLUB Cache 关注"管理什么类型对象。"

10.4 SLUB vs vmalloc:连续物理内存与虚拟连续内存

SLUB 和 vmalloc 解决的是完全不同的问题。

SLUB 关注如何高效管理小对象。

vmalloc 关注如何获得大块虚拟连续空间。

二者最大的区别在于物理连续性。

kmalloc / SLUB 通常要求:

虚拟地址连续
物理地址连续

例如:

Page0
Page1
Page2

这些页面需要满足连续关系。

而 vmalloc 只要求虚拟地址连续,物理页面可以分散。

例如:

Virtual Address:
连续

Physical:
Page5
Page100
Page300

通过页表映射让它们表现为连续空间。

因此 vmalloc 更适合大块缓冲区、不要求 DMA 的内存。

例如申请几十 MB 内存:

vmalloc(size);

但是 vmalloc 访问速度通常低于 kmalloc。

因为它可能涉及更多页表映射。

并且物理页面不连续。

而 SLUB 主要用于几十字节、几百字节、几 KB 这种大量小对象。

所以如果驱动需要一个 64 字节控制结构,使用 kmalloc。

如果需要一个 100MB 日志缓冲区,则考虑 vmalloc。

如果需要大量相同结构体,就使用 kmem_cache + SLUB。

十一、面试高频题

11.1 为什么 Linux 默认使用 SLUB?

在 Linux 内核的发展过程中,曾经先后出现过 SLOB、SLAB 以及 SLUB 等不同的小对象分配器。最终 SLUB 成为 Linux 默认选择,并不是因为它提供了完全不同的内存管理思想,而是在传统 slab 分配模型的基础之上,更好地适应了现代处理器架构以及大型系统运行需求。

首先,SLUB 具有更好的多核扩展能力。

现代 Linux 系统通常运行在多核甚至几十、上百核心的处理器环境中。如果多个 CPU 同时访问同一个全局内存缓存结构,就容易产生锁竞争。

传统 SLAB 中存在较多共享链表以及状态维护逻辑,而 SLUB 通过 per-CPU cache 机制,让大量对象申请优先在当前 CPU 本地完成。

例如:

CPU0
↓
本地 freelist
↓
Object

CPU1
↓
本地 freelist
↓
Object

不同 CPU 之间可以独立完成对象分配,减少了同步开销。

其次,SLUB 的内部结构更加简单。

传统 SLAB 为了管理不同状态的 slab,需要维护:

  • full 链表;
  • partial 链表;
  • free 链表。

这些设计虽然功能完善,但是也增加了代码复杂度。

SLUB 则弱化了这些复杂状态管理,更加依赖:

  • freelist;
  • CPU 本地缓存;
  • 简单状态转换。

这种设计使内核代码更加容易维护。

再次,SLUB 对现代 CPU 特性进行了优化。

现代处理器非常依赖 Cache Line、内存局部性、无锁原子操作。

SLUB 通过对象对齐、CPU 本地访问以及 cmpxchg 等机制,使对象分配过程更加符合现代硬件特点。

因此,Linux 默认选择 SLUB 的主要原因可以总结为:

第一,多核环境下性能更高;

第二,内部结构更加简单;

第三,更容易维护和调试;

第四,更适合现代服务器以及复杂嵌入式系统。

SLUB 并不是因为"功能最多"而成为默认,而是因为它在性能、复杂度、稳定性、维护成本之间取得了更好的平衡。

11.2 slab、page、cache 三者是什么关系?

这是理解 SLUB 最基础的问题之一。很多初学者容易混淆 Page、Slab、Cache。

实际上,它们处于不同层次。Page 是最底层概念。它表示物理内存页面。

Linux Buddy System 管理的就是 Page。

例如:

Physical Memory
↓
Page
↓
Buddy System

Slab 位于 Page 之上。它由一个或者多个 Page 组成,是 SLUB 管理对象的基本空间单位。

例如:

Page
Page
Page
↓
Slab
↓
Object

Cache 则描述如何管理这些 slab。

例如 inode_cache 表示:"我要管理 inode 类型对象。"

关系如下:

kmem_cache
       ↓
      slab
       ↓
      Page
       ↓
  Physical Memory

用大白话说:Page 是原材料;Slab 是加工后的存储区域;Cache 是管理这些区域的规则。

例如:

Buddy System 提供:

4 个 Page

SLUB:

建立 slab

Cache:

inode_cache

最终:

inode Object
inode Object
inode Object

因此 Page 解决"物理内存在哪里",Slab 解决"如何组织这些页面"。Cache 解决"如何管理某一类对象"。

理解这三者关系之后,后续分析任何 SLUB 源码都会更加清晰。

11.3 kmalloc 底层调用流程是什么?

这是 Linux 驱动开发中非常常见的问题。

表面上:

kmalloc(size,GFP_KERNEL);

只是申请一块内存。但是内部实际上经历了一系列过程。

完整流程:

kmalloc()
↓
__kmalloc()
↓
kmalloc_slab()
↓
kmem_cache_alloc()
↓
slub_alloc()
↓
返回 Object

具体过程如下。

第一步:调用 kmalloc。

例如:

kmalloc(128,GFP_KERNEL);

第二步:根据申请大小选择 Cache。

系统内部已经存在:

kmalloc-8
kmalloc-16
kmalloc-32
...
kmalloc-128

因此 128 字节请求进入:

kmalloc-128

第三步:进入 SLUB。

SLUB 首先检查当前 CPU 是否有空闲对象。

如果有:

CPU cache
↓
freelist
↓
Object

直接返回。

如果没有,进入慢速路径:

寻找 partial slab
↓
无
↓
Buddy System
↓
申请 Page
↓
创建 slab
↓
划分 Object

因此一次 kmalloc 并不是简单申请内存,而是:

大小匹配
↓
Cache 选择
↓
SLUB 对象分配
↓
必要时申请页面

这也是为什么理解 kmalloc 必须先理解 SLUB。

11.4 SLUB 如何解决多 CPU 并发问题?

多 CPU 并发访问是现代内核内存管理最大的挑战之一。

如果所有 CPU 都访问同一个 freelist:

CPU0
↓
共享 freelist

CPU1
↓
共享 freelist

那么必须加锁。但是锁竞争会降低性能。

因此 SLUB 采用 per-CPU cache。每个 CPU 拥有自己的本地对象缓存。

例如:

CPU0
Object A

CPU1
Object B

CPU2
Object C

申请对象时优先访问本地缓存。

只有本地没有对象、slab 不足才进入共享区域。

此外,SLUB 还使用 cmpxchg 原子操作。

相比传统锁:

lock
↓
修改
↓
unlock

原子操作:

比较
↓
交换

更加适合短时间、高频率操作。

因此,SLUB 解决多 CPU 并发主要依靠:

第一,CPU 本地缓存;

第二,减少共享数据访问;

第三,原子操作替代部分锁。

11.5 为什么释放后的对象不会立即归还系统?

很多开发者第一次接触 SLUB 时会产生疑问:

调用 kfree(ptr); 之后,这块内存为什么没有马上减少?

原因在于 SLUB 的目标不是立即释放,而是提高后续分配效率。

例如某个 Cache:

Object1
Object2
Object3

释放:

kfree(Object1)

实际上:

Object1
↓
重新进入 freelist

它仍然属于当前 Cache。

下一次:

kmalloc()

可以直接复用。

如果每次释放都立即:

Object
↓
Slab
↓
Page
↓
Buddy System

那么之后重新申请时又需要:

重新申请 Page;

重新建立 slab;

重新初始化对象。

这会造成大量性能浪费。

只有当 slab 长期空闲、系统内存压力较大;SLUB 才会考虑回收 slab,并将页面返回 Buddy System。

因此释放 Object ≠ 释放 Page。

这是理解 SLUB 缓存机制非常重要的一点。

11.6 如何排查 SLUB 导致的内存泄漏?

当 Linux 系统运行一段时间之后出现:

  • 内存持续下降;
  • OOM;
  • slab 占用异常增长;

就需要考虑是否存在 SLUB 对象泄漏。

通常排查流程如下。

第一步:观察 slab 使用情况。

使用:

slabtop

寻找增长异常的 Cache。

例如:

kmalloc-512
持续增长

第二步:查看详细信息。

使用:

/proc/slabinfo

确认对象数量是否持续增加。

第三步:开启调试。

例如:

slub_debug=U

记录对象申请位置。

第四步:分析生命周期。

检查申请:

kmalloc()

是否对应:

释放:

kfree()

常见错误包括:

  • exit 路径忘记释放;
  • 异常返回没有清理;
  • 引用计数错误;
  • 链表删除后没有释放对象。

例如:

错误:

obj = kmalloc();

list_add(&obj->list,...);

/*
忘记 list_remove 后释放
*/

最终导致:

对象不断增加
↓
slab cache 增长
↓
系统内存耗尽

因此,SLUB 内存泄漏定位的核心思路就是:

发现内存异常
↓
定位异常 Cache
↓
追踪对象来源
↓
检查生命周期
↓
修复释放逻辑

十二、总结:从一次 kmalloc 看懂 Linux 内核内存世界

12.1 SLUB 核心思想总结

Linux 内核运行过程中存在大量固定类型的数据结构,例如进程控制块 task_struct、文件系统中的 inode 和 dentry、网络协议栈中的 socket 以及各种设备驱动私有结构。这些对象具有明显特点:

  • 数量巨大;
  • 大小固定;
  • 生命周期频繁变化。

如果这些对象全部直接通过 Buddy System 获取页面,不仅会造成严重的空间浪费,同时也会让大量小对象申请进入复杂的页管理流程。

SLUB 的核心思想,就是在 Page 和 Object 之间建立一层高效的管理机制。

整个过程可以概括为:

Physical Page
      ↓
Buddy System
      ↓
Slab
      ↓
Object
      ↓
内核对象

Buddy System 负责管理最底层物理页面。

SLUB 从 Buddy System 获取页面之后,将其组织成为 slab。

每一个 slab 再进一步划分成为多个 Object。

最终,内核其他模块通过 kmalloc 或 kmem_cache_alloc 获取这些 Object。

这种设计带来了几个重要优势。

首先,提高了内存利用率。一个页面可以保存多个对象,而不是为了几十字节的数据浪费整个页面。

其次,提高了分配效率。大量对象申请可以直接从已有 slab 中完成,而不需要频繁访问 Buddy System。

再次,提高了多核环境下的扩展能力。通过 per-CPU cache 和无锁机制,不同 CPU 可以更加独立地完成对象分配。

因此,SLUB 的本质可以理解为:将低效的页面级分配转换成为高效的对象级分配。

12.2 Linux 内核对象分配完整链路回顾

理解 SLUB 最好的方式,就是重新回顾一次普通内核内存申请过程。

假设驱动程序执行:

buffer = kmalloc(256, GFP_KERNEL);

从代码表面看,这只是申请 256 字节内存。

但是在 Linux 内核内部,实际上经历了一条完整链路。

第一步:进入 kmalloc 接口。

kmalloc(256)

内核首先需要判断这个大小应该属于哪个 Cache。

第二步:匹配 kmalloc Cache。

Linux 预先建立了多个固定大小 Cache:

kmalloc-8
kmalloc-16
kmalloc-32
...
kmalloc-256

因此 256 字节请求:

进入:

kmalloc-256 Cache

第三步:进入 SLUB 分配流程。

SLUB 首先检查当前 CPU 是否存在空闲 Object。

如果存在:

CPU Cache
↓
freelist
↓
Object
↓
返回

整个过程非常快速。

第四步:如果 CPU 本地没有对象。

SLUB 会进入慢速路径。

首先寻找 partial slab。

如果仍然无法满足,向 Buddy System 请求页面。

流程:

SLUB
↓
申请 slab
↓
Buddy System
↓
alloc_pages()
↓
Physical Page

获得页面之后:

Page
↓
建立 slab
↓
划分 Object
↓
加入 Cache
↓
返回

最终驱动程序获得:

void *

但是这块内存背后实际上经历了:

驱动代码
↓
kmalloc
↓
kmalloc Cache
↓
SLUB
↓
Slab
↓
Buddy System
↓
Physical Memory

这就是 Linux 内核一次普通内存申请背后的完整过程。

12.3 从 SLUB 延伸学习 Linux 内存子系统

理解 SLUB,并不是 Linux 内存学习的终点,而是进入 Linux 内存管理体系的重要入口。

通过 SLUB,可以进一步延伸学习更多内核机制。

首先,可以继续深入 Buddy System。

因为 SLUB 最终依赖 Buddy System 提供页面。

进一步学习:

  • zone 管理;
  • page allocator;
  • 内存水位线;
  • 页面回收机制;

可以理解 Linux 如何管理整个物理内存。

其次,可以继续学习虚拟内存体系。

Linux 内核并不是直接让应用访问物理地址,而是通过:

Virtual Address
↓
Page Table
↓
Physical Page

完成地址转换。

进一步可以研究:

  • MMU;
  • 页表结构;
  • TLB;
  • 缺页异常;
  • mmap。

再次,可以深入内核内存回收机制。

当系统内存不足时:

Linux 不会简单地停止分配,而是会启动:

  • kswapd;
  • 页面回收;
  • swap;
  • reclaim。

这些机制共同保证系统在高负载情况下仍然能够运行。

此外,对于驱动开发者而言,SLUB 还连接着大量实际问题。

例如:

为什么 kmalloc 不能申请过大的连续内存?

为什么 DMA 需要特殊分配接口?

为什么释放之后的对象不能继续访问?

为什么一个驱动泄漏几十字节最终可能导致系统崩溃?

这些问题的答案,都可以从 Linux 内存管理体系中找到。

最终,可以用下面这条链路总结 Linux 内核内存世界:

物理内存
      ↓
Page
      ↓
Buddy System
      ↓
SLUB
      ↓
Slab
      ↓
Object
      ↓
kmalloc / kmem_cache_alloc
      ↓
内核子系统

其中:

Buddy System 负责管理资源;

SLUB 负责组织资源;

Cache 负责分类资源;

Object 负责提供资源。

Linux 内核正是通过这种层次化设计,将复杂的物理内存管理问题逐步抽象,使不同层次的代码只需要关注自身职责。

对于内核开发者而言,理解 SLUB 的意义不仅在于知道一个 kmalloc() 背后调用了什么,更重要的是建立一种系统化思维:

任何内核资源,都不是简单地被申请和释放,而是在多个管理层之间经过组织、调度以及复用之后,最终提供给使用者。

这也是 Linux 内核能够在服务器、桌面系统以及嵌入式设备中长期稳定运行的重要原因。更多关于 Linux 内核的深入讨论,也欢迎在 云栈社区 与广大开发者一起交流。




上一篇:南大蒋炎岩生成式软件工程课:没有付费Token的CS学生该退学吗
下一篇:OpenFeign超时重试的三个坑:为什么幂等接口被调了两次
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-10 16:57 , Processed in 1.101778 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表