天天看点

Linux 踩内存 slub,Linux SLUB 内存分配器分析

本文简介

本文主要介绍了Linux SLUB分配的产生原因、设计思路及其代码分析。适合于对Linux内核,特别是对Linux内存分配器感兴趣的读者。

1.为何需要SLUB?

Linux SLUB内存分配器合入Linux主分支已经整整10年了!并且是Linux目前默认的内存分配器。

然而,主流的Linux内核出版物仍然在分析SLAB而不是SLUB分配器。这似乎有点令人惊奇。

对于Linux来说,重要模块合入内核时,都会在补丁或者lwn文档里面详细记录其合入理由。SLUB分配器也不例外。我们看看作者在提交补丁是怎么说的。

1.1.准备工作

A、请使用git clone命令下载一份Linux Next分支的代码。

B、在源码目录下,用git log v2.6.22..v2.6.23 mm/slub.c,查看其最初合入记录。

C、找到最初合入补丁的commit id,即81819f0fc828。

D、使用git show 81819f0fc828查看补丁的详细信息。

在查看补丁之前,有两点需要特别注意:

1、在Linux社区,一般用SLUB、SLAB表示内存分配器算法及其实现。不管是SLUB还是SLAB,这两种算法都会使用slab来组织内存对象。我们可以简单的认为:一个slab由一个或者几个页框组成,每个页框一般包含4096个字节。在每一个slab里面,包含一个或者多个待分配的对象。

2、补丁描述一般比较精练,其阅读对象是内核社区老手。如果看起来费力,也没有关系。可以通读本文后,回头再来领会作者的意思。

1.2.作者怎么说?

我们看看作者Christoph Lameter在补丁中到底是怎么解释SLUB的:

这是一个新的slab分配器,它由mm/slab.c中现有代码的复杂性所激发。它试图处理现有SLAB实现中的种种不足之处。

A. 队列管理

在SLAB分配器中,其中一个突出的问题是:几种对象队列管理的复杂性。SLUB则没有这样的队列。相反,我们为每一个将要分配内存的CPU准备一个slab,并且直接使用slab中的对象,而不是将slab加入到队列中。

B.对象队列的存储开销

在每一个CPU中,每一个NUMA节点都存在SLAB对象队列。即使是每CPU的缓存队列,也持有一个队列数组。这些数组为每一个CPU、每一个NUMA节点而包含一个队列。对于非常大的系统来说,队列自身的数量和可能包含在这些队列中的对象的数量,将会成倍增长。 在我们使用1k个NUMA节点/处理器的系统中,我们有数G字节用于存储这些队列的引用。这甚至不包括可能包含在这些队列中的对象。人们担心,所有内存有一天会被这些队列消耗殆尽。

C. SLAB元数据开销

SLAB在每个slab的起始处都有开销。 这意味着数据不能在slab块的起始处优雅的对齐。SLUB则将所有元数据保存在相应的page_struct数据结构中。因此,对象可以在slab中优雅的对齐。例如,一个128字节的对象将在128字节的边界对齐,并且可以紧致的放入一个4k页面,而没有浪费字节。SLAB则不能做到这一点。

D. SLAB有一个复杂的缓存回收器

对于UP系统来说,SLUB并不需要缓存回收器。在SMP系统中,则应当将每CPU slab放回到半满链表中。不过,该操作不仅简单,而且不需要遍历对象链表。SLAB则不然,在缓存回收期间,它会遍历每一个CPU,及CPU共享的、CPU独有的对象队列。这可能会引起较大的CPU卡顿。

E. SLAB具有复杂的NUMA策略层支持

SLUB将NUMA策略处理转交给页面分配器。这意味着与SLAB分配器相比,分配过程负责的事情更少(当然,SLUB确实也不可避免的与页面分配这一级交互),但是在2.6.13之前,这种情况也是存在的。在SLAB中,在特定NUMA节点上分配slab对象的SLAB应用,确定会存在性能问题。这是因为,频繁的引用内存策略可能导致一系列对象来自一个接一个的NUMA节点。SLUB将从一个节点获得一个slab的所有对象,然后切换到下一个节点。

