深入理解Linux slab机制

2024-12-13 292 0

通过前面的文件介绍中我们知道,内核中的伙伴系统管理内存的最小单位是物理内存页 page。伙伴系统会将它所属物理内存区 zone 里的空闲内存划分成不同尺寸的物理内存块,这里的尺寸必须是 2 的次幂,物理内存块可以是由 1 个 page 组成,也可以是 2 个 page,4 个 page ........ 1024 个 page 组成。

内核将这些相同尺寸的内存块用一个内核数据结构 struct free_area 中的双向链表 free_list 串联组织起来。

struct free_area {
 struct list_head free_list[MIGRATE_TYPES];
 unsigned long  nr_free;
};

而这些由 free_list 串联起来的相同尺寸的内存块又会近一步根据物理内存页 page 的迁移类型 MIGRATE_TYPES 进行归类,比如:MIGRATE_UNMOVABLE (不可移动的页面类型),MIGRATE_MOVABLE (可以移动的内存页类型),MIGRATE_RECLAIMABLE (不能移动,但是可以直接回收的页面类型)等等。

这样一来,具有相同迁移类型,相同尺寸的内存块就被组织在了同一个 free_list 中。

伙伴系统所分配的物理内存页全部都是物理上连续的,并且只能分配 2 的整数幂个页

随后在物理内存分配的过程中,内核会基于这个完整的伙伴系统数据结构,进行不同尺寸的物理内存块的分配与释放,而分配与释放的单位依然是 2 的整数幂个物理内存页 page。详细的内存分配过程感兴趣的读者朋友可以回看下 《深入理解 Linux 伙伴系统》

我们从伙伴系统核心数据结构,以及伙伴系统内存分配原理的相关内容来看,伙伴系统管理物理内存的最小单位是物理内存页 page。也就是说,当我们向伙伴系统申请内存时,至少要申请一个物理内存页。

而从内核实际运行过程中来看,无论是从内核态还是从用户态的角度来说,对于内存的需求量往往是以字节为单位,通常是几十字节到几百字节不等,远远小于一个页面(4K)的大小。如果我们仅仅为了这几十字节的内存需求,而专门为其分配一整个内存页面,这无疑是对宝贵内存资源的一种巨大浪费。

于是在内核中,这种专门针对小内存的分配需求就应运而生了slab机制,slab内存池就是专门应对小内存频繁的分配和释放的场景的。

slab 首先会向伙伴系统一次性申请一个或者多个物理内存页面,正是这些物理内存页组成了 slab 内存池。

随后 slab 内存池会将这些连续的物理内存页面划分成多个大小相同的小内存块出来,同一种 slab 内存池下,划分出来的小内存块尺寸是一样的。内核会针对不同尺寸的小内存分配需求,预先创建出多个 slab 内存池出来。

这种小内存在内核中的使用场景非常之多,比如,内核中那些经常使用,需要频繁申请释放的一些核心数据结构对象:task_struct 对象,mm_struct 对象,struct page 对象,struct file 对象,socket 对象等。

而创建这些内核核心数据结构对象以及为这些核心对象分配内存,销毁这些内核对象以及释放相关的内存是需要性能开销的。

这一点我们从《深入理解 Linux 伙伴系统》一文中详细介绍的内存分配与释放全链路过程中已经非常清楚的看到了,整个内存分配链路还是比较长的,如果遇到内存不足,还会涉及到内存的 swap 和 compact ,从而进一步产生更大的性能开销。

既然 slab 专门是用于小内存块分配与回收的,那么内核很自然的就会想到,分别为每一个需要被内核频繁创建和释放的核心对象创建一个专属的 slab 对象池,这些内核对象专属的 slab 对象池会根据其所管理的具体内核对象所占用内存的大小 size,将一个或者多个完整的物理内存页按照这个 size 划分出多个大小相同的小内存块出来,每个小内存块用于存储预先创建好的内核对象。

这样一来,当内核需要频繁分配和释放内核对象时,就可以直接从相应的 slab 对象池中申请和释放内核对象,避免了链路比较长的内存分配与释放过程,极大地提升了性能。

将内核中的核心数据结构对象,池化在 slab 对象池中,除了可以避免内核对象频繁反复初始化和相关内存分配,频繁反复销毁对象和相关内存释放的性能开销之外,其实还有很多好处,比如:

  1. 利用 CPU 高速缓存提高访问速度。当一个对象被直接释放回 slab 对象池中的时候,这个内核对象还是“热的”,仍然会驻留在 CPU 高速缓存中。如果这时,内核继续向 slab 对象池申请对象,slab 对象池会优先把这个刚刚释放 “热的” 对象分配给内核使用,因为对象很大概率仍然驻留在 CPU 高速缓存中,所以内核访问起来速度会更快。

  2. 伙伴系统只能分配 2 的次幂个完整的物理内存页,这会引起占用高速缓存以及 TLB 的空间较大,导致一些不重要的数据驻留在 CPU 高速缓存中占用宝贵的缓存空间,而重要的数据却被置换到内存中。 slab 对象池针对小内存分配场景,可以有效的避免这一点。

  3. 调用伙伴系统的操作会对 CPU 高速缓存 L1Cache 中的 Instruction Cache(指令高速缓存)和 Data Cache (数据高速缓存)有污染,因为对伙伴系统的长链路调用,相关的一些指令和数据必然会填充到  Instruction Cache 和 Data Cache 中,从而将频繁使用的一些指令和数据挤压出去,造成缓存污染。而在内核空间中越浪费这些缓存资源,那么在用户空间中的进程就会越少的得到这些缓存资源,造成性能的下降。 slab 对象池极大的减少了对伙伴系统的调用,防止了不必要的 L1Cache 污染。

  1. 使用 slab 对象池可以充分利用 CPU 高速缓存,避免多个对象对同一 cache line 的争用。如果对象直接存储排列在伙伴系统提供的内存页中的话(不受 slab 管理),那么位于不同内存页中具有相同偏移的对象很可能会被放入同一个 cache line 中,即使其他 cache line 还是空的。

slab, slub, slob 区分

在开始正式介绍 slab 对象池之前,笔者觉得有必要先向大家简单交代一下 Linux 系统中关于 slab 对象池的三种实现:slab,slub,slob。

其中 slab 的实现,最早是由 Sun 公司的 Jeff Bonwick 大神在 Solaris 2.4  系统中设计并实现的,由于 Jeff Bonwick 大神公开了 slab 的实现方法,因此被 Linux 所借鉴并于 1996 年在 Linux 2.0 版本中引入了 slab,用于 Linux 内核早期的小内存分配场景。

由于 slab 的实现非常复杂,slab 中拥有多种存储对象的队列,队列管理开销比较大,slab 元数据比较臃肿,对 NUMA 架构的支持臃肿繁杂(slab 引入时内核还没支持 NUMA),这样导致 slab 内部为了维护这些自身元数据管理结构就得花费大量的内存空间,这在配置有超大容量内存的服务器上,内存的浪费是非常可观的。

针对以上 slab 的不足,内核大神 Christoph Lameter 在 2.6.22 版本(2007 年发布)中引入了新的 slub 实现。slub 简化了 slab 一些复杂的设计,同时保留了 slab 的基本思想,摒弃了 slab 众多管理队列的概念,并针对多处理器,NUMA 架构进行优化,放弃了效果不太明显的 slab 着色机制。slub 与 slab 相比,提高了性能,吞吐量,并降低了内存的浪费。成为现在内核中常用的 slab 实现。

而 slob 的实现是在内核 2.6.16 版本(2006 年发布)引入的,它是专门为嵌入式小型机器小内存的场景设计的,所以实现上很精简,能在小型机器上提供很不错的性能。

而内核中关于内存池(小内存分配器)的相关 API 接口函数均是以 slab 命名的,但是我们可以通过配置的方式来平滑切换以上三种 slab 的实现。本文我们主要讨论被大规模运用在服务器 Linux 操作系统中的 slub 对象池的实现,所以本文下面的内容,如无特殊说明,笔者提到的 slab 均是指 slub 实现。

Slab 由以下几个关键组件组成:

  1. Slab:一个 Slab 是一个内存块,通常包含一组大小相同的对象。例如,如果你有一个内核中的 struct task_struct 类型对象,Slab 中就会存储多个 task_struct 对象。
  2. Cache:每种类型的对象(例如 struct task_struct)都有一个对应的缓存(cache)。缓存是一个管理这些对象的结构,包含了多个 Slab。每个 Slab 内存区域都存储相同类型的对象。
  3. Page:页面是内存的基本管理单元,通常是 4KB。Slab 分配器管理的内存页用来存放 Slab。一个页面可以包含一个或多个 Slab,具体数量取决于每个对象的大小。
  4. Object:对象是存储在 Slab 中的实际数据结构。每个对象都具有相同的大小,并且通常由 Slab 分配器管理。

从一个简单的内存页开始聊 slab

内核会把那些频繁使用的核心对象统一放在 slab 对象池中管理,每一个核心对象对应一个专属的 slab 对象池,以便提升核心对象的分配,访问,释放相关操作的性能。

如上图所示,slab 对象池在内存管理系统中的架构层次是基于伙伴系统之上构建的,slab 对象池会一次性向伙伴系统申请一个或者多个完整的物理内存页,在这些完整的内存页内在逐步划分出一小块一小块的内存块出来,而这些小内存块的尺寸就是 slab 对象池所管理的内核核心对象占用的内存大小。

下面笔者就带大家从一个最简单的物理内存页 page 开始,我们一步一步的推演 slab 的整个架构设计与实现。

如果让我们自己设计一个对象池,首先最直观最简单的办法就是先向伙伴系统申请一个内存页,然后按照需要被池化对象的尺寸 object size,把内存页划分为一个一个的内存块,每个内存块尺寸就是 object size。

事实上,slab 对象池可以根据情况向伙伴系统一次性申请多个内存页,这里只是为了方便大家理解,我们先以一个内存页为例,为大家说明 slab 中对象的内存布局。

但是在现实的对象池设计中,我们不能这么简单粗暴的搞,因为对象的 object size 可以是任意的,并不是内存对齐的,CPU 访问一块没有进行对齐的内存比访问对齐的内存速度要慢一倍。

因为 CPU 向内存读取数据的单位是根据 word size 来的,在 64 位处理器中 word size = 8 字节,所以 CPU 向内存读写数据的单位为 8 字节。CPU 只能一次性向内存访问按照 word size ( 8 字节) 对齐的内存地址,如果 CPU 访问一个未进行 word size 对齐的内存地址,就会经历两次访存操作。

比如,我们现在需要访问 0x0007 - 0x0014 这样一段没有对 word size 进行对齐的内存,CPU只能先从 0x0000 - 0x0007 读取 8 个字节出来先放入结果寄存器中并左移 7 个字节(目的是只获取 0x0007 ),然后 CPU 在从 0x0008 - 0x0015 读取 8 个字节出来放入临时寄存器中并右移1个字节(目的是获取 0x0008 - 0x0014 )最后与结果寄存器或运算。最终得到 0x0007 - 0x0014 地址段上的 8 个字节。

从上面过程我们可以看出,CPU 访问一段未进行 word size 对齐的内存,需要两次访存操作。

内存对齐的好处还有很多,比如,CPU 访问对齐的内存都是原子性的,对齐内存中的数据会独占 cache line ,不会与其他数据共享 cache line,避免 false sharing。

基于以上原因,我们不能简单的按照对象尺寸 object size 来划分内存块,而是需要考虑到对象内存地址要按照 word size 进行对齐。于是上面的 slab 对象池的内存布局又有了新的变化。

如果被池化对象的尺寸 object size 本来就是和 word size 对齐的,那么我们不需要做任何事情,但是如果 object size 没有和 word size 对齐,我们就需要填充一些字节,目的是要让对象的 object size 按照 word size 进行对齐,提高 CPU 访问对象的速度。

但是上面的这些工作对于一个工业级的对象池来说还远远不够,工业级的对象池需要应对很多复杂的诡异场景,比如,我们偶尔在复杂生产环境中会遇到的内存读写访问越界的情况,这会导致很多莫名其妙的异常。

内核为了应对内存读写越界的场景,于是在对象内存的周围插入了一段不可访问的内存区域,这些内存区域用特定的字节 0xbb 填充,当进程访问的到内存是 0xbb 时,表示已经越界访问了。这段内存区域在 slab 中的术语为 red zone,大家可以理解为红色警戒区域。

插入 red zone 之后,slab 对象池的内存布局近一步演进为下图所示的布局:

  • 如果对象尺寸 object size 本身就是 word size 对齐的,那么就需要在对象左右两侧填充两段 red zone 区域,red zone 区域的长度一般就是 word size 大小。

  • 如果对象尺寸 object size 是通过填充 padding 之后,才与 word size 对齐。内核会巧妙的利用对象右边的这段 padding 填充区域作为 red zone。只需要额外的在对象内存区域的左侧填充一段 red zone 即可。

在有了新的内存布局之后,我们接下来就要考虑一个问题,当我们向 slab 对象池获取到一个空闲对象之后,我们需要知道它的下一个空闲对象在哪里,这样方便我们下次获取对象。那么我们该如何将内存页 page 中的这些空闲对象串联起来呢?

有读者朋友可能会说了,这很简单啊,用一个链表把这些空闲对象串联起来不就行了嘛,其实内核也是这样想的,哈哈。不过内核巧妙的地方在于不需要为串联对象所用到的 next 指针额外的分配内存空间。

因为对象在 slab 中没有被分配出去使用的时候,其实对象所占的内存中存放什么,用户根本不会关心的。既然这样,内核干脆就把指向下一个空闲对象的 freepointer 指针直接存放在对象所占内存(object size)中,这样避免了为 freepointer 指针单独再分配内存空间。巧妙的利用了对象所在的内存空间(object size)。

我们接着对 slab 内存布局进行演化,有时候我们期望知道 slab 对象池中各个对象的状态,比如是否处于空闲状态。那么对象的状态我们在哪里存储呢?

答案还是和 freepointer 的处理方式一样,巧妙的利用对象所在的内存空间(object size)。内核会在对象所占的内存空间中填充一些特殊的字符用来表示对象的不同状态。因为反正对象没有被分配出去使用,内存里存的是什么都无所谓。

当 slab 刚刚从伙伴系统中申请出来,并初始化划分物理内存页中的对象内存空间时,内核会将对象的 object size 内存区域用特殊字节 0x6b 填充,并用 0xa5 填充对象 object size 内存区域的最后一个字节表示填充完毕。

或者当对象被释放回 slab 对象池中的时候,也会用这些字节填充对象的内存区域。

这种通过在对象内存区域填充特定字节表示对象的特殊状态的行为,在 slab 中有一个专门的术语叫做 SLAB_POISON (SLAB 中毒)。POISON 这个术语起的真的是只可意会不可言传,其实就是表示 slab 对象的一种状态。

是否毒化 slab 对象是可以设置的,当 slab 对象被 POISON 之后,那么会有一个问题,就是我们前边介绍的存放在对象内存区域 object size 里的 freepointer 就被会特殊字节 0x6b 覆盖掉。这种情况下,内核就只能为 freepointer 在额外分配一个 word size 大小的内存空间了。

slab 对象的内存布局信息除了以上内容之外,有时候我们还需要去跟踪一下对象的分配和释放相关信息,而这些信息也需要在 slab 对象中存储,内核中使用一个 struct track 结构体来存储跟踪信息。

这样一来,slab 对象的内存区域中就需要在开辟出两个 sizeof(struct track) 大小的区域出来,用来分别存储 slab 对象的分配和释放信息。

上图展示的就是 slab 对象在内存中的完整布局,其中 object size 为对象真正所需要的内存区域大小,而对象在 slab 中真实的内存占用大小 size 除了 object size 之外,还包括填充的 red zone 区域,以及用于跟踪对象分配和释放信息的 track 结构,另外,如果 slab 设置了 red zone,内核会在对象末尾增加一段 word size 大小的填充 padding 区域。

当 slab 向伙伴系统申请若干内存页之后,内核会按照这个 size 将内存页划分成一个一个的内存块,内存块大小为 size 。

其实 slab 的本质就是一个或者多个物理内存页 page,内核会根据上图展示的 slab 对象的内存布局,计算出对象的真实内存占用 size。最后根据这个 size 在 slab 背后依赖的这一个或者多个物理内存页 page 中划分出多个大小相同的内存块出来。

所以在内核中,都是用 struct page 结构来表示 slab,如果 slab 背后依赖的是多个物理内存页,那就使用在 《深入理解 Linux 伙伴系统》 一文中 "设置复合页 compound_page " 小节提到的复合页 compound_page 来表示。

struct page {      
    // 首页 page 中的 flags 会被设置为 PG_head 表示复合页的第一页
    unsigned long flags; 
    // 其余尾页会通过该字段指向首页
    unsigned long compound_head;   
    // 用于释放复合页的析构函数,保存在首页中
    unsigned char compound_dtor;
    // 该复合页有多少个 page 组成,order 还是分配阶的概念,在首页中保存
    // 本例中的 order = 2 表示由 4 个普通页组成
    unsigned char compound_order;
    // 该复合页被多少个进程使用,内存页反向映射的概念,首页中保存
    atomic_t compound_mapcount;
    // 复合页使用计数,首页中保存
    atomic_t compound_pincount;
}

slab 的具体信息也是在 struct page 中存储,下面笔者提取了 struct page 结构中和 slab 相关的字段:

struct page {

        struct {    /*  slub 相关字段 */
            union {
                // slab 所在的管理链表
                struct list_head slab_list;
                struct {    /* Partial pages */
                    // 用 next 指针在相应管理链表中串联起 slab
                    struct page *next;
#ifdef CONFIG_64BIT
                    // slab 所在管理链表中的包含的 slab 总数
                    int pages;  
                    // slab 所在管理链表中包含的对象总数
                    int pobjects; 
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            // 指向 slab cache,slab cache 就是真正的对象池结构,里边管理了多个 slab
            // 这多个 slab 被 slab cache 管理在了不同的链表上
            struct kmem_cache *slab_cache;
            // 指向 slab 中第一个空闲对象
            void *freelist;     /* first free object */
            union {
                struct {            /* SLUB */
                    // slab 中已经分配出去的独享
                    unsigned inuse:16;
                    // slab 中包含的对象总数
                    unsigned objects:15;
                    // 该 slab 是否在对应 slab cache 的本地 CPU 缓存中
                    // frozen = 1 表示缓存再本地 cpu 缓存中
                    unsigned frozen:1;
                };
            };
        };

}

在笔者当前所在的内核版本 5.4 中,内核是使用 struct page 来表示 slab 的,但是考虑到 struct page 结构已经非常庞大且复杂,为了减少 struct page 的内存占用以及提高可读性,内核在 5.17 版本中专门为 slab 引入了一个管理结构 struct slab,将原有 struct page 中 slab 相关的字段全部删除,转移到了 struct slab 结构中。这一点,大家只做了解即可。

slab 的总体架构设计

在上一小节的内容中,笔者带大家从 slab 的微观层面详细的介绍了 slab 对象的内存布局,首先 slab 会从伙伴系统中申请一个或多个物理内存页 page,然后根据 slab 对象的内存布局计算出对象在内存中的真实尺寸 size,并根据这个 size,在物理内存页中划分出多个内存块出来,供内核申请使用。

有了这个基础之后,在本小节中,笔者将继续带大家从 slab 的宏观层面上继续深入 slab 的架构设计。

笔者在前边的内容中多次提及的 slab 对象池其实就是上图中的 slab cache,而上小节中介绍的 slab 只是 slab cache 架构体系中的基本单位,对象的分配和释放最终会落在 slab 这个基本单位上。

如果一个 slab 中的对象全部分配出去了,slab cache 就会将其视为一个 full slab,表示这个 slab 此刻已经满了,无法在分配对象了。slab cache 就会到伙伴系统中重新申请一个 slab 出来,供后续的内存分配使用。

当内核将对象释放回其所属的 slab 之后,如果 slab 中的对象全部归位,slab cache 就会将其视为一个 empty slab,表示 slab 此刻变为了一个完全空闲的 slab。如果超过了 slab cache 中规定的 empty slab 的阈值,slab cache 就会将这些空闲的 empty slab 重新释放回伙伴系统中。

如果一个 slab 中的对象部分被分配出去使用,部分却未被分配仍然在 slab 中缓存,那么内核就会将该 slab 视为一个 partial slab。

这些不同状态的 slab,会在 slab cache 中被不同的链表所管理,同时 slab cache 会控制管理链表中 slab 的个数以及链表中所缓存的空闲对象个数,防止它们无限制的增长。

slab cache 中除了需要管理众多的 slab 之外,还包括了很多 slab 的基础信息。比如:

  • 上小节中提到的 slab 对象内存布局相关的信息
  • slab 中的对象需要按照什么方式进行内存对齐,比如,按照 CPU 硬件高速缓存行 cache line (64 字节) 进行对齐,slab 对象是否需要进行毒化 POISON,是否需要在 slab 对象内存周围插入 red zone,是否需要追踪 slab 对象的分配与回收信息,等等。
  • 一个 slab 具体到底需要多少个物理内存页 page,一个 slab 中具体能够容纳多少个 object (内存块)。

slab 的基础信息管理

slab cache 在内核中的数据结构为 struct kmem_cache,以上介绍的这些 slab 的基本信息以及 slab 的管理结构全部定义在该结构体中:

/* * Slab cache management. */
struct kmem_cache {
    // slab cache 的管理标志位,用于设置 slab 的一些特性
    // 比如:slab 中的对象按照什么方式对齐,对象是否需要 POISON  毒化,是否插入 red zone 在对象内存周围,是否追踪对象的分配和释放信息 等等
    slab_flags_t flags;
    // slab 对象在内存中的真实占用,包括为了内存对齐填充的字节数,red zone 等等
    unsigned int size;  /* The size of an object including metadata */
    // slab 中对象的实际大小,不包含填充的字节数
    unsigned int object_size;/* The size of an object without metadata */
    // slab 对象池中的对象在没有被分配之前,我们是不关心对象里边存储的内容的。
    // 内核巧妙的利用对象占用的内存空间存储下一个空闲对象的地址。
    // offset 表示用于存储下一个空闲对象指针的位置距离对象首地址的偏移
    unsigned int offset;    /* Free pointer offset */
    // 表示 cache 中的 slab 大小,包括 slab 所需要申请的页面个数,以及所包含的对象个数
    // 其中低 16 位表示一个 slab 中所包含的对象总数,高 16 位表示一个 slab 所占有的内存页个数。
    struct kmem_cache_order_objects oo;
    // slab 中所能包含对象以及内存页个数的最大值
    struct kmem_cache_order_objects max;
    // 当按照 oo 的尺寸为 slab 申请内存时,如果内存紧张,会采用 min 的尺寸为 slab 申请内存,可以容纳一个对象即可。
    struct kmem_cache_order_objects min;
    // 向伙伴系统申请内存时使用的内存分配标识
    gfp_t allocflags; 
    // slab cache 的引用计数,为 0 时就可以销毁并释放内存回伙伴系统重
    int refcount;   
    // 池化对象的构造函数,用于创建 slab 对象池中的对象
    void (*ctor)(void *);
    // 对象的 object_size 按照 word 字长对齐之后的大小
    unsigned int inuse;  
    // 对象按照指定的 align 进行对齐
    unsigned int align; 
    // slab cache 的名称, 也就是在 slabinfo 命令中 name 那一列
    const char *name;  
};

slab_flags_t flags 是 slab cache 的管理标志位,用于设置 slab 的一些特性,比如:
- 当 flags 设置了 SLAB_HWCACHE_ALIGN 时,表示 slab 中的对象需要按照 CPU 硬件高速缓存行 cache line (64 字节) 进行对齐。

  • 当 flags 设置了 SLAB_POISON 时,表示需要在 slab 对象内存中填充特殊字节  0x6b 和  0xa5,表示对象的特定状态。

  • 当 flags 设置了 SLAB_RED_ZONE 时,表示需要在 slab 对象内存周围插入 red zone,防止内存的读写越界。

  • 当 flags 设置了 SLAB_CACHE_DMA 或者 SLAB_CACHE_DMA32 时,表示指定 slab 中的内存来自于哪个内存区域,DMA or DMA32  区域 ?如果没有特殊指定,slab 中的内存一般来自于 NORMAL 直接映射区域。

