【PHP源码学习】2019-03-07 PHP内存管理1笔记
【PHP源码学习】2019-03-07 PHP内存管理1笔记
baiyan
源视频地址:http://replay.xesv5.com/ll/24...
malloc和free函数思考
- 字符串的非二进制安全:如果存的字符串内容中带有\0,那么它会被当做字符串结束标志,提前判定字符串结束,而并非到达字符串末尾正常结束。
- malloc一块内存之后,它的size是存在哪里的?
- free一块内存,如何知道free多大的内存?
- 解决方案:在分配的内存地址空间附近额外分配一块内存记录size。
- malloc()系统调用是非常慢的,相当于你去面包店买一个面包,需要和面、发酵...等等一系列繁琐的工作,在操作系统层面会引起用户态和内核态的切换,耗时很长,用户是不能接受的,所以PHP实现了自己的一套内存管理机制,避免频繁的进行系统调用。
操作系统内存基本概念
- chunk:2MB大小内存
- page:4KB大小内存
- 二者关系:一个chunk由512个page组成(2MB/4KB = 512)(512 = 2^9,4KB = 2^12,2MB = 2^21)。
- mmap():系统调用,将一个文件或者其它对象映射进内存,分配chunk。
- 内存不够用怎么办:选择一个占用内存大的进程并挂起,并将其使用的内存置换到硬盘上,分配给新进程使用。当挂起进程被唤醒的时候,唤醒进程会到内存中找对应的页,但是无法找到,故产生缺页中断,操作系统会将硬盘中的页置换回内存,供唤醒进程继续运行。
PHP内存分配
PHP内存分类:small/large/huge
- small:size <= 3KB(有30种规格)
- large:3KB < size <= 2MB - 4KB
- huge:size >= 2MB - 4KB
- 思考:为什么large规格内存是 <= 2MB - 4KB?
答:在small和large内存分配规格中,首先会分配一个chunk,chunk的第一个page中存有一个结构体,用来保存chunk中每一个page的相关存储信息(即zend_alloc.h中的mm_heap结构体)。但是huge规格不需要,直接拿来用即可(因为huge内存已经大于2MB(即一个chunk)),无需记录内部存储细节信息)。思考一个问题:一个page有4KB,至于用这么大的空间去存吗?
struct _zend_mm_heap { #if ZEND_MM_CUSTOM int use_custom_heap; #endif #if ZEND_MM_STORAGE zend_mm_storage *storage; #endif #if ZEND_MM_STAT size_t size; /* 当前使用内存(PHP库函数memory_get_usage()方法就是取此字段的值)*/ size_t peak; /* 内存使用峰值 */ #endif zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /*它是一个大小为ZEND_MM_BINS = 30的数组,每个存储单元是一个单向链表,用来挂载30种不同规格的small内存(下面会讲)*/ #if ZEND_MM_STAT || ZEND_MM_LIMIT size_t real_size; /* current size of allocated pages */ #endif #if ZEND_MM_STAT size_t real_peak; /* peak size of allocated pages */ #endif #if ZEND_MM_LIMIT size_t limit; /* memory limit */ int overflow; /* memory overflow flag */ #endif zend_mm_huge_list *huge_list; /* list of huge allocated blocks */ zend_mm_chunk *main_chunk; zend_mm_chunk *cached_chunks; /* list of unused chunks */ int chunks_count; /* number of alocated chunks */ int peak_chunks_count; /* peak number of allocated chunks for current request */ int cached_chunks_count; /* number of cached chunks */ double avg_chunks_count; /* average number of chunks allocated per request */ int last_chunks_delete_boundary; /* numer of chunks after last deletion */ int last_chunks_delete_count; /* number of deletion over the last boundary */ #if ZEND_MM_CUSTOM union { struct { void *(*_malloc)(size_t); void (*_free)(void*); void *(*_realloc)(void*, size_t); } std; struct { void *(*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC); void (*_free)(void* ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC); void *(*_realloc)(void*, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC); } debug; } custom_heap; #endif };
- 重点关注中文注释的部分,我们看到zend_mm_heap结构体并不是很大,那么继续关注其中重点的zend_mm_chunk结构体:
struct _zend_mm_chunk { zend_mm_heap *heap; zend_mm_chunk *next; zend_mm_chunk *prev; uint32_t free_pages; /* number of free pages */ uint32_t free_tail; /* number of free pages at the end of chunk */ uint32_t num; char reserve[64 - (sizeof(void*) * 3 + sizeof(uint32_t) * 3)]; zend_mm_heap heap_slot; /* used only in main chunk */ zend_mm_page_map free_map; /* 512 bits or 64 bytes */ zend_mm_page_info map[ZEND_MM_PAGES]; /* 2 KB = 512 * 4 */ };
- 1个chunk有512个page。
- 第一个heap字段是zend_mm_heap类型的结构体指针,方便在512个page中直接找到第一个存储额外信息的page。下面两个next和prev字段构成一个双向链表,用于chunk与chunk之间的连接。
- 重点关注倒数第二个字段zend_mm_page_map类型的字段,有512bit(即64B),每一个bit用来存储当前这个chunk中的每一个页(因为一共有512个页)是否使用。
- 除了看它是否使用,还要看每个page的具体使用的大小和其他相关情况并记录相关信息。
- 最后一个zend_mm_page_info类型的字段解决了上述问题。ZEND_MM_PAGES = (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) = 2MB/4KB = 512个存储单元,每个存储单元是typedef uint32_t zend_mm_page_info类型,即4B大小用来存储额外的使用情况信息,这个字段一共是2KB。用来存储使用的大小和额外信息,所以这里2KB加上剩余字段,差不多会使用4KB的空间,所以chunk的第一个page需要留出来记录这些信息。
- 那么这个page_info(uint32_t),这里32位是如何利用的呢,先看如何标记内存规格
#define ZEND_MM_IS_FRUN 0x00000000 #define ZEND_MM_IS_LRUN 0x40000000 #define ZEND_MM_IS_SRUN 0x80000000
- 第32位为1代表small内存,即0x8。然后低5位(2^5 > 30种规格)存储bit_num(即ZEND_MM_BINS_INFO宏的索引,以便快速找到所需页的数量等信息(下面会讲))
- 第31位为1代表large内存,即0x4。然后低10位表示分配的页数(>= 512即可)为什么不用9位呢?2^9 = 512足够了???
- 第31、32位都是1,即0xC,低5位表示bit_num,这里3KB规格就是29,16~25bit表示偏移量。以申请3KB为例,一定是3个4KB的page,分为4个3KB(见下文)。第一个3KB就是0xC作为连续分配4个3KB的起始标记(约定),偏移量是0,bit_num是29;第二个3KB就是0x8,偏移量是1,bit_num同样是29;第3、4个3KB内存同理,只是偏移量不同。
- _emalloc() -> 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 (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); return ptr; } else if (size <= ZEND_MM_MAX_LARGE_SIZE) { ptr = zend_mm_alloc_large(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC); return ptr; } else { return zend_mm_alloc_huge(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC); } }
small内存
- 注意一个page可以包含多个small内存块,首先满足一次性分配的page总量必须能够除尽分配的规格大小。换句话说,尽量不要有内存碎片。例如:分配3KB的small内存,不能只分配1个page,因为会剩下1KB大小的内存碎片,不能够再次利用并分配。所以需要求得一个规格和4KB的最小公倍数,作为总的分配空间。但是如果最小公倍数过大,导致所需要的page太多,那么退而求其次,即使留有很少的内存碎片,也算是可以满足需求的。下面举几个例子:
- 例1:假如分配8B规格的small内存:我们先找最小的可以除尽8B的page大小。因为4KB可以除尽8B,所以我们取1个page就够了,故一共正好可以分成512个8B的内存。我们取1个8B内存返回给用户,然后剩余511个8B内存会挂在zend_mm_heap结构体的zend_mm_free_slot *free_slot[ZEND_MM_BINS]字段中。这里ZEND_MM_BINS宏 = 30,恰好这30个数组单元对应30种small内存规格,共有30个单向链表,每一个单向链表都是挂载的多分配的内存。如上例中的8B内存,就会挂载剩下多分配的511个8B内存。当下次再次有分配请求的时候,直接从链表上拿即可,不用再去重新分配。同时也避免了内存碎片的产生。当释放内存的时候,直接利用头插法(相比尾插法复杂度O(1))插入到链表头部即可。
- small内存最小的分配规格就是8B,因为指针在64位系统下是8B,能够满足free_slot字段单向链表的需求。
- 例2:假如分配3KB规格的small内存:我们同样先找可以整除3KB的page大小。因为4KB不可以除尽3KB,所以1个page虽然可以满足3KB的分配需求,但是还是会留有1KB的内存碎片,所以我们不能直接拿1个page来用。我们取最小的可以整除3KB的4KB倍数,求得该值为3(4KB * 3 / 3KB = 4)个3KB大小的内存块。那么同样我们取1个3KB内存返回给用户,剩余3个3KB内存挂在上面的zend_mm_heap结构体中free_slot字段的链表上,等待后续的分配请求。
例3:假如分配40B规格的small内存,为什么下面代码也仅仅用了1个page,那是因为4KB和40B的最小公倍数过大,导致一次性申请的page太多,所以不能一味的找二者的最小公倍数,所以退而求其次,只分配1个page。虽然4096/40 = 102.5,代码中直接取了102作为最终分配的数量。第1个返回给用户,剩下101个挂在free_slot链表上等待后续的分配请求。
- 下面是所有的small内存分配规格:
/* bin_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)
- 思考:给一个size,如何确定它的bin_num(也就是索引),从而快速找到count和pages?
static zend_always_inline int zend_mm_small_size_to_bin(size_t size) { ... unsigned int t1, t2; if (size <= 64) { /* we need to support size == 0 ... */ return (size - !!size) >> 3; } else { t1 = size - 1; t2 = zend_mm_small_size_to_bit(t1) - 3; t1 = t1 >> t2; t2 = t2 - 3; t2 = t2 << 2; return (int)(t1 + t2); } #endif }
- 当size <= 64的时候,size会递增8,那么直接>>3位(除以8)就可以得到索引的偏移量,很好理解。这里的!!size是为了兼容size = 0的情况,这样在右移3位之前就不会出现负值;减去!!size(即减1)是为了兼容size为8B规格等边界情况。
- 当size > 64的时候,详情参考:small内存规格的计算 核心思想:可以将size看成等差数列,按照公差分组。首先找出当前size属于哪个组,在组内的偏移量是多少,最终把二者相加。
- 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 (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链表的第一个位置上拿就可以了,然后链指针向后移,等待下次内存分配。否则调用zend_mm_alloc_small_slow函数,注意这个slow就代表重新申请chunk、分配page等等,这里暂不展开。
内存对齐
- 内存对齐:所申请的内存返回的起始地址都是逻辑地址(下面会讲),且该地址一定是2MB的整数倍,这就是内存对齐。在PHP中,可以判定一定是申请的huge规格的内存。因为small和large内存的第一个page存放了zend_mm_heap结构体,肯定不是2MB地址的整数倍。这样可以快速计算出当前地址属于第几个chunk。比如地址是4096,就属于第3个chunk。
- 所以内存对齐的优点就是如果对齐了,就可以直接判断是huge规格,不需要额外存储用于以后判断的字段。
- x64体系结构的计算机的地址共有64位,任何一个内存地址都可以分为首地址 + 偏移量,如1019地址(10进制) = 1000(首地址) + 19 (偏移量);从二进制角度来看,2MB = 10 0000 0000 0000 0000 0000,低21位全部为0(偏移量为0),高43位为首地址(第22位为1,高64-22 = 42位全部为0)就构成了64位的逻辑地址。这样可以方便地进行逻辑地址与物理地址的转换。
- 向操作系统申请内存都是以页为单位,所以申请的内存都是4KB的整数倍,不会出现4K+1这种逻辑地址。
- 思考:PHP向操作系统申请内存是以chunk为单位,但是操作系统只返回4KB整数倍的内存起始地址,并不是2MB内存的起始地址,一个chunk中,分配到2MB整数倍起始地址是1/512概率,如何让操作系统申请的地址直接是2MB的整数倍呢?
- 答案:多申请一部分存储空间,掐头去尾,保证起始地址2MB对齐。
gdb调试相关
- gdb需要指定一个可执行的二进制程序来进行调试,相关命令用法google即可,常用r/b/n/s/p
- 当利用gdb b main的时候,为什么php没有main()入口函数却仍可以正常执行上述gdb命令?因为虚拟机会默认帮助你加一个main()和return
- 编译器优化:将inline函数直接注入到函数里面去,把宏进行替换等等,所以编译的时候为方便调试需要禁用编译器优化
相关推荐
拉斯厄尔高福 2020-11-04
ThinkInLinux 2020-09-30
ljbhander 2020-08-07
xiaobaichen 2020-07-30
mingrixing 2020-07-28
iammjun 2020-07-07
emlinux 2020-06-22
fenxinzi 2020-06-21
huanmie 2020-06-16
chenzhaoguo 2020-06-08
ThinkingLink 2020-05-29
wangqing 2020-05-19
pointfish 2020-05-08
cleanerxiaoqiang 2020-05-08
qingsongzdq 2020-05-01
zuixin 2020-04-19
NeverAgain 2020-04-17
fsfsdfsdw 2020-04-08