标签归档:堆层

PHP源码阅读笔记三十一:PHP内存池中的堆(heap)层基础

PHP源码阅读笔记三十一:PHP内存池中的堆(heap)层基础

【概述】
PHP的内存管理器是分层(hierarchical)的。这个管理器共有三层:存储层(storage)、堆(heap)层和 emalloc/efree 层。在PHP源码阅读笔记三十:PHP内存池中的存储层中介绍了存储层,存储层通过 malloc()、mmap() 等函数向系统真正的申请内存,并通过 free() 函数释放所申请的内存。存储层通常申请的内存块都比较大,这里申请的内存大并不是指storage层结构所需要的内存大,只是堆层通过调用存储层的分配方法时,其以段的格式申请的内存比较大,存储层的作用是将内存分配的方式对堆层透明化。
在存储层之上就是今天我们要了解的堆层。堆层一个调度层,它与上面的emalloc/efree层交互,将通过存储层申请到的大块内存,进行拆分,按需提供。在堆层中有其一套内存的调度策略,这个整个PHP内存分配管理的核心区域。

以下的所有分享都是基于ZEND_DEBUG未打开的情况。
首先看下堆层所涉及到的结构:
【结构】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
 
/* mm block type */
typedef struct _zend_mm_block_info {
	size_t _size;	/* block的大小*/
	size_t _prev;	/* 计算前一个块有用到*/
} zend_mm_block_info;
 
 
typedef struct _zend_mm_block {
	zend_mm_block_info info;
} zend_mm_block;
 
typedef struct _zend_mm_small_free_block {	/* 双向链表 */
	zend_mm_block_info info;
	struct _zend_mm_free_block *prev_free_block;	/* 前一个块 */
	struct _zend_mm_free_block *next_free_block;	/* 后一个块 */
} zend_mm_small_free_block;	/* 小的空闲块*/
 
typedef struct _zend_mm_free_block {	/* 双向链表 + 树结构 */
	zend_mm_block_info info;
	struct _zend_mm_free_block *prev_free_block;	/* 前一个块 */
	struct _zend_mm_free_block *next_free_block;	/* 后一个块 */
 
	struct _zend_mm_free_block **parent;	/* 父结点 */
	struct _zend_mm_free_block *child[2];	/* 两个子结点*/
} zend_mm_free_block;
 
 
 
struct _zend_mm_heap {
	int                 use_zend_alloc;	/* 是否使用zend内存管理器 */
	void               *(*_malloc)(size_t);	/* 内存分配函数*/
	void                (*_free)(void*);	/* 内存释放函数*/
	void               *(*_realloc)(void*, size_t);
	size_t              free_bitmap;	/* 小块空闲内存标识 */
	size_t              large_free_bitmap;  /* 大块空闲内存标识*/
	size_t              block_size;		/* 一次内存分配的段大小,即ZEND_MM_SEG_SIZE指定的大小,默认为ZEND_MM_SEG_SIZE   (256 * 1024)*/
	size_t              compact_size;	/* 压缩操作边界值,为ZEND_MM_COMPACT指定大小,默认为 2 * 1024 * 1024*/
	zend_mm_segment    *segments_list;	/* 段指针列表 */
	zend_mm_storage    *storage;	/* 所调用的存储层 */
	size_t              real_size;	/* 堆的真实大小 */
	size_t              real_peak;	/* 堆真实大小的峰值 */
	size_t              limit;	/* 堆的内存边界 */
	size_t              size;	/* 堆大小 */
	size_t              peak;	/* 堆大小的峰值*/
	size_t              reserve_size;	/* 备用堆大小*/
	void               *reserve;	/* 备用堆 */
	int                 overflow;	/* 内存溢出数*/
	int                 internal;
#if ZEND_MM_CACHE
	unsigned int        cached;	/* 已缓存大小 */
	zend_mm_free_block *cache[ZEND_MM_NUM_BUCKETS];	/* 缓存数组/
#endif
	zend_mm_free_block *free_buckets[ZEND_MM_NUM_BUCKETS*2];	/* 小块空闲内存数组 */
	zend_mm_free_block *large_free_buckets[ZEND_MM_NUM_BUCKETS];	/* 大块空闲内存数组*/
	zend_mm_free_block *rest_buckets[2];	/* 剩余内存数组 */
 
};

对于heap结构中的内存操作函数,如果use_zend_alloc为否,则使用malloc-type 内存分配,此时所有的操作就不经过堆层中的内存管理,直接采用malloc等函数。

compact_size的大小默认为 2 * 1024 * 1024(2M),如果有设置变量ZEND_MM_COMPACT则为此指定大小,如果内存的峰值超过这个值,则会调用storage的compact函数,只是这个函数现在的实现为空,可能在后续的版本中添加。

