Glibc内存管理--ptmalloc2源代码分析(二十一)

5.6  多分配区支持

由于只有一个主分配区从堆中分配小内存块,而稍大的内存块都必须从 mmap 映射区域分配,如果有多个线程都要分配小内存块,但多个线程是不能同时调用 sbrk() 函数的,因为只有一个函数调用 sbrk() 时才能保证分配的虚拟地址空间是连续的。如果多个线程都从主分配区中分配小内存块,效率很低效。为了解决这个问题, ptmalloc 使用非主分配区来模拟主分配区的功能,非主分配区同样可以分配小内存块,并且可以创建多个非主分配区,从而在线程分配内存竞争比较激烈的情况下,可以创建更多的非主分配区来完成分配任务,减少分配区的锁竞争,提高分配效率。

Ptmalloc 怎么用非主分配区来模拟主分配区的行为呢?首先创建一个新的非主分配区,非主分配区使用 mmap() 函数分配一大块内存来模拟堆( sub-heap ),所有的从该非主分配区总分配的小内存块都从 sub-heap 中切分出来,如果一个 sub-heap 的内存用光了,或是 sub-heap 中的内存不够用时,使用 mmap() 分配一块新的内存块作为 sub-heap ,并将新的 sub-heap 链接在非主分配区中 sub-heap 的单向链表中。

分主分配区中的 sub-heap 所占用的内存不会无限的增长下去,同样会像主分配区那样进行进行 sub-heap 收缩,将 sub-heap top chunk 的一部分返回给操作系统,如果 top chunk 为整个 sub-heap ,会把整个 sub-heap 还回给操作系统。收缩堆的条件是当前 free chunk 大小加上前后能合并 chunk 的大小大于 64KB ,并且 top chunk 的大小达到 mmap 收缩阈值,才有可能收缩堆。

一般情况下,进程中有多个线程,也有多个分配区,线程的数据一般会比分配区数量多,所以必能保证没有线程独享一个分配区,每个分配区都有可能被多个线程使用,为了保证分配区的线程安全,对分配区的访问需要锁保护,当线程获得分配区的锁时,可以使用该分配区分配内存,并将该分配区的指针保存在线程的私有实例中。

当某一线程需要调用 malloc 分配内存空间时,该线程先查看线程私有变量中是否已经存在一个 分配区 ,如果存在,尝试对该 分配区 加锁,如果加锁成功,使用该 分配区 分配内存,如果失败,该线程搜分配区索循环链表试图获得一个空闲的 分配区 。如果所有的 分配区 都已经加锁,那么 malloc 会开辟一个新的 分配区 ,把该 分配区 加入到分配区的全局 分配区 循环链表并加锁,然后使用该 分配区 进行分配操作。在回收操作中,线程同样试图获得待回收块所在 分配区 的锁,如果该 分配区 正在被别的线程使用,则需要等待直到其他线程释放该 分配区 的互斥锁之后才可以进行回收操作。

5.6.1 Heap_info

Struct heap_info 定义如下:

/* A heap is a single contiguous memory region holding (coalesceable)
   malloc_chunks.  It is allocated with mmap() and always starts at an
   address aligned to HEAP_MAX_SIZE.  Not used unless compiling with
   USE_ARENAS. */

typedef struct _heap_info {
  mstate ar_ptr; /* Arena for this heap. */
  struct _heap_info *prev; /* Previous heap. */
  size_t size;   /* Current size in bytes. */
  size_t mprotect_size; /* Size in bytes that has been mprotected
                           PROT_READ|PROT_WRITE.  */
  /* Make sure the following data is properly aligned, particularly
     that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
     MALLOC_ALIGNMENT. */
  char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;

 ar_ptr 是指向所属分配区的指针, mstate 的定义为: typedef struct malloc_state *mstate;

prev 字段用于将同一个分配区中的 sub_heap 用单向链表链接起来。 prev 指向链表中的前一个 sub_heap

size 字段表示当前 sub_heap 中的内存大小,以 page 对齐。

mprotect_size 字段表示当前 sub_heap 中被读写保护的内存大小,也就是说还没有被分配的内存大小。

Pad 字段用于保证 sizeof (heap_info) + 2 * SIZE_SZ 是按 MALLOC_ALIGNMENT 对齐的。 MALLOC_ALIGNMENT_MASK 2 * SIZE_SZ - 1 ,无论 SIZE_SZ 4 8 -6 * SIZE_SZ &   MALLOC_ALIGN_MASK 的值为 0 ,如果 sizeof (heap_info) + 2 * SIZE_SZ 不是按 MALLOC_ALIGNMENT 对齐,编译的时候就会报错,编译时会执行下面的宏。

/* Get a compile-time error if the heap_info padding is not correct
   to make alignment work as expected in sYSMALLOc.  */
extern int sanity_check_heap_info_alignment[(sizeof (heap_info)
                                             + 2 * SIZE_SZ) % MALLOC_ALIGNMENT
                                            ? -1 : 1];

 为什么一定要保证对齐呢?作为分主分配区的第一个 sub_heap heap_info 存放在 sub_heap 的头部,紧跟 heap_info 之后是该非主分配区的 malloc_state 实例,紧跟 malloc_state 实例后,是 sub_heap 中的第一个 chunk ,但 chunk 的首地址必须按照 MALLOC_ALIGNMENT 对齐,所以在 malloc_state 实例和第一个 chunk 之间可能有几个字节的 pad ,但如果 sub_heap 不是非主分配区的第一个 sub_heap ,则紧跟 heap_info 后是第一个 chunk ,但 sysmalloc() 函数默认 heap_info 是按照 MALLOC_ALIGNMENT 对齐的,没有再做对齐的工作,直接将 heap_info 后的内存强制转换成一个 chunk 。所以这里在编译时保证 sizeof (heap_info) + 2 * SIZE_SZ 是按 MALLOC_ALIGNMENT 对齐的,在运行时就不用再做检查了,也不必再做对齐。

5.6.2 获取分配区

为了支持多线程, ptmalloc 定义了如下的全局变量:

static tsd_key_t arena_key;
static mutex_t list_lock;
#ifdef PER_THREAD
static size_t narenas;
static mstate free_list;
#endif

/* Mapped memory in non-main arenas (reliable only for NO_THREADS). */
static unsigned long arena_mem;

/* Already initialized? */
int __malloc_initialized = -1;

 arena_key 存放的是线程的私用实例,该私有实例保存的是分配区( arena )的 malloc_state 实例的指针。 arena_key 指向的可能是主分配区的指针,也可能是非主分配区的指针。

list_lock 用于同步分配区的单向环形链表。

如果定义了 PRE_THREAD narenas 全局变量表示当前分配区的数量, free_list 全局变量是空闲分配区的单向链表,这些空闲的分配区可能是从父进程那里继承来的。全局变量 narenas free_list 都用锁 list_lock 同步。

arena_mem 只用于单线程的 ptmalloc 版本,记录了非主分配区所分配的内存大小。

__malloc_initializd 全局变量用来标识是否 ptmalloc 已经初始化了,其值大于 0 时表示已经初始化。

 

Ptmalloc 使用如下的宏来获得分配区:

/* arena_get() acquires an arena and locks the corresponding mutex.
   First, try the one last locked successfully by this thread.  (This
   is the common case and handled with a macro for speed.)  Then, loop
   once over the circularly linked list of arenas.  If no arena is
   readily available, create a new one.  In this latter case, `size'
   is just a hint as to how much memory will be required immediately
   in the new arena. */
#define arena_get(ptr, size) do { \
  arena_lookup(ptr); \
  arena_lock(ptr, size); \
} while(0)

