关于zend内存管理的学习

看了一个多月zend的内存管理,查了很多资料,看了书籍

<<unix高级环境编程>>和<<linux系统编程>>中进程环境以及系统内存管理部分的章节

参考了博客

(如何实现一个malloc)http://blog.codinglabs.org/articles/a-malloc-tutorial.html

(php内存管理)https://blog.csdn.net/luoyu_/article/details/82857778#articleHeader7

(php内存管理,看了一点视频,第一章还没看完。。,视频看起来太费劲)https://segmentfault.com/a/1190000018488313

(tipi 胖子的php内核解析)http://www.php-internals.com/book/?p=chapt06/06-00-memory-management

(关于首次适应算法和最佳适应算法)https://blog.csdn.net/weixin_39924920/article/details/81054205

学习了linux内存中的伙伴管理算法:

https://blog.csdn.net/qq_32783703/article/details/103736754

还有写了文章(linux内存管理的一些)

https://blog.csdn.net/qq_32783703/article/details/103516782

https://blog.csdn.net/qq_32783703/article/details/103763657

 

 

在工作中还是不建议自己去写这种内存管理的,需要技术底子非常的深,而且十分容易出bug,出了bug很难排查,当然我们在写php 扩展的时候可以设置一个ALLOC的环境变量,让申请直接走系统可以配合valgrind去排查问题,所以想要用内存池的话推荐直接用jemalloc,多线程用tcmalloc,不要自己写!!!!!!

之前说道https://blog.csdn.net/qq_32783703/article/details/103516782我们在写linux程序也好windows程序也罢,频繁的使用malloc和free有一个很大的缺陷就是会造成内存碎片,因为malloc可能把一个连续内存块造成不连续,而且还会产生向系统申请资源的浪费,我们也要注意malloc不是系统调用,不是系统调用,sbrk和brk才是!!!

 

之前有一张图很好的介绍了linux进程环境

自高往下是linux内核的一些地址,环境变量,参数 ,栈 ,映射段,堆,未初始化数据段,数据段,正文段

 

下面细说一下zend内存管理,在我读代码php-cgi或者写扩展的时候经常用的是emalloc而不是malloc,emalloc可以有效的减少内存碎片,和向系统申请的开销

 

#define emalloc(size)						_emalloc((size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)

 

最后落到的代码段是在这里:

ZEND_API void* ZEND_FASTCALL _emalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
#if ZEND_MM_CUSTOM
	if (UNEXPECTED(AG(mm_heap)->use_custom_heap)) {
		if (ZEND_DEBUG && AG(mm_heap)->use_custom_heap == ZEND_MM_CUSTOM_HEAP_DEBUG) {
			return AG(mm_heap)->custom_heap.debug._malloc(size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
		} else {
			return AG(mm_heap)->custom_heap.std._malloc(size);
		}
	}
#endif
	return zend_mm_alloc_heap(AG(mm_heap), size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}

 

我们忽略掉非zend内存池的部分,直接看zend_mm_alloc_heap

 

AG这个宏就是一个全局结构体,由于c语言里没有对象的概念,所以结构体是一个及其重要的数据类型,其实如果你看过结构体和对象的底层结构其实都是一样的,因为都是一个连续的内存,在这里贴出alloc_globals这个详细的结构

typedef struct _zend_alloc_globals {
	zend_mm_heap *mm_heap;
} zend_alloc_globals;

上面这个函数我们需要重点关注的是zend_mm_alloc_heap,我们看一下这个函数做了什么

static zend_always_inline void *zend_mm_alloc_heap(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
	void *ptr;
#if ZEND_DEBUG
	size_t real_size = size;
	zend_mm_debug_info *dbg;

	/* special handling for zero-size allocation */
	size = MAX(size, 1);
	size = ZEND_MM_ALIGNED_SIZE(size) + ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info));
	if (UNEXPECTED(size < real_size)) {
		zend_error_noreturn(E_ERROR, "Possible integer overflow in memory allocation (%zu + %zu)", ZEND_MM_ALIGNED_SIZE(real_size), ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));
		return NULL;
	}
