關於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

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