• C++内存池


    malloc 实现

            我们常说的 malloc 函数是 glibc 提供的库函数。glibc 的内存管理使用的方法是 ptmalloc,除此之后还有很多其他内存管理方案,比如 tcmalloc (golang 使用的就是 tcmalloc)。

            ptmalloc 对于申请内存小于 128KB 时,分配是在堆段,使用系统调用 brk() 或者 sbrk()。如果大于 128 KB 的话,分配在映射区,使用系统调用 mmap()。

    1. // brk 将 brk 指针放置到指定地址处,成功返回 0,否则返回 -1
    2. int brk(const void *addr);
    3. // sbrk 将 brk 指针向后移动指定字节,返回依赖于系统实现,或者返回移动前的 brk 位置,或者返回移动后的 brk 位置。
    4. void *sbrk(intptr_t incr);

    申请内存小于 128KB

    申请

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

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

    释放

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

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

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

    申请内存大于 128KB

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

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

    malloc内存分配原理

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

            因为 brk/sbrk/mmap 都属于系统调用,若每次申请内存,都调用这三个,那么每次都会产生系统调用,影响性能;其次,这样申请的内存容易产生碎片,因为堆是从低地址到高地址,如果高地址的内存没有被释放,低地址的内存就不能被回收。

            所以malloc采用的是内存池的管理方式(ptmalloc),Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。

    malloc/free调用举例说明

    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获得真正意义内存释放,对应映射关系取消,内存碎片消失。

    参考文献:

    malloc底层实现原理_malloc原理_sy4331的博客-CSDN博客

  • 相关阅读:
    2022年全网最全AI绘画产品整理(一共23款,免费的绘画次数用到你手软)
    Idea加载gradle项目问题小记Gradle‘s dependency cache may be corrupt
    中央空调系统运行原理以及相关设备介绍
    【Java】权限修饰符
    jquery中的contentType和processData参数解释
    竟然还有人不会配置idea鼠标调节字体?
    6-Mysql子查询,多表连接(内连接,外连接,交叉连接)
    Vuex的简单购物车案例以及页面刷新数据丢失的解决方法
    阿里巴巴OceanBase介绍
    GM/T 0005《随机性检测规范》2012版和2021版对比
  • 原文地址:https://blog.csdn.net/qq_37070988/article/details/133861674