#endif
	if (EXPECTED(size <= ZEND_MM_MAX_SMALL_SIZE)) {
		ptr = zend_mm_alloc_small(heap, size, ZEND_MM_SMALL_SIZE_TO_BIN(size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#if ZEND_DEBUG
		dbg = zend_mm_get_debug_info(heap, ptr);
		dbg->size = real_size;
		dbg->filename = __zend_filename;
		dbg->orig_filename = __zend_orig_filename;
		dbg->lineno = __zend_lineno;
		dbg->orig_lineno = __zend_orig_lineno;
#endif
		return ptr;
	} else if (EXPECTED(size <= ZEND_MM_MAX_LARGE_SIZE)) {
		ptr = zend_mm_alloc_large(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#if ZEND_DEBUG
		dbg = zend_mm_get_debug_info(heap, ptr);
		dbg->size = real_size;
		dbg->filename = __zend_filename;
		dbg->orig_filename = __zend_orig_filename;
		dbg->lineno = __zend_lineno;
		dbg->orig_lineno = __zend_orig_lineno;
#endif
		return ptr;
	} else {
#if ZEND_DEBUG
		size = real_size;
#endif
		return zend_mm_alloc_huge(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
	}
}

php对我们malloc的内存尺寸进行了分类,我们用一张图来更加清晰的看这个程序代码块

我们先来看一下zend_mm_alloc_small

static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, size_t size, int bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
#if ZEND_MM_STAT
	do {
		size_t size = heap->size + bin_data_size[bin_num];
		size_t peak = MAX(heap->peak, size);
		heap->size = size;
		heap->peak = peak;
	} while (0);
#endif

	if (EXPECTED(heap->free_slot[bin_num] != NULL)) {
		zend_mm_free_slot *p = heap->free_slot[bin_num];
		heap->free_slot[bin_num] = p->next_free_slot;
		return (void*)p;
	} else {
		return zend_mm_alloc_small_slow(heap, bin_num ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
	}
}

如果说free_slot这个规格上有这个规格的话直接从free_slot上去取然后转换成void*就可以我门上层没有任何应用感知,在这里我们要重点说一个结构zend_mm_heap这个zend自定义的堆结构,注意一个细节,当我们要使用这块地址的时候需要把他从free_slot上摘除掉

我们这里进一步要查看zend_mm_alloc_small_slow

static zend_never_inline void *zend_mm_alloc_small_slow(zend_mm_heap *heap, uint32_t bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
    zend_mm_chunk *chunk;
    int page_num;
	zend_mm_bin *bin;
	zend_mm_free_slot *p, *end;

#if ZEND_DEBUG
	bin = (zend_mm_bin*)zend_mm_alloc_pages(heap, bin_pages[bin_num], bin_data_size[bin_num] ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#else
	bin = (zend_mm_bin*)zend_mm_alloc_pages(heap, bin_pages[bin_num] ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
#endif
	if (UNEXPECTED(bin == NULL)) {
		/* insufficient memory */
		return NULL;
	}

	chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(bin, ZEND_MM_CHUNK_SIZE);
	page_num = ZEND_MM_ALIGNED_OFFSET(bin, ZEND_MM_CHUNK_SIZE) / ZEND_MM_PAGE_SIZE;
	chunk->map[page_num] = ZEND_MM_SRUN(bin_num);
	if (bin_pages[bin_num] > 1) {
		uint32_t i = 1;

		do {
			chunk->map[page_num+i] = ZEND_MM_NRUN(bin_num, i);
			i++;
		} while (i < bin_pages[bin_num]);
	}

	/* create a linked list of elements from 1 to last */
	end = (zend_mm_free_slot*)((char*)bin + (bin_data_size[bin_num] * (bin_elements[bin_num] - 1)));
	heap->free_slot[bin_num] = p = (zend_mm_free_slot*)((char*)bin + bin_data_size[bin_num]);
	do {
		p->next_free_slot = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]);
#if ZEND_DEBUG
		do {
			zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)p + bin_data_size[bin_num] - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));
			dbg->size = 0;
		} while (0);
#endif
		p = (zend_mm_free_slot*)((char*)p + bin_data_size[bin_num]);
	} while (p != end);

	/* terminate list using NULL */
	p->next_free_slot = NULL;
#if ZEND_DEBUG
		do {
			zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)p + bin_data_size[bin_num] - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));
			dbg->size = 0;
		} while (0);
