从零开始成为GStreamer专家——GSlice

从零开始成为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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值