月度归档:2010年11月

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

PHP源码阅读笔记三十四:PHP5.3新增加的垃圾回收机制(Garbage Collection)
在之前的文章 PHP源码阅读笔记三十三:PHP5.3新增加的垃圾回收机制(Garbage Collection)基础 中有介绍了垃圾回收机制的一些基础知识。今天我们看看其初始化,添加到垃圾缓冲区和垃圾回收的过程。
官方说明文档请猛击Garbage Collection
中文版地址:http://docs.php.net/manual/zh/features.gc.php
【初始化】
在zend/zend_gc.c 121行有函数gc_init实现了gc的初始化,其代码如下:

121
122
123
124
125
126
127
128
ZEND_API void gc_init(TSRMLS_D)
{
	if (GC_G(buf) == NULL && GC_G(gc_enabled)) {
		GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES);
		GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];
		gc_reset(TSRMLS_C);
	}
}

第123行判断是否为空和是否开启了gc,如果都为真,则转124行
第124行是直接调用malloc分配了10000个gc_root_buffer内存。
第125行将全局变量last_unused设置为gc缓冲区的结束位置。
第126行重置整个垃圾收集机制,其代码从zend/zend_gc.c 88行开始,如下:

88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
ZEND_API void gc_reset(TSRMLS_D)
{
	GC_G(gc_runs) = 0;
	GC_G(collected) = 0;
 
#if GC_BENCH
	GC_G(root_buf_length) = 0;
	GC_G(root_buf_peak) = 0;
	GC_G(zval_possible_root) = 0;
	GC_G(zobj_possible_root) = 0;
	GC_G(zval_buffered) = 0;
	GC_G(zobj_buffered) = 0;
	GC_G(zval_remove_from_buffer) = 0;
	GC_G(zobj_remove_from_buffer) = 0;
	GC_G(zval_marked_grey) = 0;
	GC_G(zobj_marked_grey) = 0;
#endif
 
	GC_G(roots).next = &GC_G(roots);
	GC_G(roots).prev = &GC_G(roots);
 
	if (GC_G(buf)) {
		GC_G(unused) = NULL;
		GC_G(first_unused) = GC_G(buf);
 
		GC_G(zval_to_free) = NULL;
	} else {
		GC_G(unused) = NULL;
		GC_G(first_unused) = NULL;
		GC_G(last_unused) = NULL;
	}
}

第90~91行 设置gc运行的次数统计(gc_runs)和gc中垃圾的个数(collected)为0。
第106~107行 设置双向链表头结点的上一个结点和下一个结点指向自己。

关于gc_enabled,默认情况下是开启的,可以在php.ini中配置。
其实现代码在zend/zend.c 93行 如下:

93
STD_ZEND_INI_BOOLEAN("zend.enable_gc",	"1",	ZEND_INI_ALL,	OnUpdateGCEnabled,   gc_enabled, zend_gc_globals,        gc_globals)

初始化调用在zend/zend.c 79 行

93
94
95
96
97
98
99
100
101
102
static ZEND_INI_MH(OnUpdateGCEnabled) /* {{{ */
{
	OnUpdateBool(entry, new_value, new_value_length, mh_arg1, mh_arg2, mh_arg3, stage TSRMLS_CC);
 
	if (GC_G(gc_enabled)) {
		gc_init(TSRMLS_C);
	}
 
	return SUCCESS;
}

【添加到垃圾缓冲区】
跟踪PHP的源码 zend/zend_execute_API.c 424行
[_zval_ptr_dtor] -> [GC_ZVAL_CHECK_POSSIBLE_ROOT()] -> [gc_zval_check_possible_root()] -> [gc_zval_possible_root()]
其中在gc_zval_check_possible_root()函数中,仅对数组和对象执行垃圾回收操作

gc_zval_possible_root函数的代码如下:

130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
ZEND_API void gc_zval_possible_root(zval *zv TSRMLS_DC)
{
	if (UNEXPECTED(GC_G(free_list) != NULL &&
	               GC_ZVAL_ADDRESS(zv) != NULL &&
		           GC_ZVAL_GET_COLOR(zv) == GC_BLACK) &&
		           (GC_ZVAL_ADDRESS(zv) < GC_G(buf) ||
		            GC_ZVAL_ADDRESS(zv) >= GC_G(last_unused))) {
		/* The given zval is a garbage that is going to be deleted by
		 * currently running GC */
		return;
	}
 
	if (zv->type == IS_OBJECT) {
		GC_ZOBJ_CHECK_POSSIBLE_ROOT(zv);
		return;
	}
 
	GC_BENCH_INC(zval_possible_root);
 
	if (GC_ZVAL_GET_COLOR(zv) != GC_PURPLE) {
		GC_ZVAL_SET_PURPLE(zv);
 
		if (!GC_ZVAL_ADDRESS(zv)) {
			gc_root_buffer *newRoot = GC_G(unused);
 
			if (newRoot) {
				GC_G(unused) = newRoot->prev;
			} else if (GC_G(first_unused) != GC_G(last_unused)) {
				newRoot = GC_G(first_unused);
				GC_G(first_unused)++;
			} else {
				if (!GC_G(gc_enabled)) {
					GC_ZVAL_SET_BLACK(zv);
					return;
				}
				zv->refcount__gc++;
				gc_collect_cycles(TSRMLS_C);
				zv->refcount__gc--;
				newRoot = GC_G(unused);
				if (!newRoot) {
					return;
				}
				GC_ZVAL_SET_PURPLE(zv);
				GC_G(unused) = newRoot->prev;
			}
 
			newRoot->next = GC_G(roots).next;
			newRoot->prev = &GC_G(roots);
			GC_G(roots).next->prev = newRoot;
			GC_G(roots).next = newRoot;
 
			GC_ZVAL_SET_ADDRESS(zv, newRoot);
 
			newRoot->handle = 0;
			newRoot->u.pz = zv;
 
			GC_BENCH_INC(zval_buffered);
			GC_BENCH_INC(root_buf_length);
			GC_BENCH_PEAK(root_buf_peak, root_buf_length);
		}
	}
}

第132~140行 检查zval结点信息是否已经放入到结点缓冲区,如果已经放入到结点缓冲区,则直接返回,这样可以优化其性能

第142~145行 处理对象结点,直接返回,不再执行后面的操作

第149行 判断结点是否已经被标记为紫色,如果为紫色则不再添加到结点缓冲区,此处在于保证一个结点只执行一次添加到缓冲区的操作。

第150行 将结点的颜色标记为紫色,表示此结点已经添加到缓冲区,下次不用再做添加

第153~157行 找出新的结点的位置,如果缓冲区满了,则执行垃圾回收操作。

第176~184行 将新的结点添加到缓冲区所在的双向链表。

【垃圾回收过程】
在gc_zval_possible_root函数中,当缓冲区满时,程序调用gc_collect_cycles函数,执行垃圾回收操作。从zend/zend_gc.c文件615行开始,其实现代码如下:

