本文是对glib-2.12.9的gslice.c实现的mgazine内存管理机制代码的分析。在研读magazine内存管理相关代码前,最好是先熟悉实现slab相关的代码,因为magazine是以slab为基础的。我上一次写了一篇关于slab代码分析的文章,因此在这里涉及到slab相关的内容都可以参考那篇文章的分析。关于magazine原理的英文资料可以到代码作者提供的网站获取更多相关信息。
在这篇文章中不再对源代码每一行作详细地分析,主要是从整体上把握它,以便于代码的阅读:阅读关于内存的分配和释放的源代码可以结合内存分配和释放的流程图、内存层次图和相关解说;对于一些较为迷惑的细节如线程私有magazine的内存组织、和内存操作管理相关的二级链表也都给出了相关的图解。
1. Magazine内存管理总体描述:
Magazine为什么更适合于多线程,而slab却不适合?当多线程并发访问同一临界区时,对加锁和解锁的操作是难免的,这必然会造成多线程的串行等待最终影响效率。引入一些内存管理方法目的就是为了更好地利用内存,而效率是很重要的一个方面,如果多线程为了使用内存而受到效率的影响,无疑这种内存管理方法是值得深思的!slab分配器就是一个临界区,如果多线程只是共享这么一个分配器也就出现多线程的串行等待问题。然而gslice.c中的magazine还是多线程共享slab,但它在这方面做了两个优化,一是对slab分了两层(内存回收/分配slab层和Magazine cache缓存层),二是让每个线程有自己私有的slab(线程私有的magazine)。当使用magazine管理内存时,slab和magazine的层次关系可用下图表示:
线程在申请内存时先从线程私有层申请,如果线程私有层没有可用内存就从Magazine cache层申请,如果还没有就又往下一层申请,以此类推直到申请到内存为止。申请内存的详细流程图如下:
2. 线程私有层的数据和逻辑组织分析:
Glib库对线程接口作了封装,以下两组是语义相等的接口,在接下来的代码分析可能会用到。
Glib
|
posix
|
g_private_new
|
pthread_key_create
|
g_private_get
|
pthread_getspecific
|
g_private_set
|
pthread_setspecific
|
………………..// 点代表示省略的代码
142 typedef struct {
143 ChunkLink *chunks;
144 gsize count; /* approximative chunks list length */
145 } Magazine;
146 typedef struct {
147 Magazine *magazine1; /* array of MAX_SLAB_INDEX (allocator) */
148 Magazine *magazine2; /* array of MAX_SLAB_INDEX (allocator) */
149 } ThreadMemory; // 可看作线程私有层内存管理结构
………………..
400 static inline ThreadMemory*
401 thread_memory_from_self (void)
402 {
// 获取线程私有层内存管理结构ThreadMemory
// 如果不存在就创建它
403 ThreadMemory *tmem = g_private_get (private_thread_memory);
404 if (G_UNLIKELY (!tmem))
405 {
406 const guint n_magazines = MAX_SLAB_INDEX (allocator);
407 tmem = g_malloc0 (sizeof (ThreadMemory) + sizeof (Magazine) * 2 * n_magazines);
// 指向magazine1数组的起始地址
408 tmem->magazine1 = (Magazine*) (tmem + 1);
// 指向magazine2数组的起始地址
409 tmem->magazine2 = &tmem->magazine1[n_magazines];
410 g_private_set (private_thread_memory, tmem);
411 }
412 return tmem;
413 }
结合下图C可看出代码407行申请了一块连续内存用于三部分:ThreadMemory、ThreadMemory内部指针所指向的magazine1和magazine2。请注意magazine1和magazine2是两个独立的Magazine数组,只不过在这里它们的地址是连续的罢了。申请一块连续内存而不分别申请是一种常用的较好的做法,多次分别申请内存效率没单独一次申请那么好也更容易产生内存碎片。
magazine1和magazine2的作用和关系:
magazine1专用于分配内存。如代码143行所示每个Magazine结构都有都有一个指针指向chunk链表的表头,当magazine1分配空间时就从这个chunk链表分配。
magazine2专用于回收内存。它也是通过chunk链表来管理回收的内存。当magazine1的内存分配完时,magazine1和magazine2就相互交换,使原来的magazine1变为原来的magazine2,原来的magazine2变为原来的magazine1,请看如下交换代码:
703 thread_memory_swap_magazines (ThreadMemory *tmem,
704 guint ix)
705 {
706 Magazine xmag = tmem->magazine1[ix];
707 tmem->magazine1[ix] = tmem->magazine2[ix];
708 tmem->magazine2[ix] = xmag;
709 }
3. Magazine cache层的数据和逻辑组织分析:
Magazine cache数据结构在allocator分配器中是这么定义的:
156 typedef struct {
……………
// 这里的magazines是个指针数组,数据中的每个元素指向一个双向循环链表
163 ChunkLink **magazines; /* array of MAX_SLAB_INDEX (allocator) */
……………
172 } Allocator;
为了方便理解,还是用图来解释:
为了描述的方便,把图D中的双向循环链表叫chunk_chnuk链表(双向循环二级链表),那么Magazine cache的指针数组中的每个成员是一个指向chunk_chnuk链表的头指针。chunk_chnuk链表中的每个节点是一个chunk链表。因此,Magazine cache操作的基本单位是chunk链表,即在Magazine cache层分配和回收内存时是chnuk链表,而 非chunk链表中的一个节点chunk。所用到的主要函数有:
575 magazine_cache_push_magazine
603 magazine_cache_pop_magazine
这两个函数是chunk_chnuk链表增加/删除节点的操作。chunk_chnuk链表的操作用了一定的技巧:用每一个chunk链表中的第一个节点中的data域作为chunk_chnuk链表的next指针,用每一个chunk链表中的第四个节点中的data域作为chunk_chnuk链表的pre指针。chunk_chnuk链表更具体的代码细节就不再描述了。
chunk_chnuk链表节点的来源主要来自线程释放内存。线程释放的内存首先会把内存chunk放进线程私有的magazine2,当magazine2已满就和线程的magazine1对换,如果magazine2还是已满就把magazine2的chunk链表交给chunk_chnuk链表,而这时magazine2就为空。
4. 内存的回收:
阅读回收内存的相关代码可参考如下流程图。回收内存时用到的magazine_cache_trim函数处理时操作的数据结构有点特别,这里就只对这个函数分析,并以此结束本文。
517 #define magazine_chain_prev(mc) ((mc)->data)
// 函数的功能是在Magazine cache中找出超过工作集时间的内存,然后把这些内存交给
// slab管理。这里主要分析找出要释放的内存的相关操作代码。
524 magazine_cache_trim (Allocator *allocator,
525 guint ix,
526 guint stamp)
527 {
……………….
530 ChunkLink *current = magazine_chain_prev (allocator->magazines[ix]);
531 ChunkLink *trash = NULL; // 指向要释放的内存链表的表头
532 while (ABS (stamp - magazine_chain_uint_stamp (current)) >= allocator->config.working_set_msecs)
533 {
534 /* unlink */
// 前面提到过Magazine cache中的chunk_chnuk链表的节点是chunk链表
// 535行到538行是把当前超过工作集时间的chunk链表从chunk_chnuk链表中unlink出来
// 前面也提到过,关于二级链表的具体操作在本文章中就不再详细解说。
535 ChunkLink *prev = magazine_chain_prev (current);
536 ChunkLink *next = magazine_chain_next (current);
537 magazine_chain_next (prev) = next;
538 magazine_chain_prev (next) = prev;
539 /* clear special fields, put on trash stack */
540 magazine_chain_next (current) = NULL;
541 magazine_chain_count (current) = NULL;
542 magazine_chain_stamp (current) = NULL;
// current是一个chunk链表
// 用current链表的第一个节点的data域作为链接指针指向trash
543 magazine_chain_prev (current) = trash;
544 trash = current;
545 /* fixup list head if required */
546 if (current == allocator->magazines[ix])
547 {
548 allocator->magazines[ix] = NULL;
549 break;
550 }
551 current = prev;
552 }
…………………..
559 while (trash)
560 {
561 current = trash;
562 trash = magazine_chain_prev (current);
563 magazine_chain_prev (current) = NULL; /* clear special field */
564 while (current)
565 {
566 ChunkLink *chunk = magazine_chain_pop_head (¤t);
// 可以查阅slab释放内存的分析
567 slab_allocator_free_chunk (chunk_size, chunk);
568 }
569 }
…………………
572 }
结合517、543、544代码行可得出,把要释放的内存chunk链表组织成一个和chunk_chnuk链表不一样的二级链表,trash指向链表的表头,如下图F所示: