malloc底层实现原理

malloc作为一个库函数,用于根据开发人员的需求在堆上动态分配内存。根据需要分配的内存大小,实现方式分以下两类:

  • 分配的内存大小小于128k

申请:初始时,进程会有一个初始大小的堆空间。brk指针(_enddata)指向堆空间的堆顶,通常通过空闲链表位图管理这些空闲内存。当需要分配的空间小于128k时,将在堆上分配对应内存空间。malloc函数首先遍历已管理的堆空间(brk指针指向地址以下),若存在空闲的内存能满足所需大小,则分配该部分内存,完成内存分配。注意此时作为库函数malloc并未调用系统调用,仅仅在用户态空间即完成了内存分配。

当遍历所管理的所有空闲内存空间后发生没有能满足需要的,则调用系统调用brk函数增加brk指针(_enddata),即扩大堆空间以满足需要。

释放:当调用free释放上面所申请的内存时,malloc会将该部分内存回收(仍然用空闲链表或者位图管理)。注意,此时该部分内存并未真正意义上回收,内核端认为该内存处于使用状态,对应的物理页仍然对应该部分虚拟内存映射(通常所说的内存碎片)。若此时产生了新的内存分配需求,而该部分内存能满足需要,则分配该部分内存。当释放该部分内存后,堆顶指针brk附近的连续空闲内存大于128K时,将进行真正意义上的内存回收操作。

调用malloc时,只是分配了对应的虚拟地址空间。只有当访问该部分内存时才会真正分配物理内存并将物理内存和虚拟内存建立映射关系,并且根据实际使用多少内存分配多少物理内存。通过这种策略大大提高了内存使用率。

从上面可以看出,当分配的内存小于128k时,malloc函数扮演了一个类似于代理商的角色,它从工厂(内核)获取大批量内存,然后根据每次实际使用需求进行零售。使用brk分配内存有如下优势:

  1. 内存分配效率高。因为已有的堆空闲内存由malloc函数管理,部分内存分配需求可以直接在用户态解决,避免了系统调用(用户态和内核态切换),大大提高了内存分配效率。
  • 分配的内存大小大于128k

当分配的内存大于128k时,将调用调系统调用mmap函数分配。此时分配的内存位置也和上面不一样,不再扩展brk指针(_enddata),而是直接在堆和栈之间区域(文件映射区域)分配一块虚拟内存。当调用free接口时,即调用munmap系统调用接口直接释放该部分内存。通过mmap单独解决大块内存分配需求有如下优势:

  1. 减少内存碎片。通过brk分配的内存只有在高地址内存释放后才有可能释放,这导致了大量的内存碎片。而若整块大内存产生内存碎片时,浪费较为严重,内存利用率低。通过mmap分配可以单独释放,减少内存碎片,效率高。

根据malloc/free调用实例进行说明。如下图所示:

malloc底层实现原理

  1. 初始进程空间分布如图1所示。
  2. 调用malloc申请分配A:100k内存,堆顶指针_enddata上移,如2所示。
  3. 调用malloc申请分配B:200k内存,堆顶指针_enddata不动,底层调用mmap系统调用在堆和栈之间的文件内存映射区直接分配200k的内存,如图3所示。
  4. 调用malloc申请分配C:40k内存,堆顶指针_enddata上移,如图4所示。
  5. 调用free释放A:100k内存,由于申请释放的内存不在堆顶,因此此时堆顶指针_enddata不动,实际上没有真正意义上释放内存,该虚拟内存和实际物理内存映射关系仍然存在,形成了内存碎片。注意,此时若有申请内存,且刚好小于100k,则可能把A释放出来的内存重新分配,提高了效率。如图5所示。
  6. 调用free释放B:200k内存,堆顶指针_enddata不动,底层直接调用munmap释放了该部分内存。如图6所示。
  7. 调用free释放C:40k,由于申请释放的内存在堆顶,释放后A、C的空闲空间连续,且大小大于128k,因此此时将会将AC内存释放,同时A页释放,A、C获得真正意义内存释放,对应映射关系取消,内存碎片消失。
上一篇:WSL2 Ubuntu: ping: hostname: Temporary failure in name resolution


下一篇:What does mc:Ignorable=“d” mean in WPF?