#endif

	/* return first element */
	return (char*)bin;
}

这里我们要重点看一下zend_mm_alloc_pages这个函数,看一下他的调用代码

bin = (zend_mm_bin*)zend_mm_alloc_pages(heap, bin_pages[bin_num]);

再看一个哎zend_small 中一个非常重要的宏定义

/* num, size, count, pages */
#define ZEND_MM_BINS_INFO(_, x, y) \
	_( 0,    8,  512, 1, x, y) \
	_( 1,   16,  256, 1, x, y) \
	_( 2,   24,  170, 1, x, y) \
	_( 3,   32,  128, 1, x, y) \
	_( 4,   40,  102, 1, x, y ) \
	_( 5,   48,   85, 1, x, y) \
	_( 6,   56,   73, 1, x, y) \
	_( 7,   64,   64, 1, x, y) \
	_( 8,   80,   51, 1, x, y) \
	_( 9,   96,   42, 1, x, y) \
	_(10,  112,   36, 1, x, y) \
	_(11,  128,   32, 1, x, y) \
	_(12,  160,   25, 1, x, y) \
	_(13,  192,   21, 1, x, y) \
	_(14,  224,   18, 1, x, y) \
	_(15,  256,   16, 1, x, y) \
	_(16,  320,   64, 5, x, y) \
	_(17,  384,   32, 3, x, y) \
	_(18,  448,    9, 1, x, y) \
	_(19,  512,    8, 1, x, y) \
	_(20,  640,   32, 5, x, y) \
	_(21,  768,   16, 3, x, y) \
	_(22,  896,    9, 2, x, y) \
	_(23, 1024,    8, 2, x, y) \
	_(24, 1280,   16, 5, x, y) \
	_(25, 1536,    8, 3, x, y) \
	_(26, 1792,   16, 7, x, y) \
	_(27, 2048,    8, 4, x, y) \
	_(28, 2560,    8, 5, x, y) \
	_(29, 3072,    4, 3, x, y)

第一列是健值,第二列是字节数,第三列是数量,第四页是对应的内存页页数,在这里我在看网上一些列子的时候说linux内存页是4k,但是linux系统编程中说到,32位是4k,64位是8k,在这里就按照4k来把

以3072为例子 3k*4 就是3个4k的内存页这里的count和pages就是这么算的zend_mm_alloc_pages传入的就是堆的地址heap和要申请的页数,zend_mm_alloc_pages是一个十分重要的函数他的作用是申请count个page,下面来一点点分析这个至关重要的函数

 

在zend_mm_heap中有一个free_pages字段用来记载可以用的空闲页,当我们申请的时候free_pages小于pages_count直接goto到not_found

		if (UNEXPECTED(chunk->free_pages < pages_count)) {
			goto not_found;
#if 0
        ...
#endif
		}

我们再看一下not found 中做了什么?

如果main_chunk后面的下一个chunk就是main_chunk了(环形链表),首先看一下循环到的块是否是最后一块,如果不是那么就继续去找下一块,如果已经到最后一块了,会去看缓存块中有没有可用的,有的话从缓存块的链表中摘下来,然后把值给chunk,没有的话会用zend_mm_mmap去申请

if (chunk->next == heap->main_chunk) {
    if (heap->cached_chunks) {
		heap->cached_chunks_count--;
		chunk = heap->cached_chunks;
		heap->cached_chunks = chunk->next;
	}

        heap->chunks_count++;
	if (heap->chunks_count > heap->peak_chunks_count) {
		heap->peak_chunks_count = heap->chunks_count;
	}
	zend_mm_chunk_init(heap, chunk);
	page_num = ZEND_MM_FIRST_PAGE;
	len = ZEND_MM_PAGES - ZEND_MM_FIRST_PAGE;
	goto found;
}

