zend针对内存的操作封装了一层,用于替换直接的内存操作:malloc、free等,实现了更高效率的内存利用,其实现主要参考了tcmalloc的设计。
源码中emalloc、efree、estrdup等等就是内存池的操作。
内存池是内核中最底层的内存操作,定义了三种粒度的内存块:chunk、page、slot,每个chunk的大小为2M,page大小为4KB,一个chunk被切割为512个page,而一个或若干个page被切割为多个slot,所以申请内存时按照不同的申请大小决定具体的分配策略:
- Huge(chunk): 申请内存大于2M,直接调用系统分配,分配若干个chunk
- Large(page): 申请内存大于3092B(3/4 page_size),小于2044KB(511 page_size),分配若干个page
- Small(slot): 申请内存小于等于3092B(3/4 page_size),内存池提前定义好了30种同等大小的内存(8,16,24,32,...3072),他们分配在不同的page上(不同大小的内存可能会分配在多个连续的page),申请内存时直接在对应page上查找可用位置
1、基本数据结构
chunk由512个page组成,其中第一个page用于保存chunk结构,剩下的511个page用于内存分配,page主要用于Large、Small两种内存的分配;heap是表示内存池的一个结构,它是最主要的一个结构,用于管理上面三种内存的分配,Zend中只有一个heap结构。
struct _zend_mm_heap { #if ZEND_MM_STAT size_t size; //当前已用内存数 size_t peak; //内存单次申请的峰值 #endif zend_mm_free_slot *free_slot[ZEND_MM_BINS]; // 小内存分配的可用位置链表,ZEND_MM_BINS等于30,即此数组表示的是各种大小内存对应的链表头部 ... zend_mm_huge_list *huge_list; //大内存链表 zend_mm_chunk *main_chunk; //指向chunk链表头部 zend_mm_chunk *cached_chunks; //缓存的chunk链表 int chunks_count; //已分配chunk数 int peak_chunks_count; //当前request使用chunk峰值 int cached_chunks_count; //缓存的chunk数 double avg_chunks_count; //chunk使用均值,每次请求结束后会根据peak_chunks_count重新计算:(avg_chunks_count+peak_chunks_count)/2.0 } struct _zend_mm_chunk { zend_mm_heap *heap; //指向heap zend_mm_chunk *next; //指向下一个chunk zend_mm_chunk *prev; //指向上一个chunk int free_pages; //当前chunk的剩余page数 int free_tail; /* number of free pages at the end of chunk */ int num; char reserve[64 - (sizeof(void*) * 3 + sizeof(int) * 3)]; zend_mm_heap heap_slot; //heap结构,只有主chunk会用到 zend_mm_page_map free_map; //标识各page是否已分配的bitmap数组,总大小512bit,对应page总数,每个page占一个bit位 zend_mm_page_info map[ZEND_MM_PAGES]; //各page的信息:当前page使用类型(用于large分配还是small)、占用的page数等 }; //按固定大小切好的small内存槽 struct _zend_mm_free_slot { zend_mm_free_slot *next_free_slot;//此指针只有内存未分配时用到,分配后整个结构体转为char使用 };
chunk、page、slot三者的关系:
接下来看下内存池的初始化以及三种内存分配的过程。
2、 内存池初始化
内存池在php_module_startup阶段初始化,start_memory_manager():
ZEND_API void start_memory_manager(void) { #ifdef ZTS ts_allocate_id(&alloc_globals_id, sizeof(zend_alloc_globals), (ts_allocate_ctor) alloc_globals_ctor, (ts_allocate_dtor) alloc_globals_dtor); #else alloc_globals_ctor(&alloc_globals); #endif } static void alloc_globals_ctor(zend_alloc_globals *alloc_globals) { #ifdef MAP_HUGETLB tmp = getenv("USE_ZEND_ALLOC_HUGE_PAGES"); if (tmp && zend_atoi(tmp, 0)) { zend_mm_use_huge_pages = 1; } #endif ZEND_TSRMLS_CACHE_UPDATE(); alloc_globals->mm_heap = zend_mm_init(); }
alloc_globals 是一个全局变量,即 AG宏 ,它只有一个成员:mm_heap,保存着整个内存池的信息,所有内存的分配都是基于这个值,多线程模式下(ZTS)会有多个heap,也就是说每个线程都有一个独立的内存池,看下它的初始化:
static zend_mm_heap *zend_mm_init(void) { //向系统申请2M大小的chunk zend_mm_chunk *chunk = (zend_mm_chunk*)zend_mm_chunk_alloc_int(ZEND_MM_CHUNK_SIZE, ZEND_MM_CHUNK_SIZE); zend_mm_heap *heap; heap = &chunk->heap_slot; //heap结构实际是主chunk嵌入的一个结构,后面再分配chunk的heap_slot不再使用 chunk->heap = heap; chunk->next = chunk; chunk->prev = chunk; chunk->free_pages = ZEND_MM_PAGES - ZEND_MM_FIRST_PAGE; //剩余可用page数 chunk->free_tail = ZEND_MM_FIRST_PAGE; chunk->num = 0; chunk->free_map[0] = (Z_L(1) << ZEND_MM_FIRST_PAGE) - 1; //将第一个page的bit分配标识位设置为1 chunk->map[0] = ZEND_MM_LRUN(ZEND_