Malloc技术原理解析以及在转转搜索业务上的实践

本文主要是结合各类资料梳理了三种linux下常用的malloc:ptmalloc , tcmalloc的整体架构 , 以及对jemalloc的参数讲解 , 概念简介 , 内存分配和回收过程 , 并给出常用的参数的解析说明以及特性分析 。1 导读内存管理在三个不同的层面上发挥作用:用户程序层、C运行时库层以及内核层 。其中 , 内存分配器allocator是C运行时库中的一个关键组件 , 其主要任务是响应用户程序的内存分配请求 。分配器负责向操作系统内核请求适当大小的内存块 , 并将这些内存块分配给用户程序 。
为了提高内存分配的效率 , 分配器通常会预先分配一块稍大于用户请求的内存空间 , 并使用特定的算法来管理这块内存 , 以满足用户的内存需求 。不同之处在于 , 用户释放的内存并不会立即返回给操作系统 , 而是由分配器来管理这些空闲内存空间 , 以备将来用户的内存分配请求 。简而言之 , 分配器的任务不仅仅是管理已分配的内存块 , 还包括有效地管理可用的空闲内存块 。当需要响应用户的内存分配请求时 , 分配器会首先在已有的空闲内存中查找合适大小的内存块 , 只有在空闲内存不足时才会申请新的内存 。系统的物理内存是有限的 , 而对内存的需求是变化的, 程序的动态性越强 , 内存管理就越重要 , 选择合适的内存分配库会带来明显的性能提升 。
在转转的服务中 , 许多服务存在较高的堆外内存使用问题 , 例如转转搜索业务的排序服务 , 堆外内存的使用超出了预期 , 这已经导致了许多物理机内存的不足 。初步分析表明 , 这些内存占用较高的转转服务内部使用TensorFlow进行推断 , Linux默认使用的glibc的malloc实现在内存池资源回收方面存在缺陷 , 导致已分配的内存无法有效地返还给操作系统 , 根据此现状 , 需要对现有的可选malloc进行原理分析和合理选择 , 优化服务的内存占用表现 , 缓解服务器内存不足的现状 。常见的内存分配库包括ptmalloc(作为glibc标准库的一部分)、tcmalloc(由google开发)、jemalloc(由Facebook开发) , 由于篇幅较长 , 下文将对ptmalloc和tcmalloc的基本原理和相关参数进行介绍 , 并只介绍jemalloc的参数部分 。
2 内存管理与系统调用在介绍ptmalloc、tcmalloc等内存分配库之前 , 让我们先了解一下内存布局:

Malloc技术原理解析以及在转转搜索业务上的实践

文章插图
图片
上图描述了x86_64架构下的Linux进程默认地址空间 , 栈从顶向下扩展 , 堆从底向上扩展 , 而mmap映射区域也是从顶向下扩展 。mmap映射区域与堆相对扩展 , 直到耗尽虚拟地址空间 , 操作系统提供了brk()系统调用 , 用于设置堆的上边界 。其次 , 针对mmap映射区域的操作 , 可以使用mmap()和munmap()函数 。由于系统调用的开销较高 , 因此不太适合在每次需要内存分配时都从内核申请空间 , 特别是对于小内存分配来说更是如此 。另外 , mmap的映射区域可能会因为munmap()的释放而容易被回收 。因此 , 一般的做法是对于大内存分配 , 使用mmap()来申请内存 , 而对于小内存分配 , 则采用brk()方式 。这其中也包含了linux内存管理的基本思想:内存延迟分配 。即只有在真正访问一个地址的时候才建立这个地址的物理映射 。linux内核在用户申请内存的时候 , 只是给它分配了一个虚拟地址 , 并没有分配实际的物理地址 , 只有当用户使用这块内存的时候 , 内核才会分配具体的物理地址给用户使用 。
2.1 brk() 和 sbrk()#include <unistd.h>int brk(void *addr);brk()是一个系统调用 , 其实现定义在mmap.c中 。它的主要作用是调整堆顶的位置 , 使堆内存可以从低地址向高地址增长 。在分配内存时 , brk()会将堆段的最高地址指针mm->brk向高地址扩展 , 然后调用do_brk_flags来分配新的虚拟内存区域(Virtual Memory Area , VMA) , 并将这个VMA插入到内核的链表和红黑树中 。
需要注意的是 , 虽然使用brk()分配了一段新的虚拟内存区域 , 但这并不会立即分配物理内存 。实际的物理内存分配通常是在访问新分配的虚拟内存区域时 , 如果发生了缺页异常(Page Fault) , 操作系统才会开始分配并映射相应的物理内存页面 , 内存收缩时 , 调用__do_munmap对heap进行收缩 。


推荐阅读