F.减少半满slab链表的大小

SLAB有每节点的半满列表。这意味着随着时间的推移,大量的半满slab可能积累在这些链表中。仅仅当分配器发生在特定节点上时,这些半满slab才被重用。SLUB有一个全局的半满slab池,并将从该池中消耗slab以减少碎片。

G.可调节

SLUB对每个slab缓存都有复杂的调节能力。其中一个能力是可以仔细的维护队列大小。 然而,填充队列仍然需要使用自旋锁,以保护slab。 SLUB有一个全局参数(min_slab_order)用于调节。增加最小slab order可以减少锁开销。slab oder越大,在每个CPU和半满列表之间的页面迁移越少,SLUB扩展得越好。

G.slab合并

我们经常使用具有类似参数的slab缓存。在创建阶段,SLUB检测到这些缓存,并将它们合并到相应的通用高速缓存中。这导致内存使用更加有效。在所有缓存中,大约50%的缓存可以通过slab合并来消除。这也能够减少slab碎片,因为部分分配的slab可以被重新填满。通过在启动时指定slub_nomerge参数,可以关闭slab合并功能。

请注意,合并可能会暴露内核中的未知BUG,因为被破坏的对象现在可能被放置在不同的地方,并破坏不同的相邻对象。请启用安全性检查来找到这些潜在问题。

H.诊断

目前的slab诊断很难使用,这需要重新编译内核。SLUB则包含总是可用的调试代码(但不在热点代码路径中)。可以通过“slab_debug”选项启用SLUB诊断。可以指定参数来选择一个或一组slab高速缓存来进行诊断。 这意味着系统以正常的性能运行,同时也使得竞争条件更有可能被复现。

I.容错

如果进行了基本的健全性检查,则SLUB能够检测到常见的错误情况并尽可能恢复以使系统继续运行。

J.追踪

可以在启动时通过slab_debug = T,选项启用追踪。然后,SLUB将记录该slabcache上的所有操作,并在空闲时转储对象内容。

K.按需创建DMA缓存

通常,DMA缓存是不需要的。 如果kmalloc与_GFP_DMA一起使用,那么只需创建这个必要的单个slab缓存即可。对于没有ZONE_DMA要求的系统,这项支持被完全消除。

L.性能提升

一些基准测试显示,kernbench的速度提高了5-10%。SLUB的锁开销是基于底层分配块的大小。如果我们能够放心的分配更大order的页面,则可以更进一步提高SLUB的性能。 防碎片补丁可以使性能进一步提高。

关于NUMA对应用程序性能的影响,请参见:

http://cenalulu.github.io/linux/numa/

仔细查看过SLAB和SLUB分配器的代码后,笔者认为:SLUB代码的清晰度、简洁性优于SLAB。

目前,除了极个别的应用场景外,SLUB的性能也优于SLAB。这也是为什么当前Linux版本默认使用SLUB分配器,同时也保留SLAB分配器代码的原因。

2.SLUB概述

2.1.Linux内存分配的层次

Linux 踩内存 slub,Linux SLUB 内存分配器分析

与SLUB内存分配器相关的概念有几个层次:

1、页帧

概念

描述

存储节点(Node)

CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一些本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点

管理区(Zone)

每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存

页面(Page)

内存被细分为多个页框, 页框是最基本的页面分配的单位。一个页面的常见长度是4096字节

在NUMA系统中,处理器被划分成多个“节点”(node)。所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多。

各个节点又被划分为内存管理区域, 一个管理区域通过struct zone来描述。低端范围的内存管理区被称为ZONE_DMA, 可直接映射到内核的常规内存域被称为ZONE_NORMAL,不能直接进行线性映射的内存域被称为ZONE_HIGHMEM, 即高端内存。

页框(page frame)则代表了系统内存的最小单位, 每个页帧用struct page来描述。

2、页面分配器

