一、名词解释
slab : Slab机制最初起源于Solaris的内存管理,主要用于消除小对象(C结构体)频繁地分配和释放导致的内存碎片问题。
buddy伙伴系统 : Linux采用著名的伙伴系统(buddy system)算法来解决外碎片问题。把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续的页框。
numa node/内存节点 : CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点
二、概述
slab系统核心思想是使用对象的概念来管理内存。对象是指具体相同的数据结构和大小的某个内存单元(例如下图的mm_struct结构体,内核中需要为每个进程维护这个结构体)。内核对某些对象(如 task_struct)的使用是非常频繁的,所以用户进程堆管理常用的基于搜索的分配算法比如First-Fit(在堆中搜索到的第一个满足请求的内存块)和 Best-Fit(使用堆中满足请求的最合适的内存块)并不直接适用。
性质:
1、对对象进行统一管理和缓存;
2、适用于多处理器达到lock-free;
3、适配硬件高速缓存;
三、查看

从左往右:对象名字,活跃的对象个数,总个数,单个对象大小/byte,单个slab中含有几个对象,单个slab占几个页面
四、数据结构
五、分配与回收机制
那么,我们可以总结出整个slab分配器的初始化以及创建流程:
- 首先,内核调用kmem_cache_init,创建两个结构体boot_kmem_cache和boot_kmem_cache_node,这两个结构体将作为kmem_cache和kmem_cache_node的管理结构体。
- 然后,内核调用create_boot_cache()初始化boot_kmem_cache_node结构体的部分成员变量,被初始化的成员变量如下:name、size、object_size、align、memcg。
- 紧接着,内核继续调用kmem_cache_create继续初始化boot_kmem_cache_node结构体,而进入kmem_cache_create后又会直接进入kmem_cache_open,最终的初始化工作将会在kmem_cache_open中完成。
- 在kmem_cache_open中,内核首先初始化结构体的flag成员,注意,内核将在这一步来判断是否开启了内核调试模式,若开启,flag则为空值。
- 接下来如果内核开启了CONFIG_SLAB_FREELIST_HARDENED保护,内核将获取一个随机数存放在结构体的random成员变量中。
- 若开启了SLAB_TYPESAFE_BY_RCU选项(RCU是自Kernel 2.5.43其Linux官方加入的锁机制),则设置结构体的random成员变量reserved为rcu_head结构体的大小。
- 接下来调用calculate_sizes()计算并设置结构体内其他成员变量的值,首先会从结构体的object_size和flag中取值。
- 在calculate_sizes()中,内核首先将取到的size与sizeof(void *)指针大小对齐,这是为了能够将空闲指针存放至对象的边界中。
- 接下来,若内核的调试模式(CONFIG_SLUB_DEBUG)被启用且flag中申明了用户会在对象释放后或者申请前访问对象,则需要调整size,以期能够在对象的前方和后方插入一些数据用来在调试时检测是否存在越界写。
- 接下来设置结构体的inuse成员以表示元数据的偏移量,这也同时表示对象实际使用的大小,也意味着对象与空闲对象指针之间的可能偏移量。
- 接下来判断是否允许用户越界写,若允许越界写,则对象末尾和空闲对象之间可能会存在其余数据,若不允许,则直接重定位空闲对象指针到对象末尾,并且设置offset成员的值。
- 接下来,若内核的调试模式(CONFIG_SLUB_DEBUG)被启用且flag中申明了用户需要内核追踪该对象的使用轨迹信息,则需要调整size,在对象末尾加上两个track的空间大小,用于记录该对象的使用轨迹信息(分别是申请和释放的信息。
- 接下来,内核将创建一个kasan缓存(kasan是Kernel Address Sanitizer的缩写,它是一个动态检测内存错误的工具,主要功能是检查内存越界访问和使用已释放的内存等问题。它在Kernel 4.0被正式引入内核中。
- 接下来,若内核的调试模式(CONFIG_SLUB_DEBUG)被启用且flag中申明了用户可能会有越界写操作时,则需要调整size,以期能够在对象的后方插入空白边界用来捕获越界写的详细信息。
- 由于出现了多次size调整的情况,那么很有可能现在的size已经被破坏了对齐关系,因此需要再做一次对齐操作,并将最终的size更新到结构体的size中。
- 接下来通过calculate_order()计算单slab的页框阶数。
- 最后调用到oo_make计算kmem_cache结构的oo、min、max等相关信息后,内核回到kmem_cache_open继续执行。
- 内核在回到kmem_cache_open后,调用set_min_partial()来设置partial链表的最小值,避免过度使用页面分配器造成冲击。
- 紧接着会调用set_cpu_partial()据对象的大小以及配置的情况,对cpu_partial进行设置。
- 接下来由于slab分配器尚未完全就绪,内核将尝试使用init_kmem_cache_nodes分配并初始化整个结构体。
- 接下来内核会建立管理节点列表,并遍历每一个管理节点,遍历时,首先建立一个struct kmem_cache_node,然后内核会尝试使用slab分配器建立整个slab_cache(当且仅当slab分配器部分或完全初始化时才可以使用这个分配器进行分配),那么显然,我们此时的slab分配器状态为DOWN。
- 接下来程序将调用early_kmem_cache_node_alloc()尝试建立第一个节点对象。
- 在early_kmem_cache_node_alloc()中,内核会首先通过new_slab()创建kmem_cache_node结构空间对象的slab,它将会检查传入的flag是否合法,若合法,将会进入主分配函数allocate_slab()。
- 在主分配函数allocate_slab()中,内核会首先建立一个page结构体,此时若传入的flag带有GFP标志,程序将会启用内部中断。
- 尝试使用alloc_slab_page()进行内存页面申请,若申请失败,则会将oo调至s->min进行降阶再次尝试申请,再次失败则返回错误!
- 若申请成功,则开始初始化page结构体,设置page的object成员为从oo获取到的object,设置page的slab_cache成员为它所属的slab_cache,并将page链入节点中。
- 接下来内核会对申请下来的页面的值利用 memset 进行初始化。
- 接下来就是经过kasan的内存检查和调用shuffle_freelist 函数,shuffle_freelist 函数会根据random_seq 来把 freelist 链表的顺序打乱,这样内存申请的object后,下一个可以申请的object的地址也就变的不可预测。
- 接下来内核会返回到early_kmem_cache_node_alloc()继续运行,内核首先会检查申请下来的page和node是否对应,若对应则进行下一步操作,否则将会打印错误信息并返回。
- 接下来初始化page的相关成员,然后将取出page的第一个对象,初始化后将其加入partial链表。
- 在early_kmem_cache_node_alloc()中,内核会首先通过new_slab()创建kmem_cache_node结构空间对象的slab,它将会检查传入的flag是否合法,若合法,将会进入主分配函数allocate_slab()。
- 返回到init_kmem_cache_nodes()继续执行,继续申请下一个节点对象。(这个过程由于始终没有更新slab分配器的状态,因此还需要继续使用early_kmem_cache_node_alloc())
- 接下来内核会返回到kmem_cache_open继续运行,内核将尝试使用alloc_kmem_cache_cpus继续执行初始化操作,初始化失败则触发panic。
- 接下来内核会返回到__kmem_cache_create继续运行,如果此时slub分配器仍未初始化完毕,则直接返回。
- 紧接着,内核继续调用kmem_cache_create继续初始化boot_kmem_cache_node结构体,而进入kmem_cache_create后又会直接进入kmem_cache_open,最终的初始化工作将会在kmem_cache_open中完成。
- 接下来内核会返回到create_boot_cache()继续运行,接下来,若没有返回错误,则继续返回到父函数。
- 然后,内核调用create_boot_cache()初始化boot_kmem_cache_node结构体的部分成员变量,被初始化的成员变量如下:name、size、object_size、align、memcg。
- 接下来内核会返回到kmem_cache_init()继续运行,接下来内核将注册内核通知链回调,设定slub分配器的状态为部分初始化已完成,调用create_boot_cache创建kmem_cache对象缓冲区。
- 接下来的调用步骤大多数与之前初始化boot_kmem_cache_node结构体相同,但是,此时的slub分配器的状态为部分初始化已完成。于是此时我们在进入init_kmem_cache_nodes后,在if (slab_state == DOWN)分支处将会使得内核不再使用early_kmem_cache_node_alloc()分配节点,取而代之的使用kmem_cache_alloc_node来进行分配。
- 进入kmem_cache_alloc_node后又会直接进入slab_alloc_node,最终的初始化工作将会在slab_alloc_node中完成。
- 进入slab_alloc_node后,调用slab_pre_alloc_hook进行预处理,返回一个用于分配slub对象的 kmem_cache。
- 接下来如果flag标志位中启用了抢占功能,重新获取当前 CPU 的kmem_cache_cpu结构以及结构中的tid值。
- 接下来加入一个barrier栅栏,然后获得当前cpu的空闲对象列表以及其使用的页面。
- 当前CPU的slub空闲列表为空或者当前slub使用内存页面与管理节点不匹配时,需要重新分配slub对象,我们此时的空闲列表必定为空,因为我们之前仅仅在early_kmem_cache_node_alloc()将一个slub对象放在了partial链表中。那么,内核将会调用__slab_alloc()进行slub对象的分配。
- 在__slab_alloc()中,内核会首先禁用系统中断,并在那之后检查flag中是否允许抢占,若允许,则需要再次获取CPU。
- 在那之后,调用slaballoc()的核心函数slab_alloc()进行对象的分配。
- 在___slab_alloc()中,内核会首先检查有无活动的slub,此时必定没有,于是跳转到new slab处获取一个新的slab。
- 然后内核会检查partial是否为空,不为空则从partial中取出page,然后跳转回redo重试分配。此处我们的partial显然不为空,那么取出page继续执行redo流程。
- 首先检查本处理器所在节点是否指定节点一致,若不一致,则重新获取指定节点。
- 如果节点还是不匹配,则移除cpu slab(释放每cpu变量的所有freelist对象指针),进入new_slab流程。
- 若一致,判断当前页面属性是否为pfmemalloc,如果不是则同样移除cpu slab,进入new_slab流程。
- 再次检查空闲对象指针freelist是否为空,这是为了避免在禁止本地处理器中断前因发生了CPU迁移或者中断,导致本地的空闲对象指针不为空。
- 如果不为空的情况下,将会跳转至load_freelist。
- 如果为空,将会更新慢路径申请对象的统计信息,通过get_freelist()从非冻结页面(未在cpu缓存中)中获取空闲队列。
- 若获取空闲队列失败则需要创建新的slab。
- 此处我们之前是有初始化空闲队列操作的,因此直接跳转到load_freelist执行。
- 从此列表中取出一个空闲对象,返回。
- 接下来内核会返回到__slab_alloc()继续运行,内核启用系统中断,继续将获取到的对象返回。
- 接下来内核会返回到slab_alloc_node继续运行,内核接下来进行初始化对象操作,并进行分配后处理。
- 接下来内核会返回到kmem_cache_alloc_node继续运行,内核接收对象后进行CPU层面的相关设置,继续返回
- 进入kmem_cache_alloc_node后又会直接进入slab_alloc_node,最终的初始化工作将会在slab_alloc_node中完成。
- 接下来内核会返回到kmem_cache_init()继续运行,内核接下来将临时kmem_cache向最终kmem_cache迁移,并修正相关指针,使其指向最终的kmem_cache。这里是因为之前我们用early_kmem_cache_node_alloc()事实上是静态分配的,那么我们需要对其进行迁移。
- 接下来对kmem_cache_node进行迁移及修正。
- 至此,内核中的两大管理结构头已经分配完毕。接下来将使用kmem_cache来初始化整个kmalloc结构。⚠️:在create_kmalloc_caches中,初始化整个kmalloc结构结束后将设置slub_state为UP。
- 接下来对整个kmalloc结构的freelist进行随机排布,以增加内核攻击者的攻击成本(安全措施)。
- 至此,整个slub分配器初始化完毕。

