Linux SLUB 内存分配器分析

2018-01-29 12:41:40来源:https://mp.weixin.qq.com/s/ragFsK_AJivOGjR47tAhHw作者:人点击

分享

本文简介


本文主要介绍了 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,<slabcache>选项启用追踪。然后,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 内存分配的层次


与 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 分配器


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


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



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


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


2.2.1.快速分配流程


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


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



2.2.3.慢速分配流程

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



2.2.4.最慢速分配流程

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


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



2.2.5.快速释放流程


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


2.2.6.慢速释放流程


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


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类型的数据结构来描述的,下列出了它的主字段:



类型
名称
描述

unsigned long


flags


描述缓冲区属性的一组标志


int


size


分配给对象的内存大小 ,包含对象元数据,例如空闲链表指针,因此 可能大于对象的实际大小


int


objsize


对象的实际大小 ,不包含对象元数据


int


offset


空闲对象指针 在对象中的偏移值


int


order


表示一个slab需要2^order个物理页框 ,用于伙伴系统


kmem_cache_node


local_node


创建缓冲区的 NUMA 节点 ,其中包括后备半满缓冲池


int


objects


一个slab中的对象总个数


int


refcount


引用计数 计数器。 当新创建的缓冲区可以与已有缓冲区合并时,增加原有缓冲区引用计数。


void (*)(…)


ctor


创建slab时 , 用于初始化每个对象的构造函数


int


inuse


对象元数据在对象中的偏移


int


align


对齐 要求


const char *


name


缓冲区名字


struct list_head


list


包含所有缓冲区描述结构的双向循环队列,队列头为 slab_caches


int


defrag_ratio


防磁片的调节值, 该值越小,越倾向 于 从本节点中分配对象


struct kmem_cache_node * []


node


所有可用的NUMA节点,这些节点中的后备半满slab均可以用于本缓冲区内存分配


struct kmem_cache_cpu * []


cpu_slab


为每个处理器创建的 缓存 slab数据结构 ,以加快每个CPU上的分配速度,降低锁竞争



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

类型
名称
描述

void **


freelist


空闲对象队列的指针,即第一个空闲对象的指针


struct page *


page


slab的第一个物理页框描述符 ,保存slab的元数据。


int


node


处理器所在NUMA节点号


unsigned int


offset


在空闲对象中, 存放下一个空闲对象指针的 偏移


unsigned int


objsize


对象实际大小,与kmem_cache结构objsize字段一致



3.1.4. kmem_cache_node

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



类型
名称
描述

spinlock_t


list_lock


保护 本数据结构的 自旋锁


unsigned long


nr_partial


本节点 半满 slab的数目


atomic_long_t


nr_slabs


本节点slab的总数


struct list_head


partial


半满 slab 的 链表头



3.2. API

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



函数
描述

kmem_cache_create


创建新的缓冲区。


kmem_cache_destroy


销毁缓冲区。因为存在重用缓冲区的情况,只有当 kmem_cache 结构的 refcount字段为 0时才真正销毁。


kmem_cache_alloc


从缓冲区中分配对象。


kmem_cache_alloc_node


在指定的NUMA节点中分配对象 。


kmem_cache_free


释放对象 到缓冲区 。


kmem_ptr_validate


检查给定对象的指针是否合法。


kmem_cache_size


返回对象实际大小。


kmem_cache_shrink


内存回收器,在内存紧张时调用。


kmalloc


从通用缓冲区中分配一个对象。


kmalloc_node


从通用缓冲区中分配一个属于指定NUMA节点的对象。


kfree


释放一个通用对象。


ksize


返回分配给对象的内存大小(可能大于对象的实际大小)



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,请告诉我一下:scxby@163.com 。


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行)。


微信扫一扫

第七城市微信公众平台