然后chunk的数字+1 

heap->chunks_count++;

如果说当前使用的块大于峰值时候使用的块,则赋值peak_chunk_count,然后通过zend_mm_chunk_init给这个当前块初始化,然后直接跳入found,我们看一下found做了什么?

found:
	if (steps > 2 && pages_count < 8) {
		/* move chunk into the head of the linked-list */
		chunk->prev->next = chunk->next;
		chunk->next->prev = chunk->prev;
		chunk->next = heap->main_chunk->next;
		chunk->prev = heap->main_chunk;
		chunk->prev->next = chunk;
		chunk->next->prev = chunk;
	}
	/* mark run as allocated */
	chunk->free_pages -= pages_count;
	zend_mm_bitset_set_range(chunk->free_map, page_num, pages_count);
	chunk->map[page_num] = ZEND_MM_LRUN(pages_count);
	if (page_num == chunk->free_tail) {
		chunk->free_tail = page_num + pages_count;
	}
	return ZEND_MM_PAGE_ADDR(chunk, page_num);

首先看一下chunk变化

我们发现他把获取到的chunk从原来的链表上摘除,然后移植到了main_chunk上,chunk->free_pages -= pages_count;在空闲页上减去要申请的页数

再说一下free_map这个标志位记录,我们看一下free_map的具体情况

typedef zend_mm_bitset zend_mm_page_map[ZEND_MM_PAGE_MAP_LEN];     /* 64B */

直接一点就是存了8个long,我们知道一个字节是8位,1个long就是64位,8个long就是512位,每一位用来标识一个内存页使用情况0代表未使用,1代表已经使用,使用zend_mm_bitset_set_range就是设置free_map的使用情况的

 

再来看map这个成员变量是做什么的

zend_mm_page_info  map[ZEND_MM_PAGES];

这里有512个zend_mm_page_info指针用来存储内存页使用情况,每一个元素是一个uint32_t 4个字节

chunk->map[page_num] = ZEND_MM_LRUN(pages_count);
这个宏ZEND_MM_LRUN来设置这个内存页的使用情况

最后就是记录使用页的尾部

if (page_num == chunk->free_tail) {
	chunk->free_tail = page_num + pages_count;
}

刚才看了存在cache的情况,再看一下没有cache块的情况,直接看一个函数zend_mm_chunk_alloc,这个函数实质上就是使用的mmap,我们看一下mmap这个函数,在linux内存中,超过128k的内存申请,linux系统编程中推荐使用的是mmap,我们看一下这个函数的mmap是文件映射还是匿名映射

static void *zend_mm_mmap(size_t size)
{
#ifdef _WIN32
	void *ptr = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

	if (ptr == NULL) {
#if ZEND_MM_ERROR
		stderr_last_error("VirtualAlloc() failed");
#endif
		return NULL;
	}
	return ptr;
#else
	void *ptr;

#ifdef MAP_HUGETLB
	if (zend_mm_use_huge_pages && size == ZEND_MM_CHUNK_SIZE) {
		ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_HUGETLB, -1, 0);
		if (ptr != MAP_FAILED) {
			return ptr;
		}
	}
#endif

	ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);

	if (ptr == MAP_FAILED) {
#if ZEND_MM_ERROR
		fprintf(stderr, "\nmmap() failed: [%d] %s\n", errno, strerror(errno));
#endif
		return NULL;
	}
	return ptr;
#endif
}

采取的匿名映射,因为fd参数是-1,并不是文件映射,mmap是把文件内容映射到内存中,我认为是映射在了进程栈和堆之间的位置(详细内容看最上面的图,进程环境的图,在stack和heap之间的位置),总结一点如果发现了chunk我们走的是found,直接从之前的chunk链表上摘除掉,然后放到main_chunk上,如果说not_found,我们就首先看cache_chunk, 然后如果没有缓存块就用zend_mm_chunk_alloc去申请chunk,以此来节约内存开销,看到这里就要继续去看另一部分代码段

 