  • 当 flags 设置了 SLAB_STORE_USER 时,表示需要追踪对象的分配和释放相关信息,这样会在 slab 对象内存区域中额外增加两个 sizeof(struct track) 大小的区域出来,用于存储 slab 对象的分配和释放信息。

相关 slab cache 的标志位 flag,定义在内核文件 /include/linux/slab.h 中:

/* DEBUG: Red zone objs in a cache */
#define SLAB_RED_ZONE  ((slab_flags_t __force)0x00000400U)
/* DEBUG: Poison objects */
#define SLAB_POISON  ((slab_flags_t __force)0x00000800U)
/* Align objs on cache lines */
#define SLAB_HWCACHE_ALIGN ((slab_flags_t __force)0x00002000U)
/* Use GFP_DMA memory */
#define SLAB_CACHE_DMA  ((slab_flags_t __force)0x00004000U)
/* Use GFP_DMA32 memory */
#define SLAB_CACHE_DMA32 ((slab_flags_t __force)0x00008000U)
/* DEBUG: Store the last owner for bug hunting */
#define SLAB_STORE_USER 

struct kmem_cache 结构中的 size 字段表示 slab 对象在内存中的真实占用大小,该大小包括对象所占内存中各种填充的内存区域大小,比如下图中的 red zone,track 区域,等等。

unsigned int object_size 表示单纯的存储 slab 对象所需要的实际内存大小,如上图中的 object size 蓝色区域所示。

在上小节我们介绍 freepointer 指针的时候提到过,当对象在 slab 中缓存并没有被分配出去之前,其实对象所占内存中存储的是什么,用户根本不会去关心。内核会巧妙的利用对象的内存空间来存储 freepointer 指针,用于指向 slab 中的下一个空闲对象。

但是当 kmem_cache 结构中的 flags 设置了 SLAB_POISON 标志位之后,slab 中的对象会 POISON 毒化,被特殊字节 0x6b 和 0xa5 所填充,这样一来就会覆盖原有的 freepointer,在这种情况下,内核就需要把 freepointer 存储在对象所在内存区域的外面。

所以内核就需要用一个字段来标识 freepointer 的位置,struct kmem_cache 结构中的 unsigned int offset 字段干的就是这个事情,它表示对象的 freepointer 指针距离对象的起始内存地址的偏移 offset。

上小节中,我们也提到过,slab 的本质其实就是一个或者多个物理内存页,slab 在内核中的结构也是用 struct page 来表示的,那么一个 slab 中到底包含多少个内存页 ? 这些内存页中到底能容纳多少个内存块(object)呢?

struct kmem_cache_order_objects oo 字段就是保存这些信息的,struct kmem_cache_order_objects 结构体其实就是一个无符号的整形字段,它的高 16 位用来存储 slab 所需的物理内存页个数,低 16 位用来存储 slab 所能容纳的对象总数。

struct kmem_cache_order_objects {
     // 高 16 为存储 slab 所需的内存页个数,低 16 为存储 slab 所能包含的对象总数
    unsigned int x;
};

struct kmem_cache_order_objects max 字段表示 oo 的最大值,内核在初始化 slab 的时候,会将 max 的值设置为 oo。

struct kmem_cache_order_objects min 字段表示 slab 中至少需要容纳的对象个数以及容纳最少的对象所需要的内存页个数。内核在初始化 slab 的时候会 将 min 的值设置为至少需要容纳一个对象。

内核在创建 slab 的时候,最开始会按照 oo 指定的尺寸来向伙伴系统申请内存页,如果内存紧张,申请内存失败。那么内核会降级采用 min 的尺寸再次向伙伴系统申请内存。也就是说 slab 中至少会包含一个对象。

gfp_t allocflags 是内核在向伙伴系统为 slab 申请内存页的时候,所用到的内存分配标志位。感兴趣的朋友可以回看下 《深入理解 Linux 伙伴系统》 一文中的 “ 规范物理内存分配行为的掩码 gfp_mask ” 小节中的内容,那里有非常详细的介绍。

unsigned int inuse 表示对象的 object size 按照 word size 对齐之后的大小,如果我们设置了SLAB_RED_ZONE,inuse 也会包括对象右侧 red zone 区域的大小。

unsigned int align 在创建 slab cache 的时候,我们可以向内核指定 slab 中的对象按照 align 的值进行对齐,内核会综合 word size , cache line ,align 计算出一个合理的对齐尺寸。

const char *name 表示该 slab cache 的名称,这里指定的 name 将会在 cat /proc/slabinfo 命令中显示,该命令用于查看系统中所有 slab cache 的信息。

在 cat /proc/slabinfo 命令显示的这些系统中所有的 slab cache,内核会将这些 slab cache 用一个双向链表统一串联起来。链表的头结点指针保存在 struct kmem_cache 结构的 list 中。

struct kmem_cache {
    // 用于组织串联系统中所有类型的 slab cache
    struct list_head list;  /* List of slab caches */
}

系统中所有的这些 slab cache 占用的内存总量,我们可以通过 cat /proc/meminfo 命令查看:

除此之外,我们还可以通过 slabtop 命令来动态查看系统中占用内存最高的 slab cache,当内存紧张的时候,如果我们通过  cat /proc/meminfo 命令发现 slab 的内存占用较高的话,那么可以快速通过 slabtop 迅速定位到究竟是哪一类的 object 分配过多导致内存占用飙升。

slab 的组织架构

在上小节的内容中,笔者主要为大家介绍了 struct kmem_cache 结构中关于 slab 的一些基础信息,其中主要包括 slab cache 中所管理的 slabs 相关的容量控制,以及 slab 中对象的内存布局信息。

那么 slab cache 中的这些 slabs 是如何被组织管理的呢 ?在本小节中,笔者将为大家揭开这个谜底。

内核在对 slab cache 的设计时充分考虑了多进程并发访问 slab cache 所带来的同步性能开销,内核在 slab cache 的设计中为每个 cpu 引入了 struct kmem_cache_cpu 结构的 percpu 变量,作为 slab cache 在每个 cpu 中的本地缓存。

/* * Slab cache management. */
struct kmem_cache {
    // 每个 cpu 拥有一个本地缓存,用于无锁化快速分配释放对象
    struct kmem_cache_cpu __percpu *cpu_slab;
}

这样一来,当进程需要向 slab cache 申请对应的内存块(object)时,首先会直接来到 kmem_cache_cpu 中查看 cpu 本地缓存的 slab,如果本地缓存的 slab 中有空闲对象,那么就直接返回了,整个过程完全没有加锁。而且访问路径特别短,防止了对 CPU 硬件高速缓存 L1Cache 中的 Instruction Cache(指令高速缓存)污染。

下面我们来看一下 slab cache 它的 cpu 本地缓存 kmem_cache_cpu 结构的详细设计细节:

struct kmem_cache_cpu {
    // 指向被 CPU 本地缓存的 slab 中第一个空闲的对象
    void **freelist;    /* Pointer to next available object */
    // 保证进程在 slab cache 中获取到的 cpu 本地缓存 kmem_cache_cpu 与当前执行进程的 cpu 是一致的。
    unsigned long tid;  /* Globally unique transaction id */
    // slab cache 中 CPU 本地所缓存的 slab,由于 slab 底层的存储结构是内存页 page
    // 所以这里直接用内存页 page 表示 slab
    struct page *page;  /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
    // cpu cache 缓存的备用 slab 列表,同样也是用 page 表示
    // 当被本地 cpu 缓存的 slab 中没有空闲对象时,内核会从 partial 列表中的 slab 中查找空闲对象
    struct page *partial;   /* Partially allocated frozen slabs */
#endif
#ifdef CONFIG_SLUB_STATS
    // 记录 slab 分配对象的一些状态信息
    unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};

从前面的介绍我们知道,slab 在内核中是用 struct page 结构来描述的,这里 struct kmem_cache_cpu 结构中的 page 指针指向的就是被 cpu 本地缓存的 slab。

freelist 指针指向的是该 slab 中第一个空闲的对象,在本文介绍 slab 对象内存布局的内容中,笔者提到过,为了充分利用 slab 对象所占用的内存,内核会在对象占用内存区域内开辟一块区域来存放 freepointer 指针,而 freepointer 可以用来指向下一个空闲对象。

这样一来,通过这里的 freelist 和 freepointer 就将 slab 中所有的空闲对象串联了起来。

事实上,在 struct page 结构中也有一个 freelist 指针,用于指向该内存页中第一个空闲对象。当 slab  被缓存进 kmem_cache_cpu 中之后,page 结构中的 freelist 会赋值给 kmem_cache_cpu->freelist,然后 page->freelist 会置空。page 的 frozen 状态设置为1,表示 slab 在本地 cpu 中缓存。

struct page {
    // 指向内存页中第一个空闲对象
    void *freelist;     /* first free object */
    // 该 slab 是否在对应 slab cache 的本地 CPU 缓存中
    // frozen = 1 表示缓存再本地 cpu 缓存中
    unsigned frozen:1;
}

kmem_cache_cpu 结构中的 tid 是内核为 slab cache 的 cpu 本地缓存结构设置的一个全局唯一的 transaction id ,这个 tid 在 slab cache 分配内存块的时候主要有两个作用:

  1. 内核会将 slab cache 每一次分配内存块或者释放内存块的过程视为一个事物,所以在每次向 slab cache 申请内存块或者将内存块释放回 slab cache 之后,内核都会改变这里的 tid。

  2. tid 也可以简单看做是 cpu 的一个编号,每个 cpu 的 tid 都不相同,可以用来标识区分不同 cpu 的本地缓存 kmem_cache_cpu 结构。

其中 tid 的第二个作用是最主要的,因为进程可能在执行的过程中被更高优先级的进程抢占 cpu (开启 CONFIG_PREEMPT 允许内核抢占)或者被中断,随后进程可能会被内核重新调度到其他 cpu 上执行,这样一来,进程在被抢占之前获取到的 kmem_cache_cpu 就与当前执行进程 cpu 的 kmem_cache_cpu 不一致了。

所以在内核中,我们经常会看到如下的代码片段,目的就是为了保证进程在 slab cache 中获取到的 cpu 本地缓存 kmem_cache_cpu 与当前执行进程的 cpu 是一致的。

    do {
        // 获取执行当前进程的 cpu 中的 tid 字段
        tid = this_cpu_read(s->cpu_slab->tid);
        // 获取 cpu 本地缓存 cpu_slab
        c = raw_cpu_ptr(s->cpu_slab);
        // 如果两者的 tid 字段不一致,说明进程已经被调度到其他 cpu 上了
        // 需要再次获取正确的 cpu 本地缓存
    } while (IS_ENABLED(CONFIG_PREEMPT) &&
         unlikely(tid != READ_ONCE(c->tid)));

如果开启了 CONFIG_SLUB_CPU_PARTIAL 配置项,那么在 slab cache 的 cpu 本地缓存 kmem_cache_cpu 结构中就会多出一个 partial 列表,partial 列表中存放的都是 partial slub,相当于是 cpu 缓存的备用选择.

当 kmem_cache_cpu->page (被本地 cpu 所缓存的 slab)中的对象已经全部分配出去之后,内核会到 partial 列表中查找一个 partial slab 出来,并从这个 partial slab 中分配一个对象出来,最后将 kmem_cache_cpu->page 指向这个 partial slab,作为新的 cpu 本地缓存 slab。这样一来,下次分配对象的时候,就可以直接从 cpu 本地缓存中获取了。

如果开启了 CONFIG_SLUB_STATS 配置项,内核就会记录一些关于 slab cache 的相关状态信息,这些信息同样也会在 cat /proc/slabinfo 命令中显示。

slab cache 的架构演变到现在,笔者已经为大家介绍了三种内核数据结构了,它们分别是:

  • slab cache 在内核中的数据结构 struct kmem_cache

  • slab cache 的本地 cpu 缓存结构 struct kmem_cache_cpu

  • slab 在内核中的数据结构 struct  page

现在我们把这种三种数据结构结合起来,得到下面这副 slab cache 的架构图:

但这还不是 slab cache 的最终架构,到目前为止我们的 slab cache 架构只演进到了一半,下面请大家继续跟随笔者的思路我们接着进行 slab cache 架构的演进。

我们先把 slab cache 比作一个大型超市,超市里摆放了一排一排的商品货架,毫无疑问,顾客进入超市直接从货架上选取自己想要的商品速度是最快的。

上图中的 kmem_cache 结构就好比是超市,slab cache 的本地 cpu 缓存结构 kmem_cache_cpu 就好比超市的营业厅,营业厅内摆满了一排一排的货架,这些货架就是上图中的 slab,货架上的商品就是 slab 中划分出来的一个一个的内存块。

毫无疑问,顾客来到超市,直接去营业厅的货架上拿取商品是最快的,那么如果货架上的商品卖完了,该怎么办呢?

这时,超市的经理就会到超市的仓库中重新拿取商品填充货架,那么 slab cache 的仓库到底在哪里呢?

答案就在笔者之前文章 《深入理解 Linux 物理内存管理》 中的 “非一致性内存访问 NUMA 架构 ” 小节中介绍的内存架构,在 NUMA 架构下,内存被划分成了一个一个的 NUMA 节点,每个 NUMA 节点内包含若干个 cpu。

每个 cpu 都可以任意访问所有 NUMA 节点中的内存,但是会有访问速度上的差异, cpu 在访问本地 NUMA 节点的速度是最快的,当本地 NUMA 节点中的内存不足时,cpu 会跨节点访问其他 NUMA 节点。

slab cache 的仓库就在 NUMA 节点中,而且在每一个 NUMA 节点中都有一个仓库,当 slab cache 本地 cpu 缓存 kmem_cache_cpu 中没有足够的内存块可供分配时,内核就会来到 NUMA 节点的仓库中拿出 slab 填充到 kmem_cache_cpu 中。

那么 slab cache 在 NUMA 节点的仓库中也没有足够的货物了,那该怎么办呢?这时,内核就会到伙伴系统中重新批量申请一批 slabs,填充到本地 cpu 缓存 kmem_cache_cpu 结构中。

伙伴系统就好比上面那个超市例子中的进货商,当超市经理发现仓库中也没有商品之后,就会联系进货商,从进货商那里批发商品,重新填充货架。

slab cache 的仓库在内核中采用 struct kmem_cache_node 结构来表示:

struct kmem_cache {
    // slab cache 中 numa node 中的缓存,每个 node 一个
    struct kmem_cache_node *node[MAX_NUMNODES];
}

/* * The slab lists for all objects. */
struct kmem_cache_node {
    spinlock_t list_lock;

    ....... 省略 slab 相关字段 ........

#ifdef CONFIG_SLUB
    // 该 node 节点中缓存的 slab 个数
    unsigned long nr_partial;
    // 该链表用于组织串联 node 节点中缓存的 slabs
    // partial 链表中缓存的 slab 为部分空闲的(slab 中的对象部分被分配出去)
    struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG // 开启 slab_debug 之后会用到的字段
    // slab 的个数
    atomic_long_t nr_slabs;
    // 该 node 节点中缓存的所有 slab 中包含的对象总和
    atomic_long_t total_objects;
    // full 链表中包含的 slab 全部是已经被分配完毕的 full slab
    struct list_head full;
#endif
#endif

};

这里笔者省略了 slab 实现相关的字段,我们只关注 slub 实现的部分,nr_partial 表示该 NUMA 节点缓存中缓存的 slab 总数。这些被缓存的 slabs 也是通过一个 partial 列表被串联管理起来。

如果我们配置了 CONFIG_SLUB_DEBUG 选项,那么 kmem_cache_node 结构中就会多出一些字段来存储更加丰富的信息。nr_slabs 表示 NUMA 节点缓存中 slabs 的总数,这里会包含 partial slub 和 full slab,这时,nr_partial 表示的是 partial slab 的个数,其中 full slab 会被串联在 full 列表上。total_objects 表示该 NUMA 节点缓存中缓存的对象的总数。

在介绍完 struct kmem_cache_node 结构之后,我们终于看到了 slab cache 的架构全貌,如下图所示:

上图中展示的 slab cache 本地 cpu 缓存 kmem_cache_cpu 中的 partial 列表以及 NUMA 节点缓存 kmem_cache_node 结构中的 partial 列表并不是无限制增长的,它们的容量收到下面两个参数的限制:

/* * Slab cache management. */
struct kmem_cache {

    // slab cache 在 numa node 中缓存的 slab 个数上限,slab 个数超过该值,空闲的 empty slab 则会被回收至伙伴系统
    unsigned long min_partial;

#ifdef CONFIG_SLUB_CPU_PARTIAL
    // 限定 slab cache 在每个 cpu 本地缓存 partial 链表中所有 slab 中空闲对象的总数
    // cpu 本地缓存 partial 链表中空闲对象的数量超过该值,则会将 cpu 本地缓存 partial 链表中的所有 slab 转移到 numa node 缓存中。
    unsigned int cpu_partial;
#endif

};

  • min_partial 主要控制 NUMA  节点缓存 partial 列表 slab 个数,如果超过该值,那么列表中空闲的 empty slab 就会被释放回伙伴系统中。

  • cpu_partial 主要控制 slab cache 本地 cpu 缓存 kmem_cache_cpu 结构 partial 链表中缓存的空闲对象总数,如果超过该值,那么 kmem_cache_cpu->partial 列表中缓存的 slab 将会被全部转移至 kmem_cache_node->partial 列表中。

现在 slab cache 的整个架构全貌已经展现在了我们面前,下面我们基于 slab cache 的整个架构,来看一下它是如何分配和释放内存的。

slab 内存分配原理

同伙伴系统的内存分配原理一样,slab cache 在分配内存块的时候同样也分为快速路径 fastpath 和慢速路径 slowpath,而且 slab cache 的组织架构比较复杂,所以在分配内存块的时候又会分为很多场景,在本小节中,笔者会为大家一一列举这些场景,并用图解的方式为大家阐述 slab cache 内存分配在不同场景下的逻辑。

从本地 cpu 缓存中直接分配

我们假设现在 slab cache 中的容量情况如上如图所示,slab cache 的本地 cpu 缓存中有一个 slab,slab 中有很多的空闲对象,kmem_cache_cpu->page 指向缓存的 slab,kmem_cache_cpu->freelist 指向缓存的 slab 中第一个空闲对象。

当内核向该 slab cache 申请对象的时候,首先会进入快速分配路径 fastpath,通过 kmem_cache_cpu->freelist 直接查看本地 cpu 缓存 kmem_cache_cpu->page 中是否有空闲对象可供分配。

如果有,则将 kmem_cache_cpu->freelist 指向的第一个空闲对象拿出来分配,随后调整 kmem_cache_cpu->freelist 指向下一个空闲对象。

从本地 cpu 缓存 partial 列表中分配

当 slab cache 本地 cpu 缓存的 slab (kmem_cache_cpu->page) 中没有任何空闲的对象时(全部被分配出去了),那么 slab cache 的内存分配就会进入慢速路径 slowpath。

内核会到本地 cpu 缓存的 partial 列表中去查看是否有一个 slab 可以分配对象。这里内核会从 partial 列表中的头结点开始遍历直到找到一个可以满足分配的 slab 出来。

随后内核会将该 slab 从 partial 列表中摘下,直接提升为新的本地 cpu 缓存。

这样一来 slab cache 的本地 cpu 缓存就被更新了,内核通过 kmem_cache_cpu->freelist 指针将缓存 slab 中的第一个空闲对象分配出去,随后更新 kmem_cache_cpu->freelist 指向 slab 中的下一个空闲对象。

从 NUMA 节点缓存中分配

随着时间的推移, slab cache 本地 cpu 缓存的 slab 中的对象被一个一个的分配出去,变成了一个 full slab,于此同时本地 cpu 缓存 partial 链表中的 slab 也被全部摘除完毕,此时是一个空的链表。

那么在这种情况下,slab cache 如何分配内存呢?根据前边 《slab 的组织架构》小节介绍的内容,此时 slab cache 就该从仓库中拿 slab 了,这个仓库就是上图中的 kmem_cache_node 结构中的 partial 链表。

内核会从 kmem_cache_node->partial 链表的头结点开始遍历,将遍历到的第一个非 full slab 从链表中摘下,直接提升为新的本地 cpu 缓存 kmem_cache_cpu->page, kmem_cache_cpu->freelist 指针重新指向该 slab 中第一个空闲独享。

随后内核会接着遍历 kmem_cache_node->partial 链表,将链表中的 slab 挨个摘下填充到本地 cpu 缓存 partial 链表中。最多只能填充 cpu_partial / 2 个 slab。这里的 cpu_partial 就是前边介绍的 struct kmem_cache 结构中的属性。

struct kmem_cache {
    // 限定 slab cache 在每个 cpu 本地缓存 partial 链表中缓存的所有 slab 中空闲对象的总数
    // cpu 本地缓存 partial 链表中空闲对象的数量超过该值,则会将 cpu 本地缓存 partial 链表中的所有 slab 转移到 numa node 缓存中。
    unsigned int cpu_partial;
}

这样一来,slab cache 就从仓库 kmem_cache_node->partial 链表中重新填充了本地 cpu 缓存 kmem_cache_cpu->page 以及 kmme_cache_cpu->partial 链表。

随后内核直接从本地 cpu 缓存中,通过 kmem_cache_cpu->freelist 指针将缓存 slab 中的第一个空闲对象分配出去,随后更新 kmem_cache_cpu->freelist 指向 slab 中的下一个空闲对象。

从伙伴系统中重新申请 slab

当 slab cache 的本地 cpu 缓存 kmem_cache_cpu->page 是空的,kmem_cache_cpu->partial 链表中也是空,NUMA 节点缓存 kmem_cache_node->partial 链表中也是空的时候,比如,slab cache 在刚刚被创建出来时,就是上图中的架构,完全是一个空的 slab cache。

这时,内核就需要到伙伴系统中重新申请一个 slab 出来,具体向伙伴系统申请多少内存页是由 struct kmem_cache 结构中的 oo 来决定的,它的高 16 位表示一个 slab 所需要的内存页个数,低 16 位表示 slab 中所包含的对象总数。

struct kmem_cache {
    // 表示 cache 中的 slab 大小,包括 slab 所申请的页面个数,以及所包含的对象个数
    // 其中低 16 位表示一个 slab 中所包含的对象总数,高 16 位表示一个 slab 所占有的内存页个数。
    struct kmem_cache_order_objects oo;

    // 当按照 oo 的尺寸为 slab 申请内存时,如果内存紧张,会采用 min 的尺寸为 slab 申请内存,可以容纳一个对象即可。
    struct kmem_cache_order_objects min;
}

当系统中空闲内存不足时,无法获得 oo 指定的内存页个数,那么内核会降级采用 min 指定的内存页个数,重新到伙伴系统中去申请。这些内容笔者已经在本文 《slab 的基础信息管理》小节中详细介绍过了,忘记的读者朋友可以在回顾一下。

当内核从伙伴系统中申请出指定的内存页个数之后,就会根据笔者在 《从一个简单的内存页开始聊 Slab》 小节中介绍的内容,初始化 slab ,最后将初始化好的 slab 直接提升为本地 cpu 缓存 kmem_cache_cpu->page 。

现在 slab cache 的本地 cpu 缓存被重新填充了,内核直接从本地 cpu 缓存中,通过 kmem_cache_cpu->freelist 指针将缓存 slab 中的第一个空闲对象分配出去,随后更新 kmem_cache_cpu->freelist 指向 slab 中的下一个空闲对象。

slab 内存释放原理

slab cache 的内存释放正好和内存分配的过程相反,但内存释放的过程会比内存分配的过程复杂一些,内存释放同样也包含快速路径 fastpath 和慢速路径 slowpath,也会分为很多场景,在本小节中,笔者继续用图解的方式为大家阐述 slab cache 在不同场景下的内存释放逻辑。

释放对象所属 slab 在 cpu 本地缓存中

如果将要释放回 slab cache 的对象所在的 slab 刚好是本地 cpu 缓存中缓存的 slab,那么内核直接会把对象释放回缓存的 slab 中,这个就是 slab cache 的快速内存释放路径 fastpath。

随后修正 kmem_cache_cpu->freelist 指针使其指向刚刚被释放的对象,释放对象的 freepointer 指针指向原来 kmem_cache_cpu->freelist 指向的对象。

释放对象所属 slab 在 cpu 本地缓存 partial 列表中

当释放的对象所属的 slab 在 cpu 本地缓存 kmem_cache_cpu->partial 链表中时,内核也是直接将对象释放回 slab 中,然后修改 slab (struct page)中的 freelist 指针指向刚刚被释放的对象。释放对象的 freepointer 指向其下一个空闲对象。

释放对象所属 slab 从 full slab 变为了 partial slab

本小节中介绍的释放场景是,当前释放对象所在的 slab 原来是一个 full slab,由于对象的释放刚好变成了一个 partial slab,并且该 slab 原来并不在 slab cache 的本地 cpu 缓存中。

这种情况下,当对象释放回 slab 之后,内核为了利用局部性的优势需要把该 slab 在插入到 slab cache 的本地 cpu 缓存 kmem_cache_cpu->partial 链表中。

因为 slab 之前之所以是一个 full slab,恰恰证明了该 slab 是一个非常活跃的 slab,常常供不应求导致变成了一个 full slab,当对象释放之后,刚好变成 partial slab,这时需要将这个被频繁访问的 slab 放入 cpu 缓存中,加快下次分配对象的速度。

以上内容只是 slab 被释放回  kmem_cache_cpu->partial 链表的正常流程,但是通过本文 《slab 的组织架构》小节最后的内容介绍我们知道,slab cache 的本地 cpu 缓存 kmem_cache_cpu->partial 链表中的容量不可能是无限制增长的,它受到 kmem_cache 结构中 cpu_partial 属性的限制:

struct kmem_cache {
    // 限定 slab cache 在每个 cpu 本地缓存 partial 链表中所有 slab 中空闲对象的总数
    // cpu 本地缓存 partial 链表中空闲对象的数量超过该值,则会将 cpu 本地缓存 partial 链表中的所有 slab 转移到 numa node 缓存中。
    unsigned int cpu_partial;
};

当每次向 kmem_cache_cpu->partial 链表中填充 slab 的时候,内核都需要首先检查当前 kmem_cache_cpu->partial 链表中所有 slabs 所包含的空闲对象总数是否超过了 cpu_partial 的限制。

如果没有超过限制,则将 slab 插入到 kmem_cache_cpu->partial 链表的头部,如果超过了限制,则需要首先将当前 kmem_cache_cpu->partial 链表中的所有 slab 转移至对应的 NUMA 节点缓存 kmem_cache_node->partial 链表的尾部,然后才能将释放对象所在的 slab 插入到 kmem_cache_cpu->partial 链表中。

大家读到这里,我想一定会有这样的一个疑问,就是内核这里为什么要把  kmem_cache_cpu->partial 链表中的 slab 一次性全部移动到 kmem_cache_node->partial 链表中呢?

这样一来如果在 slab cache 的本地 cpu 缓存不够的情况下,不是还要在大老远从 kmem_cache_node->partial 链表中再次转移 slab 填充 kmem_cache_cpu 吗?这样一来路径就拉长了,内核为啥要这样设计呢?

其实我们做任何设计都是要考虑当前场景的,当 slab cache 演进到如上图所示的架构时,说明内核当前所处的场景是一个内存释放频繁的场景,由于内存频繁的释放,所以导致 kmem_cache_cpu->partial 链表中的空闲对象都快被填满了,已经超过了 cpu_partial 的限制。

所以在内存频繁释放的场景下,kmem_cache_cpu->partial 链表太满了,而内存分配的请求又不是很多,kmem_cache_cpu 中缓存的 slab 并不会频繁的消耗。这样一来,就需要将链表中的所有 slab 一次性转移到 NUMA 节点缓存 partial 链表中备用。否则的话,就得频繁的转移 slab,这样性能消耗更大。

但是当前释放对象所在的 slab 仍然会被添加到 kmem_cache_cpu->partial 表中,用以应对不那么频繁的内存分配需求。

8.4 释放对象所属 slab 从 partial slab 变为了 empty slab

如果释放对象所属的 slab 原来是一个 partial slab,在对象释放之后变成了一个 empty slab,在这种情况下,内核将会把该 slab 插入到 slab cache 的备用仓库 NUMA 节点缓存中。

因为 slab 之所以会变成 empty slab,表明该 slab 并不是一个活跃的 slab,内核已经好久没有从该 slab 中分配对象了,所以只能把它释放回 kmem_cache_node->partial 链表中作为本地 cpu 缓存的后备选项。

但是 kmem_cache_node->partial 链表中的 slab 不可能是无限增长的,链表中缓存的 slab 个数受到 kmem_cache 结构中 min_partial 属性的限制:

struct kmem_cache {
    // slab cache 在 numa node 中缓存的 slab 个数上限,slab 个数超过该值,空闲的 empty slab 则会被回收至伙伴系统
    unsigned long min_partial;
}

所以内核在将 slab 插入到 kmem_cache_node->partial 链表之前,需要检查当前 kmem_cache_node->partial 链表中缓存的 slab 个数 nr_partial 是否已经超过了 min_partial 的限制。

struct kmem_cache_node {
    // 该 node 节点中缓存的 slab 个数
    unsigned long nr_partial;
}

如果超过了限制,则直接将 slab 释放回伙伴系统中,如果没有超过限制,才会将 slab 插入到 kmem_cache_node->partial 链表中。

还有一种直接释放回  kmem_cache_node->partial 链表的情形是,释放对象所属的 slab 本来就在 kmem_cache_node->partial 链表中,这种情况下就是直接释放对象回 slab 中,无需改变 slab 的位置。

slab allocator 整个体系的状态变迁

在了解了 slab allocator 的原理后,理应梳理一下其代码的实现。但是在查看代码之前,先了解一下slab allocator 整个体系的状态变迁过程,有助于理解代码流程的分析。
slab allocater 的整个状态变化通过一个联合体表示:

// slab allocator 整个体系的状态 slab_state。
enum slab_state {
    DOWN,           /* No slab functionality yet */
    PARTIAL,        /* SLUB: kmem_cache_node available */
    UP,         /* Slab caches usable but not all extras yet */
    FULL            /* Everything is working */
};

在内核没有启动的时候,也就是 slab allocator 体系完全没有建立的情况下,slab_state 的初始化状态就是 DOWN。

当内核启动的过程中,会开始创建初始化 slab allocator 体系,第一步就是为 struct kmem_cache_node 结构创建其专属的 slab cache —— kmem_cache_node 。后续再创建新的 slab cache 的时候,其中的 NUMA 节点缓存结构就是从 kmem_cache_node 里分配。

当 kmem_cache_node 专属的 slab cache 创建完毕之后, slab_state 的状态就变为了 PARTIAL。

slab allocator 体系建立的最后一项工作,就是创建 kmalloc 内存池体系,kmalloc 体系成功创建之后,slab_state 的状态就变为了 UP,其实现在 slab allocator 体系就可以正常运转了,但是还不是最终的理想状态。

当内核的初始化工作全部完成的时候,会在 arch_call_rest_init 函数中调用 do_initcalls(),开启内核的 initcall 阶段。

asmlinkage __visible void __init start_kernel(void)
{      
      ........ 省略 .........
      /* Do the rest non-__init'ed, we're now alive */ 
      arch_call_rest_init();
}

在内核的 initcall 阶段,会调用内核中定义的所有 initcall,而建立 slab allocator 体系的最后一项工作就为其在 sys 文件系统中创建 /sys/kernel/slab 目录节点,这里会存放系统中所有 slab cache 的详细运行信息。

这一项工作就封装在 slab_sysfs_init 函数中,而 slab_sysfs_init 在内核中被定义成了一个 __initcall 函数。

__initcall(slab_sysfs_init);

static int __init slab_sysfs_init(void)
{
    struct kmem_cache *s;
    int err;

    mutex_lock(&slab_mutex);

    slab_kset = kset_create_and_add("slab", &slab_uevent_ops, kernel_kobj);
    if (!slab_kset) {
        mutex_unlock(&slab_mutex);
        pr_err("Cannot register slab subsystem.\n");
        return -ENOSYS;
    }

    slab_state = FULL;

    ....... 省略 ......

}

当 /sys/kernel/slab 目录节点被创建之后,在 slab_sysfs_init 函数中会将 slab_state 变为 FULL。至此内核中的 slab allocator 整个体系就全部建立起来了。

源码查看 slab cache 的初始化

slab 初始化有两个重要的工作:
第一,创建用于申请struct kmem_cache和struct kmem_cache_node的kmem_cache;
第二,创建用于常规kmalloc的kmem_cache。

这里分析第一部分。内核启动的核心初始化逻辑封装 /init/main.c 文件的 start_kernel 函数中,在这里会初始化内核的各个子系统,内存管理子系统的初始化工作就在这里,封装在 mm_init 函数里。

在 mm_init 函数中 kmem_cache_init() 会初始化内核的 slab allocator 体系。

static void __init mm_init(void)
{
    page_ext_init_flatmem();
    report_meminit();
    mem_init();
    // 创建并初始化 slab allocator 体系
    kmem_cache_init();
    kmemleak_init();
    pgtable_init();
    debug_objects_mem_init();
    vmalloc_init();
    ioremap_huge_init();
    init_espfix_bsp();
    pti_init();
}

在 kmem_cache_init 函数中,内核首先会定义两个静态的 static __initdata struct kmem_cache 结构 boot_kmem_cache,boot_kmem_cache_node ,用于在内核初始化内存管理子系统的时候临时静态地创建 kmem_cache(slab cache)和 kmem_cache_node (slab cache)所需要的 struct kmem_cache 和 struct kmem_cache_node 结构。

当这两个临时的 boot_kmem_cache,boot_kmem_cache_node 被创建初始化之后,随后内核会通过 bootstrap 将这两个临时 slab cache 深拷贝到全局变量 kmem_cache(slab cache)和 kmem_cache_node (slab cache)中。

从此,内核就有了正式的 kmem_cache(slab cache)和 kmem_cache_node (slab cache),后续就可以按照正常流程动态地创建 slab cache 了,正常的创建流程就是笔者在本文后边几个小节中为大家介绍的内容。

下面我们来一起看下 slab allocator 体系的初始化过程:

kmem_cache_init

// 全局变量,用于专门管理 kmem_cache 对象的 slab cache
// 定义在文件:/mm/slab_common.c
struct kmem_cache *kmem_cache;

// 全局变量,用于专门管理 kmem_cache_node 对象的 slab cache
// 定义在文件:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

void __init kmem_cache_init(void)
{
    // slab allocator 体系结构中最核心的就是 kmem_cache 结构和 kmem_cache_node 结构,而这两个结构同时又被各自的 slab cache 所管理
    // 而现在 slab allocator 体系还未创建,所以需要利用两个静态的结构来创建kmem_cache,kmem_cache_node 对象
    // 这里就是定义两个 kmem_cache 类型的静态局部变量(静态结构):内核启动的时候被加载进 BSS 段中,随后会为其分配内存。
    // boot_kmem_cache 用于临时创建 kmem_cache 结构。
    // boot_kmem_cache_node 用于临时创建 kmem_cache_node 结构
    // boot_kmem_cache 和 boot_kmem_cache_node 现在只是两个空的结构,需要静态的进行初始化。
    static __initdata struct kmem_cache boot_kmem_cache,
        boot_kmem_cache_node;

    // 暂时先将这两个静态结构赋值给对应的全局变量,后面会初始化这两个全局变量
    kmem_cache_node = &boot_kmem_cache_node;
    kmem_cache = &boot_kmem_cache;

    // 静态地初始化 boot_kmem_cache_node 
    // 从这里可以看出 slab体系,建立的第一个 slab cache 就是 kmem_cache_node(slab cache)
    create_boot_cache(kmem_cache_node, "kmem_cache_node",
        sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN, 0, 0);

    // 当 kmem_cache_node (slab cache)被创建初始化之后,slab_state 变为 PARTIAL
    // 这个状态表示目前 kmem_cache_node cache已经创建完毕,可以利用它动态分配 kmem_cache_node 对象了。
    slab_state = PARTIAL;

    // 静态地初始化 boot_kmem_cache
    // 从这里可以看出 slab 体系,建立的第二个 slab cache 就是 kmem_cache(slab cache)
    create_boot_cache(kmem_cache, "kmem_cache",
            offsetof(struct kmem_cache, node) +
                nr_node_ids * sizeof(struct kmem_cache_node *),
               SLAB_HWCACHE_ALIGN, 0, 0);

    // 流程到这里,两个静态的 kmem_cache 结构:boot_kmem_cache,boot_kmem_cache_node 就已经初始化完毕了。
    // 但是这两个静态结构只是临时的,目的是为了在 slab 体系初始化阶段静态的创建 kmem_cache 对象和 kmem_cache_node 对象。
    // 在 bootstrap 中会将 boot_kmem_cache,boot_kmem_cache_node 中的内容深拷贝到最终的 kmem_cache(slab cache),kmem_cache_node(slab cache)中。
    // 后面我们就可以利用这两个最终的核心结构来动态的进行 slab 创建。
    kmem_cache = bootstrap(&boot_kmem_cache);
    kmem_cache_node = bootstrap(&boot_kmem_cache_node);

    ........ 省略 kmalloc 相关初始化过程 .........
}

初始化 slab allocator 体系的核心就是如何静态的创建和初始化这两个静态的 slab cache: boot_kmem_cache,boot_kmem_cache_node。

create_boot_cache

在 create_boot_cache 函数中,大家需要注意该函数第一个参数 struct kmem_cache *s,参数 s 指向的是上面两个临时的静态的 slab cache。现在是内核初始化阶段,当前系统中并不存在一个正式完整的 slab cache,这一点大家在阅读本小节的时候要时刻注意。

/* Create a cache during boot when no slab services are available yet */
void __init create_boot_cache(struct kmem_cache *s, const char *name,
        unsigned int size, slab_flags_t flags,
        unsigned int useroffset, unsigned int usersize)
{
    int err;
    unsigned int align = ARCH_KMALLOC_MINALIGN;

    // 下面就是静态初始化 kmem_cache 结构的逻辑
    // 挨个对 kmem_cache 结构的核心属性进行静态赋值
    s->name = name;
    s->size = s->object_size = size;

    if (is_power_of_2(size))
        align = max(align, size);
    // 根据指定的对齐参数 align 以及 CPU Cache line 的大小计算出一个合适的 align 出来
    s->align = calculate_alignment(flags, align, size);

    s->useroffset = useroffset;
    s->usersize = usersize;
    // 这里又来到了之前介绍的创建 slab cache 的创建流程
    // 该函数是创建 slab cache 的核心函数,这里会初始化 kmem_cache 结构中的其他重要属性
    // 以及创建初始化 slab cache 中的 cpu 本地缓存 和 node 节点缓存结构
    err = __kmem_cache_create(s, flags);
    // 暂时不需要合并 merge,引用计数设置为 -1
    s->refcount = -1; 
}

这里在对静态 kmem_cache 结构进行简单初始化之后,内核又调用了 __kmem_cache_create 函数。

__kmem_cache_create

__kmem_cache_create 函数的主要工作就是建立 slab cache 的基本骨架,包括初始化 kmem_cache 结构中的其他重要属性,创建初始化本地 cpu 缓存结构以及 NUMA 节点缓存结构,这一部分的重要工作封装在 kmem_cache_open 函数中完成。

随后会检查内核 slab allocator 整个体系的状态,只有 slab_state = FULL 的状态才表示整个 slab allocator 体系已经在内核中建立并初始化完成了,可以正常运转了。这里是初始化的流程,所以 slab_state 为 DOWN,代码在 kmem_cache_open 函数结束后直接返回。如果是正常的 slab 创建流程,slab_state 为 FULL,通过 slab allocator 的状态检查之后,就是 slab cache 整个创建过程的最后一步,利用 sysfs_slab_add 为其在 sys 文件系统中创建 /sys/kernel/slab/name 目录,该目录下的文件详细记录了 slab cache 运行时的各种信息。

int __kmem_cache_create(struct kmem_cache *s, slab_flags_t flags)
{
    int err;
    // 核心函数,在这里会初始化 kmem_cache 的其他重要属性
    err = kmem_cache_open(s, flags);
    if (err)
        return err;

    // 检查内核中 slab 分配器的整体体系是否已经初始化完毕,只有状态是 FULL 的时候才是初始化完毕,其他的状态表示未初始化完毕。
    // 在 slab  allocator 体系初始化的时候在 slab_sysfs_init 函数中将 slab_state 设置为 FULL
    if (slab_state <= UP)
        return 0;
    // 在 sys 文件系统中创建 /sys/kernel/slab/name 节点,该目录下的文件包含了对应 slab cache 运行时的详细信息
    err = sysfs_slab_add(s);
    if (err)
        // 出现错误则释放 kmem_cache 结构
        __kmem_cache_release(s);

    return err;
}

kmem_cache_open

kmem_cache_open 是初始化 slab cache 内核数据结构 kmem_cache 的核心函数,在这里会初始化 kmem_cache 结构中的一些重要核心参数,以及为 slab cache 创建初始化本地 cpu 缓存结构 kmem_cache_cpu 和 NUMA 节点缓存结构 kmem_cache_node。

static int kmem_cache_open(struct kmem_cache *s, slab_flags_t flags)
{
    // 计算 slab 中对象的整体内存布局所需要的 size
    // slab 所需最合适的内存页面大小 order,slab 中所能容纳的对象个数
    // 初始化 slab cache 中的核心参数 oo ,min,max的值
    if (!calculate_sizes(s, -1))
        goto error;

    // 设置 slab cache 在 node 缓存  kmem_cache_node 中的 partial 列表中 slab 的最小个数 min_partial
    set_min_partial(s, ilog2(s->size) / 2);
    // 设置 slab cache 在 cpu 本地缓存的 partial 列表中所能容纳的最大空闲对象个数
    set_cpu_partial(s);

    // 为 slab cache 创建并初始化 node cache 数组
    if (!init_kmem_cache_nodes(s))
        goto error;
    // 为 slab cache 创建并初始化 cpu 本地缓存列表
    if (alloc_kmem_cache_cpus(s))
        return 0;
error:
    return -EINVAL;
}

calculate_sizes 函数中封装了 slab 对象内存布局的全部逻辑,除了确定 slab 对象的内存布局之外,calculate_sizes 函数还会初始化 kmem_cache 的其他核心参数:

struct kmem_cache {
    // slab 中管理的对象大小,注意:这里包含对象为了对齐所填充的字节数
    unsigned int size;  /* The size of an object including metadata */
    // slab 对象池中的对象在没有被分配之前,我们是不关心对象里边存储的内容的。
    // 内核巧妙的利用对象占用的内存空间存储下一个空闲对象的地址。
    // offset 表示用于存储下一个空闲对象指针的位置距离对象首地址的偏移
    unsigned int offset;    /* Free pointer offset */
    // 表示 cache 中的 slab 大小,包括 slab 所申请的页面个数,以及所包含的对象个数
    // 其中低 16 位表示一个 slab 中所包含的对象总数,高 16 位表示一个 slab 所占有的内存页个数。
    struct kmem_cache_order_objects oo;
    // slab 中所能包含对象以及内存页个数的最大值
    struct kmem_cache_order_objects max;
    // 当按照 oo 的尺寸为 slab 申请内存时,如果内存紧张,会采用 min 的尺寸为 slab 申请内存,可以容纳一个对象即可。
    struct kmem_cache_order_objects min;
}

在完成了对 kmem_cache 结构的核心属性初始化工作之后,内核紧接着会调用 set_min_partial 来设置 kmem_cache->min_partial,该函数的主要目的是为了计算 slab cache 在 NUMA 节点缓存 kmem_cache_node->partial 链表中的 slab 个数上限,超过该值,空闲的 empty slab 则会被回收至伙伴系统中。

struct kmem_cache {
    // slab cache 在 numa node 中缓存的 slab 个数上限,slab 个数超过该值,空闲的 empty slab 则会被回收至伙伴系统
    unsigned long min_partial;
     // 限定 slab cache 在每个 cpu 本地缓存 partial 链表中所有 slab 中空闲对象的总数
    // cpu 本地缓存 partial 链表中空闲对象的数量超过该值,则会将 cpu 本地缓存 partial 链表中的所有 slab 转移到 numa node 缓存中。
    unsigned int cpu_partial;
}

kmem_cache 结构中的 min_partial 初始值为 min = ilog2(s->size) / 2,需要保证 min_partial 的值在 [5,10] 的范围之内。

#define MIN_PARTIAL 5
#define MAX_PARTIAL 10

// 计算 slab cache 在 node 中缓存的个数,kmem_cache_node 中 partial 列表中 slab 个数的上限 min_partial
// 超过该值,空闲的 slab 就会被回收
// 初始 min = ilog2(s->size) / 2,必须保证 min_partial 的值 在 [MIN_PARTIAL,MAX_PARTIAL] 之间
static void set_min_partial(struct kmem_cache *s, unsigned long min)
{
    if (min < MIN_PARTIAL)
        min = MIN_PARTIAL;
    else if (min > MAX_PARTIAL)
        min = MAX_PARTIAL;
    s->min_partial = min;
}

调用 set_cpu_partial 来设置 kmem_cache->cpu_partial,从而限制 slab cache 在 cpu 本地缓存 partial 链表中空闲对象个数的上限。

同时该值也决定了当 kmem_cache_cpu->partial 链表为空时,内核会从 kmem_cache_node->partial 链表填充 cpu_partial / 2 个 slab 到 kmem_cache_cpu->partial 链表中。

set_cpu_partial 函数的逻辑也很简单,就是根据 slab 对象 size 大小来决定 cpu_partial 的值。

static void set_cpu_partial(struct kmem_cache *s)
{
// 当配置了 CONFIG_SLUB_CPU_PARTIAL,则 slab cache 的 cpu 本地缓存 kmem_cache_cpu 中包含 partial 列表
#ifdef CONFIG_SLUB_CPU_PARTIAL
    // 判断 kmem_cache_cpu 是否包含有 partial 列表
    if (!kmem_cache_has_cpu_partial(s))
        s->cpu_partial = 0;
    else if (s->size >= PAGE_SIZE)
        s->cpu_partial = 2;
    else if (s->size >= 1024)
        s->cpu_partial = 6;
    else if (s->size >= 256)
        s->cpu_partial = 13;
    else
        s->cpu_partial = 30;
#endif
}

最后调用 init_kmem_cache_nodes 函数为 slab cache 在每个 NUMA 节点中创建其所属的缓存结构 kmem_cache_node。

调用 alloc_kmem_cache_cpus 函数为 slab cache 创建每个 cpu 的本地缓存结构 kmem_cache_cpu。

calculate_sizes

下面解析一下 slab 的具体规划过程 calculate_sizes 函数。

static int calculate_sizes(struct kmem_cache *s, int forced_order)
{
    slab_flags_t flags = s->flags;
    unsigned int size = s->object_size;
    unsigned int order;

    // 为了提高 cpu 访问对象的速度,slab 对象的 object size 首先需要与 word size 进行对齐
    size = ALIGN(size, sizeof(void *));

#ifdef CONFIG_SLUB_DEBUG
    // SLAB_POISON:对象中毒标识,是 slab 中的一个术语,用于将对象所占内存填充某些特定的值,表示这块对象不同的使用状态,防止非法越界访问。
    // 比如:在将对象分配出去之前,会将对象所占内存用 0x6b 填充,并用 0xa5 填充 object size 区域的最后一个字节。
    // SLAB_TYPESAFE_BY_RCU:启用 RCU 锁释放 slab
    if ((flags & SLAB_POISON) && !(flags & SLAB_TYPESAFE_BY_RCU) &&
            !s->ctor)
        s->flags |= __OBJECT_POISON;
    else
        s->flags &= ~__OBJECT_POISON;

    // SLAB_RED_ZONE:表示在空闲对象前后插入 red zone 红色区域(填充特定字节 0xbb),防止对象溢出越界
    // size == s->object_size 表示对象 object size 与 word size 本来就是对齐的,并没有填充任何字节
    // 这时就需要在对象 object size 内存区域的后面插入一段 word size 大小的 red zone。
    // 如果对象 object size 与 word size 不是对齐的,填充了字节,那么这段填充的字节恰好可以作为右侧 red zone,而不需要额外分配 red zone 空间
    if ((flags & SLAB_RED_ZONE) && size == s->object_size)
        size += sizeof(void *);
#endif

    // inuse 表示 slab 中的对象实际使用的内存区域大小
    // 该值是经过与 word size 对齐之后的大小,如果设置了 SLAB_RED_ZONE,则也包括红色区域大小
    s->inuse = size;

    if (((flags & (SLAB_TYPESAFE_BY_RCU | SLAB_POISON)) ||
        s->ctor)) {
        // 如果我们开启了 RCU 保护或者设置了对象 poison或者设置了对象的构造函数
        // 这些都会占用对象中的内存空间。这种情况下,我们需要额外增加一个 word size 大小的空间来存放 free pointer,否则 free pointer 存储在对象的起始位置
        // offset 为 free pointer 与对象起始地址的偏移
        s->offset = size;
        size += sizeof(void *);
    }

#ifdef CONFIG_SLUB_DEBUG
    if (flags & SLAB_STORE_USER)
        // SLAB_STORE_USER 表示需要跟踪对象的分配和释放信息
        // 需要再对象的末尾增加两个 struct track 结构,存储分配和释放的信息
        size += 2 * sizeof(struct track);

#ifdef CONFIG_SLUB_DEBUG
    if (flags & SLAB_RED_ZONE) {
        // 在对象内存区域的左侧增加 red zone,大小为 red_left_pad
        // 防止对这块对象内存的写越界
        size += sizeof(void *);
        s->red_left_pad = sizeof(void *);
        s->red_left_pad = ALIGN(s->red_left_pad, s->align);
        size += s->red_left_pad;
    }
#endif

    // slab 从它所申请的内存页 offset 0 开始,一个接一个的存储对象
    // 调整对象的 size 保证对象之间按照指定的对齐方式 align 进行对齐
    size = ALIGN(size, s->align);
    s->size = size;
    // 这里 forced_order 传入的是 -1
    if (forced_order >= 0)
        order = forced_order;
    else
        // 计算 slab 所需要申请的内存页数(2 ^ order 个内存页)
        order = calculate_order(size);

    if ((int)order < 0)
        return 0;
    // 根据 slab 的 flag 设置,设置向伙伴系统申请内存时使用的 allocflags
    s->allocflags = 0;
    if (order)
        // slab 所需要的内存页多于 1 页时,则向伙伴系统申请复合页。
        s->allocflags |= __GFP_COMP;

    // 从 DMA 区域中获取适用于 DMA 的内存页
    if (s->flags & SLAB_CACHE_DMA)
        s->allocflags |= GFP_DMA;
    // 从 DMA32 区域中获取适用于 DMA 的内存页
    if (s->flags & SLAB_CACHE_DMA32)
        s->allocflags |= GFP_DMA32;
    // 申请可回收的内存页
    if (s->flags & SLAB_RECLAIM_ACCOUNT)
        s->allocflags |= __GFP_RECLAIMABLE;

    // 计算 slab cache 中的 oo,min,max 值
    // 一个 slab 到底需要多少个内存页,能够存储多少个对象
    // 低 16 为存储 slab 所能包含的对象总数,高 16 为存储 slab 所需的内存页个数
    s->oo = oo_make(order, size);
    // get_order 函数计算出的 order 为容纳一个 size 大小的对象至少需要的内存页个数
    s->min = oo_make(get_order(size), size);
    if (oo_objects(s->oo) > oo_objects(s->max))
        // 初始时 max 和 oo 相等
        s->max = s->oo;
    // 返回 slab 中所能容纳的对象个数
    return !!oo_objects(s->oo);
}

在内核对 slab 对象开始内存布局之前,为了提高 cpu 访问对象的速度,首先需要将 slab 对象的 object size 与 word size 进行对齐。如果 object size 与 word size 本来就是对齐的,那么内核不会做任何事情。如果不是对齐的,那么就需要在对象后面填充一些字节,达到与 word size 对齐的目的。

size = ALIGN(size, sizeof(void *));

如果我们设置了 SLAB_RED_ZONE,表示需要再对象 object size 内存区域前后各插入一段 red zone 区域,目的是为了防止内存的读写越界。

如果对象 object size 与 word size 本来就是对齐的,并没有填充任何字节:size == s->object_size,那么此时就需要在对象 object size 内存区域的后面插入一段 word size 大小的 red zone。

 if ((flags & SLAB_RED_ZONE) && size == s->object_size)
        size += sizeof(void *);

如果对象 object size 与 word size 不是对齐的,那么内核就会在 object size 区域后面填充字节达到与 word size 对齐的目的,而这段填充的字节恰好可以作为对象右侧 red zone ,而不需要额外为右侧 red zone 分配内存空间。

如果我们设置了 SLAB_POISON 或者开启了 RCU 或者设置了对象的构造函数,它们都会占用对象的实际内存区域 object size。 slab 对象的 object size 内存区域会被内核用特殊字符 0x6b 填充,并用 0xa5 填充对象 object size 内存区域的最后一个字节表示填充完毕。

这样一来,用于指向下一个空闲对象的 freepointer 就没地方存放了,所以需要在当前对象内存区域的基础上再额外开辟一段 word size 大小的内存区域专门存放 freepointer。

