1

baiyan

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

源视频地址: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。

NoSay
449 声望544 粉丝