【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。

【PHP源码学习】2019-03-07 PHP内存管理1笔记

  • 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字段单向链表的需求。

【PHP源码学习】2019-03-07 PHP内存管理1笔记

  • 例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字段的链表上,等待后续的分配请求。

【PHP源码学习】2019-03-07 PHP内存管理1笔记

  • 例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对齐。

【PHP源码学习】2019-03-07 PHP内存管理1笔记

gdb调试相关

  • gdb需要指定一个可执行的二进制程序来进行调试,相关命令用法google即可,常用r/b/n/s/p
  • 当利用gdb b main的时候,为什么php没有main()入口函数却仍可以正常执行上述gdb命令?因为虚拟机会默认帮助你加一个main()和return
  • 编译器优化:将inline函数直接注入到函数里面去,把宏进行替换等等,所以编译的时候为方便调试需要禁用编译器优化

相关推荐