从零开始成为GStreamer专家——GSlice
Gslice全称应该是GSlice allocator,是一种有效的内存管理方式,它将大小相同chunk_size的内存分成内存块组,称为一个magazine,一个magazine内有许多个内存大小一样的空间。是一种节省空间、可扩展的分配大小相同的内存块的内存管理方式,就像#GMemChunks(来自 GLib 2.8),同时避免了过多的内存浪费、可伸缩性和性能问题。
Gslice使用了一种复杂的分层设计,它使用 posix_memalign() 来优化这些相同大小内存块的分配。Gslice中每个线程都有空闲链表(所谓的magazine层),用来快速分配已知大小结构体的内存请求。 这就意味着内存Gslice并不是立即释放内存返回给系统,而是提供额外的缓存逻辑来保存这段内存信息。由于地址对齐而未使用的那部分内存,则使用缓存着色技术(cache colorization—内存地地址随机分步,有助于提高CPU cache利用率)以提高 CPU 缓存利用率,可以用chunk = (ChunkLink*) (mem + color);语句来表示,其中mem是分配的内存起始地址,clolor是多余的空间长度。 Gslice分配器的cache层自适应高锁竞争(high lock contention)来提高可扩展性。
Gslice可以分配小到只有两个指针的数据结构内存块,与 malloc() 不同,它不会为每个块保留额外的空间。而大块的内存,g_slice_new() 和 g_slice_alloc() 将自动调用系统 malloc() 实现。 对于新开发的代码,建议使用g_slice函数而不是g_malloc之类的函数分配内存,g_slice之类函数分配的内存,只要对象在其生命周期内没有调整大小,那么这段内存在释放时仍然可用。
从层次结构上讲,Gslice大致可以分为4层,也即在slab层后扩展了magazine层。分别是thread magazines层,magazine cache层,slab分配器,page分配器。
后续例子在arm 32位平台,按如下Allocator参数进行:
{min_page_size = 128, max_page_size = 8192, config = {always_malloc = 0, bypass_magazines = 0, debug_blocks = 0, working_set_msecs = 15000, color_increment = 1}, max_slab_chunk_size_for_magazine_cache = 1021, magazine_mutex = {p = 0x0, i = {0, 0}}, magazines = 0x58d360, contention_counters = 0x58d160, mutex_counter = -11, stamp_counter = 0, last_stamp = 15905188, slab_mutex = {p = 0x0, i = {0, 0}}, slab_stack = 0x58d560, color_accu = 544}
#define LARGEALIGNMENT (256)
#define P2ALIGNMENT (8)
#define ALIGN(size, base) ((base) * (gsize) (((size) + (base) - 1) / (base)))
#define NATIVE_MALLOC_PADDING 8
#define SLAB_INFO_SIZE (24)
#define MAX_MAGAZINE_SIZE (256)
#define MIN_MAGAZINE_SIZE (4)
#define MAX_STAMP_COUNTER (7)
#define MAX_SLAB_CHUNK_SIZE(al) (1021)
#define MAX_SLAB_INDEX(al) (126)
#define SLAB_INDEX(al, asize) ((asize) / 8 - 1)
#define SLAB_CHUNK_SIZE(al, ix) (((ix) + 1) * 8)
#define SLAB_BPAGE_SIZE(al,csz) (8 * (csz) + 24)
Thread magazines层:
每个线程都有两个magazine链表,记录了最近释放的内存,这个内存将在不久后被再次分配给线程中的内存使用者,线程通过g_private_get()这个函数获取线程句柄,这种方式将提高线程内的内存分配和释放效率,但同时也要注意,这种方式并没有将内存真正释放掉。Allocator的magazines的数量由MAX_SLAB_INDEX (allocator)计算出来。magazine链表通过next字段串在一起,这个链表类型是ChunkLink指针。magazine链表中,通过ChunkLink的next字段连接起来主分支结点,链表结点的ChunkLink->data字段可能指向的是一个子链表,这个子链表也是通过next字段串连在一起的,但是不允许子串的data字段再有子串,子串的ChunkLink->data是未初始化的垃圾。magazine_chain_pop_head函数展示了magazine chunk的获取过程,如下图所示,如果顶层data字段存在,则返回data字段指向的chunk,然后将data字段指向下一下next。否则返回当前的顶层magazine_chunks,将magazine_chunks指向下一个next。也就是说magazine_chain_pop_head会将当前chunk的子串的全部chunk弹出完之后,才会弹出当前chunk。
Magazine的内存释放的时候,挂在Magazine的chunks队首,然后count加1,并没有真正释放内存。data字段除了用来保存子链表的信息之外,还可能会用于链表状态的管理和维护。
ThreadMemory有两个magazine
但实际上,每个线程第一次调用g_slice_new,g_slice_alloc这类函数的时候,将会调用到 tmem = thread_memory_from_self ()来产生线程局部变量。ThreadMemory在分配时,占用的内存空间是sizeof (ThreadMemory) + sizeof (Magazine) * 2 * n_magazines,max_page_size是8192的平台,n_magazines多达126个,空间是远远大于两个指针的空间。
内存分配好会将这段内存清0,并将这段内存用&private_thread_memory作为key,使用pthread_setspecific这类函数将key设置给线程,作为线程的局部变量。在同一线程中使用这段内存的时候,不必保存这段内存的地址,只需要通过&private_thread_memory这个key,用pthread_getspecific这类函数即可获取到这段地址。这种分配方式将ThreadMemory结构体,2 * n_magazines个Magazine空间一次性分配出来。用magazine1 = (Magazine*) (tmem + 1); magazine2 = &tmem->magazine1[n_magazines]完成结构体的填充。
线程销毁的时候,才会调用private_thread_memory_cleanup函数,来释放掉这段内存。由于需要MIN_MAGAZINE_SIZE >= 4,因此,一个magazine至少有4个数据指针。
magazine cache层:
magazine size的内存块在一个全局的magazines仓库中分配和释放,仓库管理着一个15秒的已分配的magazines的工作集,超过15秒的magazine将会被认为是垃圾回收掉,15秒这个参数可配置,移除函数是magazine_cache_trim,每当向magazine中push新的chunk时检查节点的时间戳,函数是magazine_cache_push_magazine。
/*
将magazine_chunks代表的magazine链表归还给全局的allocator。
magazine是magazine_chunks结构加上count,所以二者有些许差别。
32位机上mem_size按8字节对齐,对齐后的内存大小叫chunk_size,ix是(chunk_size/8)-1,即有8种不同的mem_size共用同一个ix。allocator在系统中只有一个,ix用来做全局allocator的magazines数据下标。magazine_chunks是将归还的链表,count是代归还链表的chunk数目,至少4个 */
static void
magazine_cache_push_magazine (guint ix,
ChunkLink *magazine_chunks,
gsize count) /* must be >= 4 */
{
/* 按弹出链表的先后顺序,将前4个链表节点取出来,用next字段将这些节点串成新链表,其余链表保持不动,链在第4个链表节点的next之后 */
ChunkLink *current = magazine_chain_prepare_fields (magazine_chunks);
ChunkLink *next, *prev;
g_mutex_lock (&allocator->magazine_mutex);
/* add magazine at head */
/* 以下用变量加上小写数字表示第几次进入*/
/* magazines是全局allocator初始化的时候,分配的一块MAX_SLAB_INDEX大小的指针数组,全部置成了NULL */
/* next1=NULL, next2=current1 */
next = allocator->magazines[ix];
/* next不空说明ix代表的magazines已经挂载了空闲节点,第一次PUSH时,next肯定为NULL */
if (next)
prev = magazine_chain_prev (next);
else
next = prev = current;
/* 经过上述逻辑, next1=prev1=current1,next2=prev2=current1 */
/* 第一次进来,current1->next->next->data指向current1自己,即这条新加入的magazine1链表,第二次插入相同ix的链表时,current1->next->next->data变成current2 */
magazine_chain_next (prev) = current;
/* 第一次进来,current1->data指向current1自己,第二次插入相同ix的链表时,current1->data变成current2 */
magazine_chain_prev (next) = current;
/* 第一次此句近似重复,第二次,current2->data=currnet1 */
magazine_chain_prev (current) = prev;
/* 第一次此句近似重复,第二次,current2->next->next->data=next2,指向的也是自己 */
magazine_chain_next (current) = next;
/* 当前magazine中chunk的数目 */
magazine_chain_count (current) = (gpointer) count;
/* stamp magazine */
magazine_cache_update_stamp();
/* current->next->data保存当前的毫秒时间 */
magazine_chain_stamp (current) = GUINT_TO_POINTER (allocator->last_stamp);
/* 将当前的magazine 链表放在队首,仿照pop算法规则,放在队首并不意味着先POP。 */
allocator->magazines[ix] = current;
/* free old magazines beyond a certain threshold */
magazine_cache_trim (allocator, ix, allocator->last_stamp);
/* g_mutex_unlock (allocator->mutex); was done by magazine_cache_trim() */
}
magazine组织如下:
slab分配器:
slab分配器分配系统页面大小或其倍数的内存块,因此slab分配器分配的内存是页面对齐的,这些内存块被分割成更小的内存块来满足上层用户的内存需求,整块余下的部分用作cache color以提高处理器的Cache利用率,达到同一种块大小的多个slabs可以在O(1)的开销内分配和释放的效果。
static void
allocator_add_slab (Allocator *allocator,
guint ix,
gsize chunk_size)
{
ChunkLink *chunk;
SlabInfo *sinfo;
gsize addr, padding, n_chunks, color = 0;
gsize page_size;
int errsv;
gpointer aligned_memory;
guint8 *mem;
guint i;
/* page_size是 1 << [(8倍的chunk_size+SLAB_INFO_SIZE -1)的比特数],最小值是allocator->min_page_size */
page_size = allocator_aligned_page_size (allocator, SLAB_BPAGE_SIZE (allocator, chunk_size));
/* allocate 1 page for the chunks and the slab, SLAB_INFO_SIZE包括了NATIVE_MALLOC_PADDING size,这将其移除 */
aligned_memory = allocator_memalign (page_size, page_size - NATIVE_MALLOC_PADDING);
errsv = errno;
mem = aligned_memory;
……
/* mask page address,mem的地址对齐到page_size */
addr = ((gsize) mem / page_size) * page_size;
/* assert alignment */
mem_assert (aligned_memory == (gpointer) addr);
/* basic slab info setup,page_size包括了NATIVE_MALLOC_PADDING,减去了SLAB_INFO_SIZE(包括PADDING在内),实际就代表了SlabInfo真实需要的无PADDING的内存开始地址 */
sinfo = (SlabInfo*) (mem + page_size - SLAB_INFO_SIZE);
sinfo->n_allocated = 0;
sinfo->chunks = NULL;
/* figure cache colorization,真实有多少个chunk */
n_chunks = ((guint8*) sinfo - mem) / chunk_size;
/* 去除n_chunks和SlabInfo的空间,还剩下的空间,有可通没有留下 */
padding = ((guint8*) sinfo - mem) - n_chunks * chunk_size;
if (padding)
{ /* 预留不多于padding个空间 */
color = (allocator->color_accu * P2ALIGNMENT) % padding;
allocator->color_accu += allocator->config.color_increment;
}
/* add chunks to free list */
chunk = (ChunkLink*) (mem + color);
sinfo->chunks = chunk;
for (i = 0; i < n_chunks - 1; i++)
{/* 将chunk_size的内存块用ChunkLink串起来,起始地址就是ChunkLink的地址 */
chunk->next = (ChunkLink*) ((guint8*) chunk + chunk_size);
chunk = chunk->next;
}
chunk->next = NULL; /* last chunk */
/* add slab to slab ring,将sinfo串到slab_stack中,和以前的chunk一起形成一个新的chunk链,不区分以前分的和现在分的 */
allocator_slab_stack_push (allocator, ix, sinfo);
}
page分配器:
在大多数现代系统上,posix_memalign(3) 或memalign(3) 应该可用,所以可以基于系统页面大小的对齐方式或其倍数来分配内存。如果没有memalign之类函数,则使用valloc() 代替,并且每次分配的内存大小限于系统页面大小(不是其倍数)。如果连valloc()都没有的系统上,使用malloc(3)来进行页分配。
注意事项:
[1] 某些系统memalign(3)实现可能依赖边界标记来分发内存块,就是在内存块的边界上预留一定的空间来描述这块内存的信息,除开边界以外的空间才是用户能够使用的空间。为避免过多的页为单位的内存碎片,Gslice为memalign(3)的每个内存块保留2*sizeof (void*)大小的字节,由 NATIVE_MALLOC_PADDING 中指定。
[2] 单独使用 slab 分配器已经提供了一个快速高效的分配器,但它不能正确地扩展到单线程使用之外。 此外,slab 分配器实现了eager free(3)-ing,即不提供任何形式的缓存。 因此,如果单独使用slab 分配器,它很容易在某些时候破坏(alloc, free)对。
[3] magazine sizes最小和最大值受约束,上限大小大约16KB。
[4] 每个内存块分成更小的8个块性能最好。
由Gslice引起的错误,错误1:
Thread 4 "task0" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 1465.1471]
0xb61aa434 in magazine_chain_pop_head (magazine_chunks=<synthetic pointer>) at ../../../../gstreamer/gstreamer/subprojects/glib/glib/gslice.c:590
590 *magazine_chunks = chunk->next;
(gdb) bt
#0 0xb61aa434 in magazine_chain_pop_head (magazine_chunks=<synthetic pointer>) at ../../../../gstreamer/gstreamer/subprojects/glib/glib/gslice.c:590
#1 magazine_chain_prepare_fields (magazine_chunks=0x0) at ../../../../gstreamer/gstreamer/subprojects/glib/glib/gslice.c:663
#2 magazine_cache_push_magazine (ix=ix@entry=4, magazine_chunks=<optimized out>, count=40) at ../../../../gstreamer/gstreamer/subprojects/glib/glib/gslice.c:737
#3 0xb61ab74c in thread_memory_magazine2_unload (tmem=0x5a2a68, ix=4) at ../../../../gstreamer/gstreamer/subprojects/glib/glib/gslice.c:1167
#4 g_slice_free1 (mem_size=<optimized out>, mem_block=mem_block@entry=0x5b3e78) at ../../../../gstreamer/gstreamer/subprojects/glib/glib/gslice.c:1167
#5 0xb5fcb8a4 in _gst_buffer_free (buffer=0x1) at ../../../../gstreamer/gstreamer/subprojects/gstreamer/gst/gstbuffer.c:797
#6 0xb600e5f8 in gst_mini_object_replace (olddata=olddata@entry=0x5c61e8, newdata=newdata@entry=0x0) at ../../../../gstreamer/gstreamer/subprojects/gstreamer/gst/gstminiobject.c:754
#7 0xb57332c4 in gst_buffer_replace (nbuf=0x0, obuf=0x5c61e8) at ../../../../gstreamer/gstreamer/subprojects/gstreamer/gst/gstbuffer.h:577
错误2:
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
[14:03:21 070]_gst_buffer_free (buffer=0xb502f800) at ../../../../gstreamer/gstreamer/subprojects/gstreamer/gst/gstbuffer.c:789
[14:03:21 070]789 info->free_func (meta, buffer);
[14:03:25 439](gdb) bt
[14:03:25 442]#0 _gst_buffer_free (buffer=0xb502f800) at ../../../../gstreamer/gstreamer/subprojects/gstreamer/gst/gstbuffer.c:789
[14:03:25 442]#1 0xb573be40 in gst_buffer_unref (buf=0xb502f800) at ../../../../gstreamer/gstreamer/subprojects/gstreamer/gst/gstbuffer.h:444