通常,内存分配一般有两种情况:大对象(大的连续空间分配)、小对象(小的空间分配)。对于大的对象(超过一个页框的大小),可以使用页面分配器进行分配。在Linux中,页面分配器被称为伙伴系统。其中常见的API是__get_free_pages、alloc_pages、free_pages、__free_pages。

伙伴系统把所有的空闲页框分为11个(不同的版本有所区别)块链表,每个块链表中,分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。假设要申请一个256个页框的块,则先从结点为256个连续页框块的链表中查找空闲块,如果没有,就去512个页框的链表中找,找到了则将页框块分为2个256个页框的块,将其中一个分配给应用,另外一个移到256个页框的空闲链表中。如果512个页框的链表中仍没有空闲块,继续在1024个页框的链表进行查找—分割—分配和转移。如果仍然没有,则返回错误。

使用过的页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块,然后作为节点插入相应的链表中。

伙伴系统很好地解决了外部碎片(页框之间的碎片)问题。

3、对象分配器(SLUB)

伙伴系统分配内存时是基于页框为单位的。如果想要分配小于一个页框,例如几十个字节的内存,应该怎么办呢?此时就需要用对象分配器,例如 SLUB、SLOB、SLAB分配器。对象分配器是基于对象进行管理的,所谓的对象就是内核中的数据结构,例如task_struct,file_struct 等。相同类型的对象归为一类,由kmem_cache数据结构进行描述。每个kmem_cache对象由一组slab组成,每个slab由一个或者多个页框构成。在每个slab中,包含一个或者多个内存对象。每当要申请这样一个对象时,对象分配器就从一个slab中分配一个对象出去。当要释放对象时,将其重新保存在slab的对象列表中,而不是直接返回给伙伴系统,从而避免内部碎片。对象分配器在分配对象时,会使用最近释放的对象的内存块,因此其驻留在cpu高速缓存中的概率会大大提高。

对象分配器是针对于小对象的内存分配,它很好地解决了页框内部的内部碎片问题。

4、调用SLUB分配内存的内核代码

2.2.SLUB分配器

Linux 踩内存 slub,Linux SLUB 内存分配器分析

通过上图可以看到,SLUB分配器的主要数据结构是kmem_cache。相对于SLAB而言,该结构简化了不少。没有了队列的相关字段。

每个处理器都有一个本地的活动slab,由kmem_cache_cpu结构描述。并且,在SLUB中,没有单独的空slab队列。每个NUMA节点使用kmem_cache_node结构维护一个处于半满状态的slab队列,作为备用slab缓存池。

Linux 踩内存 slub,Linux SLUB 内存分配器分析

如上图所示,在SLUB分配器中,一个slab就是一组连续的物理内存页框,被划分成了固定数目的对象。slab没有额外的空闲对象队列,而是重用了空闲对象自身的存储空间并将其链接起来,这既节省了对象元数据空间,也大大简化了代码的复杂度。

在SLAB分配器中,每个slab需要一些元数据空间,存放在每个slab起始处,或者申请单独的存储空间,存储在slab之外。SLUB与此不同,它的slab没有额外的描述结构。由于它的slab描述字段较少,因此它在代表物理页框的page结构中重用了_mapcount等字段。在SLUB中,这些字段被解释为SLUB所需要的freelist,inuse和slab等含义。幸运的是,这并不会令page数据结构膨胀。

2.2.1.快速分配流程

Linux 踩内存 slub,Linux SLUB 内存分配器分析

正常情况下,在同一个kmem_cache描述符上进行反复的申请、释放操作后,相应的数据如上图所示:当前kmem_cache描述符的CPU缓存slab中,freelist链表有可用的空闲对象。

在这种情况下,当内核申请分配对象时,可以直接从所在处理器的kmem_cache_cpu 结构的freelist字段获得第一个空闲对象的地址,然后更新 freelist 字段,使其指向下一个空闲对象。然后将摘除的空闲对象返回给调用者。如下图所示:

Linux 踩内存 slub,Linux SLUB 内存分配器分析

2.2.3.慢速分配流程

当CPU缓存slab不存在,或者缓存slab中的freelist已经变空以后,SLUB会尝试从CPU所在NUMA节点的半满链表中,找到一个可用的半满slab,放到CPU缓存slab中。并尝试从该缓存slab中分配对象。

Linux 踩内存 slub,Linux SLUB 内存分配器分析

2.2.4.最慢速分配流程

最慢速的情况,是CPU缓存slab的freelist为空,并且NUMA节点的半满slab链表也为空。这种情况下,只能从伙伴系统中分配新的页面并填充到CPU缓存slab中。

这种情况下,最大的开销是在伙伴系统中需要使用全局的自旋锁。

Linux 踩内存 slub,Linux SLUB 内存分配器分析

2.2.5.快速释放流程

Linux 踩内存 slub,Linux SLUB 内存分配器分析

最快速的释放流程是:被释放的对象刚好可以放回到CPU缓存slab中,并且不需要做任何额外的处理。

2.2.6.慢速释放流程

Linux 踩内存 slub,Linux SLUB 内存分配器分析

在释放对象时,如果遇到如下情况,则需要进入慢速释放流程:

1、slab由全满变为半满,此时需要将slab加入到节点的半满链表中。

2、slab变为全空,此时需要将页面释放回伙伴系统。

3.SLUB代码分析

3.1.相关数据结构

与SLAB相比,SLUB分配器的最大特点,就是简化设计理念,同时也保留了SLAB分配器的基本思想:每个缓冲区由多个小的 slab 组成,每个 slab 包含固定数目的对象。SLUB 分配器简化了kmem_cache,slab 等相关的管理数据结构,摒弃了SLAB 分配器中众多的队列概念。为了保证内核其它模块能够无缝迁移到 SLUB 分配器,SLUB也保留了原有SLAB分配器所有的接口API 函数。

本文所列的数据结构和源代码均摘自Linux内核 2.6.24版本。

3.1.1.kmem_cache

每个内核对象缓冲区都是由kmem_cache类型的数据结构来描述的,下列出了它的主字段:

3.1.2.page结构相关字段

在SLAB分配器中,slab中不但保存了内存对象,同时还保存了一些元数据。相反的,在SLUB分配器中,slab 没有额外的元数据,例如空闲对象队列,而是将空闲对象指针放在了空闲对象之中。同时,没有专门的数据结构来描述slab,而是在代表物理页框的 page结构中复用如下字段来表示slab相关信息:

üfreelist:slab中第一个空闲对象的指针

üinuse:slab中已分配对象。如果等于slab中对象总数,即代表slab全满

üslab:所属缓冲区 kmem_cache 结构的指针

在每一个slab中,这些元数据保存在第一个物理页框的page结构中。

3.1.3.kmem_cache_cpu

3.1.4.kmem_cache_node

在SLUB中,没有单独的空slab队列。所有空slab都会被直接归还回伙伴系统,而不会缓存在SLUB中。在每一个kmem_cache结构中,与 NUMA 节点相关的数据结构使用 kmem_cache_node来表示,它维护一个处于半满状态的slab队列。下表列出它的主要字段:

3.2.API

在Linux中,三种对象分配器SLOB\SLAB\SLUB都提供了统一的API,以保证调用接口的一致性。下表列出主要的API函数:

3.3.函数实现

以下代码分析基于linux 2.6.24版本,可以通过如下链接查看相应版本的代码:

http://elixir.free-electrons.com/linux/v2.6.24/source

SLUB分配的主要代码位于mm/slub.c和include/linux/slub_def.h中。

3.3.1.kmem_cache_create

kmem_cache_create创建一个kmem_cache对象,其原型如下:

struct kmem_cache *kmem_cache_create(const char *name, size_t size,

size_t align, unsigned long flags,

void (*ctor)(struct kmem_cache *, void *))

参数及返回值含义:

name:kmem_cache的名称,在proc中使用

size:kmem_cache所管理的对象内存大小

align:内存对齐要求

flags:创建标志,如SLAB_HWCACHE_ALIGN