就是关于最佳适应算法,和首次适合算法这两种内存算法,首次适合算法可以降低cpu减少时间复杂度,但是肯能会造成空间复杂度高一些,最佳适应算法时间复杂度高,需要预先读取,然后再找寻最适合的块,但是时间复杂度高一些

我们看一下zend是如何处理的

int best = -1;
			uint32_t best_len = ZEND_MM_PAGES;
			uint32_t free_tail = chunk->free_tail;
			zend_mm_bitset *bitset = chunk->free_map;
			zend_mm_bitset tmp = *(bitset++);
			uint32_t i = 0;

			while (1) {
				/* skip allocated blocks */
				while (tmp == (zend_mm_bitset)-1) {
					i += ZEND_MM_BITSET_LEN;
					if (i == ZEND_MM_PAGES) {
						if (best > 0) {
							page_num = best;
							goto found;
						} else {
							goto not_found;
						}
					}
					tmp = *(bitset++);
				}
				/* find first 0 bit */
				page_num = i + zend_mm_bitset_nts(tmp);
				/* reset bits from 0 to "bit" */
				tmp &= tmp + 1;
				/* skip free blocks */
				while (tmp == 0) {
					i += ZEND_MM_BITSET_LEN;
					if (i >= free_tail || i == ZEND_MM_PAGES) {
						len = ZEND_MM_PAGES - page_num;
						if (len >= pages_count && len < best_len) {
							chunk->free_tail = page_num + pages_count;
							goto found;
						} else {
							/* set accurate value */
							chunk->free_tail = page_num;
							if (best > 0) {
								page_num = best;
								goto found;
							} else {
								goto not_found;
							}
						}
					}
					tmp = *(bitset++);
				}
				/* find first 1 bit */
				len = i + zend_ulong_ntz(tmp) - page_num;
				if (len >= pages_count) {
					if (len == pages_count) {
						goto found;
					} else if (len < best_len) {
						best_len = len;
						best = page_num;
					}
				}
				/* set bits from 0 to "bit" */
				tmp |= tmp - 1;
			}

 

其中:

/* skip allocated blocks */
while (tmp == (zend_mm_bitset)-1) {
    i += ZEND_MM_BITSET_LEN;
    if (i == ZEND_MM_PAGES) {
        if (best > 0) {
            page_num = best;
            goto found;
        } else {
            goto not_found;
        }
    }
    tmp = *(bitset++);
}

这一块说了是free_maps当前元素上64也都被标记为使用那么给i+64,如果已经找到最后一页了,那么直接让best>0,赋值给page_num,然后跳转到found,可见这个best 就是块上的最佳位置!!

再来看下面这一小块代码

/* find first 0 bit */
page_num = i + zend_mm_bitset_nts(tmp);
/* reset bits from 0 to "bit" */
tmp &= tmp + 1;
/* skip free blocks */
while (tmp == 0) {
    i += ZEND_MM_BITSET_LEN;
    if (i >= free_tail || i == ZEND_MM_PAGES) {
        len = ZEND_MM_PAGES - page_num;
        if (len >= pages_count && len < best_len) {
            chunk->free_tail = page_num + pages_count;
            goto found;
        } else {
            /* set accurate value */
            chunk->free_tail = page_num;
            if (best > 0) {
                page_num = best;
                goto found;
            } else {
                goto not_found;
            }
        }
    }
    tmp = *(bitset++);
}

这一块代码是如果当前的代码64页都没有使用的情况下运行,注意一个很关键的点!!!!

