本文主要分为两部分:探究malloc执行背后涉及的系统调用,以及分配大内存和小内存的执行差别。在此之前先说明一些关于malloc的“传闻”:通过brk系统调用控制heap的增长,自身负责管理一些内存块,对于可以满足的分配需求,可以在用户态下直接分配已有的空闲内存块完成;对于大内存需求,调用mmap系统调用;第二部分是关于现在使用的较多的malloc库的设计和实现思想,包括ptmalloc和tcmalloc。
malloc
我们通过观察一个例子中malloc的行为,探究malloc的行为。
a simple example
使用一个简单的示例程序,程序分别调用malloc分配小块内存(1KB)和大块内存(1GB)
1 |
|
同时,我们通过查看进程的内存映射情况,确定heap的范围,以及小内存和大内存分别处于哪块内存区域。在程序中插入了getchar(),方便在分配内存前、分配内存后、释放内存后分别查看内存映射的情况。我们将三个阶段的映射数据分别保存在before、alloc和free的文件中,并通过对比三个文件的差异,可得知malloc和free对内存映射的影响。
第一步,设置MEM_SIZE为1<<10,与小内存大小相同,即均为1KB时,编译并运行main,由于getchar()的存在,程序会有一些停顿(以下程序的输出做了些许调整,与正常输出不同)
1 | > gcc main.c -g -o main |
在程序停顿时,另开一窗口,每次停顿后分别执行
1 | > cat /proc/153825/maps > Templates/before |
使用diff命令检查三个文件的不同
1 | > diff before alloc |
使用strace跟踪mmap的调用情况
1 | mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fbb000 |
在输出”I am …”信息后,并未调用mmap系统调用。
三个文件内容相同,且未调用mmap。malloc分配小内存时,进程的内存映射没有变化,查看三个文件中任一一个的内容:
1 | > cat before |
heap的范围为555555559000-55555557a000,而p和q的地址分别为0x555555559ac0和0x555555559ed0,均位于其中。因此,malloc会管理一个预先分配好的heap,如果请求分配的内存可以满足,则直接分配即可。
设置MEM_SIZE为1<<30,即1GB,显然,上述heap的大小为0x7a000 - 0x59000 = 0x21000,远小于MEM_SIZE。
重新编译并执行
1 | > gcc main.c -g -o main |
使用diff查看三个文件的差异
1 | > diff before alloc |
可见,在执行malloc之前和执行free后的文件内容是相同的。而执行malloc后,内存空间的差异仅有一处:有一块内存区域的从7ffff7d7e000-7ffff7d81000变为了7fffb7d7d000-7ffff7d81000。即起始处从0x7ffff7d7e000变味了0x7fffb7d7d000,p(大内存)的地址为0x7fffb7d7d010,恰好在该内存区域的起始处偏移0x10的位置,而q(小内存)的地址为0x555555559ac0,仍位于heap中。
执行strace跟踪mmap和munmap的调用情况
1 | > strace -e mmap ./main |
可见,在malloc申请大内存后执行了mmap,返回的地址为 0x7fffb7d7d000,而free后,调用了munmap(0x7fffb7d7d000, 1073745920)
summary
malloc自身维护了一块地址较低的内存,对于小块的内存请求,malloc从自身管理的空闲内存块中分配;对于大块内存,通过调用mmap和munmap分配和释放。
malloc实例
总结几个常用的malloc库的实现思想和机制:
- glibc中的ptmalloc
- Google的tcmalloc
- Facebook的jemalloc