内核堆

这里讲slub讲得比较详细:图解slub (wowotech.net),下面的图也是来自该文章

首先声明,我参考的是Kernel 5.18.8 的源码,上面的文章是4.15.0,出现冲突的地方使用4.15.0的结构(因为更简单),两者基本分配过程一致。

分配器

最基本的分配器是伙伴系统(Buddy),可以从中按页分配。

对于小规模的内存分配,有三种分配算法,slab、slub、slob,slab是最早提出的(复杂、对多cpu不友好),slub是目前Kernel使用的(slab简化版,多cpu性能高于slab),slob是面向嵌入式的(精简),三者都是从伙伴系统中申请初始内存(不能越过Buddy直接申请)。

linux对三者使用统一的外部接口,定义在 linux/slab.h

后面只讨论slub算法,提到slab则是指内部管理的一种特殊结构。

Overview

分配的基本结构是kmem_cache,里面保存了该slab cache的各种属性,每个kmem_cache只能分配出一种大小的堆块(后面我们将堆块称为object)。

例如通过kmem_cache_alloc分配堆块时,第一个参数就是kmem_cache指针,表示通过该cache分配堆块(第二个参数含义可参考gfp.h - include/linux/gfp.h - Linux source code (v5.18.8) - Bootlin,或者这里

void *[**kmem_cache_alloc**](<https://elixir.bootlin.com/linux/latest/C/ident/kmem_cache_alloc>)(struct [**kmem_cache**](<https://elixir.bootlin.com/linux/latest/C/ident/kmem_cache>) *s, [**gfp_t**](<https://elixir.bootlin.com/linux/latest/C/ident/gfp_t>) flags)

kernel默认已经分配了kmalloc_info数组(大小26),除了前三个(0、96、192),里面分配出的object大小都为2^下标次方,内核中通常也从中分配空间。

kmalloc的行为就是找合适大小的kmalloc_info 并分配空间(当然,若超出范围则会使用kmalloc_large)。由于kmalloc 是在include中实现的,实际调用的是**__kmalloc** 或**kmem_cache_alloc_trace** ,在逆向时可能会发现该函数被内联。

具体结构

首先是kmem_cache中有两个子结构,kmem_cache_cpu**kmem_cache_node** ,前者对于每个CPU核心都有一个,在分配时会被优先使用,后者对于每个NUMA node(基本可以被视为一个插槽)只有一个。

两者中保存的都是一些page(新kernel里搞了个新结构体slab),以单向链表的形式储存,每个page管辖x页(2^order个连续的联合页)从伙伴系统申请到的内存(后面将该片内存称为page),每个page会被分割为多个object,也就是最终分配给用户的内存。

object结构结构十分简单,当它未被分配时,开头(理论上是+offset的位置,但默认内核编译参数offset为0)一个机器字长大小的空间存储一个指针,指向同一个slab中的下一个未被使用的object,若已被分配,该空间就无意义。