if (len >= pages_count && len < best_len) {

代码中的best就是我们要申请的块里的最佳的page_num

总结:这个最佳适应算法就是会找寻main_chunk上所有的块,去找寻最佳的chunk和page_num的位置

然后我们使用ZEND_MM_PAGE_ADDR(chunk, page_num);去获得偏移地址

#define ZEND_MM_PAGE_ADDR(chunk, page_num) \
	((void*)(((zend_mm_page*)(chunk)) + (page_num)))

注意加上可page_num就是加上了page_num*4k,获取的是页的地址

也就是说zend_mm_alloc_pages找到了最佳的块以及页的位置,然后把这个块的页地址返回,就是说chunkd的页地址位置,这和chunk是2M的所以我们要偏移出页的位置,因为page是4k所以可以偏移的出page的地址,如图:

`

到此位置small的我们都看完了,large也是直接调用zend_mm_alloc_pages去申请,huge的话直接调用zend_mm_chunk_alloc去申请,然后会加入到zend_mm_heap的huge_list这个列表,调用zend_mm_add_huge_block

 

到此为止zend 申请内存的全部已经整理完了,我们再用一张图来进行总结

 

看完了zend 内存的申请我们最后再要看一个释放的过程

efree

核心代码是

ZEND_API void ZEND_FASTCALL _efree(void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
#if ZEND_MM_CUSTOM
	if (UNEXPECTED(AG(mm_heap)->use_custom_heap)) {
		if (ZEND_DEBUG && AG(mm_heap)->use_custom_heap == ZEND_MM_CUSTOM_HEAP_DEBUG) {
			AG(mm_heap)->custom_heap.debug._free(ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
		} else {
			AG(mm_heap)->custom_heap.std._free(ptr);
	    }
		return;
	}
#endif
	zend_mm_free_heap(AG(mm_heap), ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}

我们再看一下zend_mm_free_heap

static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
	size_t page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE);

	if (UNEXPECTED(page_offset == 0)) {
		if (ptr != NULL) {
			zend_mm_free_huge(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
		}
	} else {
		zend_mm_chunk *chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(ptr, ZEND_MM_CHUNK_SIZE);
		int page_num = (int)(page_offset / ZEND_MM_PAGE_SIZE);
		zend_mm_page_info info = chunk->map[page_num];

		ZEND_MM_CHECK(chunk->heap == heap, "zend_mm_heap corrupted");
		if (EXPECTED(info & ZEND_MM_IS_SRUN)) {
			zend_mm_free_small(heap, ptr, ZEND_MM_SRUN_BIN_NUM(info));
		} else /* if (info & ZEND_MM_IS_LRUN) */ {
			int pages_count = ZEND_MM_LRUN_PAGES(info);

			ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(page_offset, ZEND_MM_PAGE_SIZE) == 0, "zend_mm_heap corrupted");
			zend_mm_free_large(heap, chunk, page_num, pages_count);
		}
	}
}

既然有对应的small,large和huge就会有对应的释放,但是这里我们需要思考一个核心的问题点,调用free的时候我们只是传入了一个参数的地址(注意这个地址是块上的page地址),那么他是如何通过这个参数地址来获取到我们malloc时候到底是小型的内存还是中型的内存,还是巨型内存呢?你可能也会说通过map来记录的页来表示,那么巨型内存没有map的记录他又怎么知道是一个巨型内存呢?

 

zend_mm_free_heap的时候,他会首先调用的宏是这个ZEND_MM_ALIGNED_OFFSET

#define ZEND_MM_ALIGNED_OFFSET(size, alignment) \
	(((size_t)(size)) & ((alignment) - 1))

如果说page_offset大于0那么他就是一个巨型块

if (UNEXPECTED(page_offset == 0)) {
		if (ptr != NULL) {
			zend_mm_free_huge(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
		}
	} 

如果说是0的话就去通过page的地址反推chunk的地址

zend_mm_chunk *chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(ptr, ZEND_MM_CHUNK_SIZE);

去看这个chunk的map对应的page_num的标记,标记这是长内存还是小内存

if (EXPECTED(info & ZEND_MM_IS_SRUN)) {
			zend_mm_free_small(heap, ptr, ZEND_MM_SRUN_BIN_NUM(info));
		} else /* if (info & ZEND_MM_IS_LRUN) */ {
			int pages_count = ZEND_MM_LRUN_PAGES(info);

			ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(page_offset, ZEND_MM_PAGE_SIZE) == 0, "zend_mm_heap corrupted");
			zend_mm_free_large(heap, chunk, page_num, pages_count);
		}

注意一个细节 就是zend内存释放掉之后不会立马还给linux内核,而是把他挂载到了free_slot上

static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr, int bin_num)
{
	zend_mm_free_slot *p;

#if ZEND_MM_STAT
	heap->size -= bin_data_size[bin_num];
#endif

#if ZEND_DEBUG
	do {
		zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)ptr + bin_data_size[bin_num] - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));
		dbg->size = 0;
	} while (0);
#endif

    p = (zend_mm_free_slot*)ptr;
    p->next_free_slot = heap->free_slot[bin_num];
    heap->free_slot[bin_num] = p;
}

这是small类型申请的时候,我们再看一下large

static zend_always_inline void zend_mm_free_large(zend_mm_heap *heap, zend_mm_chunk *chunk, int page_num, int pages_count)
{
#if ZEND_MM_STAT
	heap->size -= pages_count * ZEND_MM_PAGE_SIZE;
#endif
	zend_mm_free_pages(heap, chunk, page_num, pages_count);
}

代码调用了zend_mm_free_pages

static zend_always_inline void zend_mm_free_pages_ex(zend_mm_heap *heap, zend_mm_chunk *chunk, uint32_t page_num, uint32_t pages_count, int free_chunk)
{
	chunk->free_pages += pages_count;
	zend_mm_bitset_reset_range(chunk->free_map, page_num, pages_count);
	chunk->map[page_num] = 0;
	if (chunk->free_tail == page_num + pages_count) {
		/* this setting may be not accurate */
		chunk->free_tail = page_num;
	}
	if (free_chunk && chunk->free_pages == ZEND_MM_PAGES - ZEND_MM_FIRST_PAGE) {
		zend_mm_delete_chunk(heap, chunk);
	}
}

他把这个块的chunk的map信息置为了0

	chunk->map[page_num] = 0;

然后调用zend_mm_delete_chunk

static zend_always_inline void zend_mm_delete_chunk(zend_mm_heap *heap, zend_mm_chunk *chunk)
{
	chunk->next->prev = chunk->prev;
	chunk->prev->next = chunk->next;
	heap->chunks_count--;
	if (heap->chunks_count + heap->cached_chunks_count < heap->avg_chunks_count + 0.1
	 || (heap->chunks_count == heap->last_chunks_delete_boundary
	  && heap->last_chunks_delete_count >= 4)) {
		/* delay deletion */
		heap->cached_chunks_count++;
		chunk->next = heap->cached_chunks;
		heap->cached_chunks = chunk;
	} else {
#if ZEND_MM_STAT || ZEND_MM_LIMIT
		heap->real_size -= ZEND_MM_CHUNK_SIZE;
#endif
		if (!heap->cached_chunks) {
			if (heap->chunks_count != heap->last_chunks_delete_boundary) {
				heap->last_chunks_delete_boundary = heap->chunks_count;
				heap->last_chunks_delete_count = 0;
			} else {
				heap->last_chunks_delete_count++;
			}
		}
		if (!heap->cached_chunks || chunk->num > heap->cached_chunks->num) {
			zend_mm_chunk_free(heap, chunk, ZEND_MM_CHUNK_SIZE);
		} else {
//TODO: select the best chunk to delete???
			chunk->next = heap->cached_chunks->next;
			zend_mm_chunk_free(heap, heap->cached_chunks, ZEND_MM_CHUNK_SIZE);
			heap->cached_chunks = chunk;
		}
	}
}

他把这个chunk从main_chunk的链表上摘了下来,放到了cached_chunks上

 

总结:

zend内存池可以有效减少内存碎片和频繁向系统申请内存的开销,但是不推荐自己写程序时候自己设计内存池,还是推荐使用tcmalloc和jemalloc

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章