Read the fucking source code!
--By 鲁迅A picture is worth a thousand words.
--By 高尔基说明:node
以前的文章分析的都是基于页面的内存分配,而小块内存的分配和管理是经过块分配器来实现的。目前内核中,有三种方式来实现小块内存分配:slab, slub, slob
,最早有slab
分配器,slub/slob
分配器是改进版,slob
分配器适用于小内存嵌入式设备,而slub
分配器目前已逐渐成为主流块分配器。接下来的文章,就是以slub
分配器为目标,进一步深刻。缓存
先来一个初印象:
数据结构
有四个关键的数据结构:函数
struct kmem_cache
:用于管理SLAB缓存
,包括该缓存中对象的信息描述,per-CPU/Node管理slab页面等;/* * Slab cache management. */ struct kmem_cache { struct kmem_cache_cpu __percpu *cpu_slab; //每一个CPU slab页面 /* Used for retriving partial slabs etc */ unsigned long flags; unsigned long min_partial; int size; /* The size of an object including meta data */ int object_size; /* The size of an object without meta data */ int offset; /* Free pointer offset. */ #ifdef CONFIG_SLUB_CPU_PARTIAL /* Number of per cpu partial objects to keep around */ unsigned int cpu_partial; #endif struct kmem_cache_order_objects oo; //该结构体会描述申请页面的order值,以及object的个数 /* Allocation and freeing of slabs */ struct kmem_cache_order_objects max; struct kmem_cache_order_objects min; gfp_t allocflags; /* gfp flags to use on each alloc */ int refcount; /* Refcount for slab cache destroy */ void (*ctor)(void *); // 对象构造函数 int inuse; /* Offset to metadata */ int align; /* Alignment */ int reserved; /* Reserved bytes at the end of slabs */ int red_left_pad; /* Left redzone padding size */ const char *name; /* Name (only for display!) */ struct list_head list; /* List of slab caches */ //kmem_cache最终会连接在一个全局链表中 struct kmem_cache_node *node[MAX_NUMNODES]; //Node管理slab页面 };
struct kmem_cache_cpu
:用于管理每一个CPU的slab页面
,可使用无锁访问,提升缓存对象分配速度;struct kmem_cache_cpu { void **freelist; /* Pointer to next available object */ //指向空闲对象的指针 unsigned long tid; /* Globally unique transaction id */ struct page *page; /* The slab from which we are allocating */ //slab缓存页面 #ifdef CONFIG_SLUB_CPU_PARTIAL struct page *partial; /* Partially allocated frozen slabs */ #endif #ifdef CONFIG_SLUB_STATS unsigned stat[NR_SLUB_STAT_ITEMS]; #endif };
struct kmem_cache_node
:用于管理每一个Node的slab页面
,因为每一个Node的访问速度不一致,slab
页面由Node来管理;/* * The slab lists for all objects. */ struct kmem_cache_node { spinlock_t list_lock; #ifdef CONFIG_SLUB unsigned long nr_partial; //slab页表数量 struct list_head partial; //slab页面链表 #ifdef CONFIG_SLUB_DEBUG atomic_long_t nr_slabs; atomic_long_t total_objects; struct list_head full; #endif #endif };
struct page
:用于描述slab页面
,struct page
结构体中不少字段都是经过union
联合体进行复用的。struct page
结构中,用于slub
的成员以下:struct page { union { ... void *s_mem; /* slab first object */ ... }; /* Second double word */ union { ... void *freelist; /* sl[aou]b first free object */ ... }; union { ... struct { union { ... struct { /* SLUB */ unsigned inuse:16; unsigned objects:15; unsigned frozen:1; }; ... }; ... }; }; /* * Third double word block */ union { ... struct { /* slub per cpu partial pages */ struct page *next; /* Next partial slab */ #ifdef CONFIG_64BIT int pages; /* Nr of partial slabs left */ int pobjects; /* Approximate # of objects */ #else short int pages; short int pobjects; #endif }; struct rcu_head rcu_head; /* Used by SLAB * when destroying via RCU */ }; ... struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */ ... }
图来了:
工具
针对Slub的使用,能够从三个维度来分析:atom
下边将进一步分析。3d
在内核中经过kmem_cache_create
接口来建立一个slab缓存
。指针
先看一下这个接口的函数调用关系图:
code
kmem_cache_create
完成的功能比较简单,就是建立一个用于管理slab缓存
的kmem_cache
结构,并对该结构体进行初始化,最终添加到全局链表中。kmem_cache
结构体初始化,包括了上文中分析到的kmem_cache_cpu
和kmem_cache_node
两个字段结构。对象
在建立的过程当中,当发现已有的slab缓存
中,有存在对象大小相近,且具备兼容标志的slab缓存
,那就只须要进行merge操做并返回,而无需进一步建立新的slab缓存
。
calculate_sizes
函数会根据指定的force_order
或根据对象大小去计算kmem_cache
结构体中的size/min/oo
等值,其中kmem_cache_order_objects
结构体,是由页面分配order
值和对象数量
二者经过位域拼接起来的。
在建立slab缓存
的时候,有一个先鸡后蛋的问题:kmem_cache
结构体来管理一个slab缓存
,而建立kmem_cache
结构体又是从slab缓存
中分配出来的对象,那么这个问题是怎么解决的呢?能够看一下kmem_cache_init
函数,内核中定义了两个静态的全局变量kmem_cache
和kmem_cache_node
,在kmem_cache_init
函数中完成了这两个结构体的初始化以后,至关于就是建立了两个slab缓存
,一个用于分配kmem_cache
结构体对象的缓存池,一个用于分配kmem_cache_node
结构体对象的缓存池。因为kmem_cache_cpu
结构体是经过__alloc_percpu
来分配的,所以不须要建立一个相关的slab缓存
。
kmem_cache_alloc
接口用于从slab缓存池中分配对象。
看一下大致的调用流程图:
从上图中能够看出,分配slab对象与Buddy System
中分配页面相似,存在快速路径和慢速路径两种,所谓的快速路径就是per-CPU缓存
,能够无锁访问,于是效率更高。
总体的分配流程大致是这样的:优先从per-CPU缓存
中进行分配,若是per-CPU缓存
中已经所有分配完毕,则从Node
管理的slab页面中迁移slab页
到per-CPU缓存
中,再从新分配。当Node
管理的slab页面也不足的状况下,则从Buddy System
中分配新的页面,添加到per-CPU缓存
中。
仍是用图来讲明更清晰,分为如下几步来分配:
fastpath
快速路径下,以原子的方式检索per-CPU缓存的freelist列表中的第一个对象,若是freelist为空而且没有要检索的对象,则跳入慢速路径操做,最后再返回到快速路径中重试操做。
slowpath-1
将per-CPU缓存中page指向的slab页中的空闲对象迁移到freelist中,若是有空闲对象,则freeze该页面,没有空闲对象则跳转到slowpath-2
。
slowpath-2
将per-CPU缓存中partial链表中的第一个slab页迁移到page指针中,若是partial链表为空,则跳转到slowpath-3
。
slowpath-3
将Node管理的partial链表中的slab页迁移到per-CPU缓存中的page中,并重复第二个slab页将其添加到per-CPU缓存中的partial链表中。若是迁移的slab中空闲对象超过了kmem_cache.cpu_partial
的一半,则仅迁移slab页
,而且再也不重复。
若是每一个Node的partial链表都为空,跳转到slowpath-4
。
slowpath-4
从Buddy System
中获取页面,并将其添加到per-CPU的page中。
kmem_cache_free
的操做,能够当作是kmem_cache_alloc
的逆过程,所以也分为快速路径和慢速路径两种方式,同时,慢速路径中又分为了好几种状况,能够参考kmem_cache_alloc
的过程。
调用流程图以下:
效果以下:
快速路径释放
快速路径下,直接将对象返回到freelist中便可。
put_cpu_partial
put_cpu_partial
函数主要是将一个刚freeze的slab页,放入到partial链表中。
在put_cpu_partial
函数中调用unfreeze_partials
函数,这时候会将per-CPU管理的partial链表中的slab页面添加到Node管理的partial链表的尾部。若是超出了Node的partial链表,溢出的slab页面中没有分配对象的slab页面将会返回到伙伴系统。
add_partial
添加slab页到Node的partial链表中。
remove_partial
从Node的partial链表移除slab页。
具体释放的流程走哪一个分支,跟对象的使用状况,partial链表的个数nr_partial/min_partial
等相关,细节就再也不深刻分析了。