glibc内存分配之arena

分配区

内存分配器启动默认初始化一个分配区,我们叫主分配区。一般内存分配会从主分配区进行分配,但对于多线程来说,每次分配都要等待其他线程分配完成,这个效率是非常低的。

为了解决这个问题引入子分配区,子分配区和主分配区构成一个环形链表。 image

分配内存的时候,遍历链表,看哪个分配区没有加锁,说明这个分配区没有被占用,直接使用。如果所有的分配区全部被使用,就创建一个新的分配区。

代码实现在 arena.carena_get函数:

#define arena_get(ptr, size) do { \
  Void_t *vptr = NULL; \
  // 优先判断线程私有变量的分配区是否被占用
  ptr = (mstate)tsd_getspecific(arena_key, vptr); \
  if(ptr && !mutex_trylock(&ptr->mutex)) { \
    THREAD_STAT(++(ptr->stat_lock_direct)); \
  } else \
    // 线程私有变量的分配区被占用, 用 arena_get2 获取分配区
    ptr = arena_get2(ptr, (size)); \
} while(0)

每个线程的私有变量会存放一个分配区指针,分配时先判断这个分配区是否已经被占用。

  1. 如果未被占用,直接返回这个分配区
  2. 线程私有变量的分配区被占用,使用 arena_get2 来获取可用的分配区
arena_get2(mstate a_tsd, size_t size) {

  ...

  // 遍历已有的分配区链表,尝试找到没有被占用的分配区
  do {
    if(!mutex_trylock(&a->mutex)) {
      THREAD_STAT(++(a->stat_lock_loop));
      tsd_setspecific(arena_key, (Void_t *)a);
      return a;
    }
    a = a->next;
  } while(a != a_tsd);

  ...

  // 如果没有找到空闲的分配区,则新建一个新的分配区
  a = _int_new_arena(size);
  if(!a)
    return 0;

  // 新分配区指针放到线程私有变量
  tsd_setspecific(arena_key, (Void_t *)a);
  // 新分配区加入分配区链表
  mutex_init(&a->mutex);
  err = mutex_lock(&a->mutex); /* remember result */

  /* Add the new arena to the global list.  */
  (void)mutex_lock(&list_lock);
  a->next = main_arena.next;
  atomic_write_barrier ();
  main_arena.next = a;
  (void)mutex_unlock(&list_lock);

}

arena_get2 的实现很简单,先从分配区链表分配,如果失败,创建新的分配区。也就是说如果在不做限制的情况下, N 个线程就可能会有 N 个分配区。

区别

我们一般分配虚拟内存可以有两种方式,一种是直接调用 sbrk 系统调用来分配内存,另外一种方式是通过 mmap 来分配一块虚拟内存。 对于主分配区两种分配方式都可以使用,而对于子分配区,则是每次通过 mmap 来申请一大块内存,这个是这两种分配方式的主要区别。

在 32位的机器上,子分配区每次申请的 1M 大小的虚拟内存,64 位每次申请 64M.

写在最后

14年看了 glibc 的内存分配器 ptmalloc, 一直想做点总结。直到现在才做,一方面是很多细节没看到位,另外一方面还是因为懒。

对于博客一直都是想写就写,想放就放,没有太大的紧迫感。还是希望能够持续的写,也能对看过一些比较有意思的东西有比较好的梳理和总结。