我们常说的 malloc 函数是 glibc 提供的库函数。glibc 的内存管理使用的方法是 ptmalloc,除此之后还有很多其他内存管理方案,比如 tcmalloc (golang 使用的就是 tcmalloc)。
ptmalloc 对于申请内存小于 128KB 时,分配是在堆段,使用系统调用 brk() 或者 sbrk()。如果大于 128 KB 的话,分配在映射区,使用系统调用 mmap()。
- // brk 将 brk 指针放置到指定地址处,成功返回 0,否则返回 -1
- int brk(const void *addr);
- // sbrk 将 brk 指针向后移动指定字节,返回依赖于系统实现,或者返回移动前的 brk 位置,或者返回移动后的 brk 位置。
- void *sbrk(intptr_t incr);
初始时,进程会有一个初始大小的堆空间。brk指针(_enddata)指向堆空间的堆顶,通常通过空闲链表和位图管理这些空闲内存。当需要分配的空间小于128k时,将在堆上分配对应内存空间。malloc函数首先遍历已管理的堆空间(brk指针指向地址以下),若存在空闲的内存能满足所需大小,则分配该部分内存,完成内存分配。注意此时作为库函数malloc并未调用系统调用,仅仅在用户态空间即完成了内存分配。
当遍历所管理的所有空闲内存空间后发生没有能满足需要的,则调用系统调用brk函数增加brk指针(_enddata),即扩大堆空间以满足需要。
当调用free释放上面所申请的内存时,malloc会将该部分内存回收(仍然用空闲链表或者位图管理)。注意,此时该部分内存并未真正意义上回收,内核端认为该内存处于使用状态,对应的物理页仍然对应该部分虚拟内存映射(通常所说的内存碎片)。若此时产生了新的内存分配需求,而该部分内存能满足需要,则分配该部分内存。当释放该部分内存后,堆顶指针brk附近的连续空闲内存大于128K时,将进行真正意义上的内存回收操作。
从上面可以看出,当分配的内存小于128k时,malloc函数扮演了一个类似于代理商的角色,它从工厂(内核)获取大批量内存,然后根据每次实际使用需求进行零售。使用brk分配内存有如下优势:
当分配的内存大于128k时,将调用调系统调用mmap函数分配。此时分配的内存位置也和上面不一样,不再扩展brk指针(_enddata),而是直接在堆和栈之间区域(文件映射区域)分配一块虚拟内存。当调用free接口时,即调用munmap系统调用接口直接释放该部分内存。通过mmap单独解决大块内存分配需求有如下优势:
调用malloc时,只是分配了对应的虚拟地址空间。只有当访问该部分内存时才会真正分配物理内存并将物理内存和虚拟内存建立映射关系,并且根据实际使用多少内存分配多少物理内存。通过这种策略大大提高了内存使用率。
因为 brk/sbrk/mmap 都属于系统调用,若每次申请内存,都调用这三个,那么每次都会产生系统调用,影响性能;其次,这样申请的内存容易产生碎片,因为堆是从低地址到高地址,如果高地址的内存没有被释放,低地址的内存就不能被回收。
所以malloc采用的是内存池的管理方式(ptmalloc),Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。
参考文献: