详细了解malloc()
什么是malloc()
了解malloc()
在头文件<cstdlib>
和<stdlib.h>
中的定义是 void* malloc(std::size_t size)
和 void* malloc(size_t size)
malloc()
负责分配size
字节的未初始化内存
若分配成功,则返回 指向分配的 (可以对任何标量对齐) 的内存块中 最低字节的指针
若size为0,则这个行为是实现定义的,也就是其行为并不是完全由 C 语言标准定义的。这意味着不同的编译器和不同的操作系统可能会以不同的方式处理这种情况。一些实现可能会返回一个空指针(NULL
),因为它们认为请求零字节内存是没有意义的。而其他实现可能会返回一个非空指针。这个非空指针通常指向一个实际不包含任何用户可用内存的空间,这个空间可能非常小,甚至可能无法安全地被访问或修改。
即使 malloc
返回了一个非空指针,你也不应该对这个指针进行解引用操作(即试图访问或修改它所指向的内存),因为这种行为可能会导致未定义的行为,比如程序崩溃。然而,即使返回了一个非空指针,当你不再需要这段内存时,你应该将这个指针传递给 free
函数。这样做是为了避免内存泄漏,即使这个指针并没有真正指向任何有用的内存。如果不调用 free
,一些系统可能会认为这部分内存仍然被程序使用,从而随着时间的推移导致内存耗尽。
什么是对齐?
每个完整对象类型拥有一个称作对齐要求的属性,它是一个 size_t
类型的整数值,表示此类型对象可以分配的相继地址之间的字节数。合法的对齐值是二的非负数次幂。
对齐要求
每个数据类型都有一个对齐要求(alignment requirement),这是指该类型对象在内存中分配时地址应该遵守的规则。对齐要求通常是为了提高内存访问的效率,特别是在硬件层面上。许多处理器在访问未对齐的数据时会有性能损失,甚至可能不支持未对齐访问。
对齐要求是一个 size_t
类型的整数值,表示此类型对象可以分配的相继地址之间的字节数。这个值必须是二的非负数次幂,即 2^0, 2^1, 2^2, 2^3, … 等等。常见的对齐值包括 1, 2, 4, 8, 16, 32, 64 字节等。
假设一个类型的对齐要求是 4 字节,那么当你在内存中分配这个类型的对象时,对象的地址应该是一个 4 的倍数。如果一个对象的地址是 0x1000,那么下一个同类型对象的地址应该是 0x1004,而不是 0x1001 或 0x1002。
所以声明数据结构时,不要把小成员参杂声明在字节对齐的数据之间,小成员组合在一起,能省去一些浪费的空间。
对象类型
对象类型(object type)是指数据类型的实例,它表示了一个具体的实体,可以是变量、函数、数组、类实例等。
如下面代码所示,结构体的总大小为 5 字节(4 字节的 int
加上 1 字节的 char
)
满足结构体的对齐要求,编译器会在 c
后面添加 3 字节的填充(padding),使得结构体的总大小成为 8 字节,这样结构体的结束地址也是一个 4 字节的边界。
#include <stdalign.h>
#include <stdio.h>
// struct S 的对象可以分配于任何地址
// 因为 S.a 和 S.b 可以分配于任何地址
struct S
{
char a; // 成员对象大小:1,对齐:1
char b; // 成员对象大小:1,对齐:1
}; // 结构体对象大小:2,对齐:1
// struct X 的对象必须分配于 4字节边界
// 因为 X.n 必须分配于 4 字节边界
// 因为 int 的对齐要求(通常)是 4
struct X
{
int n; // 成员对象大小:4,对齐:4
char c; // 成员对象大小:1,对齐:1
// 剩余的三个字节进行空位填充
}; // 结构体对象大小:8,对齐:4
int main(void)
{
printf("sizeof(struct S) = %zu\n", sizeof(struct S));
printf("alignof(struct S) = %zu\n", alignof(struct S));
printf("sizeof(struct X) = %zu\n", sizeof(struct X));
printf("alignof(struct X) = %zu\n", alignof(struct X));
}
sizeof(struct S) = 2
alignof(struct S) = 1
sizeof(struct X) = 8
alignof(struct X) = 4
malloc()的原理
- 当所需内存小于128kb时,调用
brk()
通过系统调用从堆分配内存,其实就是将堆顶指针_edata向高地址移动,获得新的内存,这一步其实是要了更大的内存,剩下的当做内存池来管理,下次malloc()
时,会先从内存池的空闲链表中寻找合适的值。而且通过brk()
分配的内存,在free()
的时候不会被回收,而是放回内存池里。 - 当大于128kb时,调用
mmap()
通过系统调用,在文件映射区(堆和栈中间)分配内存。这个分配的内存,在free的时候会把内存归还给操作系统。
而且呢malloc分配的是虚拟内存,只有在访问这块虚拟内存的时候,才会通过缺页中断映射到物理内存。
使用 brk()分配
进程调用A=malloc(30K)以后,内容空间如图上所示
malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配
_edata+30K只是完成虚拟地址(注意只是虚拟地址)的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。
使用 mmap() 分配
默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存
这样子做主要是因为:
brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,因为只有一个_edata 指针,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放
- 进程调用free(C)以后,C对应的虚拟内存和物理内存一起释放
- 进程调用free(B)以后,B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了
- 进程调用free(D)以后,B和D连接起来,变成一块140K的空闲内存
默认情况下:
当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示