linux 内存管理 ------ malloc 与 mmap 的关系
低于128K的内存分配采用具有内存池缓存机制的 brk 方式,可以减少缺页中断、系统调用的次数。高于128K时采用匿名内存映射区的mmap方式, 避免产生太大的内存碎片
如果分配后的虚拟内存没有被访问的话,虚拟内存是不会映射到物理内存的,这样就不会占用物理内存了。只有在访问已分配的虚拟地址空间的时候,操作系统通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系。
malloc(1) 会分配多大的虚拟内存?
malloc() 在分配内存的时候,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池。
具体会预分配多大的空间,跟 malloc 使用的内存管理器有关系,我们就以 malloc 默认的内存管理器(Ptmalloc2)来分析。
接下里,我们做个实验,用下面这个代码,通过 malloc 申请 1 字节的内存时,看看操作系统实际分配了多大的内存空间。
#include <stdio.h> #include <malloc.h> int main() { //申请1字节的内存 void *addr = malloc(1); printf("此1字节的内存起始地址:%x\n", addr); printf("使用cat /proc/%d/maps查看内存分配\n",getpid()); //将程序阻塞,当输入任意字符时才往下执行 getchar(); //释放内存 free(addr); printf("释放了1字节的内存,但heap堆并不会释放\n"); getchar(); return 0; }
[root@xiaolin ~]# cat /proc/3191/maps | grep d730 00d73000-00d94000 rw-p 00000000 00:00 0 [heap]
这个例子分配的内存小于 128 KB,所以是通过 brk() 系统调用向堆空间申请的内存,因此可以看到最右边有 [heap] 的标识。
可以看到,堆空间的内存地址范围是 00d73000-00d94000,这个范围大小是 132KB,也就说明了 malloc(1) 实际上预分配 132K 字节的内存。
可能有的同学注意到了,程序里打印的内存起始地址是 d73010
,而 maps 文件显示堆内存空间的起始地址是 d73000
,为什么会多出来 0x10
(16字节)呢?这个问题,我们先放着,后面会说。
free 释放内存,会归还给操作系统吗?
从下图可以看到,通过 free 释放内存后,堆内存还是存在的,并没有归还给操作系统。
这是因为与其把这 1 字节释放给操作系统,不如先缓存着放进 malloc 的内存池里,当进程再次申请 1 字节的内存时就可以直接复用,这样速度快了很多。
当然,当进程退出后,操作系统就会回收进程的所有资源。
上面说的 free 内存后堆内存还存在,是针对 malloc 通过 brk() 方式申请的内存的情况。
如果 malloc 通过 mmap 方式申请的内存,free 释放内存后就会归归还给操作系统。
我们做个实验验证下, 通过 malloc 申请 128 KB 字节的内存,来使得 malloc 通过 mmap 方式来分配内存。
#include <stdio.h> #include <malloc.h> int main() { //申请1字节的内存 void *addr = malloc(128*1024); printf("此128KB字节的内存起始地址:%x\n", addr); printf("使用cat /proc/%d/maps查看内存分配\n",getpid()); //将程序阻塞,当输入任意字符时才往下执行 getchar(); //释放内存 free(addr); printf("释放了128KB字节的内存,内存也归还给了操作系统\n"); getchar(); return 0; }
查看进程的内存的分布情况,可以发现最右边没有 [heap] 标志,说明是通过 mmap 以匿名映射的方式从文件映射区分配的匿名内存。
然后我们释放掉这个内存看看:
再次查看该 128 KB 内存的起始地址,可以发现已经不存在了,说明归还给了操作系统。
总结:
- malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用;
- malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放。
为什么不全部使用 mmap 来分配内存?
因为向操作系统申请内存,是要通过系统调用的,执行系统调用是要进入内核态的,然后在回到用户态,运行态的切换会耗费不少时间。
所以,申请内存的操作应该避免频繁的系统调用,如果都用 mmap 来分配内存,等于每次都要执行系统调用。
另外,因为 mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断。
也就是说,频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
为了改进这两个问题,malloc 通过 brk() 系统调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。
等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗。
既然 brk 那么牛逼,为什么不全部使用 brk 来分配?
前面我们提到通过 brk 从堆空间分配的内存,并不会归还给操作系统,那么我们那考虑这样一个场景。
如果我们连续申请了 10k,20k,30k 这三片内存,如果 10k 和 20k 这两片释放了,变为了空闲内存空间,如果下次申请的内存小于 30k,那么就可以重用这个空闲内存空间。
但是如果下次申请的内存大于 30k,没有可用的空闲内存空间,必须向 OS 申请,实际使用内存继续增大。
因此,随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。
所以,malloc 实现中,充分考虑了 brk 和 mmap 行为上的差异及优缺点,默认分配大块内存 (128KB) 才使用 mmap 分配内存空间。
free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?
还记得,我前面提到, malloc 返回给用户态的内存起始地址比进程的堆空间起始地址多了 16 字节吗?
这个多出来的 16 字节就是保存了该内存块的描述信息,比如有该内存块的大小。
这样当执行 free() 函数时,free 会对传入进来的内存地址向左偏移 16 字节,然后从这个 16 字节的分析出当前的内存块的大小,自然就知道要释放多大的内存了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
2021-06-03 Makefile 基础