ctor:对象构建函数,在初始化对象时调用

返回值:创建的kmem_cache对象

该函数实现如下:

1、获得slub_lock写锁(3041行),该锁保护全局kmem_cache链表。

2、查找与当前创建参数匹配的,可以合并的kmem_cache对象(3042行)。

3、如果这样的kmem_cache对象存在(3042行),那么:

A、增加kmem_cache对象的引用计数(3046行)。

B、修正kmem_cache对象的对象大小(3051行)。

C、遍历所有CPU,修改所有CPU缓存slab中的对象大小(3057行)。

D、改变kmem_cache对象inuse值(3059行),该值表示slab对,每个对象的元数据在对象中的偏移。我猜测,这里可能会存在BUG,如果有哪位读者通过补丁记录能够证实真的有BUG,请告诉我一下:[email protected]。

E、释放slub_lock写锁(3060行)。

F、在sys文件系统中,为新创建的kmem_cache对象创建别名,将其链接到原对象上(3061行)。

G、返回匹配的kmem_cache对象(3063行)。

4、否则,没有匹配的kmem_cache对象,必须要新创建一个。首先为kmem_cache描述符分配内存(3066行)。

5、如果内存分配成功(3067行),则:

A、调用kmem_cache_open将kmem_cache描述符准备就绪(3068行)。

B、如果kmem_cache_open执行成功(3068行),那么:

BA、将kmem_cache描述符添加到全局slab_caches链表中(3070行)。

BB、释放slub_lock写锁(3071行)。

BC、在sys文件系统中,为新创建的kmem_cache对象创建文件对象(3072行)。

BD、返回新创建的kmem_cache对象(3074行)。

C、否则,创建kmem_cache不成功,释放其描述符(3076行)。

6、释放slub_lock写锁(3078行)。

7、运行到此,说明创建过程中出现错误(3080行)。

8、如果调用者传入了SLAB_PANIC标志,则将系统hung住(3082行)。

9、否则返回NULL(3084行)。

kmem_cache_open对新创建的kmem_cache对象进行初始化,其实现如下:

1、将kmem_cache描述符置0(2063行)

2、设置其name,ctor等初始值(2064行)

3、计算对象长度、在伙伴系统中分配slab页面的order值(2271行)。如果值过大,无法通过SLUB内存分配器管理,则返回错误。

4、设置防碎片调节参数(2276行)

5、为kmem_cache对象初始化NUMA节点缓存相关的数据结构(2278行)。注意,在系统初始化阶段,需要调用boot内存分配函数来分配相关数据结构。在SLUB初始化完毕后,由SLUB系统自身来分配相应的数据结构。

6、为kmem_cache对象初始化每CPU缓存slab数据结构(2281行)

7、如果初始化失败,则释放前面分配的NUMA节点缓存数据结构(2283行)

3.3.2.kmem_cache_destroy

该函数是kmem_cache_create相对应的反初始化函数,其实现比较简单。读者可以自行分析。

3.3.3.kmem_cache_alloc

kmem_cache_alloc是SLUB内存分配器的分配接口。位于slub.c的1593行。其函数原型是:

void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)

参数及返回值含义如下:

s:kmem_cache描述符

gfpflags:内存分配标志,如GFP_ATOMIC、GFP_KERNEL。当需要从伙伴系统中分配slab时,将此参数传递给伙伴系统。

返回值:分配成功的对象地址,如果失败则返回NULL。

它直接调用slab_alloc从slab中分配对象地址。slab_alloc的实现如下:

1、关闭本地CPU中断(1575行)。在此,关闭中断有两个目的:A、避免中断打断当前slab_alloc的执行,造成逻辑错误。因为随后的代码需要维护kmem_cache_cpu数据结构。B、防止进程被迁移到其他CPU上执行。

2、获得当前CPU对应的kmem_cache_cpu数据结构。该数据结构是当前CPU缓存的,用于内存分配的slab(1576行)。

3、如果(1)CPU缓存的slab没有空闲对象了,或者(2)调用者希望从特定NUMA节点中分配数据,而缓存的slab所在的节点与之并不匹配(1577行),那么:

A、调用__slab_alloc分配对象,这是慢速分配过程(1579行)。

4、否则(1581行):

A、从kmem_cache_cpu数据结构中,获得第一个空闲对象(1582行)。

B、将kmem_cache_cpu数据结构的空闲对象后移到下一个空闲对象。下一个空闲对象指针保存在当前空闲对象的offset偏移处(1583行)。

5、恢复本地CPU中断(1585主)。

6、如果(1)调用者要求将对象初始化为0,并且(2)成功分配了对象(1587行),那么:

调用memset将对象置0(1588行)

7、返回所分配的对象,可能为NULL(1590行)。

3.3.4.__slab_alloc

__slab_alloc就SLUB的慢速分配流程,如下:

1、如果当前CPU缓存的slab还不存在(1496行),则:

A、跳转到new_slab标签处,从伙伴系统中分配slab页面(1497行)。

2、锁住页面(1499行)。实际上,PG_locked主要用于磁盘IO时的页面锁定,防止形成并发问题。但是在SLUB分配器中,相应的页并不会被磁盘IO交换出去。这里仅仅是借用PG_locked标志来保护page结构中,与SLUB分配器相关的几个字段。

3、如果slab所在的NUMA节点编号与所要求的不匹配,也就是调用者希望在特定NUMA节点上分配对象,而当前缓存的slab位于另外的节点(1500行),那么

A、跳转到another_slab(1501行),分配另外一个slab并缓存到当前CPU。

4、获取当前slab的第一个空闲对象(1503行)。

5、如果当前slab没有空闲对象(1504行),那么:

A、跳转到another_slab(1505行),分配另外一个slab并缓存到当前CPU。

6、如果用户希望调试当前slab(1506行),那么:

A、跳转到debug标签(1507行),略。

7、获取当前slab的第一个空闲对象(1509行)。

8、将slab的空闲对象指针下移到下一个空闲对象,并将其交给kmem_cache_cpu对象管理,此后空闲链表不再属于slab对象(1510行)。

9、slab已经被托管到当前CPU缓存中了,设置slab的对象占用值为该slab中,所有的对象个数(1511行)。

10、slab中所有对象已经交给kmem_cache_cpu进行管理,那么slab的空闲对象链表也应当设置为NULL(1512行)。

11、设置kmem_cache_cpu的节点号为页面所在的节点(1513行)。

12、slab中的相关数据结构已经设置完毕,释放页面锁(1514行)。

13、返回分配成功的对象。

14、当CPU中缓存的slab对象kmem_cache_cpu,与调用者期望的NUMA节点不一致时,跳转到这里,调用deactivate_slab解除当前slab与CPU之前的绑定关系(1518行)。

15、当缓存的slab还没有分配页面时,跳转到这里,为当前CPU分配可用页面(1520行)。

16、调用get_partial(1521行),优先从特定NUMA节点中获得一个半满slab。

17、如果成功的从NUMA节点中获得一个半满slab(1522行),那么:

A、设置当前CPU缓存的slab为该slab(1523行)。

B、跳转到1502行,开始从缓存的slab中分配对象(1524行)。

18、否则,需要从伙伴系统中分配新页。如果分配标志允许在页面不足时睡眠等待(1527行),那么:

A、强制打开中断(1528行)。因为在关中断下等待睡眠是非法的,而上层调用函数关闭了中断,所以此处必须打开。

19、从伙伴系统中分配新的页面,形成一个slab(1530行)。

20、如果分配标志允许在页面不足时睡眠等待(1532行),则说明前面强制打开了中断,那么:

A、强制关闭中断(1533行),与1528行匹配。

21、如果成功的从伙伴系统中分配到页面(1535行),那么:

A、获得当前CPU缓存的slab对象(1536行)。

B、如果当前CPU缓存的slab对象真实有效(1537行),那么:

BA、调用flush_slab解除当前slab对象与CPU之间的关系(1538行)。

