标签归档:PHP源码

PHP源码阅读笔记三十三:PHP5.3新增加的垃圾回收机制(Garbage Collection)基础

PHP源码阅读笔记三十三:PHP5.3新增加的垃圾回收机制(Garbage Collection)基础
PHP5.3中新增加了垃圾回收机制,据说很先进,据说引诱了我去看看其先进的实现。
官方说明文档请猛击Garbage Collection
中文版地址:http://docs.php.net/manual/zh/features.gc.php
【垃圾回收机制的嵌入方式】
zend_gc.h文件在zend.h的749行被引用:#include “zend_gc.h”
从而替换覆盖了在237行引用的zend_alloc.h文件中的ALLOC_ZVAL等宏
zend/zend_gc.h文件的202行开始

202
203
204
205
206
207
208
/* The following macroses override macroses from zend_alloc.h */
#undef  ALLOC_ZVAL
#define ALLOC_ZVAL(z) 							\
	do {								\
		(z) = (zval*)emalloc(sizeof(zval_gc_info));		\
		GC_ZVAL_INIT(z);					\
	} while (0)

ALLOC_ZVAL宏在zend_alloc.h中的定义是分配一个zval结构的内存空间。新的ALLOC_ZVAL宏分配了一个zval_gc_info结构的宏。zval_gc_info的结构如下:
zend/zend_gc.h文件的91行开始:

91
92
93
94
95
96
97
typedef struct _zval_gc_info {
	zval z;
	union {
		gc_root_buffer       *buffered;
		struct _zval_gc_info *next;
	} u;
} zval_gc_info;

zval_gc_info的第一个成员为zval结构,这就确保其和以zval变量分配的内存的开始对齐,从而在zval_gc_info类型指针的强制转换时,其可以作为zval使用。关于gc_root_buffer等将在后面的结构和实现时介绍,它定义的PHP垃圾回收机制的缓存结构。GC_ZVAL_INIT用来初始化替代了zval的zval_gc_info,它会把zval_gc_info中的成员u的buffered字段设置成NULL,此字段仅在将其放入垃圾回收缓冲区时才会有值,否则会一直是NULL。
由于PHP中所有的变量都是以zval变量的形式存在,这里以zval_gc_info替换zval,从而成功实现垃圾收集机制在原有系统中的集成。
这个有点面向对象中多态的感觉。

【垃圾回收机制的存储方式】
结点结构:

81
82
83
84
85
86
87
88
89
typedef struct _gc_root_buffer {
	struct _gc_root_buffer   *prev;		/* double-linked list               */
	struct _gc_root_buffer   *next;
	zend_object_handle        handle;	/* must be 0 for zval               */
	union {
		zval                 *pz;
		zend_object_handlers *handlers;
	} u;
} gc_root_buffer;

很明显(见注释,虽然PHP中的注释很少,但是有些纯粹是纠结的注释),这是一个双向链表。
在联合体中的pz变量很明显就是之前定义的多态的zval_gc_info结构,于是其在链表中的当前结点指针可以通过((zval_gc_info*)(pz))->u.buffered获取,不过在看其源码中有多处使用到这个调用方式,为何不另起一个宏呢?难道是怕宏太多,不是啊,PHP就是以宏多著称,比这个宏嵌套多的宏海了去了。不懂。另外handle等结构是特别针对对象变量的。

缓冲区是话在全局变量中的,和其它模块的全局变量一样,gc也有其自己的全局变量访问宏 GC_G(v),同样对于全局变量访问宏在是否ZTS下有不同的实现。
在zend_gc.h中定义的全局变量如下:

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
typedef struct _zend_gc_globals {
	zend_bool         gc_enabled;	/* 是否开启垃圾收集机制 */
	zend_bool         gc_active;	/* 是否正在进行 */
 
	gc_root_buffer   *buf;				/* 预分配的缓冲区数组,默认为10000(preallocated arrays of buffers)   */
	gc_root_buffer    roots;			/* 列表的根结点(list of possible roots of cycles) */
	gc_root_buffer   *unused;			/* 没有使用过的缓冲区列表(list of unused buffers)           */
	gc_root_buffer   *first_unused;		/* 指向第一个没有使用过的缓冲区结点(pointer to first unused buffer)   */
	gc_root_buffer   *last_unused;		/* 指向最后一个没有使用过的缓冲区结点,此处为标记结束用(pointer to last unused buffer)    */
 
	zval_gc_info     *zval_to_free;		/* 将要释放的zval变量的临时列表(temporaryt list of zvals to free) */
	zval_gc_info     *free_list;		/* 临时变量,需要释放的列表开头 */
	zval_gc_info     *next_to_free;		/* 临时变量,下一个将要释放的变量位置*/
 
	zend_uint gc_runs;	/* gc运行的次数统计 */
	zend_uint collected;    /* gc中垃圾的个数 */
 
	// 省略...

【垃圾回收机制中的颜色标记】

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#define GC_COLOR  0x03
 
#define GC_BLACK  0x00
#define GC_WHITE  0x01
#define GC_GREY   0x02
#define GC_PURPLE 0x03
 
#define GC_ADDRESS(v) \
	((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR))
#define GC_SET_ADDRESS(v, a) \
	(v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & GC_COLOR) | ((zend_uintptr_t)(a))))
