1

baiyan

全部视频:https://segmentfault.com/a/11...

原视频地址:http://replay.xesv5.com/ll/24...

本笔记中部分图片截自视频中的片段,图片版权归视频原作者所有。

malloc函数深入

  • PHP内存管理1笔记中提到,malloc()函数会在分配的内存空间前面额外分配32位,用来存储分配的大小和几个标志位,如图:

  • 那么究竟是否是这样的呢?我们写一段测试代码验证一下:
#include <stdlib.h>
int main() {
    void *ptr = malloc(8);
    return 1;
}
  • 利用gdb调试这段代码:

  • 首先打印ptr的地址,为0x602010,利用x命令往后看20个内存单元(1个内存单元 = 4个字节),故一共展示了80个字节,后面的x是以16进制打印内容。
  • 我们发现紧邻0x602010地址的上面32位均是0,没有任何内容,不符合我们的预期。
  • 上图只是一个最简单的思路,但绝大多数操作系统是按照如下的方式实现的:
操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表(Free List)。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块(根据不同的算法而定(将最先找到的不小于申请的大小内存块分配给请求者,将最合适申请大小的空闲内存分配给请求者,或者是分配最大的空闲块内存块)。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。

结构体与联合体

结构体

  • 在b是char类型的时候,a和b的内存地址是紧邻的;如果b是int类型的话,就会出现如图所示的情况。我们可以这样记忆:不看b之后的字段,a和b之前也是按照它们的最小公倍数对齐的(如果b是int类型,a和b的最小公倍数是4,按4对齐;如果b是char类型,最小公倍数为1,按1对齐,就会出现a和b紧邻的情况)
  • 如果不想对齐,有如下解决方案:

    • 编译的时候不加优化参数
    • 代码层面:在struct后加关键字,例如redis中的sds简单动态字符串的实现:

          struct __attribute__ ((packed)) sdshdr16 {
              uint16_t len;
              uint16_t alloc;
              unsigned char flags;
              char buf[];
          }

联合体

  • 所有字段共用一段内存,用于PHP中变量值的存储(因为变量只有一种类型),也可以用来判断机器的大小端问题。

宏定义

  • 宏就是替换。
  • 关于下面这段代码的复杂宏替换问题,在PHP内存管理3笔记中已经有详细解释,此处不再赘述。
#define _BIN_DATA_SIZE(num, size, elements, pages, x, y) size,
static const uint32_t bin_data_size[] = {
  ZEND_MM_BINS_INFO(_BIN_DATA_SIZE, x, y)
};

PHP7中的基本变量

  • 在PHP7中,所有变量都以zval结构体来表示。一个zval是16字节;在PHP5中,一个zval是48字节。
struct _zval_struct {
    zend_value value;
    union u1;
    union u2;
};
  • 存储变量需要考虑两个要素:值与类型。

变量值的存放

  • 在PHP7中,变量的值存在zend_value 这个联合体中。只有整型和浮点型是直接存在zend_value中,其余类型都只存放了一个指向专门存放该类型的结构体指针。这个联合体共占用8字节。
typedef union _zend_value {
    zend_long         lval;    //整型
    double            dval;    //浮点
    zend_refcounted  *counted; //引用计数
    zend_string      *str; //字符串
    zend_array       *arr; //数组
    zend_object      *obj; //对象
    zend_resource    *res; //资源
    zend_reference   *ref; //引用
    zend_ast_ref     *ast; //抽象语法树
    zval             *zv;  //内部使用
    void             *ptr; //不确定类型,取出来之后强转
    zend_class_entry *ce;  //类
    zend_function    *func;//函数
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww; //这个union一共8B,这个结构体每个字段都是4B,因为所有联合体字段共用一块内存,故相当于取了一半的union
} zend_value;

变量类型的存放

  • 在PHP7中,其变量的类型存放在zval中的u1联合体中:
...
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,     /* 在这里用unsigned char存放PHP变量值的类型 */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)        /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
...
  • PHP7中所有的变量类型:
/* regular data types */
#define IS_UNDEF                    0
#define IS_NULL                        1
#define IS_FALSE                    2
#define IS_TRUE                        3
#define IS_LONG                        4
#define IS_DOUBLE                    5
#define IS_STRING                    6
#define IS_ARRAY                    7
#define IS_OBJECT                    8
#define IS_RESOURCE                    9
#define IS_REFERENCE                10

/* constant expressions */
#define IS_CONSTANT                    11
#define IS_CONSTANT_AST                12

/* fake types */
#define _IS_BOOL                    13
#define IS_CALLABLE                    14
#define IS_ITERABLE                    19
#define IS_VOID                        18

/* internal types */
#define IS_INDIRECT                 15
#define IS_PTR                        17
#define _IS_ERROR                    20

PHP7中的字符串

字符串基本结构

  • 设计字符串存储的数据结构两大要素:字符串值和长度。
  • PHP7字符串存储结构的设计:
struct _zend_string {
    zend_refcounted_h gc;         /*引用计数,与垃圾回收相关,暂不展开*/
    zend_ulong        h;          /* 冗余的hash值,计算数组key的哈希值时避免重复计算*/
    size_t            len;        /* 长度 */
    char              val[1];     /* 柔性数组,真正存放字符串值 */
};
  • 由为什么存长度引申出二进制安全的问题。二进制安全:写入的数据和读出来的数据完全相同,就是二进制安全的,详情见PHP字符串笔记

字符串写时复制

  • 看下面一段PHP代码:
<?php
$a = "string" . time("Y-m-d");
echo $a;
$b = $a;
echo $a;
echo $b;
$b = "new string";
echo $a;
echo $b;
  • 利用gdb调试这段代码,观察其引用计数情况。
  • 在第一个echo语句处打断点,并查看$a中zend_stinrg中的引用计数gc.refcount = 1(下简称refcount)。因为现在只有一个$a引用zend_string。

  • 利用gdb的c命令继续运行下一行PHP代码$b = $a,然后观察$a的zend_sting,我们发现$a引用的zend_string的refcount变为2:

  • 查看此时的$b,发现引用的zend_string的refcount也是2,且地址均是0x7ffff5e6b0f0,说明$a与$b所引用的是同一个zend_string。

  • 此时的内存结构如图所示:

  • 这样做的优点就是仅仅需要1个zend_string就可以存储两个PHP变量的值,而不是2个zend_string,节省了1个zend_string的内存空间。
  • 那么我们看接下来$b = "new string",这样的话,$a和$b由于存储的内容不同,故不可以继续引用同一个zend_string,这时就会发生写时复制。我们继续gdb调试,看一下是否符合预期:
  • 给$b赋值后,观察$a的存储情况:

  • 我们看到,此时$a所指向的zend_string的refcount变为了1,接下来再看一下$b的存储情况:

  • 注意此时$b所指向的zend_string的refcount变为了0(注意这里为什么是0而不是1呢?下面会讲),而且b指向的zend_string的地址为0x7ffff5e6a5c8,与$a所指向的zend_string的地址0x7ffff5e6b0f0不同,说明发生了写时复制,即由于字符串值的改变,被迫生成了一个新的zend_string结构体,用来专门存储$b的值;而$a指向的zend_string只是refcount减少了1,其余并未发生变化。
  • 那么为什么$b所指向的zend_string的refcount是0呢,我们先给PHP中的字符串分个类:

    • 常量字符串:在PHP代码中硬编码的字符串,在编译阶段初始化,存储在全局变量表中,refcount一直为0,其在请求结束之后才被销毁(方便重复利用)。
    • 临时字符串:计算出来的临时字符串,是执行阶段经过zend虚拟机执行opcode计算出来的字符串,存储在临时变量区。
  • 我们举一个例子:
<?php
$a = "hello" . time("Y-m-d"); //临时字符串,因为time()会随时间变化而变化
$b = "hello world";           //常量字符串
  • 这里$a由于调用了time()函数,所以最终的值是不确定的,是临时字符串。
  • $b也可以叫做字面量,是被硬编码在PHP代码中的,是常量字符串。
  • 我们画一下最终$a与$b的内存结构图:

  • 由此我们可以清晰地看到,$a与$b不在引用同一个zend_string。那么我们给写时复制下一个定义:给$b重新赋值而导致不能与$a共用一个zend_string的现象,叫做写时复制。

PHP7中的数组

  • PHP7中的数组是一个hashtable,key-value对存储于bucket中。

  • PHP7数组基本结构:
struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    consistency)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask;       //数组大小减一,用来做或运算,packed array初始值是-2,hash array初始值是-8
    Bucket            *arData;          //指针,指向实际存储数组元素的bucket
    uint32_t          nNumUsed;         //使用了多少bucket,但是unset的时候这个值不减少
    uint32_t          nNumOfElements;   //真正有多少元素,unset的时候会减少
    uint32_t          nTableSize;       //bucket的个数
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement; //支持$arr[] = 1;语法,没插入1个元素就会递增1
    dtor_func_t       pDestructor;
};

typedef struct _zend_array HashTable;
  • 此结构在内存中的结构图如下:

  • 思考:为什么要存储gc字段?因为gc字段冗余存储了变量的类型,给任意一个变量,把它强转成zend_refcounted_h类型,都可以拿到它的类型,zend_refcounted_h类型结构如下:
typedef struct _zend_refcounted_h {
    uint32_t         refcount;            /* 引用计数 */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;
  • 进行强制类型转换之后,通过取该变量的u.type字段,就可以拿到当前变量的类型了。
  • 我们接着看一下bucket的结构:
typedef struct _Bucket {
    zval              val;   //元素的值,注意这里直接存了zval而不是一个zval指针
    zend_ulong        h;     //冗余的哈希值,避免重复计算哈希值
    zend_string      *key;   //元素的key值,指向一个zend_string结构体
} Bucket;
  • 思考如果利用$arr[] = 1;语法进行数组赋值,key字段的值是多少?答案是0x0,就是一个空指针。
  • hashtable的问题:哈希冲突,解决冲突的方法有开放定制法和链地址法,常用的是链地址法。
  • PHP7中并没有采用真正的链表结构,而是利用数组模拟链表。这个时候需要在Bucket数组之前额外开辟一段内存空间(叫做索引数组,每个索引数组的单元叫一个slot),来存储同一hash值的第一个bucket的索引下标。
  • 看一个简单的数组查找过程:

    • 经过time33哈希算法算出哈希值h
    • 计算出索引数组的nIndex = h | nTableMask = -7(假设),这个nIndex也别称做slot
    • 访问索引数组,取出索引为-7位置上的元素值为3
    • 访问bucket数组,取出索引为3位置上的key,为x,发现并不等于s,那么继续查找,访问val.u2.next指针,为2
    • 取出索引为2位置上的key,为s,发现正好是我们要找的那个key
    • 取出对应的val值3
  • 注意如果bucket的存储空间满了,需要重新计算和nIndex(即slot)的值并将值放到正确的bucket位置上,这个过程也叫做rehash。
  • 具体的插入过程详见PHP基本变量笔记的文章末尾。
  • PHP7中的数组分为两种:packed array与hash array。

    • packed array:

      • key是数字,且顺序递增
      • 位置固定,如访问key是0的元素,即$arr1[0],就直接访问bucket数组的第0个位置即可(即arData[0]),这样就不需要前面的索引数组。
    • 如果不满足上述条件,就是hash array

NoSay
449 声望544 粉丝