C、为slab而锁住新分配的页面(1539行)。

D、设置当前页面frozen标志,表示当前分配的页面用于当前CPU的页面分配,避免被其他CPU竞争走(1540行)。

E、将新分配的页面与当前CPU绑定(1541行),这样新页面将可用于SLUB分配器。

F、跳转到1502行(1542行),进入正常的分配流程。

22、否则,从伙伴系统中分配页面失败,向调用者返回NULL(1544行)。

23、后续代码用于调试(1545行),略。

当解除slab与CPU之间的绑定关系时,会调用deactivate_slab函数。该函数会将CPU缓存对象的freelist中的对象,还给slab对象。分析如下:

1、获得slab对象的第一个页面(1398行)。

2、遍历CPU缓存对象freelist(1404行)。

3、获得第一个空闲对象(1409行)。

4、使CPU缓存对象空闲链表指向下一个空闲对象(1409行),也就是将freelist的第一个对象从CPU缓存对象中摘除。

5、将slab对象的第一个对象链接到刚刚摘除下来的队列后面(1412行)。

6、将slab的空闲链表头指向刚刚摘除的对象(1413行)。也就是将刚摘除的对象链接到slab的空闲链表中。

7、递减slab的使用计数(1414行)。

8、循环,直到将所有空闲链表中的对象全部返还给slab(1415行)。

9、CPU缓存对象已经解决与slab页面的绑定,设置其slab页面对象为NULL(1416行)。

10、调用unfreeze_slab(1417行)。该函数将页面归还给kmem_cache的NUMA节点缓存,或者将其归还给伙伴系统,视情况而定。

3.3.5.kmem_cache_free

kmem_cache_free是SLUB内存分配器的释放接口。位于mm/slub.c的第1697行。其函数原型是:

void kmem_cache_free(struct kmem_cache *s, void *x)

参数及返回值含义如下:

s:kmem_cache描述符

x:要释放的内存对象地址

kmem_cache_free的实现如下:

1、根据对象地址,找到其所在的slab对象(1724行),具体查找方法如下:

A、根据对象地址找到其页面page结构。

B、在page结构中,根据first_page找到伙伴系统中的领头页面。

2、调用slab_free将对象返回给slab。

3.3.6.slab_free

slab_free实现真正的释放工作,其实现如下:

1、关闭当前CPU本地中断(1707行)。

2、安全性检查工作(1708行),略。

3、获得当前CPU缓存slab对象(1709行)。

4、如果(1)要释放的内存位于CPU缓存slab对象中,并且(2)当前CPU缓存slab对象的节点编号大于等于0(一般是满足的,小于0的情况,只存在于作者的调试代码中)(1710行),那么进入快速释放流程:

A、将freelist链接到被释放对象的后面(1711行)

B、将freelist指向当前对象(1712行),实际上是将当前对象置为freelist头。

5、否则调用__slab_free进入慢速释放流程。

6、打开中断。

__slab_free的实现如下:

1、获得slab的自旋锁(1643行)

2、处理slab的调试信息(1645行),略。

3、将slab的空闲链表链接到当前对象后面(1649行)。

4、将当前对象作为slab的空闲链表头(1650)。

5、递减slab的使用计数(1651行)。

6、如果当前页面有frozen标志(1653行),表示该slab由当前CPU锁定,其他CPU不能在此slab中分配。因此不能将其归还给节点或者伙伴系统。那么:

A、跳转到un_lock标签(1654行),并退出。

7、如果当前slab全部对象都被释放了(1656行),那么:

A、跳转到slab_empty,将页面释放回伙伴系统。

8、如果在释放对象之前,slab是全满的,现在变为半满了(1664行),那么:

A、调用add_partial_tail将其添加到NUMA节点的半满链表中。

9、释放slab的自旋锁。

10、如果(1)释放当前对象之前,slab为半满状态。(2)当前已经全空(1672行),那么:

A、从NUMA半满链表中摘除(1676行)。

11、释放slab的自旋锁。

12、将页面归还给伙伴系统(1680行)。