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动态申请内存块时,一定要进行返回值的判断。
结构体与联合体
结构体
- 在PHP内存管理2笔记中,我们谈到了一种特殊情况:
- 在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)
};
- 关于C语言宏定义中的##等特殊符号的用法,参考:#define宏定义中的#,##,@#, 这些符号的神奇用法
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
-
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。