    if (((flags & (SLAB_TYPESAFE_BY_RCU | SLAB_POISON)) ||
        s->ctor)) {
        // offset 为 free pointer 与对象起始地址的偏移
        s->offset = size;
        size += sizeof(void *);
    }

如果我们设置了 SLAB_STORE_USER,表示我们期望跟踪 slab 对象的分配与释放相关的信息,而这些跟踪信息内核使用一个 struct track 结构来存储。

所以在这种情况下,内核需要在目前 slab 对象的内存区域后面额外增加两个 sizeof(struct track) 大小的区域出来,用来分别存储 slab 对象的分配和释放信息。

如果我们设置了 SLAB_RED_ZONE,最后,还需要再 slab 对象内存区域的左侧填充一段 red_left_pad 大小的内存区域作为左侧 red zone。另外还需要再 slab 对象内存区域的末尾再次填充一段 word size 大小的内存区域作为 padding 部分。

   if (flags & SLAB_RED_ZONE) {
        size += sizeof(void *);
        s->red_left_pad = sizeof(void *);
        s->red_left_pad = ALIGN(s->red_left_pad, s->align);
        size += s->red_left_pad;
    }

现在关于 slab 对象内存布局的全部内容,我们就介绍完了,最终我们得到了 slab 对象真实占用内存大小 size,内核会根据这个 size,在物理内存页中划分出一个一个的对象出来。

那么一个 slab 到底需要多少个物理内存页呢?内核会通过 calculate_order 函数根据一定的算法计算出一个合理的 order 值。这个过程笔者后面会细讲,现在我们主要关心整体流程。

slab 所需的物理内存页个数计算出来之后,内核会根据 slab 对象占用内存的大小 size,计算出一个 slab 可以容纳的对象个数。并将这个结果保存在 kmem_cache 结构中的 oo 属性中。

s->oo = oo_make(order, size);

内核会通过 struct kmem_cache_order_objects 这样一个结构来保存 slab 所需要的物理内存页个数以及 slab 所能容纳的对象个数,其中 kmem_cache_order_objects 的高 16 位保存 slab 所需要的物理内存页个数,低 16 位保存 slab 所能容纳的对象个数。

struct kmem_cache {
    // 表示 cache 中的 slab 大小,包括 slab 所申请的页面个数,以及所包含的对象个数
    // 其中低 16 位表示一个 slab 中所包含的对象总数,高 16 位表示一个 slab 所占有的内存页个数。
    struct kmem_cache_order_objects oo;
}
#define OO_SHIFT    16

struct kmem_cache_order_objects {
     // 高 16 为存储 slab 所需的内存页个数,低 16 为存储 slab 所能包含的对象总数
    unsigned int x;
};

static inline struct kmem_cache_order_objects oo_make(unsigned int order,
        unsigned int size)
{
    struct kmem_cache_order_objects x = {
        // 高 16 为存储 slab 所需的内存页个数,低 16 为存储 slab 所能包含的对象总数
        (order << OO_SHIFT) + order_objects(order, size)
    };

    return x;
}

static inline unsigned int order_objects(unsigned int order, unsigned int size)
{
    // 根据 slab 中包含的物理内存页个数以及对象的 size,计算 slab 可容纳的对象个数
    return ((unsigned int)PAGE_SIZE << order) / size;
}

static inline unsigned int oo_order(struct kmem_cache_order_objects x)
{
    // 获取高 16 位,slab 中所需要的内存页 order
    return x.x >> OO_SHIFT;
}

// 十进制为:65535,二进制为:16 个 1,用于截取低 16 位
#define OO_MASK     ((1 << OO_SHIFT) - 1) 

static inline unsigned int oo_objects(struct kmem_cache_order_objects x)
{
    // 获取低 16 位,slab 中能容纳的对象个数
    return x.x & OO_MASK;
}

随后内核会通过 get_order 函数来计算,容纳一个 size 大小的对象所需要的最少物理内存页个数。用这个值作为 kmem_cache 结构中的 min 属性

s->min = oo_make(get_order(size), size);
struct kmem_cache {
 struct kmem_cache_order_objects min;
}

内核在创建 slab 的时候,最开始会按照 oo 指定的尺寸来向伙伴系统申请内存页,如果内存紧张,申请内存失败。那么内核会降级采用 min 的尺寸再次向伙伴系统申请内存。也就是说 slab 中至少会包含一个对象。

最后会设置 max 的值,从源码中我们可以看到 max 的值与 oo 的值是相等的

  if (oo_objects(s->oo) > oo_objects(s->max))
        // 初始时 max 和 oo 相等
        s->max = s->oo;

calculate_order

一个 slab 究竟需要多少个物理内存页就是在这里计算出来的,这里内核会根据一定的算法,尽量保证 slab 中的内存碎片最小化,综合计算出一个合理的 order 值。下面我们来一起看下这个计算逻辑:

static unsigned int slub_min_order;
static unsigned int slub_max_order = PAGE_ALLOC_COSTLY_ORDER;// 3
static unsigned int slub_min_objects;

static inline int calculate_order(unsigned int size)
{
    unsigned int order;
    unsigned int min_objects;
    unsigned int max_objects;

    // 计算 slab 中可以容纳的最小对象个数
    min_objects = slub_min_objects;
    if (!min_objects)
        // nr_cpu_ids 表示当前系统中的 cpu 个数
        // fls 可以获取参数的最高有效 bit 的位数,比如 fls(0)=0,fls(1)=1,fls(4) = 3
        // 如果当前系统中有4个cpu,那么 min_object 的初始值为 4*(3+1) = 16 
        min_objects = 4 * (fls(nr_cpu_ids) + 1);
    // slab 最大内存页 order 初始为 3,计算 slab 最大可容纳的对象个数
    max_objects = order_objects(slub_max_order, size);
    min_objects = min(min_objects, max_objects);

    while (min_objects > 1) {
        // slab 中的碎片控制系数,碎片大小不能超过 (slab所占内存大小 / fraction)
        // fraction 值越大,slab 中所能容忍的碎片就越小
        unsigned int fraction;

        fraction = 16;
        while (fraction >= 4) {
            // 根据当前 fraction 计算 order,需要查找出能够使 slab 产生碎片最小化的 order 值出来
            order = slab_order(size, min_objects,
                    slub_max_order, fraction);
             // order 不能超过 max_order,否则需要降低 fraction,放宽对碎片的要求限制,重新循环计算
            if (order <= slub_max_order)
                return order;
            fraction /= 2;
        }
        // 进一步放宽对 min_object 的要求,slab 会尝试少放一些对象
        min_objects--;
    }

    // 经过前边 while 循环的计算,我们无法在这一个 slab 中放置多个 size 大小的对象,因为 min_object = 1 的时候就退出循环了。
    // 那么下面就会尝试看能不能只放入一个对象
    order = slab_order(size, 1, slub_max_order, 1);
    if (order <= slub_max_order)
        return order;
    // 流程到这里表示,我们要池化的对象 size 太大了,slub_max_order 都放不下
    // 现在只能放宽对 max_order 的限制到 MAX_ORDER = 11
    order = slab_order(size, 1, MAX_ORDER, 1);
    if (order < MAX_ORDER)
        return order;
    return -ENOSYS;
}

首先内核会计算出 slab 需要容纳对象的最小个数 min_objects,计算公式: min_objects = 4 * (fls(nr_cpu_ids) + 1):

  1. nr_cpu_ids 表示当前系统中的 cpu 个数

  2. fls 获取参数二进制形式的最高有效 bit 的位数,比如 fls(0)=0,fls(1)=1,fls(4) = 3

这里我们看到 min_objects 是和当前系统中的 cpu 个数有关系的。

内核规定 slab 所需要的物理内存页个数的最大值 slub_max_order 初始化为 3,也就是 slab 最多只能向伙伴系统中申请 8 个内存页。

根据这里的 slub_max_order 和 slab 对象的 size 通过 order_objects 函数计算出 slab 所能容纳对象的最大值。

slab 所能容纳的对象个数越多,那么所需要的物理内存页就越多,slab 所能容纳的对象个数越少,那么所需要的物理内存页就越少。

内核通过刚刚计算出的 min_objects 可以计算出 slab 所需要的最小内存页个数,我们暂时称为 min_order。

随后内核会遍历 min_order 与 slub_max_order 之间的所有 order 值,直到找到满足内存碎片限制要求的一个 order。

那么内核对于内存碎片限制的要求具体如何定义呢?

内核会定义一个 fraction 变量作为 slab 内存碎片的控制系数,内核要求 slab 中内存碎片大小不能超过 (slab所占内存大小 / fraction),fraction 的值越大,表示 slab 中所能容忍的内存碎片就越小。fraction 的初始值为 16。

在内核寻找最佳合适 order 的过程中,最高优先级是要将内存碎片控制在一个非常低的范围内,在这个基础之上,遍历 min_order 与 slub_max_order 之间的所有 order 值,看他们产生碎片的大小是否低于 (slab所占内存大小 / fraction) 的要求。如果满足,那么这个 order 就是最终的计算结果,后续 slab 会根据这个 order 值向伙伴系统申请物理内存页。这个逻辑封装在 slab_order 函数中。

如果内核遍历完一遍 min_order 与 slub_max_order 之间的所有 order 值均不符合内存碎片限制的要求,那么内核只能尝试放宽对内存碎片的要求,将 fraction 调小一些——fraction /= 2 ,再次重新遍历所有 order。但 fraction 系数最低不能低于 4。

如果 fraction 系数低于 4 了,说明内核已经将碎片限制要求放到最宽了,在这么宽松的条件下依然无法找到一个满足限制要求的 order 值,那么内核会在近一步的降级,放宽对 min_objects 的要求——min_objects--,尝试在 slab 中少放一些对象。fraction 系数恢复为 16,在重新遍历,尝试查找符合内存碎片限制要求的 order 值。

最极端的情况就是,无论内核怎么放宽对内存碎片的限制,无论怎么放宽 slab 中容纳对象的最小个数要求,内核始终无法找到一个 order 值能够满足如此宽松的内存碎片限制条件。当 min_objects == 1 的时候就会退出 while (min_objects > 1) 循环停止寻找。

最终内核的托底方案是将 min_objects 调整为 1,fraction 调整为 1,再次调用 slab_order ,这里的语义是:在这种极端的情况下,slab 中最少只能容纳一个对象,那么内核就分配容纳一个对象所需要的内存页。

如果 slab 对象太大了,有可能突破了 slub_max_order = 3 的限制,内核会近一步放宽至 MAX_ORDER = 11,这里我们可以看出内核的决心,无论如何必须保证 slab 中至少容纳一个对象。

下面是 slab_order 函数的逻辑,它是整个计算过程的核心:

// 一个 page 最多允许存放 32767 个对象
#define MAX_OBJS_PER_PAGE 32767

static inline unsigned int slab_order(unsigned int size,
        unsigned int min_objects, unsigned int max_order,
        unsigned int fract_leftover)
{
    unsigned int min_order = slub_min_order;
    unsigned int order;

    // 如果 2^min_order个内存页可以存放的对象个数超过 32767 限制
    // 那么返回 size * MAX_OBJS_PER_PAGE 所需要的 order 减 1
    if (order_objects(min_order, size) > MAX_OBJS_PER_PAGE)
        return get_order(size * MAX_OBJS_PER_PAGE) - 1;

    // 从 slab 所需要的最小 order 到最大 order 之间开始遍历,查找能够使 slab 碎片最小的 order 值
    for (order = max(min_order, (unsigned int)get_order(min_objects * size));
            order <= max_order; order++) {
        // slab 在当前 order 下,所占用的内存大小
        unsigned int slab_size = (unsigned int)PAGE_SIZE << order;
        unsigned int rem;
        // slab 的碎片大小:分配完 object 之后,所产生的碎片大小
        rem = slab_size % size;
        // 碎片大小 rem 不能超过 slab_size / fract_leftover 即符合要求
        if (rem <= slab_size / fract_leftover)
            break;
    }

    return order;
}

get_order(size) 函数的逻辑就比较简单了,它不会像 calculate_order 函数那样复杂,不需要考虑内存碎片的限制。它的逻辑只是简单的计算分配一个 size 大小的对象所需要的最少内存页个数,用于在 calculate_sizes 函数的最后计算 kmem_cache 结构的 min 值。

s->min = oo_make(get_order(size), size);

get_order 函数的计算逻辑如下:

  1. 如果给定的 size 在 [0,PAGE_SIZE] 之间,那么 order = 0 ,需要一个内存页面即可。
  2. size 在 [PAGE_SIZE + 1, 2^1 * PAGE_SIZE] 之间, order = 1
  3. size 在 [2^1 * PAGE_SIZE + 1, 2^2 * PAGE_SIZE] 之间, order = 2
  4. size 在 [2^2 * PAGE_SIZE + 1, 2^3 * PAGE_SIZE] 之间, order = 3
  5. size 在 [2^3 * PAGE_SIZE + 1, 2^4 * PAGE_SIZE] 之间, order = 4
// 定义在文件 /include/asm-generic/getorder.h
// 该函数的主要作用就是根据给定的 size 计算出所需最小的 order
static inline __attribute_const__ int get_order(unsigned long size)
{
    if (__builtin_constant_p(size)) {
        if (!size)
            return BITS_PER_LONG - PAGE_SHIFT;

        if (size < (1UL << PAGE_SHIFT))
            return 0;

        return ilog2((size) - 1) - PAGE_SHIFT + 1;
    }

    size--;
    size >>= PAGE_SHIFT;
#if BITS_PER_LONG == 32
    return fls(size);
#else
    return fls64(size);
#endif
}

init_kmem_cache_nodes

slab 的具体规划过程 calculate_sizes 函数完成后,回到kmem_cache_open函数。

slab cache 在每个 NUMA 节点中都有自己的缓存结构 kmem_cache_node,init_kmem_cache_nodes 函数需要遍历所有的 NUMA 节点,并利用 struct kmem_cache_node 专属的 slab cache —— 全局变量 kmem_cache_node,分配一个 kmem_cache_node 对象,并调用 init_kmem_cache_node 对其进行初始化。

static int init_kmem_cache_nodes(struct kmem_cache *s)
{
    int node;
    // 遍历所有的 numa 节点,为 slub cache 创建初始化 node cache 数组
    for_each_node_state(node, N_NORMAL_MEMORY) {
        struct kmem_cache_node *n;
        // 当 slub 在系统启动阶段初始化时,创建 kmem_cache_node cache 的时候,此时 slab_state == DOWN
        // 由于此时 kmem_cache_node cache 正在创建,所以无法利用 kmem_cache_node 所属的 slub cache 动态的分配 kmem_cache_node 对象
        // 这里会通过 early_kmem_cache_node_alloc 函数静态的分配 kmem_cache_node 对象,并初始化。
        if (slab_state == DOWN) {
             // 创建 boot_kmem_cache_node 时会走到这个分支
            early_kmem_cache_node_alloc(node);
            continue;
        }

        // 当 slab 体系在初始化 boot_kmem_cache 时,这时 slab_state 为 PARTIAL,流程就会走到这里。
        // 表示此时 boot_kmem_cache_node 已经初始化,可以利用它动态的分配 kmem_cache_node 对象了
        // 这里的 kmem_cache_node 就是 boot_kmem_cache_node
        n = kmem_cache_alloc_node(kmem_cache_node,
                        GFP_KERNEL, node);
        // 初始化 kmem_cache_node 对象
        init_kmem_cache_node(n);
        // 初始化 slab cache 结构 kmem_cache 中的 node 数组
        s->node[node] = n;
    }
    return 1;
}

在 slab allocator 体系中,第一个被创建出来的 slab cache 就是这里的 boot_kmem_cache_node,但是在初始化阶段,当前 slab_state == DOWN,所以目前内核无法利用 boot_kmem_cache_node 来动态的分配 kmem_cache_node 对象。流程会进入 if (slab_state == DOWN) 分支,通过 early_kmem_cache_node_alloc 函数来静态分配 kmem_cache_node 对象。

当 slab 体系在初始化 boot_kmem_cache 时,这时 slab_state 为 PARTIAL,表示此时 boot_kmem_cache_node 已经初始化,可以利用 kmem_cache_alloc_node 动态的分配 kmem_cache_node 对象了,这时传入的 kmem_cache_node 就是 boot_kmem_cache_node。

early_kmem_cache_node_alloc

static void early_kmem_cache_node_alloc(int node)
{
    // slab 的本质就是一个或者多个物理内存页 page,这里用于指向 slab 所属的 page。
    // 如果 slab 是由多个物理页 page 组成(复合页),这里指向复合页的首页
    struct page *page;
    // 这里主要为 boot_kmem_cache_node 初始化它的 node cache 数组
    // 这里会静态创建指定 node 节点对应的 kmem_cache_node 结构
    struct kmem_cache_node *n;

    // 此时 boot_kmem_cache_node 这个 kmem_cache 结构已经初始化好了。
    // 根据 kmem_cache 结构中的 kmem_cache_order_objects oo 属性向指定 node 节点所属的伙伴系统申请 2^order 个内存页 page
    // 这里返回复合页的首页,目的是为 kmem_cache_node 结构分配 slab,后续该 slab 会挂在 kmem_cache_node 结构中的 partial 列表中
    page = new_slab(kmem_cache_node, GFP_NOWAIT, node);

    // struct page 结构中的 freelist 指向 slab 中第一个空闲的对象
    // 这里的对象就是  struct kmem_cache_node 结构
    n = page->freelist;
#ifdef CONFIG_SLUB_DEBUG
    // 根据 slab cache 中的 flag 初始化 kmem_cache_node 对象
    init_object(kmem_cache_node, n, SLUB_RED_ACTIVE);
#endif
    // 重新设置 slab 中的下一个空闲对象。
    // 这里是获取对象 n 中的 free_pointer 指针,指向 n 的下一个空闲对象
    page->freelist = get_freepointer(kmem_cache_node, n);
    // 表示 slab 中已经有一个对象被使用了
    page->inuse = 1;
    // 这里可以看出 boot_kmem_cache_node 的 NUMA 节点缓存在这里初始化的时候
    // 内核会为每个 NUMA 节点申请一个 slab,并缓存在它的 partial 链表中
    // 并不是缓存在 boot_kmem_cache_node 的本地 cpu 缓存中
    page->frozen = 0;
    // 这里的 kmem_cache_node 指的是 boot_kmem_cache_node
    // 初始化 boot_kmem_cache_node 中的 node cache 数组
    kmem_cache_node->node[node] = n;
    // 初始化 node 节点对应的 kmem_cache_node 结构
    init_kmem_cache_node(n);
    // kmem_cache_node 结构中的 nr_slabs 计数加1,total_objects 加 page->objects
    inc_slabs_node(kmem_cache_node, node, page->objects);
    // 将新创建出来的 slab (page表示),添加到对象 n (kmem_cache_node结构)中的 partial 列表头部
    __add_partial(n, page, DEACTIVATE_TO_HEAD);
}
static void
init_kmem_cache_node(struct kmem_cache_node *n)
{
    n->nr_partial = 0;
    spin_lock_init(&n->list_lock);
    INIT_LIST_HEAD(&n->partial);
#ifdef CONFIG_SLUB_DEBUG
    atomic_long_set(&n->nr_slabs, 0);
    atomic_long_set(&n->total_objects, 0);
    INIT_LIST_HEAD(&n->full);
#endif
}

当 boot_kmem_cache_node 被初始化之后,它的整个结构如下图所示:

alloc_kmem_cache_cpus

这里主要是为 slab cache 创建其 cpu 本地缓存结构 kmem_cache_cpu,每个 cpu 一个这样的结构,并调用 per_cpu_ptr 将创建好的 kmem_cache_cpu 结构与对应的 cpu 相关联初始化。

struct kmem_cache {
    // 每个 cpu 拥有一个本地缓存,用于无锁化快速分配释放对象
    struct kmem_cache_cpu __percpu *cpu_slab;
}
static inline int alloc_kmem_cache_cpus(struct kmem_cache *s)
{
    // 为 slab cache 分配 cpu 本地缓存结构 kmem_cache_cpu
    // __alloc_percpu 函数在内核中专门用于分配 percpu 类型的结构体(the percpu allocator)
    //  kmem_cache_cpu 结构也是 percpu 类型的,这里通过 __alloc_percpu 直接分配
    s->cpu_slab = __alloc_percpu(sizeof(struct kmem_cache_cpu),
                     2 * sizeof(void *));
    // 初始化 cpu 本地缓存结构 kmem_cache_cpu
    init_kmem_cache_cpus(s);
    return 1;
}
static void init_kmem_cache_cpus(struct kmem_cache *s)
{
    int cpu;
    // 遍历所有CPU,通过 per_cpu_ptr 将前面分配的 kmem_cache_cpu 结构与对应的CPU关联对应起来
    // 同时初始化 kmem_cache_cpu 变量里面的 tid 为其所关联 cpu 的编号
    for_each_possible_cpu(cpu)
        per_cpu_ptr(s->cpu_slab, cpu)->tid = init_tid(cpu);
}

slab cache 的 cpu 本地缓存结构 struct kmem_cache_cpu 是一个 percpu 类型的变量,由 __alloc_percpu直接创建,并不需要一个专门的 slab cache 来管理。

流程到这里 boot_kmem_cache,boot_kmem_cache_node 这两个静态结构就已经被初始化好了,现在内核就可以通过他们来动态的创建 kmem_cache 对象和 kmem_cache_node 对象了。

但是这里的 boot_kmem_cache 和 boot_kmem_cache_node 只是临时的 kmem_cache 结构,目的是在 slab allocator 体系初始化的时候用于静态创建 kmem_cache (slab cache), kmem_cache_node (slab cache)。

bootstrap

既然是临时的结构,所以这里需要创建两个最终的全局 kmem_cache 结构,然后将这两个静态临时结构深拷贝到最终的全局 kmem_cache 结构中。

static struct kmem_cache * __init bootstrap(struct kmem_cache *static_cache)
{
    int node;
    // kmem_cache 指向专门管理 kmem_cache 对象的 slab cache
    // 该 slab cache 现在已经全部初始化完毕,可以利用它动态的分配最终的 kmem_cache 对象
    struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);
    struct kmem_cache_node *n;
    // 将静态的 kmem_cache 对象,比如:boot_kmem_cache,boot_kmem_cache_node
    // 深拷贝到最终的 kmem_cache 对象 s 中
    memcpy(s, static_cache, kmem_cache->object_size);

    // 释放本地 cpu 缓存的 slab
    __flush_cpu_slab(s, smp_processor_id());
    // 遍历 node cache 数组,修正 kmem_cache_node 结构中 partial 链表中包含的 slab ( page 表示)对应 page 结构的 slab_cache 指针
    // 使其指向最终的 kmem_cache 结构,之前在 create_boot_cache 中指向的静态 kmem_cache 结构,这里需要修正
    for_each_kmem_cache_node(s, node, n) {
        struct page *p;

        list_for_each_entry(p, &n->partial, slab_list)
            p->slab_cache = s;
    }
    // 将最终的 kmem_cache 结构加入到全局 slab cache 链表中
    list_add(&s->list, &slab_caches);
    return s;
}

现在关于 slab alloactor 体系的初始化流程笔者就为大家全部介绍完了,再对这个流程做一个简单的总体回顾:

首先 slab cache 创建要依赖两个核心的数据机构,kmem_cache,kmem_cache_node,其中 kmem_cache 结构是 slab cache 在内核中的数据结构,同样也需要被一个专门的 slab cache 所管理,但是在内核初始化阶段 slab 体系还未建立,所以内核通过定义两个局部静态变量来解决 kmem_cache 结构的创建问题。

随后内核会在 calculate_size 函数中初始化 struct kmem_cache 结构中的核心属性。

内核首先创建 boot_kmem_cache_node,并通过 early_kmem_cache_node_alloc 函数为 boot_kmem_cache_node 创建 kmem_cache_node 结构。

当 boot_kmem_cache_node 被创建出来之后,内核就可以动态的分配 kmem_cache_node 对象了。

然后创建 boot_kmem_cache,在遇到 kmem_cache_node 结构创建的时候,直接使用 boot_kmem_cache_node 进行动态创建。

最后通过 bootstrap 将这两个临时静态的 slab cache : boot_kmem_cache,boot_kmem_cache_node 深拷贝到最终的全局 slab cache 中。

从此以后,内核就可以动态创建 slab cache 了。

源码查看 slab cache 的创建

现在我们已经了解了 slab cache 的原理以及初始化,接下来以内核创建 slab cache 的接口函数 kmem_cache_create 为起点,来梳理 slab 的创建过程。

kmem_cache_create

struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
        slab_flags_t flags, void (*ctor)(void *))
{
    return kmem_cache_create_usercopy(name, size, align, flags, 0, 0,
                      ctor);
}

kmem_cache_create 接口中的参数,是由用户指定的关于 slab cache 的一些核心属性,这些属性值与我们在前文《slab 的基础信息管理》小节中介绍 struct kmem_cache 结构的相应属性一一对应,在创建 slab cache 的过程中,内核会将 kmem_cache_create 接口中参数指定的值一一赋值到 struct kmem_cache 结构中。

struct kmem_cache {
    // slab cache 的名称, 也就是在 slabinfo 命令中 name 那一列
    const char *name;  
    // 对应参数 size,指 slab 中对象的实际大小,不包含填充的字节数
    unsigned int object_size;/* The size of an object without metadata */
    // 对象按照指定的 align 进行对齐
    unsigned int align; 
    // slab cache 的管理标志位,用于设置 slab 的一些特性
    slab_flags_t flags;
    // 池化对象的构造函数,用于创建 slab 对象池中的对象
    void (*ctor)(void *);
}

