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
。
分享到:
相关推荐
NULL 博文链接:https://mqzhuang.iteye.com/blog/1064966
Glibc内存管理 Ptmalloc2 源代码分析
glibc内存管理ptmalloc源代码分析-电子资料-高清PDF版-pdf打印版
简介133.2.2内存管理的设计假设 143.2.3内存管理数据结构概述 143.2.4内存分配概述 193.2.5内存回收概述 213.2.6配置选项概述 2
淘宝网的研发人员写的文档,对了解GNU C的内存分配机制有很大的帮助!
该文档详细分析了c库中的内存管理细节,可广大程序员做参考。
简介133.2.2内存管理的设计假设 143.2.3内存管理数据结构概述 143.2.4内存分配概述 193.2.5内存回收概述 213.2.6配置选项概述 2
清晰版,对内存分析,程序设计非常有帮助。适合进阶的。
glibc内存管理ptmalloc源代码分析,对比了tcmalloc与ptmalloc
glibc内存管理ptmalloc源代码分析.pdf
源代码分析 glibc的ptmalloc 分析
glibc内存管理ptmalloc源代码分析4.pdf
学习自《glibc内存管理ptmalloc源代码分析》庄明强 著 部分资料参考自互联网 chunk 描述: 当用户通过malloc等函数申请空间时,实际上是从堆中分配内存 目前 Linux 标准发行版中使用的是 glibc 中的堆分配器:...