共计 3093 个字符,预计需要花费 8 分钟才能阅读完成。
2019-03-08 PHP 内存管理 2 笔记
baiyan
源视频地址:http://replay.xesv5.com/ll/24…
复习
宏的用法:
typedef struct _zend_alloc_globals {
zend_mm_heap *mm_heap;
} zend_alloc_globals;
…
# define AG(v) (alloc_globals.v)
static zend_alloc_globals alloc_globals;
这个带有参数的宏取得 zend_alloc_globals 结构体类型里的 zend_mm_heap 结构体字段,如 AG(mm_heap) = alloc_globals.mm_heap
宏就是替换。
结构体与结构体内存对齐
结构体
先看一段结构体代码 struct.c:
#include <stdio.h>
int main() {
struct a{
char a;
int b;
long c;
void *d;
int e;
char *f;
}s;
s.a = ‘c’;
s.b = 1;
s.c = 22l;
s.d = NULL;
s.e = 1;
s.f = &s.a;
printf(“sizeof struct a is %d”, (int)sizeof(s));
}
编译:gcc -g struct.c -o struct
运行:./struct,sizeof(a) 的打印结果为:40
对这个结构体进行 gdb 调试:
在这里我们可以看到第一行是所有结构体变量的初始值,注意指针变量是一个随机的地址,在给 s.d 赋值的过程中,地址变成了 0x0,它是一个特殊的地址值,代表 NULL。
除此之外,我们注意到结构体 s 的地址和 a 变量的地址是相同的。
用表达式表示以上结论:&s = &s.a = f 或者 s = s.a = *f
结构体内存对齐
核心结论:它是编译器做的优化,空间换时间的思想。
对齐方式:按照结构体所有字段的最小公倍数做对齐(最小公倍数是 8B 就按 8B 对齐;是 4B 就按 4B 对齐),且与结构体字段的排列顺序是相关。如图:
我们利用 gdb 验证一下以上的结论,还是利用上述同样的代码:
我们看到变量 a 的起始地址是 150,而 b 的地址是 154,显然做了对齐。如果不对齐,b 的地址应该为 151,说明 a 和 b 中间空了 3B 的大小。c 的地址是 158,就是下一个 8B 的起始地址,而 d 的地址是 160(注意这里是 16 进制,158+8 = 160 的时候才会进位),证明了 c 占用了 8B,下面 d 变量也同理占用了 8B。注意 e 变量,它是一个 int,如果不对齐应该只占用 4B。而它占用了 170(f 的起始地址)- 168(e 的起始地址)= 8B,所以一定是做了内存对齐的。
注意一个特例:如果 b 是一个 char 类型,那么是直接紧跟在上一个 char 后面,如图:
注意这里的地址均为逻辑地址,每次编译后的逻辑地址是相同的,而物理地址是不同的。
注意:如果调换顺序,把 b 和 c 调换位置,就会变成 8B(a)+8B(c)+8B(b)+8B(d)+8B(e)+8B(f) = 48B
注意:一定是所有字段的最小公倍数是几字节,就按几字节对齐,我们看一下结构体中只有 char 类型变量的情况:
#include <stdio.h>
int main() {
struct a{
char a;
char b;
char c;
}s;
s.a = ‘c’;
s.b = ‘b’;
s.c = ‘a’;
printf(“sizeof struct a is %d\n”, (int)sizeof(s));
}
这个结构体中只有 char 类型变量
编译运行,输出 sizeof(s) 的结果为:3
为什么不是 4 或者 8?因为 1,1,1 的最小公倍数就是 1,就按照 1B 来对齐,而不是 4B、8B
同理,如果都是 int 类型的变量,那么 sizeof(s) 的结果为 12,也很好理解了
联合体
核心结论:所有联合体字段共用一块内存空间。整个联合体占用的空是所有字段单独占用空间大小中取占用空间大小最大的字段。
同样先看一段代码:
#include <stdio.h>
int main() {
union a{
char a;
int b;
long c;
void *d;
int e;
char *f;
}s;
s.a = ‘c’;
s.b = 1;
s.c = 22l;
s.d = NULL;
s.e = 1;
s.f = &s.a;
printf(“sizeof struct a is %d”, (int)sizeof(s));
}
这段代码与上段结构体的代码只将 struct 修改为 union,其他均不变
编译运行,输出 sizeof(a) 的结果为:8
我们利用 gdb 调试一下这段代码:
我们可以看到,后面的变量一赋值,就会覆盖前面的变量值
再看一下每个变量的地址,我们可以清楚地看到,所有变量的起始地址都是一样的。
其他
思考:一个 void * 类型的变量,能否直接取它的内容?答:不可以,其他有类型的指针变量可以取内容是因为记录了当前类型的长度,而 void * 类型没有长度,无法直接取,除非使用强制类型转换或者指定长度。
延伸:PHP 所有变量基于 zval,zval 就是由 3 个联合体组成(zend_value,u1,u2)这里不展开
大小端:
大端:也叫高尾端,即数据尾端(低位)放在高地址
小端:也叫低尾端,即数据尾端(低位)放在低地址
网络字节序是大端的,所以小端机器需要对收到或发出的数据包进行大小端的转换
如何判断是大端还是小端:参考:判断机器大小端的两种实现方式
利用指针:int 为 4 个字节,通过强转成 char 类型,取低 1 个字节,如果恰好是 0x78,那就说明低 1 个字节恰好存在低地址,为小端。如果取低 1 个字节的结果是 0x12,低 1 个字节存在高地址,为大端。
int main() {
int i = 0x12345678;
if (*(char*)&i == 78) {
printf(“ 小端 ”);
} else {
printf(“ 大端 ”);
}
}
利用联合体:本质思想和指针方法相同,利用了联合体共用同一块存储空间的特性。
int main() {
union w{
int a;
char b;
}c;
c.a = 1;
if (c.b == 1) {
printf(“ 小端 ”);
} else {
printf(“ 大端 ”);
}
}
利用 gdb 观察 PHP 内存分配时的情况,示例代码:
<?php
$a = 1;
echo $a;
gdb php 并在_emalloc() 处打断点,运行代码:
我们可以清晰地看到 mm_heap 这个变量,它在 small、large 内存中存储一些额外的 page 分配信息等等。其中比较重要的 size、peak、free_slot、main_chunk 等变量。free_slot 数组暂时还是空。再看 mm_heap 中的 main_chunk 字段,它来表示一个 chunk,是一个双向链表。第一个字段 heap 是一个指向 zend_mm_heap 的指针,可以快速找到第一个记录信息的 page,并且可以发现它的地址和上面直接打印 alloc_globals.mm_heap 的地址是相同的。再观察 free_map 字段,它由 8 个个 uint64 类型组成,代表各个 page 的使用情况,这里第 1 个 page 存了 zend_mm_heap 结构体,已经被使用。再看 map 字段,它是一个数组,大小为 512,每个都是 uint32 类型,打印数组第一项的值,以 16 进制表示为 0x40000001,代表 large 内存,最后一位是 1,代表分配 1 个 page。
使用 c 命令继续运行:
我们可以看到 free_slot 上有 3 项已经不是 0x0 了,说明上次分配过 small 内存。且现在 free_map 的值发生了变化,而 map 说明第 1、2、3、4、9 页已经被使用了,那么为什么中间有几个 0 呢,是因为第 4 个 map 值,按照 16 进制打印为 0x40000005,是 large 内存,且需要分配 5 个页,所以后面的 4 个页都是 0。