reserve_size为备用堆的大小,默认情况下为ZEND_MM_RESERVE_SIZE,其大小为(8*1024)
*reserve为备用堆,其大小为reserve_size,其用作内存溢出时报告错误用。

【关于USE_ZEND_ALLOC】
环境变量 USE_ZEND_ALLOC 可用于允许在运行时选择 malloc 或 emalloc 内存分配。使用 malloc-type 内存分配将允许外部调试器观察内存使用情况,而 emalloc 分配将使用 Zend 内存管理器抽象,要求进行内部调试。
[zend_startup() -> start_memory_manager() -> alloc_globals_ctor()]

2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
static void alloc_globals_ctor(zend_alloc_globals *alloc_globals TSRMLS_DC)
{
	char *tmp;
	alloc_globals->mm_heap = zend_mm_startup();
 
	tmp = getenv("USE_ZEND_ALLOC");
	if (tmp) {
		alloc_globals->mm_heap->use_zend_alloc = zend_atoi(tmp, 0);
		if (!alloc_globals->mm_heap->use_zend_alloc) {	/* 如果不使用zend的内存管理器,同直接使用malloc函数*/
			alloc_globals->mm_heap->_malloc = malloc;
			alloc_globals->mm_heap->_free = free;
			alloc_globals->mm_heap->_realloc = realloc;
		}
	}
}

【初始化】
[zend_mm_startup()]
初始化storage层的分配方案,初始化段大小,压缩边界值,并调用zend_mm_startup_ex()初始化堆层。

[zend_mm_startup() -> zend_mm_startup_ex()]
【内存对齐】
在PHP的内存分配中使用了内存对齐,内存对齐计算显然有两个目标:一是减少CPU的访存次数;第二个就是还要保持存储空间的效率足够高。

1
2
3
4
5
6
7
8
# define ZEND_MM_ALIGNMENT 8
 
#define ZEND_MM_ALIGNMENT_MASK ~(ZEND_MM_ALIGNMENT-1)
 
#define ZEND_MM_ALIGNED_SIZE(size)	(((size) + ZEND_MM_ALIGNMENT - 1) & ZEND_MM_ALIGNMENT_MASK)
 
#define ZEND_MM_ALIGNED_HEADER_SIZE			ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_block))
#define ZEND_MM_ALIGNED_FREE_HEADER_SIZE	ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_small_free_block))

PHP在分配块的内存中,用到内存对齐,如果所需要的内存的大小的低三位不为0(不能为8整除),则将低三位加上7,并~7进行与操作,即对于大小不是8的整数倍的内存大小补全到可以被8整除。
在win32机器上,一些宏对应的数值大小为:
ZEND_MM_MIN_SIZE=8
ZEND_MM_MAX_SMALL_SIZE=272
ZEND_MM_ALIGNED_HEADER_SIZE=8
ZEND_MM_ALIGNED_FREE_HEADER_SIZE=16
ZEND_MM_MIN_ALLOC_BLOCK_SIZE=8
ZEND_MM_ALIGNED_MIN_HEADER_SIZE=16
ZEND_MM_ALIGNED_SEGMENT_SIZE=8

如果要分配一个大小为9个字节的块,则其实际分配的大小为ZEND_MM_ALIGNED_SIZE(9 + 8)=24

【块的定位】
所分配的内存的右边的两位是用来标记内存的类型。
其大小的定义为#define ZEND_MM_TYPE_MASK ZEND_MM_LONG_CONST(0×3)

如下所示代码为块的定位

1
2
3
4
5
6
#define ZEND_MM_NEXT_BLOCK(b)			ZEND_MM_BLOCK_AT(b, ZEND_MM_BLOCK_SIZE(b))
#define ZEND_MM_PREV_BLOCK(b)			ZEND_MM_BLOCK_AT(b, -(int)((b)->info._prev & ~ZEND_MM_TYPE_MASK))
 
#define ZEND_MM_BLOCK_AT(blk, offset)	((zend_mm_block *) (((char *) (blk))+(offset)))
#define ZEND_MM_BLOCK_SIZE(b)			((b)->info._size & ~ZEND_MM_TYPE_MASK)
#define ZEND_MM_TYPE_MASK		ZEND_MM_LONG_CONST(0x3)

当前块的下一个元素,即为当前块的头位置加上整个块(去掉了类型的长度)的长度。
当前块的上一个元素,即为当前块的头位置减去前一个块(去掉了类型的长度)的长度。
关于前一个块的长度,在块的初始化时设置为当前块的大小与块类型的或操作的结果。