malloc, brk 和 mmap
这篇笔记可能存在错误,可能需要重新修改
malloc
不是系统调用。是C库函数
看过侯捷老师的视频,我们都有内存池这个概念。但是malloc
如何向系统申请内存?
我们仅用Linux下的方法说明。
malloc(1)
会分配多大的虚拟内存?
malloc()
在分配内存的时候,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池。(这一段具体原理可以看侯捷老师视频)
malloc
会使用两种方式向操作系统申请堆内存
brk
系统调用
brk
的原理是把edata
指针(堆顶指针)往上(往栈的方向)推。来获得新的虚拟内存空间。使用free
释放的时候,brk
分配的内存不会交回给操作系统。而是会缓存在malloc
的内存池中。等待下次使用。
mmap
系统调用
mmap是在堆和栈的中间也就是文件映射区(注意这里使用的是私有匿名映射,所以不需要实体磁盘文件)分配一块虚拟内存。使用free
释放的时候,mmap
分配的内存会交回给操作系统。得到真正的释放。
何时决定使用何种系统调用?
一般来说,会有一个默认阈值(128KB)。
- 如果用户分配的内存小于 128 KB,则通过
brk()
申请内存; - 如果用户分配的内存大于 128 KB,则通过
mmap()
申请内存;
为什么brk
的内存不会交回操作系统?
因为brk
是推指针。假如我们有 A B C D 四块内存。我们指针指向D。我们此时释放了B,指针不可以往回推,因为C和D还在。而且B的区域是可以重用的。直到C D 被回收 也就是从指针地址开始算有连续的大于某一个阈值(128K)的空闲内存了,edata指针才会紧缩。这就是内存碎片(这里不区分内部和外部碎片。和操作系统维护的物理内存的内部外部碎片不同。)产生的原因。 而且,假设我释放的B的大小是10K。如果此时需要给新的E分配一个30K的大小,这个B的空间是不可用的。所以依旧需要继续推指针。
为什么不统一使用mmap
申请内存?
因为向操作系统申请内存,是要通过系统调用的,执行系统调用是要进入内核态的,然后在回到用户态,运行态的切换会耗费不少时间。
所以,申请内存的操作应该避免频繁的系统调用,如果都用 mmap
来分配内存,等于每次都要执行系统调用。
另外,因为 mmap
分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap
分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断。
也就是说,频繁通过 mmap
分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
为了改进这两个问题,malloc
通过 brk()
系统调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。等下次再申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗。
再次记住,malloc不是系统调用
为什么不统一使用brk
申请内存?
因为上文提到的内存碎片,随着系统频繁地 malloc
和 free
,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。
brk()和sbrk()
brk
是系统调用,sbrk
是封装了brk
的库函数。- 来自这里
brk
的作用是直接设置指针到某个地址。sbrk
的作用是移动一段距离。- 所以说
sbrk
的入参可以为负数。这样就是往回缩。
free()
函数只传入一个内存地址,为什么能知道要释放多大的内存?
内存笔记的第一章。有cookie。cookie不仅会保存分配内存的大小,而且会用最后一个bit保存是否已分配。如果已分配,就是能被析构的。就是1,未分配也就是不能析构的就是0.
malloc
分配的是虚拟内存
如果分配后的虚拟内存没有被访问的话,是不会将虚拟内存不会映射到物理内存,这样就不会占用物理内存了。
只有在访问已分配的虚拟地址空间的时候,操作系统通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系。当真正有memcpy
这样的函数访问的时候才会触发缺页中断来分配物理内存
外部碎片(在分配单元间的未使用的内存);内部碎片(在分配单元中未使用的内存)这里的碎片指的是物理内存。不是虚拟内存。
上面讲的内存碎片指的是虚拟内存碎片,OS是不管的,OS只管物理内存。
平时我们说的内存碎片整理(defragment)或内存紧缩(memory compaction),是指OS对物理内存进行的碎片整理,把分开小的物理内存页移动在一起形成一个大的整块。OS整理完物理内存后,会用新的物理内存地址来更新虚拟内存与物理内存映射表,这些对于上层逻辑都是透明的。虚拟内存是不能进行碎片整理的,主要原因是碎片整理会移动内存,上层逻辑的指针地址确还是指向老的地址,这会导致致命错误。
new一定陷入内核态吗? – 不一定
因为new的底层是malloc。malloc会选择使用:内存池 或 brk 或 mmap进行内存的分配。如果选择了brk或mmap则因为是系统调用会陷入内核。而brk申请的内存释放后会进入malloc自己的内存池。
- 注意,内存池是在malloc里面的而不是在brk或mmap里面的
所以如果下次调用的时候发现brk归还的内存在内存池里(内存池有余量)则不会调用brk或mmap这种系统调用,而是直接拿出内存池的内存。这样就避免了陷入内核态。
杂项
- 使用
munmap
解除映射 - 使用
mprotect
修改映射权限 - 使用
msync
将对使用mmap
映射到内存中的文件的核内副本所做的更改刷新回文件系统。如果不使用此调用,则不能保证在调用munmap
之前将更改写回。
STL的空间配置器
一级空间配置器
一级空间配置器也就是封装了malloc和free
二级空间配置器
二级空间配置器就是内存池(自由链表)
申请
申请的时候,如果大于128字节直接调用一级配置器。如果小于128字节调用二级配置器。
二级配置器就相当于我们说的,一次申请一大块内存,然后拆分成8字节大小的块,挂在链表上。申请就从链表拿走,归还就挂回链表。可以看memory4笔记。
malloc源码讲解:https://www.52pojie.cn/thread-1581911-1-1.html
参考链接:https://mp.weixin.qq.com/s/HXRGr90baCvM-NQbPIgn-g