16 malloc 虚拟内存分配的调试
Posted 蓝风9
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了16 malloc 虚拟内存分配的调试相关的知识,希望对你有一定的参考价值。
前言
呵呵 在 c 语言中 malloc 应该是初学者必须了解的一个函数了吧
但凡 涉及到堆内存分配的相关, 必定会使用到 malloc, realloc, calloc 这几个函数
其中 malloc 最常见, 也是最 实用
在 HotspotVM 中也经常会看到 malloc 的身影
我们这里 来调试一下 malloc 的相关的一些场景
本文主要的主题有四个
1. malloc(20) 申请内存, 然后 brk 传递的参数是 132K
2. malloc 第一次内存分配 和 第二次内存分配
3. malloc 分配的内存的 header
4. malloc 20, 但实际可用的空间只有 16
测试用例
测试用例如下, 很简单的一个 case, 主要的目的在于使用 malloc, 以及观察 malloc 分配的内存的虚拟地址信息
root@ubuntu:~/ClionWorkStations/HelloWorld# cat Test01Sum.c
#include "stdio.h"
int main(int argc, char** argv)
int x = 2;
int y = 3;
int z = x + y;
void *p1 = malloc(20);
void *p2 = malloc(20);
void *p3 = malloc(20);
printf("p1 : 0x%x\\n", p1);
printf("p2 : 0x%x\\n", p2);
printf("p3 : 0x%x\\n", p3);
printf(" x + y = %d\\n ", z);
编译使用我们自己手动编译的 glibc 库
export mainClass=Test01Sum
gcc $mainClass.c -o $mainClass -L /root/Desktop/linux/glibc-2.23/install/lib -Wl,--rpath=/root/Desktop/linux/glibc-2.23/install/lib -Wl,-I /root/Desktop/linux/glibc-2.23/install/lib/ld-linux-x86-64.so.2
程序输出结果如下
p1 : 0x602010
p2 : 0x602030
p3 : 0x602050
x + y = 5
1. malloc(20) 申请内存, 然后 brk 传递的参数是 132K
如上测试用例, malloc 20 申请内存的时候, 会使用系统调用 brk 来进行虚拟内存分配
但是 奇怪的是分配的内存是 132k, 我命名只需要 20字节, 为什么 malloc 调用 brk 却申请了 132k 的虚拟内存, 这是为什么呢?
如下图, 查看 brk 系统调用, 申请的 135168 字节, 合计 132k 的虚拟内存
接下来我们看一下 malloc 是怎么做的? 这个 132k 又是怎么来的呢?
大致看一下 上下文, 是在 main 中的第一个 malloc(20) 的地方调用了 brk 系统调用
看一下 increment, 果然是 135168
然后我们再来看一下 这个 135168 是怎么计算出来的
这里有两个过程, 计算加对齐, 计算阶段为 32 + 131072 + 32 = 131136
然后和 4096 对齐一下, 之后的结果为 135168
然后另外一个问题是, 用户程序 申请的是 20 字节, 为什么实际申请的是 32 字节呢
如下 checked_request2size 就是做的这个转换
checked_request2size 的计算方式如下
代入 req 为 20, (20 + 8 + 15) & (0xfff0) = 32
整个表达式的意思是找到比 (20 + 8) 大的最小的 16 的整数倍
注意这里的 8, 额外只是加了一个 SIZE_SZ
2. malloc 第一次内存分配 和 第二次内存分配
malloc 的分配流程为 fastbin, small_bin, unsorted_chunks, 迭代 bins, top
在 Test01Sum 进入第一个 malloc 的时候均无可用空间, 直接到 use_top
此时 av->top 无可用空间, 调用 sysmalloc 申请空间
sysmalloc 中调用 brk 申请 所需要的 135168 字节的空间
然后之后是 申请需要的 32 字节 空间
p / av->top 初始的值为 0x602000, 分配了 32 字节之后 remainder / av-> top 为 0x602020
分别更新申请的 chunk 块的 size, 和 remainder 的 size
然后 chunk2mem 的转换, 增加了 2 * SIZE_SZ = 16, 值为 0x602010
分配的 chunk 的地址 + 两个头的大小[prev_size, size] 即为响应给用户的地址
第二次 malloc 的内存分配, 同上面 其他 fast_bin, small_bin 均无可用空间, 到 use_top
第一次内存分配之后, av->top 为 0x602020, 用户需要空间为 32 字节
因此 切割一个 32 byte 的 chunk 块 p 之后, av->top 为 0x602040
分别更新申请的 chunk 块的 size, 和 remainder 的 size
p 的值为 0x602020, 然后 chunk2mem 的转换, 增加了 2 * SIZE_SZ = 16, 值为 0x602030
至于第三次内存分配, 就和 第二次差不多了, 然后 我们再来回顾一下 程序的输出
p1 : 0x602010
p2 : 0x602030
p3 : 0x602050
x + y = 5
3. malloc 分配的内存的 header
看一下我们的 malloc_chunk, 头部有 2 * 8 个字节, 分别的逻辑意义为 prev_size, size
在上面的流程中我们没有看到 prev_size 的使用, 呵呵 这个我们后续再来记录
如果 前一个 chunk 空闲, 则 prev_size 记录的事该 chunk 的空间大小, 如果前一个 chunk 在使用, 则 prev_size 可以被前一个 chunk 使用
size 记录的数据如下
其中 PREV_INUSE 为 1, NON_MAIN_AREA 为 4, 这里可以知道最后 1bit 标记的是 PREV_INUSE, 倒数第三bit 标记的是 NON_MAIN_ARENA
倒数第二 bit 标记的是, 内存块是使用的 brk 还是 mmap 来分配的空间
然后 mask 掉后面 3bit, 的大小即为当前 chunk 的空间大小
第一次 malloc 分配的 chunk, chunk 的起始地址为 0x602000, 给用户的起始地址为 0x602010
0x602000 起始的的 long 值为 0, 默认第一个 chunk 的前一个 chunk 在使用, prev_size 没有作用
0x602008 起始的的 long 值为 chunk 的 size, 为 0x21, 表示了当前 chunk 空间是 32byte, MAIN_ARENA, 系统调用 brk 分配的内存
第二次 malloc 分配的 chunk, chunk 的起始地址为 0x602020, 给用户的起始地址为 0x602030
0x602020 起始的的 long 值为 0, 前一个 chunk 在使用, prev_size 没有作用
0x602028 起始的的 long 值为 chunk 的 size, 为 0x21, 表示了当前 chunk 空间是 32byte, MAIN_ARENA, 系统调用 brk 分配的内存
当然 这里也可以看到 上面 第一个 chunk 的情况
4. malloc 20, 但实际可用的空间只有 16
如果 您够仔细的话, 你会发现 用户申请的是 20 字节
然后 malloc 头, 占用的空间为 16 字节, 那么 至少应该申请 48 字节才对
但是 为什么只申请了 32 字节?
去掉头的 16 字节, 只有 16 字节了?? 这是 怎么回事
如果 前一个 chunk 空闲, 则 prev_size 记录的事该 chunk 的空间大小, 如果前一个 chunk 在使用, 则 prev_size 可以被前一个 chunk 使用
这个需要理解 prev_size 的使用了, 按照上面的反向推导一下
如果当前 chunk 正在使用, 那么 他是可以使用下一个 chunk 的 prev_size 的
因此 可以回顾一下上面这一段, 根据 用户申请的字节数, 计算需要申请的字节数 这一段, 计算的时候 只是额外增加了一个 SIZE_SZ, 因为当前 chunk 正在使用, 可以额外使用下一个 chunk 的 prev_size, 因此 这里的理解 和 上面的 checked_request2size
注意这里的 8, 额外只是加了一个 SIZE_SZ
5. malloc 0, 会怎么处理?
如果 bytes 为 0, 在 checked_request2size 的时候, malloc 会有最小分配空间的需求, 按照 MINSIZE 来进行分配空间, MINSIZE 为 32 字节
所以 得到的还是一块 32 字节的空间, 但是 至于你如何使用 并没有具体强制约束
checked_request2size 如下, 调整了 sz, 如果 小于 MINSIZE 设置 sz 为 MINSIZE
完
malloc 在分配内存时是不是保留更多空间?
【中文标题】malloc 在分配内存时是不是保留更多空间?【英文标题】:Does malloc reserve more space while allocating memory?malloc 在分配内存时是否保留更多空间? 【发布时间】:2019-08-13 04:00:31 【问题描述】:我在我的测试程序中观察到以下行为:
我正在为 1 MB 执行 malloc()
,然后在 sleep(10)
之后执行 free()
。我这样做了五次。我正在观察程序运行时top
的内存消耗。
一旦free()
-d,我预计程序的虚拟内存 (VIRT) 消耗将减少 1 MB。但实际上并非如此。它保持稳定。这种行为的解释是什么? malloc()
在分配内存时会做一些保留吗?
【问题讨论】:
相关:How do I free memory in C? Memory usage doesn't decrease when free() used的可能重复 @Useless 这个问题比旧问题有更好的答案,所以我打破惯例,将旧问题标记为这个问题的副本。 我认为几乎所有 malloc/free 实现都使用一些内部管理,这些管理确实请求更大的块并机会主义地释放它们。这可以使用brk(2)
或 mmap。这也意味着页面在被触摸之前可能实际上并没有被使用(有时甚至在空闲时未提交,因此虚拟或数据段大小并不那么重要)
【参考方案1】:
一旦
free()
-d,我预计程序的虚拟内存 (VIRT) 消耗将减少 1MB。
嗯,C 标准不能保证这一点。它只是说,一旦你free()
内存,你就不应该再访问它了。
内存块是实际返回到可用内存池还是保留以供将来分配由内存管理器决定。
【讨论】:
是否可以将 free() 的内存块释放回操作系统? @user1228352 不,C 语言不允许这样做。如果你想要更多的控制,你需要实现你自己的内存管理器,它依赖于平台特定的操作系统系统调用。 @user1228352 我理解这之后的感觉,让我们说诡计,但是 - 你真的不想走那条路,从长远来看也没有意义,因为它只是浪费是时候弄清楚如何制作自己的内存管理器(如果操作系统允许)并对其进行调试。遵循 C 标准,您将获得更舒适的体验,而操作系统则完成了它的目标。好吧,除非您的目标是制作自己的操作系统,否则您可能不会问这个问题。 @user1228352 为什么要这样做?虚拟内存实际上是免费的。 为什么要减少对非稀缺物品的不必要消耗?如果您想要一个有用的答案,您应该告诉我们更多关于您的环境的信息。一些不寻常的环境也有不寻常的malloc
和free
实现。如果你有一个真正的问题(这不仅仅是装饰性的),你可以用一个永远不会拥有任何额外虚拟内存的分配器来替换分配器,但是由于碎片等问题,它有大约 99% 的机会只会让事情变得更糟。跨度>
【参考方案2】:
C 标准不强制malloc
和free
的实现者直接将内存返回给操作系统。所以不同的 C 库实现会有不同的行为。他们中的一些人可能会直接还给它,而有些人可能不会。事实上,相同的实现也会因分配大小和模式而有所不同。
这种行为当然是有充分理由的:
-
这并不总是可能的。操作系统级别的内存分配通常在页面中完成(一次 4KB、4MB 或 ... 大小)。如果在释放另一部分后页面的一小部分仍在使用,则在该部分也被释放之前,该页面无法返回给操作系统。
效率。应用程序很可能会再次请求内存。那么为什么要把它还给操作系统并在不久之后再次要求它。 (当然,保留的内存大小可能有限制。)
在大多数情况下,如果实现决定保留它(假设它是一个很好的实现),那么您无需为 free
的内存负责。迟早它会被重新分配或返回给操作系统。因此,内存使用的优化应该基于你拥有malloc
-ed 而你没有free
-d 的数量。您必须担心的情况是,当您的分配模式/大小开始导致内存碎片时,这本身就是一个非常大的话题。
但是,如果您使用的是嵌入式系统并且可用内存量有限,并且您需要更多地控制何时/如何分配和释放内存,那么您需要直接从操作系统请求内存页面并进行管理手动。
编辑:我没有解释为什么你不对你空闲的内存负责。 原因是,在现代操作系统上,分配的内存是虚拟的。这意味着如果您在 32 位系统或 10TB 的 64 位系统上分配 512MB,只要您不读取或写入该内存,它就不会为它保留任何物理空间。实际上,它只会为您从那个大块而不是整个块中触摸的页面保留物理内存。在“一段时间不使用该内存”之后,其内容将被复制到磁盘,而底层物理内存将用于其他用途。
【讨论】:
请注意,某些分配器可能会通过使用操作系统特定的调用来避免将数据复制到磁盘的可能性,这些调用会说“这些页面没有被使用,所以请随意删除它们的内容,即使我不释放虚拟内存本身”。例如,在 Linux 上使用madvise
调用和 MADV_DONTNEED
。【参考方案3】:
这在很大程度上取决于实际使用的 malloc 实现。
在 Linux 下,有一个阈值 (MMAP_THRESHOLD
) 来决定给定 malloc()
请求的内存来自哪里。
如果请求的数量小于或等于MMAP_THRESHOLD
,则通过从所谓的“空闲列表”中取出它来满足请求,如果任何内存块已经是free()
d。否则,程序的"break line"(即数据段的结尾)会增加,并且此进程为程序提供的内存用于请求。
在free()
上,释放的内存块被添加到空闲列表中。如果在数据段的最后有足够的空闲内存,则再次移动断线(上面提到)以缩小数据段,将多余的内存返回给操作系统。
如果请求的数量超过MMAP_THRESHOLD
,操作系统会请求一个单独的内存块并在free()
期间再次返回。
详情请参阅https://linux.die.net/man/3/malloc。
【讨论】:
以上是关于16 malloc 虚拟内存分配的调试的主要内容,如果未能解决你的问题,请参考以下文章