#define arena_lookup(ptr) do { \
  Void_t *vptr = NULL; \
  ptr = (mstate)tsd_getspecific(arena_key, vptr); \
} while(0)

#ifdef PER_THREAD
#define arena_lock(ptr, size) do { \
  if(ptr) \
    (void)mutex_lock(&ptr->mutex); \
  else \
    ptr = arena_get2(ptr, (size)); \
} while(0)
#else
#define arena_lock(ptr, size) do { \
  if(ptr && !mutex_trylock(&ptr->mutex)) { \
    THREAD_STAT(++(ptr->stat_lock_direct)); \
  } else \
    ptr = arena_get2(ptr, (size)); \
} while(0)
#endif

/* find the heap and corresponding arena for a given ptr */
#define heap_for_ptr(ptr) \
 ((heap_info *)((unsigned long)(ptr) & ~(HEAP_MAX_SIZE-1)))
#define arena_for_chunk(ptr) \
 (chunk_non_main_arena(ptr) ? heap_for_ptr(ptr)->ar_ptr : &main_arena)

 arena_get 首先调用 arena_lookup 查找本线程的私用实例中是否包含一个分配区的指针,返回该指针,调用 arena_lock 尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,如果对该分配区加锁失败,调用 arena_get2 获得一个分配区指针。如果定义了 PRE_THREAD arena_lock 的处理有些不同,如果本线程拥有的私用实例中包含分配区的指针,则直接对该分配区加锁,否则,调用 arena_get2 获得分配区指针, PRE_THREAD 的优化保证了每个线程尽量从自己所属的分配区中分配内存,减少与其它线程因共享分配区带来的锁开销,但 PRE_THREAD 的优化并不能保证每个线程都有一个不同的分配区,当系统中的分配区数量达到配置的最大值时,不能再增加新的分配区,如果再增加新的线程,就会有多个线程共享同一个分配区。所以 ptmalloc PRE_THREAD 优化,对线程少时可能会提升一些性能,但线程多时,提升性能并不明显。即使没有线程共享分配区的情况下,任然需要加锁,这是不必要的开销,每次加锁操作会消耗 100ns 左右的时间。

每个 sub_heap 的内存块使用 mmap() 函数分配,并以 HEAP_MAX_SIZE 对齐,所以可以根据 chunk 的指针地址,获得这个 chunk 所属的 sub_heap 的地址。 heap_for_ptr 根据 chunk 的地址获得 sub_heap 的地址。由于 sub_heap 的头部存放的是 heap_info 的实例, heap_info 中保存了分配区的指针,所以可以通过 chunk 的地址获得分配区的地址,前提是这个 chunk 属于非主分配区, arena_for_chunk 用来做这样的转换。

#define HEAP_MIN_SIZE (32*1024)
#ifndef HEAP_MAX_SIZE
# ifdef DEFAULT_MMAP_THRESHOLD_MAX
#  define HEAP_MAX_SIZE (2 * DEFAULT_MMAP_THRESHOLD_MAX)
# else
#  define HEAP_MAX_SIZE (1024*1024) /* must be a power of two */
# endif
#endif

 HEAP_MIN_SIZE 定义了 sub_heap 内存块的最小值, 32KB HEAP_MAX_SIZE 定义了 sub_heap 内存块的最大值,在 32 位系统上, HEAP_MAX_SIZE 默认值为 1MB 64 为系统上, HEAP_MAX_SIZE 的默认值为 64MB

 

 

你可能感兴趣的:(多线程,thread,配置管理)