slab cache 的整个创建过程其实是封装在 kmem_cache_create_usercopy 函数中,kmem_cache_create 直接调用了该函数,并将创建参数透传过去。

kmem_cache_create_usercopy

struct kmem_cache *
kmem_cache_create_usercopy(const char *name,
          unsigned int size, unsigned int align,
          slab_flags_t flags,
          unsigned int useroffset, unsigned int usersize,
          void (*ctor)(void *))
{
    struct kmem_cache *s = NULL;
    const char *cache_name;
    int err;

    // 获取 cpu_hotplug_lock,防止 cpu 热插拔改变 online cpu map
    get_online_cpus();
    // 获取 mem_hotplug_lock,防止访问内存的时候进行内存热插拔
    get_online_mems();
    // memory cgroup 相关,获取 memcg_cache_ids_sem 读写信号量
    // 防止 memcg_nr_cache_ids (caches array 大小)被修改
    memcg_get_cache_ids();
    // 获取 slab cache 链表的全局互斥锁
    mutex_lock(&slab_mutex);

    // 入参检查,校验 name 和 size 的有效性,防止创建过程在中断上下文中进行
    err = kmem_cache_sanity_check(name, size);
    if (err) {
        goto out_unlock;
    }

    // 检查有效的 slab flags 标记位,如果传入的 flag 是无效的,则拒绝本次创建请求
    if (flags & ~SLAB_FLAGS_PERMITTED) {
        err = -EINVAL;
        goto out_unlock;
    }

    // 设置创建 slab  cache 时用到的一些标志位
    flags &= CACHE_CREATE_MASK;

    // 校验 useroffset 和 usersize 的有效性
    if (WARN_ON(!usersize && useroffset) ||
        WARN_ON(size < usersize || size - usersize < useroffset))
        usersize = useroffset = 0;

    if (!usersize)
        // 在全局 slab cache 链表中查找与当前创建参数相匹配的 kmem_cache
        // 如果有,就不需要创建新的了,直接和已有的  slab cache  合并
        // 并且在 sys 文件系统中使用指定的 name 作为已有  slab cache  的别名
        s = __kmem_cache_alias(name, size, align, flags, ctor);
    if (s)
        goto out_unlock;
    // 在内核中为指定的 name 生成字符串常量并分配内存
    // 这里的 cache_name 就是将要创建的 slab cache 名称,用于在 /proc/slabinfo 中显示
    cache_name = kstrdup_const(name, GFP_KERNEL);
    if (!cache_name) {
        err = -ENOMEM;
        goto out_unlock;
    }
    // 按照我们指定的参数,创建新的 slab cache
    s = create_cache(cache_name, size,
             calculate_alignment(flags, align, size),
             flags, useroffset, usersize, ctor, NULL, NULL);
    if (IS_ERR(s)) {
        err = PTR_ERR(s);
        kfree_const(cache_name);
    }

out_unlock:
    // 走到这里表示创建 slab cache 失败,释放相关的自旋锁和信号量
    mutex_unlock(&slab_mutex);
    memcg_put_cache_ids();
    put_online_mems();
    put_online_cpus();

    if (err) {
        if (flags & SLAB_PANIC)
            panic("kmem_cache_create: Failed to create slab '%s'. Error %d\n",
                name, err);
        else {
            pr_warn("kmem_cache_create(%s) failed with error %d\n",
                name, err);
            dump_stack();
        }
        return NULL;
    }
    return s;
}

内核提供 kmem_cache_create_usercopy 函数的目的其实是为了防止 slab cache 中管理的内核核心对象被泄露,通过 useroffset 和 usersize 两个变量来指定内核对象内存布局区域中 useroffset 到 usersize 的这段内存区域可以被复制到用户空间中,其他区域则不可以。

在 kmem_cache_create_usercopy 函数的开始,内核为了保证整个创建过程是并发安全的,所以需要先获取一系列的锁:

  1. 获取 cpu_hotplug_lock,mem_hotplug_lock 来防止在创建 slab cache 的过程中 cpu 或者内存进行热插拔。
  2. 防止 memory group 相关的 caches array 被修改,cgroup 相关的不是本文重点,这里简单了解一下即可。
  3. 内核中使用一个全局的双向链表来串联起系统中所有的 slab cache,这里需要获取全局链表 list 的锁,防止并发对 list 进行修改。

kmem_cache_sanity_check

在确保 slab cache 的整个创建过程并发安全之后,通过 kmem_cache_sanity_check 函数校验传递进来的那些创建参数的合法有效性,比如 name 不能为空,不能再中断上下文, object size 在 8 字节到 4M 之间。

#define MAX_ORDER       11
#define PAGE_SHIFT      12

// 定义在 /include/linux/slab.h 文件
#ifdef CONFIG_SLUB
#define KMALLOC_SHIFT_MAX   (MAX_ORDER + PAGE_SHIFT - 1)
/* Maximum allocatable size */
#define KMALLOC_MAX_SIZE    (1UL << KMALLOC_SHIFT_MAX)

static int kmem_cache_sanity_check(const char *name, unsigned int size)
{   
    // 1: 传入 slab cache 的名称不能为空
    // 2: 创建 slab cache 的过程不能处在中断上下文中
    // 3: 传入的对象大小 size 需要在 8 字节到 KMALLOC_MAX_SIZE = 4M 之间
    if (!name || in_interrupt() || size < sizeof(void *) ||
        size > KMALLOC_MAX_SIZE) {
        pr_err("kmem_cache_create(%s) integrity check failed\n", name);
        return -EINVAL;
    }

    WARN_ON(strchr(name, ' ')); /* It confuses parsers */
    return 0;
}

__kmem_cache_alias

在 __kmem_cache_alias 函数中,内核会遍历系统中 slab cache 的全局链表 list,试图在系统现有 slab cache 中查找到一个各项核心参数与我们指定的创建参数贴近的 slab cache。比如,系统中存在一个 slab cache 它的各项核心参数,object size,align,slab_flags_t 和我们指定的创建参数非常贴近。

这样一来内核就不需要重复创建新的 slab cache 了,直接复用原有的 slab cache 即可,将我们指定的 name 作为原有 slab cache 的别名。

如果找不到这样一个可以被复用的 slab cache,那么内核就会调用 create_cache 开始创建 slab cache 流程。

struct kmem_cache *
__kmem_cache_alias(const char *name, unsigned int size, unsigned int align,
           slab_flags_t flags, void (*ctor)(void *))
{
    struct kmem_cache *s, *c;
    // 在全局 slab cache 链表中查找与当前创建参数相匹配的 slab cache
    // 如果在全局查找到一个  slab cache,它的核心参数和我们指定的创建参数很贴近
    // 那么就没必要再创建新的 slab cache了,复用已有的 slab cache
    s = find_mergeable(size, align, flags, name, ctor);
    if (s) {
        // 如果存在可复用的 kmem_cache,则将它的引用计数 + 1
        s->refcount++;
        // 采用较大的值,更新已有的 kmem_cache 相关的元数据
        s->object_size = max(s->object_size, size);
        s->inuse = max(s->inuse, ALIGN(size, sizeof(void *)));
        // 遍历 mem cgroup 中的 cache array,更新对应的元数据
        // cgroup 相关,这里简单了解也可直接忽略
        for_each_memcg_cache(c, s) {
            c->object_size = s->object_size;
            c->inuse = max(c->inuse, ALIGN(size, sizeof(void *)));
        }
        // 由于这里我们会复用已有的 kmem_cache 并不会创建新的,而且我们指定的 kmem_cache 名称是 name。
        // 为了看起来像是创建了一个名称为 name 的新 kmem_cache,所以要给被复用的 kmem_cache 起一个别名,这个别名就是我们指定的 name
        // 在 sys 文件系统中使用我们指定的 name 为被复用 kmem_cache 创建别名
        // 这样一来就会在 sys 文件系统中出现一个这样的目录 /sys/kernel/slab/name ,该目录下的文件包含了对应 slab cache 运行时的详细信息
        if (sysfs_slab_alias(s, name)) {
            s->refcount--;
            s = NULL;
        }
    }

    return s;
}

__kmem_cache_alias 函数的核心是在 find_mergeable 方法中,内核在 find_mergeable 方法里边会遍历 slab cache 的全局链表 list,查找与当前创建参数贴近可以被复用的 slab cache。一个可以被复用的 slab cache 需要满足以下四个条件:

  1. 指定的 slab_flags_t 相同。
  2. 指定对象的 object size 要小于等于已有 slab cache 中的对象 size (kmem_cache->size)。
  3. 如果指定对象的 object size 与已有 kmem_cache->size 不相同,那么它们之间的差值需要再一个 word size 之内。
  4. 已有 slab cache 中的 slab 对象对齐 align (kmem_cache->align)要大于等于指定的 align 并且可以整除 align 。

如果通过 find_mergeable 在现有系统中所有 slab cache 中找到了一个可以复用的 slab cache,那么就不需要在创建新的了,直接返回已有的 slab cache 就可以了。

但是在返回之前,需要更新一下已有 slab cache 结构 kmem_cache 中的相关信息:

struct kmem_cache {
    // slab cache 的引用计数,为 0 时就可以销毁并释放内存回伙伴系统重
    int refcount;   
    // slab 中对象的实际大小,不包含填充的字节数
    unsigned int object_size;/* The size of an object without metadata */
    // 对象的 object_size 按照 word 字长对齐之后的大小
    unsigned int inuse;  
}

最后调用 sysfs_slab_alias 在 sys 文件系统中创建一个这样的目录 /sys/kernel/slab/name,name 就是 kmem_cache_create 接口函数传递过来的参数,表示要创建的 slab cache 名称。

find_mergeable 查找可被复用的 slab cache

struct kmem_cache *find_mergeable(unsigned int size, unsigned int align,
        slab_flags_t flags, const char *name, void (*ctor)(void *))
{
    struct kmem_cache *s;
    // 与 word size 进行对齐
    size = ALIGN(size, sizeof(void *));
    // 根据我们指定的对齐参数 align 并结合 CPU cache line 大小,计算出一个合适的对齐参数
    align = calculate_alignment(flags, align, size);
    // 对象 size 重新按照 align 进行对齐
    size = ALIGN(size, align);

    // 如果 flag 设置的是不允许合并,则停止
    if (flags & SLAB_NEVER_MERGE)
        return NULL;

    // 开始遍历内核中已有的 slab cache,寻找可以合并的 slab cache
    list_for_each_entry_reverse(s, &slab_root_caches, root_caches_node) {
        if (slab_unmergeable(s))
            continue;
        // 指定对象 size 不能超过已有 slab cache 中的对象 size
        if (size > s->size)
            continue;
        // 校验指定的 flag 是否与已有 slab cache 中的 flag 一致
        if ((flags & SLAB_MERGE_SAME) != (s->flags & SLAB_MERGE_SAME))
            continue;
        // 两者的 size 相差在一个 word size 之内 
        if (s->size - size >= sizeof(void *))
            continue;
        // 已有 slab cache 中对象的对齐 align 要大于等于指定的 align并且可以整除 align。
        if (IS_ENABLED(CONFIG_SLAB) && align &&
            (align > s->align || s->align % align))
            continue;
        // 查找到可以合并的已有 slab cache,不需要再创建新的 slab cache 了
        return s;
    }
    return NULL;
}

内核并不会完全按照我们指定的 align 进行内存对齐,而是通过 calculate_alignment 综合考虑 cpu 硬件 cache line 的大小,以及 word size 计算出一个合理的 align 值。

// 定义在文件:/include/linux/slab.h
#define ARCH_SLAB_MINALIGN __alignof__(unsigned long long)

static unsigned int calculate_alignment(slab_flags_t flags,
        unsigned int align, unsigned int size)
{
    // SLAB_HWCACHE_ALIGN 表示需要按照硬件 cache line 对齐
    if (flags & SLAB_HWCACHE_ALIGN) {
        unsigned int ralign;
        // 获取 cache line 大小 通常为 64 字节
        ralign = cache_line_size();
        // 根据指定对齐参数 align ,对象 object size 以及 cache line 大小
        // 综合计算出一个合适的对齐参数 ralign 出来
        while (size <= ralign / 2)
            ralign /= 2;
        align = max(align, ralign);
    }

    // ARCH_SLAB_MINALIGN 为 slab 设置的最小对齐参数, 8 字节大小,align 不能小于该值
    if (align < ARCH_SLAB_MINALIGN)
        align = ARCH_SLAB_MINALIGN;
    // 与 word size 进行对齐
    return ALIGN(align, sizeof(void *));
}

如果通过 __kmem_cache_alias 找到可以复用的 slab cache,直接返回找到的 slab cache。如果没有,则调用 create_cache 创建一个 slab cache。

create_cache

static struct kmem_cache *create_cache(const char *name,
        unsigned int object_size, unsigned int align,
        slab_flags_t flags, unsigned int useroffset,
        unsigned int usersize, void (*ctor)(void *),
        struct mem_cgroup *memcg, struct kmem_cache *root_cache)
{
    struct kmem_cache *s;
    // 为将要创建的 slab cache 分配 kmem_cache 结构
    // kmem_cache 也是内核的一个核心数据结构,同样也会被它对应的 slab cache 所管理
    // 这里就是从 kmem_cache 所属的 slab cache 中拿出一个 kmem_cache 对象出来
    s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);

    // 利用我们指定的创建参数初始化 kmem_cache 结构
    s->name = name;
    s->size = s->object_size = object_size;
    s->align = align;
    s->ctor = ctor;
    s->useroffset = useroffset;
    s->usersize = usersize;
    // 创建 slab cache 的核心函数,这里会初始化 kmem_cache 结构中的其他重要属性
    // 包括创建初始化 kmem_cache_cpu 和 kmem_cache_node 结构
    err = __kmem_cache_create(s, flags);
    if (err)
        goto out_free_cache;
    // slab cache 初始状态下,引用计数为 1
    s->refcount = 1;
    // 将刚刚创建出来的 slab cache 加入到 slab cache 在内核中的全局链表管理
    list_add(&s->list, &slab_caches);

out:
    if (err)
        return ERR_PTR(err);
    return s;

out_free_cache:
    // 创建过程出现错误之后,释放 kmem_cache 对象
    kmem_cache_free(kmem_cache, s);
    goto out;
}

在 create_cache 函数的开始,内核会调用 kmem_cache_zalloc 从 kmem_cache 专属的 slab cache 中申请一个 kmem_cache 对象。

内核中的每个核心数据结构都会有其专属的 slab cache 来管理。
而这里的 slab cache 的数据结构 struct kmem_cache 同样也属于内核的核心数据结构,它也有其专属的 slab cache 来专门管理 kmem_cache 对象的分配与释放。
内核在启动阶段,会专门为 struct kmem_cache 创建其专属的 slab cache,保存在全局变量 kmem_cache 中。

// 全局变量,用于专门管理 kmem_cache 对象的 slab cache
// 定义在文件:/mm/slab_common.c
struct kmem_cache *kmem_cache;

同理,slab cache 的 NUMA 节点缓存 kmem_cache_node 结构也是如此,内核也会为其创建一个专属的 slab cache,保存在全局变量 kmem_cache_node 中。

// 全局变量,用于专门管理 kmem_cache_node 对象的 slab cache
// 定义在文件:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

随后会在 __kmem_cache_create 函数中近一步初始化 kmem_cache 对象的其他重要属性。比如,初始化 slab 对象的内存布局相关信息,计算 slab 所需要的物理内存页个数以及所能容纳的对象个数,创建初始化 cpu 本地缓存结构以及 NUMA 节点的缓存结构。

最后将刚刚创建出来的 slab cache 加入到 slab cache 在内核中的全局链表 list 中管理。

至此,slab cache 的整个骨架就全部被创建出来了,最终得到的 slab cache 完整架构如下图所示:

源码查看 slab cache 的分配

内核实现的接口有kmalloc,kmalloc_node,kmem_cache_alloc,keme_cache_alloc_node 等。

本文就从 kmem_cache_alloc 与 kemem_cache_alloc_node 接口分析 slab cache 的分配过程。

内核中通过 kmem_cache_alloc_node 函数要求 slab cache 从指定的 NUMA 节点中分配对象。

// 定义在文件:/mm/slub.c
void *kmem_cache_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node)
{
    void *ret = slab_alloc_node(s, gfpflags, node, _RET_IP_);
    return ret;
}

不支持 NUMA 节点的指定,仅从当前 CPU 所属的节点中分配内存。

// 定义在文件:/mm/slub.c
static __always_inline void *slab_alloc(struct kmem_cache *s,
        gfp_t gfpflags, unsigned long addr)
{
    return slab_alloc_node(s, gfpflags, NUMA_NO_NODE, addr);
}

void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
{
    void *ret = slab_alloc(s, gfpflags, _RET_IP_);
    //用于监控和调试内存分配的跟踪点
    trace_kmem_cache_alloc(_RET_IP_, ret, s->object_size,
                s->size, gfpflags);

    return ret;
}

可以看到,两个函数最终都会调用 slab_alloc_node 这个接口,下面就以这个接口为起点,查看一下 slab cache 分配的快速和慢速流程。

slab_alloc_node

static __always_inline void *slab_alloc_node(struct kmem_cache *s,
        gfp_t gfpflags, int node, unsigned long addr)
{
    // 用于指向分配成功的对象
    void *object;
    // slab cache 在当前 cpu 下的本地 cpu 缓存
    struct kmem_cache_cpu *c;
    // object 所在的内存页
    struct page *page;
    // 当前 cpu 编号
    unsigned long tid;

redo:
    // slab cache 首先尝试从当前 cpu 本地缓存 kmem_cache_cpu 中获取空闲对象
    // 这里的 do..while 循环是要保证获取到的 cpu 本地缓存 c 是属于执行进程的当前 cpu
    // 因为进程可能由于抢占或者中断的原因被调度到其他 cpu 上执行,所需需要确保两者的 tid 是否一致
    do {
        // 获取执行当前进程的 cpu 中的 tid 字段
        tid = this_cpu_read(s->cpu_slab->tid);
        // 获取 cpu 本地缓存 cpu_slab
        c = raw_cpu_ptr(s->cpu_slab);
        // 如果开启了 CONFIG_PREEMPT 表示允许优先级更高的进程抢占当前 cpu
        // 如果发生抢占,当前进程可能被重新调度到其他 cpu 上运行,所以需要检查此时运行当前进程的 cpu tid 是否与刚才获取的 cpu 本地缓存一致
        // 如果两者的 tid 字段不一致,说明进程已经被调度到其他 cpu 上了, 需要再次获取正确的 cpu 本地缓存
    } while (IS_ENABLED(CONFIG_PREEMPT) &&
         unlikely(tid != READ_ONCE(c->tid)));

    // 从 slab cache 的 cpu 本地缓存 kmem_cache_cpu 中获取缓存的 slub 空闲对象列表
    // 这里的 freelist 指向本地 cpu 缓存的 slub 中第一个空闲对象
    object = c->freelist;
    // 获取本地 cpu 缓存的 slub,这里用 page 表示,如果是复合页,这里指向复合页的首页 head page
    page = c->page;
    if (unlikely(!object || !node_match(page, node))) {
        // 如果 slab cache 的 cpu 本地缓存中已经没有空闲对象了
        // 或者 cpu 本地缓存中的 slub 并不属于我们指定的 NUMA 节点
        // 那么我们就需要进入慢速路径中分配对象:
        // 1. 检查 kmem_cache_cpu 的 partial 列表中是否有空闲的 slub
        // 2. 检查 kmem_cache_node 的 partial 列表中是否有空闲的 slub
        // 3. 如果都没有,则只能重新到伙伴系统中去申请内存页
        object = __slab_alloc(s, gfpflags, node, addr, c);
        // 统计 slab cache 的状态信息,记录本次分配走的是慢速路径 slow path
        stat(s, ALLOC_SLOWPATH);
    } else {
        // 走到该分支表示,slab cache 的 cpu 本地缓存中还有空闲对象,直接分配
        // 快速路径 fast path 下分配成功,从当前空闲对象中获取下一个空闲对象指针 next_object        
        void *next_object = get_freepointer_safe(s, object);
        // 更新 kmem_cache_cpu 结构中的 freelist 指向 next_object
        if (unlikely(!this_cpu_cmpxchg_double(
                s->cpu_slab->freelist, s->cpu_slab->tid,
                object, tid,
                next_object, next_tid(tid)))) {

            note_cmpxchg_failure("slab_alloc", s, tid);
            goto redo;
        }
        // cpu 预取 next_object 的 freepointer 到 cpu 高速缓存,加快下一次分配对象的速度
        prefetch_freepointer(s, next_object);
        stat(s, ALLOC_FASTPATH);
    }

    // 如果 gfpflags 掩码中设置了  __GFP_ZERO,则需要将对象所占的内存初始化为零值
    if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object)
        memset(object, 0, s->object_size);
    // 返回分配好的对象
    return object;
}

slab cache 的快速分配路径

slab cache 在最开始会进入 fastpath 分配对象,也就是说首先会从 cpu 本地缓存 kmem_cache_cpu->freelist 中获取对象。

在获取 kmem_cache_cpu 结构的时候需要保证这个 cpu 本地缓存是属于当前执行进程的 cpu。

在开启了 CONFIG_PREEMPT 的情况下,内核是允许优先级更高的进程抢占当前 cpu 的,当发生 cpu 抢占之后,进程会被内核重新调度到其他 cpu 上执行,这样一来,进程在被抢占之前获取到的 kmem_cache_cpu 就与当前执行进程 cpu 的 kmem_cache_cpu 不一致了。

内核在 slab_alloc_node 函数开始的地方通过在 do..while 循环中不断判断两者的 tid 是否一致来保证这一点。

随后内核会通过 kmem_cache_cpu->freelist 来获取 cpu 缓存 slab 中的第一个空闲对象。

如果当前 cpu 缓存 slab 是空的(没有空闲对象可供分配)或者该 slab 所在的 NUMA 节点并不是我们指定的。那么就会通过 __slab_alloc 进入到慢速分配路径 slowpath 中。

如果当前 cpu 缓存 slab 有空闲的对象并且 slab 所在的 NUMA 节点正是我们指定的,那么将当前 kmem_cache_cpu->freelist 指向的第一个空闲对象从 slab 中拿出,并分配出去。

随后通过 get_freepointer_safe 获取当前分配对象的 freepointer 指针(指向其下一个空闲对象),然后将 kmem_cache_cpu->freelist 更新为 freepointer (指向的下一个空闲对象)。

// slub 中的空闲对象中均保存了下一个空闲对象的指针 free_pointer
// free_pointor  在 object 中的位置由 kmem_cache 结构的 offset 指定
static inline void *get_freepointer_safe(struct kmem_cache *s, void *object)
{
    // freepointer 在 object 内存区域的起始地址
    unsigned long freepointer_addr;
    // 指向下一个空闲对象的 free_pontier
    void *p;
    // free_pointer 位于 object 起始地址的 offset 偏移处
    freepointer_addr = (unsigned long)object + s->offset;
    // 获取 free_pointer 指向的地址(下一个空闲对象)
    probe_kernel_read(&p, (void **)freepointer_addr, sizeof(p));
    // 返回下一个空闲对象地址
    return freelist_ptr(s, p, freepointer_addr);
}

slab cache 的慢速分配路径

static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
              unsigned long addr, struct kmem_cache_cpu *c)
{
    void *p;
    unsigned long flags;
    // 关闭 cpu 中断,防止并发访问
    local_irq_save(flags);
#ifdef CONFIG_PREEMPT
    // 当开启了 CONFIG_PREEMPT,表示允许其他进程抢占当前 cpu
    // 运行进程的当前 cpu 可能会被其他优先级更高的进程抢占,当前进程可能会被调度到其他 cpu 上
    // 所以这里需要重新获取 slab cache 的 cpu 本地缓存
    c = this_cpu_ptr(s->cpu_slab);
#endif
    // 进入 slab cache 的慢速分配路径
    p = ___slab_alloc(s, gfpflags, node, addr, c);
    // 恢复 cpu 中断
    local_irq_restore(flags);
    return p;
}

内核为了防止 slab cache 在慢速路径下的并发安全问题,在进入 slowpath 之前会把中断关闭掉,并重新获取 cpu 本地缓存。这样做的目的是为了防止再关闭中断之前,进程被抢占,调度到其他 cpu 上。

static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
              unsigned long addr, struct kmem_cache_cpu *c)
{
    // 指向 slub 中可供分配的第一个空闲对象
    void *freelist;
    // 空闲对象所在的 slub (用 page 表示)
    struct page *page;
    // 从 slab cache 的本地 cpu 缓存中获取缓存的 slub
    page = c->page;
    if (!page)
        // 如果缓存的 slub 中的对象已经被全部分配出去,没有空闲对象了
        // 那么就会跳转到 new_slab 分支进行降级处理走慢速分配路径
        goto new_slab;
redo:

    // 这里需要再次检查 slab cache 本地 cpu 缓存中的 freelist 是否有空闲对象
    // 因为当前进程可能被中断,当重新调度之后,其他进程可能已经释放了一些对象到缓存 slab 中
    // freelist 可能此时就不为空了,所以需要再次尝试一下
    freelist = c->freelist;
    if (freelist)
        // 从 cpu 本地缓存中的 slub 中直接分配对象
        goto load_freelist;

    // 本地 cpu 缓存的 slub 用 page 结构来表示,这里是检查 page 结构的 freelist 是否还有空闲对象
    // c->freelist 表示的是本地 cpu 缓存的空闲对象列表,刚我们已经检查过了
    // 现在我们检查的 page->freelist ,它表示由其他 cpu 所释放的空闲对象列表
    // 因为此时有可能其他 cpu 又释放了一些对象到 slub 中这时 slub 对应的  page->freelist 不为空,可以直接分配
    freelist = get_freelist(s, page);
    // 注意这里的 freelist 已经变为 page->freelist ,并不是 c->freelist;
    if (!freelist) {
        // 此时 cpu 本地缓存的 slub 里的空闲对象已经全部耗尽
        // slub 从 cpu 本地缓存中脱离,进入 new_slab 分支走慢速分配路径
        c->page = NULL;
        stat(s, DEACTIVATE_BYPASS);
        goto new_slab;
    }

    stat(s, ALLOC_REFILL);

load_freelist:
    // 被 slab cache 的 cpu 本地缓存的 slub 所属的 page 必须是 frozen 冻结状态,只允许本地 cpu 从中分配对象
    VM_BUG_ON(!c->page->frozen);
    // kmem_cache_cpu 中的 freelist 指向被 cpu 缓存 slub 中第一个空闲对象
    // 由于第一个空闲对象马上要被分配出去,所以这里需要获取下一个空闲对象更新 freelist
    c->freelist = get_freepointer(s, freelist);
    // 更新 slab cache 的 cpu 本地缓存分配对象时的全局 transaction id
    // 每当分配完一次对象,kmem_cache_cpu 中的 tid 都需要改变
    c->tid = next_tid(c->tid);
    // 返回第一个空闲对象
    return freelist;

new_slab:
    // 查看 kmem_cache_cpu->partial 链表中是否有 slab 可供分配对象
    if (slub_percpu_partial(c)) {
        // 获取 cpu 本地缓存 kmem_cache_cpu 的 partial 列表中的第一个 slub (用 page 表示)
        // 并将这个 slub 提升为 cpu 本地缓存中的 slub,赋值给 c->page
        page = c->page = slub_percpu_partial(c);
        // 将 partial 列表中第一个 slub (c->page)从 partial 列表中摘下
        // 并将列表中的下一个 slub 更新为 partial 列表的头结点
        slub_set_percpu_partial(c, page);
        // 更新状态信息,记录本次分配是从 kmem_cache_cpu 的 partial 列表中分配
        stat(s, CPU_PARTIAL_ALLOC);
        // 重新回到 redo 分支,这下就可以从 page->freelist 中获取对象了
        // 并且在 load_freelist 分支中将  page->freelist 更新到 c->freelist 中,page->freelist 设置为 null
        // 此时 slab cache 中的 cpu 本地缓存 kmem_cache_cpu 的 freelist 以及 page 就变为了 partial 列表中的 slub
        goto redo;
    }

    // 流程走到这里表示 slab cache 中的 cpu 本地缓存 partial 列表中也没有 slub 了
    // 需要近一步降级到 numa node cache —— kmem_cache_node 中的 partial 列表去查找
    // 如果还是没有,就只能去伙伴系统中申请新的 slub,然后分配对象
    // 该函数为 slab cache 在慢速路径下分配对象的核心逻辑
    freelist = new_slab_objects(s, gfpflags, node, &c);

    if (unlikely(!freelist)) {
        // 如果伙伴系统中无法分配 slub 所需的 page,那么就提示内存不足,分配失败,返回 null
        slab_out_of_memory(s, gfpflags, node);
        return NULL;
    }

    page = c->page;
    if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags)))
        // 此时从 kmem_cache_node->partial 列表中获取的 slub 
        // 或者从伙伴系统中重新申请的 slub 已经被提升为本地 cpu 缓存了 kmem_cache_cpu->page
        // 这里需要跳转到 load_freelist 分支,从本地 cpu 缓存 slub 中获取第一个对象返回
        goto load_freelist;

}

在 slab cache 进入慢速路径之前,内核还需要再次检查本地 cpu 缓存的 slab 的存储容量,确保其真的没有空闲对象了。

如果本地 cpu 缓存的 slab 为空 kmem_cache_cpu->page == null,直接跳转到 new_slab 分支进入 slow path。

如果本地 cpu 缓存的 slab 不为空,那么需要再次检查 slab 中是否有空闲对象,这么做的目的是因为当前进程可能被中断,当重新调度之后,其他进程可能已经释放了一些对象到缓存 slab 中了,所以在进入 slowpath 之前还是有必要再次检查一下 kmem_cache_cpu->freelist。

如果碰巧,其他进程在当前进程被中断之后,已经释放了一些对象回缓存 slab 中了,那么就直接跳转至 load_freelist 分支,走 fastpath 路径,直接从缓存 slab (kmem_cache_cpu->freelist) 中分配对象,避免进入 slowpath。

如果 kmem_cache_cpu->freelist 还是为空,则需要再次检查 slab 本身的 freelist 是否空,注意这里指的是 struct page 结构中的 freelist。

struct page {
           // 指向内存页中第一个空闲对象
           void *freelist;     /* first free object */
           // 该 slab 是否在对应 slab cache 的本地 CPU 缓存中
           // frozen = 1 表示缓存再本地 cpu 缓存中
           unsigned frozen:1;
}

关于这两个 freelist 之间的关系还有不了解的,可以回头看一下 slab cache 原理分析部分。

// 查看 page->freelist 中是否有其他 cpu 释放的空闲对象
static inline void *get_freelist(struct kmem_cache *s, struct page *page)
{
    // 用于存放要更新的 page 属性值
    struct page new;
    unsigned long counters;
    void *freelist;

    do {
        // 获取 page 结构的 freelist,当其他 cpu 向 page 释放对象时 freelist 指向被释放的空闲对象
        // 当 page 被 slab cache 的 cpu 本地缓存时,freelist 置为 null
        freelist = page->freelist;
        counters = page->counters;

        new.counters = counters;
        VM_BUG_ON(!new.frozen);
        // 更新 inuse 字段,表示 page 中的对象 objects 全部被分配出去了
        new.inuse = page->objects;
        // 如果 freelist != null,表示其他 cpu 又释放了一些对象到 page 中 (slub)。
        // 则 page->frozen = 1 , slub 依然冻结在 cpu 本地缓存中
        // 如果 freelist == null,则 page->frozen = 0, slub 从 cpu 本地缓存中脱离解冻
        new.frozen = freelist != NULL;
        // 最后 cas 原子更新 page 结构中的相应属性
        // 这里需要注意的是,当 page 被 slab cache 本地 cpu 缓存时,page -> freelist 需要置空。
        // 因为在本地 cpu 缓存场景下 page -> freelist 指向其他 cpu 释放的空闲对象列表
        // kmem_cache_cpu->freelist 指向的是被本地 cpu 缓存的空闲对象列表
        // 这两个列表中的空闲对象共同组成了 slub 中的空闲对象
    } while (!__cmpxchg_double_slab(s, page,
        freelist, counters,
        NULL, new.counters,
        "get_freelist"));

    return freelist;
}

如果两个 freelist 链表都没有空闲对象了,那就证明 slab cache 在当前 cpu 本地缓存中的 slab 已经为空了,将该 slab 从当前 cpu 本地缓存中脱离解冻,程序跳转到 new_slab 分支进入慢速分配路径。

从本地 cpu 缓存 partial 列表中分配

内核经过在 redo 分支的检查,现在已经确认了 slab cache 在当前 cpu 本地缓存的 slab 已经没有任何可供分配的空闲对象了。

下面内核正式进入到 slowpath 开始分配对象,首先内核会到本地 cpu 缓存的 partial 列表中去查看是否有一个 slab 可以分配对象。这里内核会从 partial 列表中的头结点开始遍历直到找到一个可以满足分配的 slab 出来。

随后内核会将该 slab 从 partial 列表中摘下,直接提升为新的本地 cpu 缓存,这样一来 slab cache 的本地 cpu 缓存就被更新了,内核通过 kmem_cache_cpu->freelist 指针将缓存 slab 中的第一个空闲对象分配出去,随后更新 kmem_cache_cpu->freelist 指向 slab 中的下一个空闲对象。

内核对 kmem_cache_cpu->partial 链表的相关操作:

// 定义在文件 /include/linux/slub_def.h 中
#ifdef CONFIG_SLUB_CPU_PARTIAL
// 获取 slab cache 本地 cpu 缓存的 partial 列表
#define slub_percpu_partial(c)      ((c)->partial)
// 将 partial 列表中第一个 slub 摘下,提升为 cpu 本地缓存,用于后续快速分配对象
#define slub_set_percpu_partial(c, p)       \
({                      \
    slub_percpu_partial(c) = (p)->next; \
})

从 NUMA 节点缓存中分配

如果 slab cache 本地 cpu 缓存 kmem_cache_cpu->partial 链表也是空的,接下来内核就只能到对应 NUMA 节点缓存中去分配对象了。

// slab cache 慢速路径下分配对象核心逻辑
static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,
            int node, struct kmem_cache_cpu **pc)
{
    // 从 numa node cache 中获取到的空闲对象列表
    void *freelist;
    // slab cache 本地 cpu 缓存
    struct kmem_cache_cpu *c = *pc;
    // 分配对象所在的内存页
    struct page *page;
    // 尝试从指定的 node 节点缓存 kmem_cache_node 中的 partial 列表获取可以分配空闲对象的 slub
    // 如果指定 numa 节点的内存不足,则会根据 cpu 访问距离的远近,进行跨 numa 节点分配
    freelist = get_partial(s, flags, node, c);

    if (freelist)
        // 返回 numa cache 中缓存的空闲对象列表
        return freelist;
    // 流程走到这里说明 numa cache 里缓存的 slub 也用尽了,无法找到可以分配对象的 slub 了
    // 只能向底层伙伴系统重新申请内存页(slub),然后从新的 slub 中分配对象
    page = new_slab(s, flags, node);
    // 将新申请的内存页 page (slub),缓存到 slab cache 的本地 cpu 缓存中
    if (page) {
        // 获取 slab cache 的本地 cpu 缓存
        c = raw_cpu_ptr(s->cpu_slab);
        // 刷新本地 cpu 缓存,将旧的 slub 缓存与 cpu 本地缓存解绑
        if (c->page)
            flush_slab(s, c);

        // 将新申请的 slub 与 cpu 本地缓存绑定,page->freelist 赋值给 kmem_cache_cpu->freelist
        freelist = page->freelist;
        // 绑定之后  page->freelist 置空
        // 现在新的 slub 中的空闲对象就已经缓存再了 slab cache 的本地 cpu 缓存中,后续就直接从这里分配了
        page->freelist = NULL;

        stat(s, ALLOC_SLAB);
        // 将新申请的 slub 对应的 page 赋值给 kmem_cache_cpu->page
        c->page = page;
        *pc = c;
    }
    // 返回空闲对象列表
    return freelist;
}

内核首先会在 get_partial 函数中找到我们指定的 NUMA 节点缓存结构 kmem_cache_node ,然后开始遍历 kmem_cache_node->partial 链表直到找到一个可供分配对象的 slab。然后将这个 slab 提升为 slab cache 的本地 cpu 缓存,并从 kmem_cache_node->partial 链表中依次填充 slab 到 kmem_cache_cpu->partial。

如果我们指定的 NUMA 节点 kmem_cache_node->partial 链表也是空的,随后内核就会跨 NUMA 节点进行查找,按照访问距离由近到远,开始查找其他 NUMA 节点 kmem_cache_node->partial 链表。

static void *get_partial(struct kmem_cache *s, gfp_t flags, int node,
        struct kmem_cache_cpu *c)
{
    // 从指定 node 的 kmem_cache_node 缓存中的 partial 列表中获取到的对象
    void *object;
    // 即将要所搜索的 kmem_cache_node 缓存对应 numa node
    int searchnode = node;
    // 如果我们指定的 numa node 已经没有空闲内存了,则选取访问距离最近的 numa node 进行跨节点内存分配
    if (node == NUMA_NO_NODE)
        searchnode = numa_mem_id();
    else if (!node_present_pages(node))
        searchnode = node_to_mem_node(node);

    // 从 searchnode 的 kmem_cache_node 缓存中的 partial 列表中获取对象
    object = get_partial_node(s, get_node(s, searchnode), c, flags);
    if (object || node != NUMA_NO_NODE)
        return object;
    // 如果 searchnode 对象的 kmem_cache_node 缓存中的 partial 列表是空的,没有任何可供分配的 slub
    // 那么继续按照访问距离,遍历 searchnode 之后的 numa node,进行跨节点内存分配
    return get_any_partial(s, flags, c);
}

get_partial 函数的主要内容是选取合适的 NUMA 节点缓存,优先使用我们指定的 NUMA 节点,如果指定的 NUMA 节点中没有足够的内存,内核就会跨 NUMA 节点按照访问距离的远近,选取一个合适的 NUMA 节点。

然后通过 get_partial_node 在选取的 NUMA 节点缓存 kmem_cache_node->partial 链表中查找 slab。

/*
 * Try to allocate a partial slab from a specific node.
 */
static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n,
                struct kmem_cache_cpu *c, gfp_t flags)
{
    // 接下来就会挨个遍历 kmem_cache_node 的 partial 列表中的 slub
    // 这两个变量用于临时存储遍历的 slub
    struct page *page, *page2;
    // 用于指向从 partial 列表 slub 中申请到的对象
    void *object = NULL;
    // 用于记录 slab cache 本地 cpu 缓存 kmem_cache_cpu 中所缓存的空闲对象总数(包括 partial 列表)
    // 后续会向 kmem_cache_cpu 中填充 slub
    unsigned int available = 0;
    // 临时记录遍历到的 slub 中包含的剩余空闲对象个数
    int objects;

    spin_lock(&n->list_lock);
    // 开始挨个遍历 kmem_cache_node 的 partial 列表,获取 slub 用于分配对象以及填充 kmem_cache_cpu
    list_for_each_entry_safe(page, page2, &n->partial, slab_list) {
        void *t;
        // page 表示当前遍历到的 slub,这里会从该 slub 中获取空闲对象赋值给 t
        // 并将 slub 从 kmem_cache_node 的 partial 列表上摘下
        t = acquire_slab(s, n, page, object == NULL, &objects);
        // 如果 t 是空的,说明 partial 列表上已经没有可供分配对象的 slub 了
        // slub 都满了,退出循环,进入伙伴系统重新申请 slub
        if (!t)            
            break;
        // objects 表示当前 slub 中包含的剩余空闲对象个数
        // available 用于统计目前遍历的 slub 中所有空闲对象个数
        // 后面会根据 available 的值来判断是否继续填充 kmem_cache_cpu
        available += objects;
        if (!object) {
            // 第一次循环会走到这里,第一次循环主要是满足当前对象分配的需求
            // 将 partila 列表中第一个 slub 缓存进 kmem_cache_cpu 中
            c->page = page;
            stat(s, ALLOC_FROM_PARTIAL);
            object = t;
        } else {
            // 第二次以及后面的循环就会走到这里,目的是从 kmem_cache_node 的 partial 列表中
            // 摘下 slub,然后填充进 kmem_cache_cpu 的 partial 列表里
            put_cpu_partial(s, page, 0);
            stat(s, CPU_PARTIAL_NODE);
        }
        // 这里是用于判断是否继续填充 kmem_cache_cpu 中的 partial 列表
        // kmem_cache_has_cpu_partial 用于判断 slab cache 是否配置了 cpu 缓存的 partial 列表
        // 配置了 CONFIG_SLUB_CPU_PARTIAL 选项意味着开启 kmem_cache_cpu 中的 partial 列表,没有配置的话, cpu 缓存中就不会有 partial 列表
        // kmem_cache_cpu 中缓存被填充之后的空闲对象个数(包括 partial 列表)不能超过 ( kmem_cache 结构中 cpu_partial 指定的个数 / 2 )
        if (!kmem_cache_has_cpu_partial(s)
            || available > slub_cpu_partial(s) / 2)
            // kmem_cache_cpu 已经填充满了,就退出循环,停止填充
            break;

    }

    spin_unlock(&n->list_lock);
    return object;
}

get_partial_node 函数通过遍历 NUMA 节点缓存结构 kmem_cache_node->partial 链表主要做两件事情:

  1. 将第一个遍历到的 slab 从 partial 链表中摘下,提升为本地 cpu 缓存 kmem_cache_cpu->page。

  2. 继续遍历 partial 链表,后面遍历到的 slab 会填充进本地 cpu 缓存 kmem_cache_cpu->partial 链表中,直到当前 cpu 缓存的所有空闲对象数目 available (既包括 kmem_cache_cpu->page 中的空闲对象也包括 kmem_cache_cpu->partial 链表中的空闲对象)超过了 kmem_cache->cpu_partial / 2 的限制。

现在 slab cache 的本地 cpu 缓存已经被填充好了,随后内核会从 kmem_cache_cpu->freelist 中分配一个空闲对象出来给进程使用。

// 从 kmem_cache_node 的 partial 列表中摘下一个 slub 分配对象
// 随后将摘下的 slub 放入 cpu 本地缓存 kmem_cache_cpu 中缓存,后续分配对象直接就会 cpu 缓存中分配
static inline void *acquire_slab(struct kmem_cache *s,
        struct kmem_cache_node *n, struct page *page,
        int mode, int *objects)
{
    void *freelist;
    unsigned long counters;
    struct page new;

    lockdep_assert_held(&n->list_lock);
    // page 表示即将从 kmem_cache_node 的 partial 列表摘下的 slub
    // 获取 slub  中的空闲对象列表 freelist
    freelist = page->freelist;
    counters = page->counters;
    new.counters = counters;
    // objects 存放该 slub 中还剩多少空闲对象
    *objects = new.objects - new.inuse;
    // mode = true 表示将 slub 摘下之后填充到 kmem_cache_cpu 缓存中
    // mode = false 表示将 slub 摘下之后填充到 kmem_cache_cpu 缓存的 partial 列表中
    if (mode) {
        new.inuse = page->objects;
        new.freelist = NULL;
    } else {
        new.freelist = freelist;
    }
    // slub 放入 kmem_cache_cpu 之后需要冻结,其他 cpu 不能从这里分配对象,只能释放对象
    new.frozen = 1;
    // 更新 slub (page表示)中的 freelist 和 counters
    if (!__cmpxchg_double_slab(s, page,
            freelist, counters,
            new.freelist, new.counters,
            "acquire_slab"))
        return NULL;
    // 将 slub (page表示)从 kmem_cache_node 的 partial 列表上摘下
    remove_partial(n, page);
    // 返回 slub 中的空闲对象列表
    return freelist;
}

如果还是不行,最后就只能通过 new_slab 函数到伙伴系统中重新申请一个 slab,并将这个 slab 提升为本地 cpu 缓存。

从伙伴系统中重新申请 slab

假设 slab cache 当前的架构如上图所示,本地 cpu 缓存 kmem_cache_cpu->page 为空,kmem_cache_cpu->partial 为空,kmem_cache_node->partial 链表也为空,比如 slab cache 在刚刚被创建出来的时候就是这个架构。

在这种情况下,内核就需要通过 new_slab 函数到伙伴系统中申请一个新的 slab,填充到 slab cache 的本地 cpu 缓存 kmem_cache_cpu->page 中。

static struct page *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{
    return allocate_slab(s,
        flags & (GFP_RECLAIM_MASK | GFP_CONSTRAINT_MASK), node);
}

static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
    // 用于指向从伙伴系统中申请到的内存页
    struct page *page;
    // kmem_cache 结构的中的 kmem_cache_order_objects oo,表示该 slub 需要多少个内存页,以及能够容纳多少个对象
    // kmem_cache_order_objects 的高 16 位表示需要的内存页个数,低 16 位表示能够容纳的对象个数
    struct kmem_cache_order_objects oo = s->oo;
    // 控制向伙伴系统申请内存的行为规范掩码
    gfp_t alloc_gfp;
    void *start, *p, *next;
    int idx;
    bool shuffle;
    // 向伙伴系统申请 oo 中规定的内存页
    page = alloc_slab_page(s, alloc_gfp, node, oo);
    if (unlikely(!page)) {
        // 如果伙伴系统无法满足正常情况下 oo 指定的内存页个数
        // 那么这里再次尝试用 min 中指定的内存页个数向伙伴系统申请内存页
        // min 表示当内存不足或者内存碎片的原因无法满足内存分配时,至少要保证容纳一个对象所使用内存页个数
        oo = s->min;
        alloc_gfp = flags;
        // 再次向伙伴系统申请容纳一个对象所需要的内存页(降级)
        page = alloc_slab_page(s, alloc_gfp, node, oo);
        if (unlikely(!page))
            // 如果内存还是不足,则走到 out 分支直接返回 null
            goto out;
        stat(s, ORDER_FALLBACK);
    }
    // 初始化 slub 对应的 struct page 结构中的属性
    // 获取 slub 可以容纳的对象个数
    page->objects = oo_objects(oo);
    // 将 slub cache  与 page 结构关联
    page->slab_cache = s;
    // 将 PG_slab 标识设置到 struct page 的 flag 属性中
    // 表示该内存页 page 被 slub 所管理
    __SetPageSlab(page);
    // 用 0xFC 填充 slub 中的内存,用于内核对内存访问越界检查
    kasan_poison_slab(page);
    // 获取内存页对应的虚拟内存地址
    start = page_address(page);
    // 在配置了 CONFIG_SLAB_FREELIST_RANDOM 选项的情况下
    // 会在 slub 的空闲对象中以随机的顺序初始化 freelist 列表
    // 返回值 shuffle = true 表示随机初始化 freelist,shuffle = false 表示按照正常的顺序初始化 freelist    
    shuffle = shuffle_freelist(s, page);
    // shuffle = false 则按照正常的顺序来初始化 freelist
    if (!shuffle) {
        // 获取 slub 第一个空闲对象的真正起始地址
        // slub 可能配置了 SLAB_RED_ZONE,这样会在 slub 对象内存空间两侧填充 red zone,防止内存访问越界
        // 这里需要跳过 red zone 获取真正存放对象的内存地址
        start = fixup_red_left(s, start);
        // 填充对象的内存区域以及初始化空闲对象
        start = setup_object(s, page, start);
        // 用 slub 中的第一个空闲对象作为 freelist 的头结点,而不是随机的一个空闲对象
        page->freelist = start;
        // 从 slub 中的第一个空闲对象开始,按照正常的顺序通过对象的 freepointer 串联起 freelist
        for (idx = 0, p = start; idx < page->objects - 1; idx++) {
            // 获取下一个对象的内存地址
            next = p + s->size;
            // 填充下一个对象的内存区域以及初始化
            next = setup_object(s, page, next);
            // 通过 p 的 freepointer 指针指向 next,设置 p 的下一个空闲对象为 next
            set_freepointer(s, p, next);
            // 通过循环遍历,就把 slub 中的空闲对象按照正常顺序串联在 freelist 中了
            p = next;
        }
        // freelist 中的尾结点的 freepointer 设置为 null
        set_freepointer(s, p, NULL);
    }
    // slub 的初始状态 inuse 的值为所有空闲对象个数
    page->inuse = page->objects;
    // slub 被创建出来之后,需要放入 cpu 本地缓存 kmem_cache_cpu 中
    page->frozen = 1;

out:
    if (!page)
        return NULL;
    // 更新 page 所在 numa 节点在 slab cache 中的缓存 kmem_cache_node 结构中的相关计数
    // kmem_cache_node 中包含的 slub 个数加 1,包含的总对象个数加 page->objects
    inc_slabs_node(s, page_to_nid(page), page->objects);
    return page;
}

内核在向伙伴系统申请 slab 之前,需要知道一个 slab 具体需要多少个物理内存页,而这些信息定义在 struct kmem_cache 结构中的 oo 属性中:

struct kmem_cache {
    // 其中低 16 位表示一个 slab 中所包含的对象总数,高 16 位表示一个 slab 所占有的内存页个数。
    struct kmem_cache_order_objects oo;
}

通过 oo 的高 16 位获取 slab 需要的物理内存页数,然后调用 alloc_pages 或者 __alloc_pages_node 向伙伴系统申请。

static inline struct page *alloc_slab_page(struct kmem_cache *s,
        gfp_t flags, int node, struct kmem_cache_order_objects oo)
{
    struct page *page;
    unsigned int order = oo_order(oo);

    if (node == NUMA_NO_NODE)
        page = alloc_pages(flags, order);
    else
        page = __alloc_pages_node(node, flags, order);

    return page;
}

如果当前 NUMA 节点中的空闲内存不足,或者由于内存碎片的原因导致伙伴系统无法满足 slab 所需要的内存页个数,导致分配失败。

那么内核会降级采用 kmem_cache->min 指定的尺寸,向伙伴系统申请只容纳一个对象所需要的最小内存页个数。

struct kmem_cache {
    // 当按照 oo 的尺寸为 slab 申请内存时,如果内存紧张,会采用 min 的尺寸为 slab 申请内存,可以容纳一个对象即可。
    struct kmem_cache_order_objects min;
}

如果伙伴系统仍然无法满足,那么就只能跨 NUMA 节点分配了。如果成功地向伙伴系统申请到了 slab 所需要的内存页 page。紧接着就会初始化 page 结构中与 slab 相关的属性。

通过 kasan_poison_slab 函数将 slab 中的内存用 0xFC 填充,用于 kasan 对于内存越界相关的检查。

// 定义在文件:/mm/kasan/kasan.h
#define KASAN_KMALLOC_REDZONE   0xFC  /* redzone inside slub object */

// 定义在文件:/mm/kasan/common.c
void kasan_poison_slab(struct page *page)
{
    unsigned long i;
    // slub 可能包含多个内存页 page,挨个遍历这些 page
    // 清除这些 page->flag 中的内存越界检查标记
    // 表示当访问到这些内存页的时候临时禁止内存越界检查
    for (i = 0; i < compound_nr(page); i++)
        page_kasan_tag_reset(page + i);
    // 用 0xFC 填充这些内存页的内存,用于内存访问越界检查
    kasan_poison_shadow(page_address(page), page_size(page),
            KASAN_KMALLOC_REDZONE);
}

最后会初始化 slab 中的 freelist 链表,将内存页中的空闲内存块通过 page->freelist 链表组织起来。

