【PHP7源码学习】2019-03-18 复习前面的内容

baiyanphp

所有视频:https://segmentfault.com/a/11...redis

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

本笔记中部分图片截自视频中的片断,图片版权归视频原做者全部。express

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紧邻的状况)
  • 若是不想对齐,有以下解决方案:segmentfault

    • 编译的时候不加优化参数
    • 代码层面:在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
相关文章
相关标签/搜索