基本原理
Linux 在保护模式下,由伙伴系统负责内存分配,分配内存的最小单位是页。在伙伴系统之上,Linux 又增加了 slab 机制,其工作是针对一些经常分配并释放的对象,如 task_struct 结构体等,这种对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,不仅会造成大量的内部碎片,而且处理速度也太慢。而 slab 机制是基于对象进行管理的,相同类型的对象归为一类(如 tast_struct 就是一类),每次要申请这样一个对象,slab 缓存器就从一个 slab 列表中分配一个这样大小的单元出去,而当要释放时,将其重新归还给 slab 缓存器,而不是直接归还给伙伴系统。slab 分配对象时,会使用最近释放的对象内存块,因此其驻留在 CPU 高速缓存的概率较高。并且 slab 机制针对 SMP 和 NUMA 架构进行了处理,还考虑到了硬件 cache 方面的优化。
内存管理这一部分的处理框架就是下图:
管理机构图示
slab 机制管理机构如图:
主要数据结构
kmem_cache缓存器结构体
slab 机制为每种对象建立单独的 kmem_cache(下文统称缓存器),每个 缓存器实际上就是指定了一种规则,符合该缓存器描述规则的对象都将从该缓存器分配。缓存器自身被组织成了一个双向链表,它的头节点是 cache_chain。
实际上,有两种缓存器,general cache(通用缓存器) 和 specific cache(专用缓存器),通用缓存器包括:
- 缓存 kmem_cache 的缓存器(呵呵,有点玄乎,kmem_cache 即缓存器,它自己也是一个小内存,也需要使用 slab 机制来分配,所以这就是一个鸡与蛋的问题,我们需要“手工”制造一个缓存器,名曰 cache_cache 来缓存 kmem_cache 这个通用缓存器,完了再取而代之就行)。
- kmalloc 使用的对象按照 malloc_sizes[] 表的大小分属不同的缓存器,32、64、128…,每种大小对应两个两个缓存器,一个对应DMA,一个用于普通分配,这也是通用缓存器。
通用缓存器是按照大小进行划分,所以大小就是通用缓存器的主要规则。
专用缓存器为系统特定结构创建的对象,比如 struct file,此类缓存器对象来源于同一个结构。
下面来看缓存器的结构体:
//缓存器
struct kmem_cache {
/* 1) per-cpu data, touched during every alloc/free */
//per-cpu数据,本地缓存,记录了本地高速缓存的信息,也用于跟踪最近释放的对象,每次分配和释放都先访问它
struct array_cache *array[NR_CPUS];
/* 2) Cache tunables. Protected by cache_chain_mutex */
unsigned int batchcount; //本地缓存转入或转出的大批对象数目
unsigned int limit; //本地缓存空闲对象的最大数目
unsigned int shared; //是否支持本节点共享一部分cache的标志,如果支持,那就存在本地共享缓存
unsigned int buffer_size; //管理的对象大小
u32 reciprocal_buffer_size; //上面这个大小的倒数,貌似利用这个可用牛顿迭代法求什么:)
/* 3) touched by every alloc & free from the backend */
unsigned int flags; //cache 的永久标志
unsigned int num; //一个 slab 所包含的对象数目!!! 也就是说,kmem_cache 控制了它所管辖的所有对象大小数目及其他属性
/* 4) cache_grow/shrink */
/* order of pgs per slab (2^n) */
unsigned int gfporder; //一个slab所包含的 page 的对数,也就是一个slab分配 2^gfporder 个 page
/* force GFP flags, e.g. GFP_DMA */
gfp_t gfpflags; //与伙伴系统交互时所提供的分配标识
size_t colour; /* cache colouring range */ //着色的范围吧
unsigned int colour_off; /* colour offset */ //着色的偏移量
struct kmem_cache *slabp_cache;//如果将slab描述符存储在外部,该指针指向slab描述符的 cache,否则为 NULL
unsigned int slab_size; // slab 的大小
unsigned int dflags; /* dynamic flags */ //FIXME: 动态标志
/* constructor func */
void (*ctor) (void *, struct kmem_cache *, unsigned long);
/* 5) cache creation/removal */
const char *name; //名字:)
struct list_head next; //构造链表所用
/* 6) statistics */
#if STATS
//都是调试信息,略去
#endif
/*
* We put nodelists[] at the end of kmem_cache, because we want to size
* this array to nr_node_ids slots instead of MAX_NUMNODES
* (see kmem_cache_init())
* We still use [MAX_NUMNODES] and not [1] or [0] because cache_cache
* is statically defined, so we reserve the max number of nodes.
*/
//nodelists 用于组织所有节点的 slab,每个节点寻找自己拥有的 cache 将自己作为 nodelists 的下标就可以访问了
//不过从这里访问的只是每个节点的 slab 管理的 cache 以及每个节点的共享 cache ,per-cpu cache 是上面的array数组管理的
//当然,针对同一个缓存器kmem_cache,它管理的是同一种对象,所以通过本 kmem_cache 结构体的 nodelists 成员访问的也就只是同种对象的 cache
struct kmem_list3 *nodelists[MAX_NUMNODES];
/*
* Do not add fields after nodelists[]?
*/
};
始终记住一点:缓存器的职责就是为它负责缓存的对象指定规则! 符合某个规则的对象就会从相应的缓存器去申请内存。
从上面的结构体可以看出,同一个缓存器中所有对象大小是相同的(buffer_size),并且同一缓存器中所有 slab 大小也是相同的(gfporder、num)。
缓存器最重要有三个成员:
- array[] 数组。它是每 CPU(per_cpu) 数据,也就是我们的本地缓存。这是为了减小 SMP 架构下自旋锁而设置的成员,无论是对象的分配还是回收都优先考虑本地缓存。
- shared 标志。该标志如果使能的话,缓存器成员 nodelist 对应的三链就会启用 struct array_cache* 类型的成员 shared,它用来串接共享的缓存对象。这就是所谓的本地共享缓存,用于 CPU 之间的对象共享。
- nodelist[] 数组。该数组的 index 是 NUMP 架构下的内存结点 node,数组成员都是三链。针对每个内存结点,缓存器都为它维持一个三链。三链是指 struct kmem_list3 结构体,该结构体内部有满(full)、部分满(partial)、空(free)三种以 slab 内部成员分配情况为依据建立的链表,所有的 slab 缓存都维系在三链上。
为什么要引入本地共享缓存?
考虑到下面的场景:
CPU1 收到大量的网络报文,分配 struct sk_buff 对象,报文处理完成后,由 CPU2 发出并释放,这样对象就被 CPU2 正好回收。这会造成 CPU1 的本地缓存耗尽,需要从三链中分配对象。而 CPU2 的本地缓存过多需要释放对象到三链中。此时本地共享缓存相当于给它们架了一座桥梁,有了它,上述情形执行由本地共享缓存负责交互即可,无需再访问三链。
array_cache结构体
本地缓存其实就是一个 array_cache 结构体。不过由于处于多 CPU 环境,因此就组建了一个该类型的 array 数组,将各个 CPU 的本地缓存组织在一起,作为缓存器的成员,统一起来方便管理。
/*
* struct array_cache
*
* Purpose:
* - LIFO ordering, to hand out cache-warm objects from _alloc
* - reduce the number of linked list operations
* - reduce spinlock operations
*
* The limit is stored in the per-cpu structure to reduce the data cache
* footprint.
*
*/ //array_cache中都是per-cpu数据,不会共享,这可以减少NUMA架构中多CPU的自旋锁竞争
struct array_cache {
unsigned int avail; //本地缓存中可用的空闲对象数
unsigned int limit; //本地缓存空闲对象数目上限
unsigned int batchcount; //本地缓存一次性转入和转出的对象数量
unsigned int touched; //标识本地对象是否最近被使用
spinlock_t lock; //自旋锁
void *entry[0]; /* //这是一个柔性数组,便于对后面用于跟踪空闲对象的指针数组的访问
* Must have this definition in here for the proper
* alignment of array_cache. Also simplifies accessing
* the entries.
* [0] is for gcc 2.95. It should really be [].
*/
};
注意该结构体的最后一个元素 void* entry[0],这就是它能串接缓存对象的原因。
kmem_list3三链结构体
三链结构体如下:
/*
* The slab lists for all objects.
*/
struct kmem_list3 {
struct list_head slabs_partial;//部分满的slab链表,也就是部分对象呗分配出去的slab
struct list_head slabs_full; //满slab链表
struct list_head slabs_free; //空slab链表
unsigned long free_objects; //空闲对象的个数
unsigned int free_limit; //空闲对象的上限数目
unsigned int colour_next; /* Per-node cache coloring */ //每个节点下一个slab使用的颜色
spinlock_t list_lock;
struct array_cache *shared; /* shared per node */ //每个节点共享出去的缓存
struct array_cache **alien; /* on other nodes */ //FIXME: 其他节点的缓存,应该是共享的
unsigned long next_reap; /* updated without locking */
int free_touched; /* updated without locking */
};
申请对象三部曲,先从本地缓存申请,本地缓存没有就找本地共享缓存,如果还没有,就来找三链了。最差的情况是,三链也没有,那就只能去伙伴系统要点 page,新建 slab 了。
当空闲对象比较富余时,free 链表的部分 slab 可能被定期回收。
slab结构体
struct slab 结构体如下,它的另外一个名字是 slab 描述符(与kmem_bufclt数组组成 slab 管理者(manager)。
/*
* struct slab
*
* Manages the objs in a slab. Placed either at the beginning of mem allocated
* for a slab, or allocated from an general cache. //该管理者可以在slab头部申请内存,也可以从general cache处申请内存。
* Slabs are chained into three list: fully used, partial, fully free slabs.
*/
struct slab {
struct list_head list; //用于将slab纳入三链之中
unsigned long colouroff; //该slab的着色偏移,是一大块,不是单位
void *s_mem; //指向slab中的第一个对象
unsigned int inuse; //slab中已分配出去的对象数目
kmem_bufctl_t free; //下一个空闲对象的下标
unsigned short nodeid; //NUMA架构节点标识号
};
这个结构体负责描述一个 slab 的情况。slab 机制还采用 kmem_bufctl_t (unsigned int) 类型数组来存储对象的下标(在这个结构体内没出现该数组,是在外部和它组合的),它们的存储位置是相连的。将它们两个统称为“slab管理者”。
slab 管理者的自身存储位置有两种,一种是 on-slab(内置式),一种是 off-slab (外置式)。内置式需要占据 slab 内部空间,所以计算偏移量什么的要加上。而外置式是重新申请一块 slab 专门用来存放 slab 管理者(总共两块 slab)。该数组使用指针强制转化方式访问,所以没有设为结构体成员。
通常对象小于 512 的小对象采用内置式 slab,大于等于 512 的大对象采用外置式 slab。
内置式:
外置式:
参考:
后记
先默哀一秒钟,从 9 号晚上开始看 slab,这几天晕晕乎乎的,尤其是所谓的缓存器的缓存 cache_cache,我至少想了一天才想明白套路。内核确实不好剖析,主要还是没有人出相应的书吧,期待有一天有相关的书,那就可以节约学习内核的人大量时间了。