学习计算机原理,最好是实践或看高手写的源代码,在一定程度上就不再会感到原理的抽象。关于slab一些原理资料,可以在这里下载或到网站有更多的信息和资料。Slab内存管理机制已被广泛使用,要找到使用slab管理内存的开源代码也不难,如一些OS内核中的内存管理。既然要分析理解slab,最好还是选择复杂度和代码量都不要太大的,在这里我选取了glib-2.12.9的gslice.c实现的slab机制相关代码作为分析对象。注意Glib库是针对用户级的而非OS内核级别的。
gslice.c中实现了三种内存分配机制:一是slab;二是比slab更适合于多CPU/多线程的magazine;三是只使用纯粹的malloc。本文章只针对slab相关的源代码进行分析。
在分析代码时主要从以下几个方面入手:先从分配器总体数据结构的关系进行描述;二是看分配器allocator是如何初始化的;接下来是分析分配器如何分配和回收内存(chunk)。
Allocator分配器总体结构:
下面先来看一些重要的数据结构和变量:
…………. //点代表省略的代码
130 typedef struct _ChunkLink ChunkLink;
131 typedef struct _SlabInfo SlabInfo;
132 typedef struct _CachedMagazine CachedMagazine;
// 这个结构也表明了一个Chunk的最小值是两个指针大小
133 struct _ChunkLink {
134 ChunkLink *next;
135 ChunkLink *data; //这字段在slab中未被使用
136 };
137 struct _SlabInfo {
138 ChunkLink *chunks;
139 guint n_allocated;
140 SlabInfo *next, *prev;
141 };
………….
150 typedef struct {
151 gboolean always_malloc; // 为TRUE表示使用纯粹的malloc
152 gboolean bypass_magazines; // 为TRUE表示使用slab
153 gsize working_set_msecs;
154 guint color_increment;
155 } SliceConfig;
156 typedef struct {
157 /* const after initialization */
158 gsize min_page_size, max_page_size;
159 SliceConfig config;
……………
168 /* slab allocator */
169 GMutex *slab_mutex;
// SlabInfo指针数组,最大值为MAX_SLAB_INDEX (allocator)
170 SlabInfo **slab_stack; /* array of MAX_SLAB_INDEX (allocator) */
171 guint color_accu;
172 } Allocator;
………….
// 这个变量如果是0说明allocator还未被初始化,如果是大于0的数说明allocator
// 已被初始化,并且它的值就是系统页面值的大小
189 static gsize sys_page_size = 0;
190 static Allocator allocator[1] = { { 0, }, }; // 内存分配器
// 在变量slice_config中配置选取用那种分配机制,由以下值可知默认情况
// 是使用magazine分配机制
191 static SliceConfig slice_config = {
192 FALSE, /* always_malloc */
193 FALSE, /* bypass_magazines */ // 把这个值设为TRUE,才真正使用slab
194 15 * 1000, /* working_set_msecs */
195 1, /* color increment, alt: 0x7fffffff */
196 };
…………
根据以上的数据结构和程序的逻辑实现,可以把它们的关系用如下的图表示:
Allocator有个SlabInfo指针数组slab_stack成员,stab_stack的每个成员或者是空指针或是一个指向SlabInfo双向循环链表。双向循环链表中的每个成员有个指针指向chunk链表的表头。而chunk链表中的每个成员就是调用接口g_slice_alloc时被分配的空间。
当调用接口g_slice_alloc申请空间时,根据申请的空间大小通过宏SLAB_INDEX找到对应指针数组slab_stack的正确下标,找到对应的slab_stack数组下标就要以找到相应的SlabInfo双向循环链表,也就可以找到Chunk链表并从Chunk链表取出一个节点作为被申请的空间返回。即实际分配内存要先找到SlabInfo双向循环链表,然后再通过它分配内存。
要注意,在下面的分析中会经常用到上图中的几个名词。这些名词有SlabInfo指针数组(allocator->slab_stack)、SlabInfo双向循环链表、每个SlabInfo管理的Chunk链表。还有在下面分析时会把allocator->slab_stack[ix]叫当前的SlabInfo。
allocator分配器初始化:
初始化的调用关系是:g_slice_alloc--->allocator_categorize--->g_slice_init_nomessage---> slice_config_init。
// 以下相关代码是初始化190行定义的allocator变量
751 g_slice_alloc (gsize mem_size)
752 {
……………// 点代表省略的代码
755 guint acat;
……………
757 acat = allocator_categorize (chunk_size);
……………
779 }
// 这个函数的作用是获取allocator分配器的分配机制。
// 返回值:0表示使用纯粹的malloc;1表示使用magazine;2表示使用slab
335 static inline guint
336 allocator_categorize (gsize aligned_chunk_size)
337 {
……………
346 if (!sys_page_size)
347 g_slice_init_nomessage ();
……………
357 }
281 g_slice_init_nomessage (void)
282 {
……………
// 获取系统页面大小,并把值赋给sys_page_size变量
287 #ifdef G_OS_WIN32
288 {
289 SYSTEM_INFO system_info;
290 GetSystemInfo (&system_info);
291 sys_page_size = system_info.dwPageSize;
292 }
293 #else
294 sys_page_size = sysconf (_SC_PAGESIZE); /* = sysconf (_SC_PAGE_SIZE); = getpagesize(); */
295 #endif
……………
298 slice_config_init (&allocator->config);
299 allocator->min_page_size = sys_page_size;
……………
// 建立SlabInfo指针数组(allocator->slab_stack),
// 数组里每个指针值都初始化成NULL值
323 allocator->slab_stack = g_new0 (SlabInfo*, MAX_SLAB_INDEX (allocator));
……………
<!--[if !supportLists]-->333 }<!--[endif]-->
264 slice_config_init (SliceConfig *config)
265 {
……………
// 通过使用191行代码(代码在总揽中已给出)定义的slice_config初始化
// allocator中的config,以此决定了使用哪种分配机制。
273 *config = slice_config;
……………
allocator分配器分配内存chunk:
在分析主要代码之前有必要先了解操作chunk_size字节对齐和求SlabInfo指针数组(allocator->slab_stack)下标的几个宏定义。
chunk_size的字节对齐是通过宏P2ALIGN来实现,P2ALIGN是以P2ALIGNMENT字节对齐的。
// gsize和下文的GLIB_SIZEOF_SIZE_T是同等意义的,它等于一个指针的字节数。可见
// P2ALIGNMENT为两个指针字节数,也即在总揽给出的代码133行中声明的Chunk的最
//小字节数。我们一般假设在32位机器中,一个指针的字节数为4,
// 那么P2ALIGNMENT的值为8。
// 在下面的分析中,如果有假设数据字节数,就认为P2ALIGNMENT的值为8
103 #define P2ALIGNMENT (2 * sizeof (gsize)) /* fits 2 pointers (assumed to be 2 * GLIB_SIZEOF_SIZE_T below) */
// ALIGN功能是求size以base字节对齐的数据。这是一种常用的方法,如果看不明白
// 可以假设一些真实的数据进去运算,当然假设数据时base值最好是2的n次方。
104 #define ALIGN(size, base) ((base) * (gsize) (((size) + (base) - 1) / (base)))
// 下面的P2ALIGN也是以一定的字节数对齐的操作,它用的了一些二进制的技巧
// 可以参考本人写的另外一篇文章讲二进制技巧那部分或有关这方面知识的其它资料。
116 /* optimized version of ALIGN (size, P2ALIGNMENT) */
117 #if GLIB_SIZEOF_SIZE_T * 2 == 8 /* P2ALIGNMENT */
118 #define P2ALIGN(size) (((size) + 0x7) & ~(gsize) 0x7) // 以8字节对齐
119 #elif GLIB_SIZEOF_SIZE_T * 2 == 16 /* P2ALIGNMENT */
120 #define P2ALIGN(size) (((size) + 0xf) & ~(gsize) 0xf) // 以16字节对齐
121 #else
122 #define P2ALIGN(size) ALIGN (size, P2ALIGNMENT)
123 #endif
求SlabInfo指针数组(allocator->slab_stack)下标的宏是代码112行的SLAB_INDEX(al, asize),从代码981行可知宏SLAB_INDEX里的asize就是chunk_size。代码981行的chunk_size是从代码756行调用P2ALIGN获得的,可见传给宏SLAB_INDEX的asize已是P2ALIGNMENT字节对齐的了。下图更直接明了地说明了chunk_size和SlabInfo指针数组(allocator->slab_stack)下标的关系。可见SlabInfo指针数组(allocator->slab_stack)下标从小到大的成员所指向的SlabInfo双向循环链表(此链表见总揽图)的chunk_size是从小到大的P2ALIGNMENT整数倍数。
// 通过asize求SlabInfo指针数组(allocator->slab_stack)的下标,
// asize以P2ALIGNMENT字节对齐
112 #define SLAB_INDEX(al, asize) ((asize) / P2ALIGNMENT - 1) /* asize must be P2ALIGNMENT aligned */
750 gpointer
751 g_slice_alloc (gsize mem_size)
752 {
753 gsize chunk_size;
754 gpointer mem;
755 guint acat;
// 对要分配的内存大小通过宏P2ALIGN变为以P2ALIGNMENT字节对齐的大小
// 并把它赋给chunk_size
756 chunk_size = P2ALIGN (mem_size);
……………
772 g_mutex_lock (allocator->slab_mutex);
773 mem = slab_allocator_alloc_chunk (chunk_size);
774 g_mutex_unlock (allocator->slab_mutex);
……………
778 return mem;
779 }
977 static gpointer
978 slab_allocator_alloc_chunk (gsize chunk_size)
979 {
980 ChunkLink *chunk;
// 求SlabInfo指针数组(allocator->slab_stack)下标,
// 也即找到chunk_size对应的SlabInfo双向循环链表
981 guint ix = SLAB_INDEX (allocator, chunk_size);
982 /* ensure non-empty slab */
// 判断chunk_size对应的SlabInfo双向循环链表的循环是否还未建立或是
// 当前的SlabInfo中的chunk是否已被分配完。
// 如果两者的任何一个成立,那么就重新建立一个新的SlabInfo,并把
// 当前SlabInfo指针allocator->slab_stack[ix]指向新建的SlabInfo,新建
// 的SlabInfo包含了新分配的chunk链表,这功能在allocator_add_slab函数完成。
983 if (!allocator->slab_stack[ix] || !allocator->slab_stack[ix]->chunks)
984 allocator_add_slab (allocator, ix, chunk_size);
985 /* allocate chunk */
986 chunk = allocator->slab_stack[ix]->chunks;
// 让被分配的chunk脱离chunk链表
987 allocator->slab_stack[ix]->chunks = chunk->next;
988 allocator->slab_stack[ix]->n_allocated++;
989 /* rotate empty slabs */
// 如果当前的SlabInfo的chunk已分配完,就让当前的SlabInfo指针指
// 向下一个SlabInfo。
990 if (!allocator->slab_stack[ix]->chunks)
991 allocator->slab_stack[ix] = allocator->slab_stack[ix]->next;
992 return chunk;
993 }
// 这函数的代码分析也可以直接看下面图A解释或是两者结合起来理解。
926 static void
927 allocator_add_slab (Allocator *allocator,
928 guint ix,
929 gsize chunk_size)
930 {
931 ChunkLink *chunk;
932 SlabInfo *sinfo;
933 gsize addr, padding, n_chunks, color = 0;
934 gsize page_size = allocator_aligned_page_size (allocator, SLAB_BPAGE_SIZE (allocator, chunk_size));
935 /* allocate 1 page for the chunks and the slab */
/* 分配一页内存给slab和chunk链表 */
936 gpointer aligned_memory = allocator_memalign (page_size, page_size - NATIVE_MALLOC_PADDING);
937 guint8 *mem = aligned_memory;
938 guint i;
……………
952 /* basic slab info setup */
// 把SlabInfo结构信息放在刚分配的一页内存的高地址处
953 sinfo = (SlabInfo*) (mem + page_size - SLAB_INFO_SIZE);
954 sinfo->n_allocated = 0;
955 sinfo->chunks = NULL;
956 /* figure cache colorization */
// 计算这一页内存能够划分成多少(n_chunks)个chunk。
957 n_chunks = ((guint8*) sinfo - mem) / chunk_size;
// 再判断是否还有剩余的空间padding,如果有另作他用。
958 padding = ((guint8*) sinfo - mem) - n_chunks * chunk_size;
959 if (padding)
960 {
961 color = (allocator->color_accu * P2ALIGNMENT) % padding;
962 allocator->color_accu += allocator->config.color_increment;
963 }
964 /* add chunks to free list */
// 找出chunk链表的表头
965 chunk = (ChunkLink*) (mem + color);
966 sinfo->chunks = chunk;
// 循环构建chunk链表:把地址相邻的chunk链接起来
967 for (i = 0; i < n_chunks - 1; i++)
968 {
// 当前chunk指向下一个chunk,
// (chunk + chunk_size)是下一个chunk的起始地址。
969 chunk->next = (ChunkLink*) ((guint8*) chunk + chunk_size);
970 chunk = chunk->next;
971 }
// 最后一个chunk指向NULL
972 chunk->next = NULL; /* last chunk */
973 /* add slab to slab ring */
974 allocator_slab_stack_push (allocator, ix, sinfo);
975 }
// 函数功能是根据SlabInfo指针数组(allocator->slab_stack)下标ix
// 把新建的SlabInfo链入对应的SlabInfo双向循环链表,并把当前SlabInfo指针
// allocator->slab_stack[ix]指向新建的SlabInfo。
896 allocator_slab_stack_push (Allocator *allocator,
897 guint ix,
898 SlabInfo *sinfo)
899 {
900 /* insert slab at slab ring head */
901 if (!allocator->slab_stack[ix])
902 {
903 sinfo->next = sinfo;
904 sinfo->prev = sinfo;
905 }
906 else
907 {
908 SlabInfo *next = allocator->slab_stack[ix], *prev = next->prev;
909 next->prev = sinfo;
910 prev->next = sinfo;
911 sinfo->next = next;
912 sinfo->prev = prev;
913 }
914 allocator->slab_stack[ix] = sinfo;
915 }
对于以上的代码,重点对allocator_add_slab函数进行更为详细的分析,它的功能主要是申请一页内存,用这一内存新建立一个SlabInfo,并把它链入对应的SlabInfo双向循环链表。对于新建立的SlabInfo,几乎所有跟它相关的内部信息都在申请的那页内存上:
现在结合上图展开说明。代码936、937行申请一页内存,并把起始地址给mem变量。代码953行把页面的高地址分给了SlabInfo结构。如果有padding的话,代码958到963是把padding另作它用。而上图color的大小和空白的大小相加就是padding的值了,这点细节也可以不用太多关注它。chunk链表的起始地址,即链表表头在代码的965行确定的,而上图SlabInfo有个指向chunk链表头的指针是在代码966行实现的。图中chunk链表的建立是在代码967到972实现的。
allocator分配器回收内存chunk:
分配器对内存的分配和回收就很简单了,通过函数g_slice_free1调用了函数slab_allocator_free_chunk,下面仅对slab_allocator_free_chunk函数分析:
// 参数中的mem就是要释放回收的内存chunk
996 slab_allocator_free_chunk (gsize chunk_size,
997 gpointer mem)
998 {
999 ChunkLink *chunk;
1000 gboolean was_empty;
1001 guint ix = SLAB_INDEX (allocator, chunk_size);
1002 gsize page_size = allocator_aligned_page_size (allocator, SLAB_BPAGE_SIZE (allocator, chunk_size));
// 这是求mem所在的页面的起始地址。因地址在程序逻辑中是扁平线性的,
// 所以(mem / page_size)就是mem所属的是第几个页面,那么它乘上page_size就是
// mem所在的页面的起始地址。
1003 gsize addr = ((gsize) mem / page_size) * page_size;
1004 /* mask page adress */
1005 guint8 *page = (guint8*) addr;
// 获取管理mem的SlabInfo的指针。在上面已提到过SlabInfo是放在一个页面
// 的高地址处。
1006 SlabInfo *sinfo = (SlabInfo*) (page + page_size - SLAB_INFO_SIZE);
1007 /* assert valid chunk count */
1008 mem_assert (sinfo->n_allocated > 0);
1009 /* add chunk to free list */
1010 was_empty = sinfo->chunks == NULL;
1011 chunk = (ChunkLink*) mem;
// 把要回收的chunk链入SlabInfo管理的chunk链表的表头
1012 chunk->next = sinfo->chunks;
1013 sinfo->chunks = chunk;
1014 sinfo->n_allocated--;
1015 /* keep slab ring partially sorted, empty slabs at end */
// was_empty为TRUE,表明管理要回收的chunk的SlabInfo所在的SlabInfo双向循环链表
// 中的每一个SlabInfo都可能已把它自己的chunk分配完,即它们都没有空间可分配了。
// 那么就应该把当前的SlabInfo改为这次回收了内存chunk的SlabInfo,以备下次分配用。
1016 if (was_empty)
1017 {
1018 /* unlink slab */
1019 SlabInfo *next = sinfo->next, *prev = sinfo->prev;
1020 next->prev = prev;
1021 prev->next = next;
1022 if (allocator->slab_stack[ix] == sinfo)
1023 allocator->slab_stack[ix] = next == sinfo ? NULL : next;
1024 /* insert slab at head */
// 重新把SlabInfo链入SlabInfo双向循环链表,为的是把当
// 前SlabInfo(allocator->slab_stack[ix])改为这次回收了内存chunk的SlabInfo
1025 allocator_slab_stack_push (allocator, ix, sinfo);
1026 }
1027 /* eagerly free complete unused slabs */
1028 if (!sinfo->n_allocated)
1029 {
1030 /* unlink slab */
1031 SlabInfo *next = sinfo->next, *prev = sinfo->prev;
1032 next->prev = prev;
1033 prev->next = next;
1034 if (allocator->slab_stack[ix] == sinfo)
1035 allocator->slab_stack[ix] = next == sinfo ? NULL : next;
1036 /* free slab */
// allocator_memfree函数功能是系统回收内存空间
1037 allocator_memfree (page_size, page);
1038 }
1039 }
到此已把slab相关的代码已分析完,比slab更适合于多CPU/多线程的magazine内存管理机制比slab更复杂但也更有用更有意思,我将在下一篇给出。
278 }
从以上的代码可知,如果只看初始化相关的代码,这一过程极其的简单!它主要做了三样事情:一是获得系统页面大小sys_page_size;二是初始化config,以此决定了allocator分配器使用的分配机制;三是建立了SlabInfo指针数组。