615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
ZEND_API int gc_collect_cycles(TSRMLS_D)
{
	int count = 0;
 
	if (GC_G(roots).next != &GC_G(roots)) {
		zval_gc_info *p, *q, *orig_free_list, *orig_next_to_free;
 
		if (GC_G(gc_active)) {
			return 0;
		}
		GC_G(gc_runs)++;
		GC_G(zval_to_free) = FREE_LIST_END;
		GC_G(gc_active) = 1;
		gc_mark_roots(TSRMLS_C);
		gc_scan_roots(TSRMLS_C);
		gc_collect_roots(TSRMLS_C);
 
		orig_free_list = GC_G(free_list);
		orig_next_to_free = GC_G(next_to_free);
		p = GC_G(free_list) = GC_G(zval_to_free);
		GC_G(zval_to_free) = NULL;
		GC_G(gc_active) = 0;
 
		/* First call destructors */
		while (p != FREE_LIST_END) {
			if (Z_TYPE(p->z) == IS_OBJECT) {
				if (EG(objects_store).object_buckets &&
					EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].valid &&
					EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount <= 0 &&
					EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.dtor &&
					!EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].destructor_called) {
 
					EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].destructor_called = 1;
					EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount++;
					EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.dtor(EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.object, Z_OBJ_HANDLE(p->z) TSRMLS_CC);
					EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount--;
				}
			}
			count++;
			p = p->u.next;
		}
 
		/* Destroy zvals */
		p = GC_G(free_list);
		while (p != FREE_LIST_END) {
			GC_G(next_to_free) = p->u.next;
			if (Z_TYPE(p->z) == IS_OBJECT) {
				if (EG(objects_store).object_buckets &&
					EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].valid &&
					EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount <= 0) {
					EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount = 1;
					Z_TYPE(p->z) = IS_NULL;
					zend_objects_store_del_ref_by_handle_ex(Z_OBJ_HANDLE(p->z), Z_OBJ_HT(p->z) TSRMLS_CC);
				}
			} else if (Z_TYPE(p->z) == IS_ARRAY) {
				Z_TYPE(p->z) = IS_NULL;
				zend_hash_destroy(Z_ARRVAL(p->z));
				FREE_HASHTABLE(Z_ARRVAL(p->z));
			} else {
				zval_dtor(&p->z);
				Z_TYPE(p->z) = IS_NULL;
			}
			p = GC_G(next_to_free);
		}
 
		/* Free zvals */
		p = GC_G(free_list);
		while (p != FREE_LIST_END) {
			q = p->u.next;
			FREE_ZVAL_EX(&p->z);
			p = q;
		}
		GC_G(collected) += count;
		GC_G(free_list) = orig_free_list;
		GC_G(next_to_free) = orig_next_to_free;
	}
 
	return count;
}

第619行 判断缓冲区是否为空,如果为空则不会执行垃圾回收操作
第622行 判断垃圾回收操作是否正则进行,如果正在进行,则直接返回
第625~627行 将垃圾回收操作次数加1,初始化空闲列表,设置gc_active为1表示垃圾回归正在进行
第628行 此处为其官方文档中算法的步骤 B ,算法使用深度优先搜索查找所有可能的根,找到后将每个变量容器中的引用计数减1″,为确保不会对同一个变量容器减两次”1″,用灰色标记已减过1的。
第629行 这是算法的步骤 C ,算法再一次对每个根节点使用深度优先搜索,检查每个变量容器的引用计数。如果引用计数是 0 ,变量容器用白色来标记。如果引用次数大于0,则恢复在这个点上使用深度优先搜索而将引用计数减1的操作(即引用计数加1),然后将它们重新用黑色标记。
第630行 算法的最后一步 D ,算法遍历根缓冲区以从那里删除变量容器根(zval roots),同时,检查是否有在上一步中被白色标记的变量容器。每个被白色标记的变量容器都被清除。
在[gc_collect_cycles() -> gc_collect_roots() -> zval_collect_white() ]中我们可以看到,对于白色标记的结点会被添加到全局变量zval_to_free列表中。此列表在后面的操作中有用到。
第632~633行 将全局变量free_list和next_to_free存放在相对应当的临时变量中,在最后会恢复到此时的状态。
第634~635行 初始化需要清除的列表,清空将要清空的zval列表并且将垃圾收集的操作状态为不激活状态。
第639~655行 第一次调用析构函数,并统计清除的变量个数
第657~678行 清除变量
第682~686行 释放内存
第687~689行 处理垃圾个数统计,恢复free_list和next_to_free变量

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将整段内存清除,如果不是开始块,则将合并后的块添加到空闲块列表。