如果内核开启了 CONFIG_SLAB_FREELIST_RANDOM 选项,那么就会通过 shuffle_freelist 函数将内存页中空闲的内存块按照随机的顺序串联在 page->freelist 中。

如果没有开启,则会在 if (!shuffle) 分支中,按照正常的顺序初始化 page->freelist。

最后通过 inc_slabs_node 更新 NUMA 节点缓存 kmem_cache_node 结构中的相关计数。

struct kmem_cache_node {
    // slab 的个数
    atomic_long_t nr_slabs;
    // 该 node 节点中缓存的所有 slab 中包含的对象总和
    atomic_long_t total_objects;
};
static inline void inc_slabs_node(struct kmem_cache *s, int node, int objects)
{
    // 获取 page 所在 numa node 再 slab cache 中的缓存
    struct kmem_cache_node *n = get_node(s, node);

    if (likely(n)) {
        // kmem_cache_node 中的 slab 计数加1
        atomic_long_inc(&n->nr_slabs);
        // kmem_cache_node 中包含的总对象计数加 objects
        atomic_long_add(objects, &n->total_objects);
    }
}

初始化 slab freelist 链表

内核在对 slab 中的 freelist 链表初始化的时候,会有两种方式,一种是按照内存地址的顺序,一个一个的通过对象 freepointer 指针顺序串联所有空闲对象。

另外一种则是通过随机的方式,随机获取空闲对象,然后通过对象的 freepointer 指针将 slab 中的空闲对象按照随机的顺序串联起来。

考虑到顺序初始化 freelist 比较直观,为了方便大家的理解,笔者先为大家介绍顺序初始化的方式。

static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
    // 获取 slab 的起始内存地址
    start = page_address(page);
    // shuffle_freelist 随机初始化 freelist 链表,返回 false 表示需要顺序初始化 freelist
    shuffle = shuffle_freelist(s, page);
    // shuffle = false 则按照正常的顺序来初始化 freelist
    if (!shuffle) {
        // 获取 slub 第一个空闲对象的真正起始地址
        // slub 可能配置了 SLAB_RED_ZONE,这样会在 slub 对象内存空间两侧填充 red zone,防止内存访问越界
        // 这里需要跳过 red zone 获取真正存放对象的内存地址
        start = fixup_red_left(s, start);
        // 填充对象的内存区域以及初始化空闲对象
        start = setup_object(s, page, start);
        // 用 slub 中的第一个空闲对象作为 freelist 的头结点,而不是随机的一个空闲对象
        page->freelist = start;
        // 从 slub 中的第一个空闲对象开始,按照正常的顺序通过对象的 freepointer 串联起 freelist
        for (idx = 0, p = start; idx < page->objects - 1; idx++) {
            // 获取下一个对象的内存地址
            next = p + s->size;
            // 填充下一个对象的内存区域以及初始化
            next = setup_object(s, page, next);
            // 通过 p 的 freepointer 指针指向 next,设置 p 的下一个空闲对象为 next
            set_freepointer(s, p, next);
            // 通过循环遍历,就把 slub 中的空闲对象按照正常顺序串联在 freelist 中了
            p = next;
        }
        // freelist 中的尾结点的 freepointer 设置为 null
        set_freepointer(s, p, NULL);
    }
}

内核在顺序初始化 slab 中的 freelist 之前,首先需要知道 slab 的起始内存地址 start,但是考虑到 slab 如果配置了 SLAB_RED_ZONE 的情况,那么在 slab 对象左右两侧,内核均会插入两段 red zone,为了防止内存访问越界。

所以在这种情况下,我们通过 page_address 获取到的只是 slab 的起始内存地址,正是 slab 中第一个空闲对象的左侧 red zone 的起始位置。

所以我们需要通过 fixup_red_left 方法来修正 start 位置,使其越过 slab 对象左侧的 red zone,指向对象内存真正的起始位置,如上图中所示。

void *fixup_red_left(struct kmem_cache *s, void *p)
{
    // 如果 slub 配置了 SLAB_RED_ZONE,则意味着需要再 slub 对象内存空间两侧填充 red zone,防止内存访问越界
    // 这里需要跳过填充的 red zone 获取真正的空闲对象起始地址
    if (kmem_cache_debug(s) && s->flags & SLAB_RED_ZONE)
        p += s->red_left_pad;
    // 如果没有配置 red zone,则直接返回对象的起始地址
    return p;
}

当我们确定了对象的起始位置之后,对象所在的内存块也就确定了,随后调用 setup_object 函数来初始化内存块,这里会按照 slab 对象的内存布局进行填充相应的区域。

当初始化完对象的内存区域之后,slab 中的 freelist 指针就会指向这第一个已经被初始化好的空闲对象。

page->freelist = start;

随后通过 start + kmem_cache->size 顺序获取下一个空闲对象的起始地址,重复上述初始化对象过程。直到 slab 中的空闲对象全部串联在 freelist 中,freelist 中的最后一个空闲对象 freepointer 指向 null。

一般来说,都会使用顺序的初始化方式来初始化 freelist, 但出于安全因素的考虑,防止被攻击,会配置 CONFIG_SLAB_FREELIST_RANDOM 选项,这样就会使 slab 中的空闲对象以随机的方式串联在 freelist 中,无法预测。

在我们明白了 slab freelist 的顺序初始化方式之后,随机的初始化方式其实就很好理解了。

随机初始化和顺序初始化唯一不同的点在于,获取空闲对象起始地址的方式不同:

  1. 顺序初始化的方式是直接获取 slab 中第一个空闲对象的地址,然后通过 start + kmem_cache->size 按照顺序一个一个地获取后面对象地址。

  2. 随机初始化的方式则是通过随机的方式获取 slab 中空闲对象,也就是说 freelist 中的头结点可能是 slab 中的第一个对象,也可能是第三个对象。后续也是通过这种随机的方式来获取下一个随机的空闲对象。

// 返回值为 true 表示随机的初始化 freelist,false 表示采用第一个空闲对象初始化 freelist
static bool shuffle_freelist(struct kmem_cache *s, struct page *page)
{
    // 指向第一个空闲对象
    void *start;
    void *cur;
    void *next;
    unsigned long idx, pos, page_limit, freelist_count;
    // 如果没有配置 CONFIG_SLAB_FREELIST_RANDOM 选项或者 slub 容纳的对象个数小于 2
    // 则无需对 freelist 进行随机初始化
    if (page->objects < 2 || !s->random_seq)
        return false;
    // 获取 slub 中可以容纳的对象个数
    freelist_count = oo_objects(s->oo);
    // 获取用于随机初始化 freelist 的随机位置
    pos = get_random_int() % freelist_count;
    page_limit = page->objects * s->size;
    // 获取 slub 第一个空闲对象的真正起始地址
    // slub 可能配置了 SLAB_RED_ZONE,这样会在 slub 中对象内存空间两侧填充 red zone,防止内存访问越界
    // 这里需要跳过 red zone 获取真正存放对象的内存地址
    start = fixup_red_left(s, page_address(page));

   // 根据随机位置 pos 获取第一个随机对象的距离 start 的偏移 idx
   // 返回第一个随机对象的内存地址 cur = start + idx
    cur = next_freelist_entry(s, page, &pos, start, page_limit,
                freelist_count);
    // 填充对象的内存区域以及初始化空闲对象
    cur = setup_object(s, page, cur);
    // 第一个随机对象作为 freelist 的头结点
    page->freelist = cur;
    // 以 cur 为头结点随机初始化 freelist(每一个空闲对象都是随机的)
    for (idx = 1; idx < page->objects; idx++) {
        // 随机获取下一个空闲对象
        next = next_freelist_entry(s, page, &pos, start, page_limit,
            freelist_count);
        // 填充对象的内存区域以及初始化空闲对象
        next = setup_object(s, page, next);
        // 设置 cur 的下一个空闲对象为 next
        // next 对象的指针就是 freepointer,存放于 cur 对象的 s->offset 偏移处
        set_freepointer(s, cur, next);
        // 通过循环遍历,就把 slub 中的空闲对象随机的串联在 freelist 中了
        cur = next;
    }
    // freelist 中的尾结点的 freepointer 设置为 null
    set_freepointer(s, cur, NULL);
    // 表示随机初始化 freelist
    return true;
}

slab 对象的初始化

内核按照 kmem_cache->size 指定的尺寸,将物理内存页中的内存划分成一个一个的小内存块,每一个小内存块即是 slab 对象占用的内存区域。setup_object 函数用于初始化这些内存区域,并对 slab 对象进行内存布局。

static void *setup_object(struct kmem_cache *s, struct page *page,
                void *object)
{
    // 初始化对象的内存区域,填充相关的字节,比如填充 red zone,以及 poison 对象
    setup_object_debug(s, page, object);
    object = kasan_init_slab_obj(s, object);
    // 如果 kmem_cache 中设置了对象的构造函数 ctor,则用构造函数初始化对象
    if (unlikely(s->ctor)) {
        kasan_unpoison_object_data(s, object);
        // 使用用户指定的构造函数初始化对象
        s->ctor(object);
        // 在对象内存区域的开头用 0xFC 填充一段 KASAN_SHADOW_SCALE_SIZE 大小的区域
        // 用于对内存访问越界的检查
        kasan_poison_object_data(s, object);
    }
    return object;
}
// 定义在文件:/mm/kasan/kasan.h
#define KASAN_KMALLOC_REDZONE   0xFC  /* redzone inside slub object */
#define KASAN_SHADOW_SCALE_SIZE (1UL << KASAN_SHADOW_SCALE_SHIFT)
// 定义在文件:/arch/x86/include/asm/kasan.h
#define KASAN_SHADOW_SCALE_SHIFT 3

void kasan_poison_object_data(struct kmem_cache *cache, void *object)
{
    // 在对象内存区域的开头用 0xFC 填充一段 KASAN_SHADOW_SCALE_SIZE 大小的区域
    // 用于对内存访问越界的检查
    kasan_poison_shadow(object,
            round_up(cache->object_size, KASAN_SHADOW_SCALE_SIZE),
            KASAN_KMALLOC_REDZONE);
}

关于 slab 对象内存布局的核心逻辑封装在 setup_object_debug 函数中:

// 定义在文件:/include/linux/poison.h
#define SLUB_RED_INACTIVE 0xbb

static void setup_object_debug(struct kmem_cache *s, struct page *page,
                                void *object)
{
    // SLAB_STORE_USER:存储最近访问该对象的 owner 信息,方便 bug 追踪
    // SLAB_RED_ZONE:在 slub 中对象内存区域的前后填充分别填充一段 red zone 区域,防止内存访问越界
    // __OBJECT_POISON:在对象内存区域中填充一些特定的字符,表示对象特定的状态。比如:未被分配状态
    if (!(s->flags & (SLAB_STORE_USER|SLAB_RED_ZONE|__OBJECT_POISON)))
        return;
    // 初始化对象内存,比如填充 red zone,以及 poison
    init_object(s, object, SLUB_RED_INACTIVE);
    // 设置 SLAB_STORE_USER 起作用,初始化访问对象的所有者相关信息
    init_tracking(s, object);
}

init_object 函数主要针对 slab 对象的内存区域进行布局,这里包括对 red zone 的填充,以及 POISON 对象的 object size 区域。

// 定义在文件:/include/linux/poison.h
#define SLUB_RED_INACTIVE   0xbb

// 定义在文件:/include/linux/poison.h
#define POISON_FREE 0x6b /* for use-after-free poisoning */
#define POISON_END 0xa5 /* end-byte of poisoning */

static void init_object(struct kmem_cache *s, void *object, u8 val)
{
    // p 为真正存储对象的内存区域起始地址(不包含填充的 red zone)
    u8 *p = object;
    // red zone 位于真正存储对象内存区域 object size 的左右两侧,分别有一段 red zone
    if (s->flags & SLAB_RED_ZONE)
        // 首先使用 0xbb 填充对象左侧的 red zone
        // 左侧 red zone 区域为对象的起始地址到  s->red_left_pad 的长度
        memset(p - s->red_left_pad, val, s->red_left_pad);

    if (s->flags & __OBJECT_POISON) {
        // 将对象的内容用 0x6b 填充,表示该对象在 slub 中还未被使用
        memset(p, POISON_FREE, s->object_size - 1);
        // 对象的最后一个字节用 0xa5 填充,表示 POISON 的末尾
        p[s->object_size - 1] = POISON_END;
    }

    // 在对象内存区域 object size 的右侧继续用 0xbb 填充右侧 red zone
    // 右侧 red zone 的位置为:对象真实内存区域的末尾开始一个字长的区域
    // s->object_size 表示对象本身的内存占用,s->inuse 表示对象在 slub 管理体系下的真实内存占用(包含填充字节数)
    // 通常会在对象内存区域末尾处填充一个字长大小的 red zone 区域
    // 对象右侧 red zone 区域后面紧跟着的就是 freepointer
    if (s->flags & SLAB_RED_ZONE)
        memset(p + s->object_size, val, s->inuse - s->object_size);
}

内核首先会用 0xbb 来填充对象左侧 red zone,长度为 kmem_cache-> red_left_pad。

随后内核会用 0x6b 填充 object size 内存区域,并用 0xa5 填充该区域的最后一个字节。object size 内存区域正是真正存储对象的区域。

最后用 0xbb 来填充对象右侧 red zone,右侧 red zone 的起始地址为:p + s->object_size,长度为:s->inuse - s->object_size。如下图所示:

源码查看 slab cache 的回收

在接下来的内容中,笔者为大家介绍一下内核是如何将内存块释放回 slab cache 的。我们还是先从 slab cache 释放内存的内核 API 开始聊起

内核提供了 kmem_cache_free 函数,用于将对象释放回其所属的 slab cache 中,参数 x 表示我们要释放的内存块(对象)的虚拟内存地址,参数 s 指向内存块所属的 slab cache。

void kmem_cache_free(struct kmem_cache *s, void *x)
{
    // 确保指定的是 slab cache : s 为对象真正所属的 slab cache
    s = cache_from_obj(s, x);
    if (!s)
        return;
    // 将对象释放会 slab cache 中
    slab_free(s, virt_to_head_page(x), x, NULL, 1, _RET_IP_);
}

内存释放之前的校验工作

在开始释放内存块 x 之前,内核需要首先通过 cache_from_obj 函数确认内存块 x 是否真正属于我们指定的 slab cache。不能将内存块释放到其他的 slab cache 中。

随后在 virt_to_head_page 函数中通过内存块的虚拟内存地址 x 找到其所在的物理内存页 page。然后调用 slab_free 将内存块释放回 slab cache 中。

通过虚拟内存地址寻找物理内存页 page 的过程涉及到的背景知识比较复杂,这个笔者后面会单独拎出来介绍,这里大家只需要简单了解 virt_to_head_page 函数的作用即可。

static inline struct kmem_cache *cache_from_obj(struct kmem_cache *s, void *x)
{
    struct kmem_cache *cachep;
    // 通过对象的虚拟内存地址 x 找到对象所属的 slab cache
    cachep = virt_to_cache(x);
    // 校验指定的 slab cache : s 是否是对象真正所属的 slab cache : cachep
    WARN_ONCE(cachep && !slab_equal_or_root(cachep, s),
          "%s: Wrong slab cache. %s but object is from %s\n",
          __func__, s->name, cachep->name);
    return cachep;
}

virt_to_cache 函数首先会通过释放对象的虚拟内存地址找到其所在的物理内存页 page,然后通过 struct page 结构中的 slab_cache 指针找到 page 所属的 slab cache。

static inline struct kmem_cache *virt_to_cache(const void *obj)
{
    struct page *page;
    // 根据对象的虚拟内存地址 *obj 找到其所在的内存页 page
    // 如果 slub 背后是多个内存页(复合页),则返回复合页的首页 head page
    page = virt_to_head_page(obj);
    if (WARN_ONCE(!PageSlab(page), "%s: Object is not a Slab page!\n",
                    __func__))
        return NULL;
    // 通过 page 结构中的 slab_cache 属性找到其所属的 slub
    return page->slab_cache;
}

slab cache 在快速路径下回收内存

static __always_inline void slab_free(struct kmem_cache *s, struct page *page,
                      void *head, void *tail, int cnt,
                      unsigned long addr)
{
    if (slab_free_freelist_hook(s, &head, &tail))
        do_slab_free(s, page, head, tail, cnt, addr);
}

slab cache 回收内存相关的逻辑封装在 do_slab_free 函数中:

static __always_inline void do_slab_free(struct kmem_cache *s,
                struct page *page, void *head, void *tail,
                int cnt, unsigned long addr)
  1. 参数 kmem_cache *s 表示释放对象所在的 slab cache,指定我们要将对象释放到哪里。
  2. 参数 page 表示释放对象所在的 slab,slab 在内核中使用 struct page 结构来表示。
  3. 参数 head 指向释放对象的虚拟内存地址(起始内存地址)。
  4. 该函数支持向 slab cache 批量的释放多个对象,参数 tail 指向批量释放对象中最后一个对象的虚拟内存地址。
  5. 参数 cnt 表示释放对象的个数,也是用于批量释放对象
  6. 参数 addr 用于 slab 调试,这里我们不需要关心。

内存回收总体也是分为快速路径 fastpath 和慢速路径 slow path,在 do_slab_free 函数中内核会首先尝试 fastpath 的回收流程。

如果释放对象所在的 slab 刚好是 slab cache 在本地 cpu 缓存 kmem_cache_cpu->page 缓存的 slab,那么内核就会直接将对象释放回缓存 slab 中。

static __always_inline void do_slab_free(struct kmem_cache *s,
                struct page *page, void *head, void *tail,
                int cnt, unsigned long addr)
{
    void *tail_obj = tail ? : head;
    struct kmem_cache_cpu *c;
    // slub 中对象分配与释放流程的全局事务 id
    // 既可以用来标识同一个分配或者释放的事务流程,也可以用来标识区分所属 cpu 本地缓存
    unsigned long tid;
redo:
    // 接下来我们需要获取 slab cache 的 cpu 本地缓存
    // 这里的 do..while 循环是要保证获取到的 cpu 本地缓存 c 是属于执行进程的当前 cpu
    // 因为进程可能由于抢占或者中断的原因被调度到其他 cpu 上执行,所需需要确保两者的 tid 是否一致
    do {
        // 获取执行当前进程的 cpu 中的 tid 字段
        tid = this_cpu_read(s->cpu_slab->tid);
        // 获取 cpu 本地缓存 cpu_slab
        c = raw_cpu_ptr(s->cpu_slab);
        // 如果两者的 tid 字段不一致,说明进程已经被调度到其他 cpu 上了
        // 需要再次获取正确的 cpu 本地缓存
    } while (IS_ENABLED(CONFIG_PREEMPT) &&
         unlikely(tid != READ_ONCE(c->tid)));

    // 如果释放对象所属的 slub (page 表示)正好是 cpu 本地缓存的 slub
    // 那么直接将对象释放到 cpu 缓存的 slub 中即可,这里就是快速释放路径 fastpath
    if (likely(page == c->page)) {
        // 将对象释放至 cpu 本地缓存 freelist 中的头结点处
        // 释放对象中的 freepointer 指向原来的 c->freelist
        set_freepointer(s, tail_obj, c->freelist);
        // cas 更新 cpu 本地缓存 s->cpu_slab 中的 freelist,以及 tid
        if (unlikely(!this_cpu_cmpxchg_double(
                s->cpu_slab->freelist, s->cpu_slab->tid,
                c->freelist, tid,
                head, next_tid(tid)))) {

            note_cmpxchg_failure("slab_free", s, tid);
            goto redo;
        }
        stat(s, FREE_FASTPATH);
    } else
        // 如果当前释放对象并不在 cpu 本地缓存中,那么就进入慢速释放路径 slowpath
        __slab_free(s, page, head, tail_obj, cnt, addr);

}

既然是快速路径释放,那么在 do_slab_free 函数的开始首先就获取 slab cache 的本地 cpu 缓存结构 kmem_cache_cpu,为了保证我们获取到的 cpu 本地缓存结构与运行当前进程所在的 cpu 是相符的,所以这里还是需要在 do .... while 循环内判断两者的 tid。

内核在确保已经获取了正确的 kmem_cache_cpu 结构之后,就会马上判断该释放对象所在的 slab 是否正是 slab cache 本地 cpu 缓存了的 slab —— page == c->page

如果是的话,直接将对象释放回缓存 slab 中,调整 kmem_cache_cpu->freelist 指向刚刚释放的对象,调整释放对象的 freepointer 指针指向原来的 kmem_cache_cpu->freelist 。

如果当前释放对象并不在 slab cache 的本地 cpu 缓存中,那么就会进入慢速路径 slowpath 释放内存。

slab cache 在慢速路径下回收内存

slab cache 在慢速路径下回收内存的逻辑比较复杂,因为这里涉及到很多的场景,需要改变释放对象所属 slab 在 slab cache 架构中的位置。

在将对象释放回对应的 slab 中之前,内核需要首先清理一下对象所占的内存,重新填充对象的内存布局恢复到初始未使用状态。因为对象所占的内存此时包含了很多已经被使用过的无用信息。这项工作内核在 free_debug_processing 函数中完成。

在将对象所在内存恢复到初始状态之后,内核首先会将对象直接释放回其所属的 slab 中,并调整 slab 结构 page 的相关属性。

接下来就到复杂的处理部分了,内核会在这里处理多种场景,并改变 slab 在 slab cache 架构中的位置。

  1. 如果 slab 本来就在 slab cache 本地 cpu 缓存 kmem_cache_cpu->partial 链表中,那么对象在释放之后,slab 的位置不做任何改变。

  2. 如果 slab 不在 kmem_cache_cpu->partial 链表中,并且该 slab 由于对象的释放刚好由一个 full slab 变为了一个 partial slab,为了利用局部性的优势,内核需要将该 slab 插入到 kmem_cache_cpu->partial 链表中。

  1. 如果 slab 不在 kmem_cache_cpu->partial 链表中,并且该 slab 由于对象的释放刚好由一个 partial slab 变为了一个 empty slab,说明该 slab 并不是很活跃,内核会将该 slab 放入对应 NUMA 节点缓存 kmem_cache_node->partial 链表中,刀枪入库,马放南山。

  1. 如果不符合第 2, 3 种场景,但是 slab 本来就在对应的 NUMA 节点缓存 kmem_cache_node->partial 链表中,那么对象在释放之后,slab 的位置不做任何改变。

下面我们就到内核的源码实现中,来一一验证这四种慢速释放场景。

static void __slab_free(struct kmem_cache *s, struct page *page,
            void *head, void *tail, int cnt,
            unsigned long addr)

{
    // 用于指向对象释放回 slub 之前,slub 的 freelist
    void *prior;
    // 对象所属的 slub 之前是否在本地 cpu 缓存 partial 链表中
    int was_frozen;
    // 后续会对 slub 对应的 page 结构相关属性进行修改
    // 修改后的属性会临时保存在 new 中,后面通过 cas 替换
    struct page new;
    unsigned long counters;
    struct kmem_cache_node *n = NULL;
    stat(s, FREE_SLOWPATH);

    // free_debug_processing 中会调用 init_object,清理对象内存无用信息,重新恢复对象内存布局到初始状态
    if (kmem_cache_debug(s) &&
     !free_debug_processing(s, page, head, tail, cnt, addr))
        return;

    do {
        // 获取 slub 中的空闲对象列表,prior = null 表示此时 slub 是一个 full slub,意思就是该 slub 中的对象已经全部被分配出去了
        prior = page->freelist;
        counters = page->counters;
        // 将释放的对象插入到 freelist 的头部,将对象释放回 slub
        // 将 tail 对象的 freepointer 设置为 prior
        set_freepointer(s, tail, prior);
        // 将原有 slab 的相应属性赋值给 new page
        new.counters = counters;
        // 获取原来 slub 中的 frozen 状态,是否在 cpu 缓存 partial 链表中
        was_frozen = new.frozen;
        // inuse 表示 slub 已经分配出去的对象个数,这里是释放 cnt 个对象,所以 inuse 要减去 cnt
        new.inuse -= cnt;
        // !new.inuse 表示此时 slub 变为了一个 empty slub,意思就是该 slub 中的对象还没有分配出去,全部在 slub 中
        // !prior 表示由于本次对象的释放,slub 刚刚从一个 full slub 变成了一个 partial slub (意思就是该 slub 中的对象部分分配出去了,部分没有分配出去)
        // !was_frozen 表示该 slub 不在 cpu 本地缓存中
        if ((!new.inuse || !prior) && !was_frozen) {
            // 注意:进入该分支的 slub 之前都不在 cpu 本地缓存中
            // 如果配置了 CONFIG_SLUB_CPU_PARTIAL 选项,那么表示 cpu 本地缓存 kmem_cache_cpu 结构中包含 partial 列表,用于 cpu 缓存部分分配的 slub
            if (kmem_cache_has_cpu_partial(s) && !prior) {
                // 如果 kmem_cache_cpu 包含 partial 列表并且该 slub 刚刚由 full slub 变为 partial slub
                // 冻结该 slub,后续会将该 slub 插入到 kmem_cache_cpu 的 partial 列表中
                new.frozen = 1;

            } else { 
                // 如果 kmem_cache_cpu 中没有配置 partial 列表,那么直接释放至 kmem_cache_node 中
                // 或者该 slub 由一个 partial slub 变为了 empty slub,调整 slub 的位置到 kmem_cache_node->partial 链表中
                n = get_node(s, page_to_nid(page));
                // 后续会操作 kmem_cache_node 中的 partial 列表,所以这里需要获取 list_lock
                spin_lock_irqsave(&n->list_lock, flags);

            }
        }
        // cas 更新 slub 中的 freelist 以及 counters
    } while (!cmpxchg_double_slab(s, page,
        prior, counters,
        head, new.counters,
        "__slab_free"));

    // 该分支要处理的场景是:
    // 1: 该 slub 原来不在 cpu 本地缓存的 partial 列表中(!was_frozen),但是该 slub 刚刚从 full slub 变为了 partial slub,需要放入 cpu-> partial 列表中
    // 2: 该 slub 原来就在 cpu 本地缓存的 partial 列表中,直接将对象释放回 slub 即可
    if (likely(!n)) {
        // 处理场景 1
        if (new.frozen && !was_frozen) {
            // 将 slub 插入到 kmem_cache_cpu 中的 partial 列表中
            put_cpu_partial(s, page, 1);
            stat(s, CPU_PARTIAL_FREE);
        }

        // 处理场景2,因为之前已经通过 set_freepointer 将对象释放回 slub 了,这里只需要记录 slub 状态即可
        if (was_frozen)
            stat(s, FREE_FROZEN);
        return;
    }

    // 后续的逻辑就是处理需要将 slub 放入 kmem_cache_node 中的 partial 列表的情形
    // 在将 slub 放入 node 缓存之前,需要判断 node 缓存的 nr_partial 是否超过了指定阈值 min_partial(位于 kmem_cache 结构)
    // nr_partial 表示 kmem_cache_node 中 partial 列表中缓存的 slub 个数
    // min_partial 表示 slab cache 规定 kmem_cache_node 中 partial 列表可以容纳的 slub 最大个数
    // 如果 nr_partial 超过了最大阈值 min_partial,则不能放入 kmem_cache_node 里
    if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
        // 如果 slub 变为了一个 empty slub 并且 nr_partial 超过了最大阈值 min_partial
        // 跳转到 slab_empty 分支,将 slub 释放回伙伴系统中
        goto slab_empty;

    // 如果 cpu 本地缓存中没有配置 partial 列表并且 slub 刚刚从 full slub 变为 partial slub
    // 则将 slub 插入到 kmem_cache_node 中
    if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {
        remove_full(s, n, page);
        add_partial(n, page, DEACTIVATE_TO_TAIL);
        stat(s, FREE_ADD_PARTIAL);
    }
    spin_unlock_irqrestore(&n->list_lock, flags);
    // 剩下的情况均属于 slub 原来就在 kmem_cache_node 中的 partial 列表中
    // 直接将对象释放回 slub 即可,无需改变 slub 的位置,直接返回
    return;

slab_empty:
    // 该分支处理的场景是: slub 太多了,将 empty slub 释放会伙伴系统
    // 首先将 slub 从对应的管理链表上删除
    if (prior) {
        /*
         * Slab on the partial list.
         */
        remove_partial(n, page);
        stat(s, FREE_REMOVE_PARTIAL);
    } else {
        /* Slab must be on the full list */
        remove_full(s, n, page);
    }
    spin_unlock_irqrestore(&n->list_lock, flags);
    stat(s, FREE_SLAB);
    // 释放 slub 回伙伴系统,底层调用 __free_pages 将 slub 所管理的所有 page 释放回伙伴系统
    discard_slab(s, page);
}

