Linux|malloc 内存分配位置及进程内存布局

进程内存分布
典型的 Linux 进程内存分布图,图片来自这里:
Linux|malloc 内存分配位置及进程内存布局
文章图片

这张图中有映射段的位置,但是还有一个重要的部分的缺失,就是运行时的参数和环境变量,在 Linux/Unix 系统编程手册这本书第 6 章讲进程的内存分配里有给:
Linux|malloc 内存分配位置及进程内存布局
文章图片

malloc 内存分配在映射段
malloc 申请分配的内存过大(128K以上),内部将使用 mmap 而不是 brk 来分配内存。
测试代码:

printf("program break:%p\n", sbrk(0)); char* ptr[10]; for(int i = 0; i < 10; ++i) { ptr[i] = (char* )malloc(200 * 1024); printf("malloc:%p program_break_%d:%p\n", ptr[i], i, sbrk(0)); }printf("stack:%p %p\n", &ptr, ptr); while(1) { sleep(3); }

输出如下图:
Linux|malloc 内存分配位置及进程内存布局
文章图片

显而易见的几点:
  • 程序 program break 的位置没变,说明分配的内存不在堆上;而且 malloc 分配的地址与 program break 的地址差异巨大,应该在不同的区。
  • 不在堆上,而且离栈相对较近,且此时 malloc 分配的内存在栈的下方。
查看 /proc/pid/maps 目录下,获取动态库的映射地址。
【Linux|malloc 内存分配位置及进程内存布局】Linux|malloc 内存分配位置及进程内存布局
文章图片

关于 maps 里同一个动态库映射多次的原因的解释看这里。
用 python 快速的给分配出的内存地址和这里的 maps 的动态库地址作个排序:
a = [ ('0x7f423e4ad000', "/usr/lib64/libc-2.28.so"), ('0x7f423e666000', "/usr/lib64/libc-2.28.so"), ('0x7f423e865000', "/usr/lib64/libc-2.28.so"), ('0x7f423e869000', "/usr/lib64/libc-2.28.so"), ('0x7f423e86f000', "/usr/lib64/ld-2.28.so"), ('0x7f423ea97000', "/usr/lib64/ld-2.28.so"), ('0x7f423ea98000', "/usr/lib64/ld-2.28.so")] #动态库的地址b = [ ('0x7f423ea4c010', "program_break_0:0x2482000"), ('0x7f423ea19010', "program_break_1:0x2482000"), ('0x7f423e9e6010', "program_break_2:0x2482000"), ('0x7f423e9b3010', "program_break_3:0x2482000"), ('0x7f423e980010', "program_break_4:0x2482000"), ('0x7f423e94d010', "program_break_5:0x2482000"), ('0x7f423e91a010', "program_break_6:0x2482000"), ('0x7f423e8e7010', "program_break_7:0x2482000"), ('0x7f423e8b4010', "program_break_8:0x2482000"), ('0x7f423e47a010', "program_break_9:0x2482000")] #malloc 内存分配的地址c = a + b c.sort(key = lambda x : int(x[0], base = 16))

地址升序排列:
Linux|malloc 内存分配位置及进程内存布局
文章图片

可以看到 malloc 分配的内存区域与动态库的地址是交织在一起的。Linux/Unix 系统编程手册 49.7 章节有描述:
glibc 中的 malloc()实现使用 MAP_PRIVATE 匿名映射来分配大小大于 MMAP_
THRESHOLD 字节的内存块;MMAP_THRESHOLD 在默认情况下是 128 kB
符合这里分配 200k 时,直接将内存分配在动态区。
malloc 内存分配在堆上
如果分配的内存是100k(100 * 1024),程序的输出如下,可以看到 program break 的位置有变化,说明是在堆上分配的内存。
program break:0x663000
malloc:0x642900 program_break_0:0x663000
malloc:0x65b910 program_break_1:0x695000
malloc:0x674920 program_break_2:0x695000
malloc:0x68d930 program_break_3:0x6c7000
malloc:0x6a6940 program_break_4:0x6c7000
malloc:0x6bf950 program_break_5:0x6f9000
malloc:0x6d8960 program_break_6:0x6f9000
malloc:0x6f1970 program_break_7:0x72b000
malloc:0x70a980 program_break_8:0x72b000
malloc:0x723990 program_break_9:0x75d000
stack:0x7fff79915ac0 0x7fff79915ac0
两次分配内存之间的地址差值是 102416(0x65b910 - 0x642900 = 102416 = 100k + 16),100k 返回给 malloc 的调用者,多余的 16 字节用来保存长度相关信息。
总结
  • 当调用者请求的内存超过临界值,malloc 分配的内存是进程的映射区,和动态库的内存占用相同区域;小于临界值时,分配的内存才是在堆上。
  • malloc内存分配在堆上时,实际会多分配几个字节(位于实际返回给调用者的地址之前),用来记录分配的内存的长度;这样 free 才能正确的释放内存。
  • 即便返回的内存是在堆上,malloc 也可能并未调用 brk 来改变program break 的位置:其一是因为program break下边本来就有部分未使用的空闲内存(malloc 调用 brk 会超额分配内存,多余的至于堆顶);其二在于 free 所释放的内存保存在空闲内存列表里,malloc 会首先检查这个列表中是否有合适的内存块(2种策略 first-fit 和 best-fit),之后才会去调用 brk 分配更多的堆内存。

    推荐阅读