#define GC_GET_COLOR(v) \
	(((zend_uintptr_t)(v)) & GC_COLOR)
#define GC_SET_COLOR(v, c) \
	(v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & ~GC_COLOR) | (c)))
#define GC_SET_BLACK(v) \
	(v) = ((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR))
#define GC_SET_PURPLE(v) \
	(v) = ((gc_root_buffer*)(((zend_uintptr_t)(v)) | GC_PURPLE))

在PHP的内存管理中我们也有看到类似的以最后位作为某种类型的标记方式。
这里以内存分配的最后两位作为整个结构的颜色标记。其中
白色表示垃圾
紫色表示已放入缓冲区
灰色表示已经进行了一次refcount的减一操作
黑色是默认颜色,正常

【zval定义的改变】
PHP3.0版本 在zend/zend.h文件中,其定义如下:

316
317
318
319
320
321
322
struct _zval_struct {
	/* Variable information */
	zvalue_value value;		/* value */
	zend_uint refcount__gc;
	zend_uchar type;	/* active type */
	zend_uchar is_ref__gc;
};

在php3.0之前的版本,如php5.2.9版本,在zend/zend.h文件中,其定义如下:

307
308
309
310
311
312
313
struct _zval_struct {
	/* Variable information */
	zvalue_value value;		/* value */
	zend_uint refcount;
	zend_uchar type;	/* active type */
	zend_uchar is_ref;
};

PHP源码阅读笔记三十二:PHP内存池中的emalloc/efree层与堆(heap)层

PHP源码阅读笔记三十二:PHP内存池中的emalloc/efree层与堆(heap)层
emalloc/efree层是整个内存体系中最上层结构,它通过与堆层的交换使用PHP自带的内存管理机制。如果有设置USE_ZEND_ALLOC为0,则直接使用malloc/free等函数直接操作内存。
这里将从emalloc与efree两个函数的实现解析emalloc/efree层与堆层的交互,及堆层对于内存的管理机制。

【emalloc】
emalloc函数是从zend_alloc.h 70行开始。
emalloc是一个宏,其对应了_emalloc函数。
在_emalloc函数中,如果未使用zend的内存管理机制,则直接调用malloc函数,否则调用_zend_mm_alloc_int

[emalloc() -> _emalloc() -> _zend_mm_alloc_int() ]
在_zend_mm_alloc_int函数中,程序会处理真实需要的内存小于或大于等于ZEND_MM_MAX_SMALL_SIZE(272)两种情况,如果小于ZEND_MM_MAX_SMALL_SIZE,则会搜索free_buckets,看是否有合适的内存块,如果可以在free_buckets中找到合适的块使用,同直接跳转到zend_mm_finished_searching_for_block,否则执行zend_mm_search_large_block()

[emalloc() -> _emalloc() -> _zend_mm_alloc_int() -> zend_mm_search_large_block()]
zend_mm_search_large_block函数用来在large_free_buckets中查找合适的内存块。其中当对于ZEND_MM_LARGE_BUCKET_INDEX(true_size)大小的没有找到时,需要查找更大块列表中的最小块。

如果在大块列表和小块列表中都没有,则需要从剩余列表块中查找,如果找到,则同样跳转到zend_mm_finished_searching_for_block
如果三个列表中都没有找到,则需要重新增加内存分配。此时调用storage层的分配函数进行分配,其中内存的大小,如果需要分配的内存大于block_size,则需要根据大小重新计算,否则直接分配block_size大小的内存。
分配内存完后,需要重新整理堆,此时需要重新计算堆中的内存大小,将新分配的内存添加到segments_list的前面。

如果在上面的操作中是直接跳转到zend_mm_finished_searching_for_block,则需要将使用了的内存块从对应的列表中移除(此处应该是一个标记的过程,伪移除)

接下来,根据剩下的内存大小,将其移到空闲列表或剩余列表。

最后返回分配的块。

在emalloc整个过程中,有以下一些注意点。
ZEND_MM_BUCKET_INDEX(true_size)定位在bucket中的位置,这个值大于等于0,小于32。
其实现如下:
#define ZEND_MM_BUCKET_INDEX(true_size) ((true_size>>ZEND_MM_ALIGNMENT_LOG2)-(ZEND_MM_ALIGNED_MIN_HEADER_SIZE>>ZEND_MM_ALIGNMENT_LOG2))
free_bitmap和large_free_bitmap的值都是0到31。

【efree】
efree函数是从zend_alloc.h 72行开始。
efree是一个宏,其对应了_efree函数。
在_efree函数中,如果未使用zend的内存管理机制,则直接调用free函数,否则调用_zend_mm_free_int

[efree() -> _efree() -> _zend_mm_free_int() ]
堆首先将整个堆的大小减少,如果当前块的后一个块是空闲块,则将后一个空闲块从空闲块列表中删除并与当前块合并,如果当前块的前一个块是空闲块,则将前一个空闲块从空闲块列表中删除并与当前块合并,指针指向前一个空闲块。如果此时当前块是开始的块,则调用zend_mm_del_segment将整段内存清除,如果不是开始块,则将合并后的块添加到空闲块列表。

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)

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