首页 Malloc, brk 和 mmap
文章
取消

Malloc, brk 和 mmap

malloc, brk 和 mmap

这篇笔记可能存在错误,可能需要重新修改

malloc不是系统调用。是C库函数

看过侯捷老师的视频,我们都有内存池这个概念。但是malloc如何向系统申请内存?

我们仅用Linux下的方法说明。

malloc(1) 会分配多大的虚拟内存?

malloc()在分配内存的时候,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池。(这一段具体原理可以看侯捷老师视频)

malloc会使用两种方式向操作系统申请堆内存

brk系统调用

brk的原理是把edata指针(堆顶指针)往上(往栈的方向)推。来获得新的虚拟内存空间。使用free释放的时候,brk分配的内存不会交回给操作系统。而是会缓存在malloc的内存池中。等待下次使用。

QQ截图20220725064954

mmap系统调用

mmap是在堆和栈的中间也就是文件映射区(注意这里使用的是私有匿名映射,所以不需要实体磁盘文件)分配一块虚拟内存。使用free释放的时候,mmap分配的内存会交回给操作系统。得到真正的释放。

QQ截图20220725065018

何时决定使用何种系统调用?

一般来说,会有一个默认阈值(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的空间是不可用的。所以依旧需要继续推指针。

QQ截图20220725065150

为什么不统一使用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

本文由作者按照 CC BY 4.0 进行授权