直接释放对象回 slab,调整 slab 相关属性

这一部分的逻辑比较简单,在 __slab_free 内存释放流程的开始,内核不管三七二十一,首先会将对象直接释放回其所在的 slab 中。

当对象被释放回 slab 中之后,slab 结构中的相应属于就需要做出相应的调整,比如:

  1. 调整 page 结构中的 freelist,它需要指向刚刚被释放的对象。
  2. 调整 page 结构中的 inuse,inuse 表示 slab 中已经被分配出去的对象个数,此时对象已经释放回 slab 中,需要调整 inuse 字段。
  3. 后续内核会根据不同情况,调整 page 结构的 frozen 属性。

内核会定义一个新的 page 结构 new,将原有 slab 的 page 结构需要更新的上述属性的新值,先一一复制给 new 的对应属性,最后通过 cmpxchg_double_slab 原子更新 slab 对应的属性。

struct page {

        struct {    /*  slub 相关字段 */
             ........ 省略 .........

            // 指向 page 所属的 slab cache
            struct kmem_cache *slab_cache;
            // 指向 slab 中第一个空闲对象
            void *freelist;     /* first free object */
            union {
                unsigned long counters;
                struct {            /* SLUB */             
                    // slab 中已经分配出去的对象
                    unsigned inuse:16;
                    // slab 中包含的对象总数
                    unsigned objects:15;
                    // 该 slab 是否在对应 slab cache 的本地 CPU 缓存中
                    // frozen = 1 表示缓存再本地 cpu 缓存中
                    unsigned frozen:1;
                };
            };
        };

}

内核明明在 do .... while 循环中更新了 freelist,inuse,frozen 这三个属性,而 counters 属性只是读取并没有更新操作,是由于 counters,inuse,frozen 共用一块内存,当 inuse,frozen 的值发生变化之后,虽然 counters 的值没有发生变化,但是我们可以通过更新 counters 来将原有 slab 中的这块内存一起更新掉,这样 inuse,frozen 的值也跟着被更新了。

释放对象所属 slab 本来就在 cpu 缓存 partial 链表中

was_frozen 指向释放对象所属 slab 结构中的 frozen 属性,用来表示 slab 是否在 slab cache 的本地 cpu 缓存 partial 链表中。

 was_frozen = new.frozen;

如果 was_frozen == true 表示释放对象所属 slab 本来就在 kmem_cache_cpu->partial 链表中,内核将对象直接释放回 slab 中,slab 的原有位置不做改变。

下面我们看下 was_frozen == fasle 也就是 slab 不在 kmem_cache_cpu->partial 链表中 的时候,内核又是如何处理的 ?

释放对象所属 slab 从 full slab 变为了 partial slab

如果释放对象所属 slab 原来是一个 full slab,恰恰说明该 slab 拥有比较好的局部性,进程经常从该 slab 中分配对象,slab 十分活跃,才导致它变为了一个 full slab。

 prior = page->freelist = null
 ```

随着对象的释放,该 slab 从一个 full slab 变为了 partial slab,内核为了更好的利用该 slab 的局部性,所以需要将该 slab 插入到 slab cache 的本地 cpu 缓存 kmem_cache_cpu->partial 链表中。

```c
        if (kmem_cache_has_cpu_partial(s) && !prior) {
                new.frozen = 1;

        } 

        if (new.frozen && !was_frozen) {
            // 将 slub 插入到 kmem_cache_cpu 中的 partial 列表中
            put_cpu_partial(s, page, 1);
            stat(s, CPU_PARTIAL_FREE);
        }

将 slab 插入到 kmem_cache_cpu->partial 链表的逻辑封装在 put_cpu_partial 中,put_cpu_partial 函数最重要的一个考量逻辑是需要确保 kmem_cache_cpu->partial 链表中所有 slab 中包含的空闲对象总数不能超过 kmem_cache->cpu_partial 的限制。

struct kmem_cache {
    // 限定 slab cache 在每个 cpu 本地缓存 partial 链表中所有 slab 中空闲对象的总数
    unsigned int cpu_partial;
};

在释放对象所在的 slab 插入到 kmem_cache_cpu->partial 链表之前,put_cpu_partial 函数需要判断当前 kmem_cache_cpu->partial 链表中包含的空闲对象总数 pobjects 是否超过了 kmem_cache->cpu_partial 的限制。

如果超过了,则需要先将当前 kmem_cache_cpu->partial 链表中所有的 slab 转移到其对应的 NUMA 节点缓存 kmem_cache_node->partial 链表中。转移完成之后,在将释放对象所属的 slab 插入到 kmem_cache_cpu->partial 链表中。

static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain)
{
// 只有配置了 CONFIG_SLUB_CPU_PARTIAL 选项,kmem_cache_cpu 中才有会 partial 列表
#ifdef CONFIG_SLUB_CPU_PARTIAL
    // 指向原有 kmem_cache_cpu 中的 partial 列表
    struct page *oldpage;
    // slub 所在管理列表中的 slub 个数,这里的列表是指 partial 列表
    int pages;
    // slub 所在管理列表中的包含的空闲对象总数,这里的列表是指 partial 列表
    // 内核会将列表总体的信息存放在列表首页 page 的相关字段中
    int pobjects;
    // 禁止抢占
    preempt_disable();
    do {
        pages = 0;
        pobjects = 0;
        // 获取 slab cache 中原有的 cpu 本地缓存 partial 列表首页
        oldpage = this_cpu_read(s->cpu_slab->partial);
        // 如果 partial 列表不为空,则需要判断 partial 列表中所有 slub 包含的空闲对象总数是否超过了 s->cpu_partial 规定的阈值
        // 超过 s->cpu_partial 则需要将 kmem_cache_cpu->partial 列表中原有的所有 slub 转移到 kmem_cache_node-> partial 列表中
        // 转移之后,再把当前 slub 插入到 kmem_cache_cpu->partial 列表中
        // 如果没有超过 s->cpu_partial ,则无需转移直接插入
        if (oldpage) {
            // 从 partial 列表首页中获取列表中包含的空闲对象总数
            pobjects = oldpage->pobjects;
            // 从 partial 列表首页中获取列表中包含的 slub 总数
            pages = oldpage->pages;

            if (drain && pobjects > s->cpu_partial) {
                unsigned long flags;
                // 关闭中断,防止并发访问
                local_irq_save(flags);
                // partial 列表中所包含的空闲对象总数 pobjects 超过了 s->cpu_partial 规定的阈值
                // 则需要将现有 partial 列表中的所有 slub 转移到相应的 kmem_cache_node->partial 列表中
                unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));
                // 恢复中断
                local_irq_restore(flags);
                // 重置 partial 列表
                oldpage = NULL;
                pobjects = 0;
                pages = 0;
                stat(s, CPU_PARTIAL_DRAIN);
            }
        }
        // 无论 kmem_cache_cpu-> partial 列表中的 slub 是否需要转移
        // 释放对象所在的 slub 都需要填加到  kmem_cache_cpu-> partial 列表中
        pages++;
        pobjects += page->objects - page->inuse;

        page->pages = pages;
        page->pobjects = pobjects;
        page->next = oldpage;
        // 通过 cas 将 slub 插入到 partial 列表的头部
    } while (this_cpu_cmpxchg(s->cpu_slab->partial, oldpage, page)
                                != oldpage);

    // s->cpu_partial = 0 表示 kmem_cache_cpu->partial 列表不能存放 slub
    // 将释放对象所在的 slub 转移到  kmem_cache_node-> partial 列表中
    if (unlikely(!s->cpu_partial)) {
        unsigned long flags;
        local_irq_save(flags);
        unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));
        local_irq_restore(flags);
    }
    preempt_enable();
#endif  /* CONFIG_SLUB_CPU_PARTIAL */
}

如何知道 kmem_cache_cpu->partial 链表所包含的空闲对象总数到底是多少呢,这就用到了 struct page 结构中的两个重要属性:

struct page {
      // slab 所在链表中的包含的 slab 总数
      int pages;  
      // slab 所在链表中包含的对象总数
      int pobjects; 
}

我们都知道 slab 在内核中的数据结构用 struct page 中的相关结构体表示,slab 在 slab cache 架构中一般是由 kmem_cache_cpu->partial 链表和 kmem_cache_node->partial 链表来组织管理。

内核会将 parital 链表中的这些总体统计信息存储在链表首个 slab 结构中。也就是说存储在首个 page 结构中的 pages 属性和 pobjects 属性中。

在 put_cpu_partial 函数的开始,内核直接获取 parital 链表的首个 slab —— oldpage,并通过 oldpage->pobjectss->cpu_partial 比较,来判断当前 kmem_cache_cpu->partial 链表中包含的空闲对象总数是否超过了 kmem_cache 结构中规定的 cpu_partial 阈值。

如果超过了,则通过 unfreeze_partials 转移 kmem_cache_cpu->partial 链表中的所有 slab 到对应的 kmem_cache_node->partial 链表中。

既然 kmem_cache_cpu->partial 链表有容量的限制,那么同样 kmem_cache_node->partial 链表中的容量也会有限制。

kmem_cache_node->partial 链表中所包含 slab 个数的上限由 kmem_cache 结构中的 min_partial 属性决定。

struct kmem_cache {

    // slab cache 在 numa node 中缓存的 slab 个数上限,slab 个数超过该值,空闲的 empty slab 则会被回收至伙伴系统
    unsigned long min_partial;
}

如果当前要转移的 slab 是一个 empty slab,并且此时 kmem_cache_node->partial 链表所包含的 slab 个数 kmem_cache_node->nr_partial 已经超过了kmem_cache-> min_partial 的限制,那么内核就会直接将这个 empty slab 释放回伙伴系统中。

// 将 kmem_cache_cpu->partial 列表中包含的 slub unfreeze
// 并转移到对应的 kmem_cache_node->partial 列表中
static void unfreeze_partials(struct kmem_cache *s,
        struct kmem_cache_cpu *c)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL
    struct kmem_cache_node *n = NULL, *n2 = NULL;
    struct page *page, *discard_page = NULL;
    // 挨个遍历 kmem_cache_cpu->partial 列表,将列表中的 slub 转移到对应 kmem_cache_node->partial 列表中
    while ((page = c->partial)) {
        struct page new;
        struct page old;
        // 将当前遍历到的 slub 从 kmem_cache_cpu->partial 列表摘下
        c->partial = page->next;
        // 获取当前 slub 所在的 numa 节点对应的 kmem_cache_node 缓存
        n2 = get_node(s, page_to_nid(page));
        // 如果和上一个转移的 slub 所在的 numa 节点不一样
        // 则需要释放上一个 numa 节点的 list_lock,并对当前 numa 节点的 list_lock 加锁
        if (n != n2) {
            if (n)
                spin_unlock(&n->list_lock);

            n = n2;
            spin_lock(&n->list_lock);
        }

        do {

            old.freelist = page->freelist;
            old.counters = page->counters;
            VM_BUG_ON(!old.frozen);

            new.counters = old.counters;
            new.freelist = old.freelist;
            // unfrozen 当前 slub,因为即将被转移到对应的 kmem_cache_node->partial 列表
            new.frozen = 0;
            // cas 更新当前 slub 的 freelist,frozen 属性
        } while (!__cmpxchg_double_slab(s, page,
                old.freelist, old.counters,
                new.freelist, new.counters,
                "unfreezing slab"));
        // 因为 kmem_cache_node->partial 列表中所包含的 slub 个数是受 s->min_partial 阈值限制的
        // 所以这里还需要检查 nr_partial 是否超过了 min_partial
        // 如果当前被转移的 slub 是一个 empty slub 并且 nr_partial 超过了 min_partial 的限制,则需要将 slub 释放回伙伴系统中
        if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {
            // discard_page 用于将需要释放回伙伴系统的 slub 串联起来
            // 后续统一将 discard_page 链表中的 slub 释放回伙伴系统
            page->next = discard_page;
            discard_page = page;
        } else {
            // 其他情况,只要 slub 不为 empty ,不管 nr_partial 是否超过了 min_partial
            // 都需要将 slub 转移到对应 kmem_cache_node->partial 列表的末尾
            add_partial(n, page, DEACTIVATE_TO_TAIL);
            stat(s, FREE_ADD_PARTIAL);
        }
    }

    if (n)
        spin_unlock(&n->list_lock);
    // 将 discard_page 链表中的 slub 统一释放回伙伴系统
    while (discard_page) {
        page = discard_page;
        discard_page = discard_page->next;

        stat(s, DEACTIVATE_EMPTY);
        // 底层调用 __free_pages 将 slub 所管理的所有 page 释放回伙伴系统
        discard_slab(s, page);
        stat(s, FREE_SLAB);
    }
#endif  /* CONFIG_SLUB_CPU_PARTIAL */
}

释放对象所属 slab 从 partial slab 变为了 empty slab

如果释放对象所在的 slab 原来是一个 partial slab ,由于对象的释放刚好变成了一个 empty slab,恰恰说明该 slab 并不是一个活跃的 slab,它的局部性不好,内核已经好久没有从该 slab 中分配对象了,所以内核选择刀枪入库,马放南山。将它释放回 kmem_cache_node->partial 链表中作为本地 cpu 缓存的后备选项。

在将这个 empty slab 插入到 kmem_cache_node->partial 链表之前,同样需要检查当前 partial 链表中的容量 kmem_cache_node->nr_partial 不能超过 kmem_cache-> min_partial 的限制。如果超过限制了,直接将这个 empty slab 释放回伙伴系统中。

        if ((!new.inuse || !prior) && !was_frozen) {
            if (kmem_cache_has_cpu_partial(s) && !prior) {
                new.frozen = 1;
            } else { 
                // !new.inuse 表示当前 slab 刚刚从一个 partial slab 变为了 empty slab
                n = get_node(s, page_to_nid(page));
                spin_lock_irqsave(&n->list_lock, flags);

            }
        }

      if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
        // 如果 slub 变为了一个 empty slub 并且 nr_partial 超过了最大阈值 min_partial
        // 跳转到 slab_empty 分支,将 slub 释放回伙伴系统中
        goto slab_empty;

释放对象所属的 slab 本来就在 kmem_cache_node->partial 链表中,这种情况下就是直接释放对象回 slab 中,无需改变 slab 的位置。

源码查看 slab cache 的销毁

slab cache 的销毁过程刚刚好和 slab cache 的创建过程相反。slab cache 销毁的核心步骤如下:

  1. 首先需要释放 slab cache 在所有 cpu 中的缓存 kmem_cache_cpu 中占用的资源,包括被 cpu 缓存的 slab (kmem_cache_cpu->page),以及 kmem_cache_cpu->partial 链表中缓存的所有 slab,将它们统统归还到伙伴系统中。

  2. 释放 slab cache 在所有 NUMA 节点中的缓存 kmem_cache_node 占用的资源,也就是将 kmem_cache_node->partial 链表中缓存的所有 slab ,统统释放回伙伴系统中。

  3. 在 sys 文件系统中移除 /sys/kernel/slab/<cacchename> 节点相关信息。

  4. 从 slab cache 的全局列表中删除该 slab cache。

  5. 释放 kmem_cache_cpu 结构,kmem_cache_node 结构,kmem_cache 结构。

下面我们一起到内核源码中看一下具体的销毁过程:

void kmem_cache_destroy(struct kmem_cache *s)
{
    int err;

    if (unlikely(!s))
        return;

    // 获取 cpu_hotplug_lock,防止 cpu 热插拔改变 online cpu map
    get_online_cpus();
    // 获取 mem_hotplug_lock,防止访问内存的时候进行内存热插拔
    get_online_mems();
    // 获取 slab cache 链表的全局互斥锁
    mutex_lock(&slab_mutex);
    // 将 slab cache 的引用技术减 1
    s->refcount--;
    // 判断 slab cache 是否还存在其他地方的引用
    if (s->refcount)
        // 如果该 slab cache 还存在引用,则不能销毁,跳转到 out_unlock 分支
        goto out_unlock;
    // 销毁 memory cgroup 相关的 cache ,这里不是本文重点
    err = shutdown_memcg_caches(s);
    if (!err)
        // slab cache 销毁的核心函数,销毁逻辑就封装在这里
        err = shutdown_cache(s);

    if (err) {
        pr_err("kmem_cache_destroy %s: Slab cache still has objects\n",
               s->name);
        dump_stack();
    }
out_unlock:
    // 释放相关的自旋锁和信号量
    mutex_unlock(&slab_mutex);

    put_online_mems();
    put_online_cpus();
}

在开始正式销毁 slab cache 之前,首先需要将 slab cache 的引用计数 refcount 减 1。并需要判断 slab cache 是否还存在其他地方的引用。

当我们利用 kmem_cache_create 创建 slab cache 的时候,内核会检查当前系统中是否存在一个各项参数和我们要创建 slab cache 参数差不多的一个 slab cache,如果存在,那么内核就不会再继续创建新的 slab cache,而是复用已有的 slab cache。

随后会在 sys 文件系统中为复用 slab cache 起一个别名 alias 并创建一个 /sys/kernel/slab/aliasname 目录,但是该目录下的文件需要软链接到原有 slab cache 在 sys 文件系统对应目录下的文件。这里的 aliasname 就是我们通过 kmem_cache_create 指定的 slab cache 名称。

在这种情况,系统中的 slab cache 就可能在多个地方产生引用,所以在销毁的时候需要判断这一点。

如果存在其他地方的引用,则需要停止销毁流程,如果没有其他地方的引用,则调用 shutdown_cache 开始正式的销毁流程。

static int shutdown_cache(struct kmem_cache *s)
{
    // 这里会释放 slab cache 占用的所有资源
    if (__kmem_cache_shutdown(s) != 0)
        return -EBUSY;
    // 从 slab cache 的全局列表中删除该 slab cache
    list_del(&s->list);
    // 释放 sys 文件系统中移除 /sys/kernel/slab/name 节点的相关资源
    sysfs_slab_unlink(s);
    sysfs_slab_release(s);
    // 释放 kmem_cache_cpu 结构
    // 释放 kmem_cache_node 结构
    // 释放 kmem_cache 结构
    slab_kmem_cache_release(s);

    }

    return 0;
}

释放 slab cache 占用的所有资源

  1. 首先需要释放 slab cache 在所有 cpu 中的缓存 kmem_cache_cpu 中占用的资源,包括被 cpu 缓存的 slab (kmem_cache_cpu->page),以及 kmem_cache_cpu->partial 链表中缓存的所有 slab,将它们统统归还到伙伴系统中。

  2. 释放 slab cache 在所有 NUMA 节点中的缓存 kmem_cache_node 占用的资源,也就是将 kmem_cache_node->partial 链表中缓存的所有 slab ,统统释放回伙伴系统中。

  3. 在 sys 文件系统中移除 /sys/kernel/slab/<cacchename> 节点相关信息。

/*
 * Release all resources used by a slab cache.
 */
int __kmem_cache_shutdown(struct kmem_cache *s)
{
    int node;
    struct kmem_cache_node *n;
    // 释放 slab cache 本地 cpu 缓存 kmem_cache_cpu 中缓存的 slub 以及 partial 列表中的 slub,统统归还给伙伴系统
    flush_all(s);

    // 释放 slab cache 中 numa 节点缓存 kmem_cache_node 中 partial 列表上的所有 slub
    for_each_kmem_cache_node(s, node, n) {
        free_partial(s, n);
        if (n->nr_partial || slabs_node(s, node))
            return 1;
    }
    // 在 sys 文件系统中移除 /sys/kernel/slab/name 节点相关信息
    sysfs_slab_remove(s);
    return 0;
}

释放 slab cache 在各个 cpu 中的缓存资源

内核通过 on_each_cpu_cond 挨个遍历所有 cpu,在遍历的过程中通过 has_cpu_slab 判断 slab cache 是否在该 cpu 中还占有缓存资源,如果是则调用 flush_cpu_slab 将缓存资源释放回伙伴系统中。

// 释放 kmem_cache_cpu 中占用的所有内存资源
static void flush_all(struct kmem_cache *s)
{
    // 遍历每个 cpu,通过 has_cpu_slab 函数检查 cpu 上是否还有 slab cache 的相关缓存资源
    // 如果有,则调用 flush_cpu_slab 进行资源的释放
    on_each_cpu_cond(has_cpu_slab, flush_cpu_slab, s, 1, GFP_ATOMIC);
}

static bool has_cpu_slab(int cpu, void *info)
{
    struct kmem_cache *s = info;
    // 获取 cpu 在 slab cache 上的本地缓存
    struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, cpu);
    // 判断 cpu 本地缓存中是否还有缓存的 slub
    return c->page || slub_percpu_partial(c);
}

static void flush_cpu_slab(void *d)
{
    struct kmem_cache *s = d;
    // 释放 slab cache 在 cpu 上的本地缓存资源
    __flush_cpu_slab(s, smp_processor_id());
}

static inline void __flush_cpu_slab(struct kmem_cache *s, int cpu)
{
    struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, cpu);

    if (c->page)
        // 释放 cpu 本地缓存的 slub 到伙伴系统
        flush_slab(s, c);
    // 将 cpu 本地缓存中的 partial 列表里的 slub 全部释放回伙伴系统
    unfreeze_partials(s, c);
}

释放 slab cache 的核心数据结构

void slab_kmem_cache_release(struct kmem_cache *s)
{
    // 释放 slab cache 中的 kmem_cache_cpu 结构以及 kmem_cache_node 结构
    __kmem_cache_release(s);
    // 最后释放 slab cache 的核心数据结构 kmem_cache
    kmem_cache_free(kmem_cache, s);
}

总结

本文在伙伴系统的基础上又为大家详细介绍了一款内核专门应对小内存块管理的 slab 内存池,并列举了 slab 内存池在内核中的几种应用场景。

然后我们从一个简单的内存页开始聊起,首先详细介绍了在 slab 内存池中所管理的内存块在内存中的布局:

在此基础上,笔者带大家继续采用一步一图的方式,一步一步地推演出 slab cache 的整体架构:

在我们得到了 slab cache 的整体架构之后,后续笔者基于此架构图,又为大家详细介绍了 slab cache 的运行原理,其中包括内核在多种不同场景下针对内存块的分配和回收逻辑。
基于内核 5.4 版本,从源码角度详细讨论了 slab cache 的创建初始化过程,创建流程如下图所示:

基于 slab cache 的完整的架构,近一步深入到内核源码中详细介绍了 slab cache 关于内存分配的完整流程:

清除了 slab 内存池如何分配内存块的源码实现之后,紧接着笔者又介绍了 slab 内存池如何进行内存块的回收:

最后笔者介绍了 slab 内存池的销毁过程:

好了,整个 slab cache 相关的内容到此就结束了,感谢大家的收看,我们下篇文章见

相关文章

深入理解 Linux Page Cache 机制
深入理解 Linux PageFault
深入理解 Linux mmap 流程
深入理解 Linux 页表体系
深入理解Linux kmalloc 体系
深入理解 Linux 伙伴系统

发布评论