堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。一般称管理堆的那部分程序为堆管理器,堆管理器处于用户程序与内核中间。
在内存分配与使用过程中,Linux基本内存管理思想,只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系。虽然操作系统已经给程序分配了很大的一块内存,但这块内存其实只是虚拟内存。只有当用户使用到相应的内存时,系统才会通过缺页真正分配物理页面给用户使用。
Ptmalloc2 主要是通过malloc/free函数来分配和释放内存块
malloc 返回对应大小字节的内存块的指针,也对一些异常情况进行了处理:
malloc(n)
- 当 n = 0 时,返回当前系统允许的堆的最小内存块。
- 当 n 为负数时,size_t是无符号数,所以程序会申请很大的内存空间,但通常都会失败,因为系统没有那么多的内存可以分配。
free 函数会释放由 p 所指向的内存块。这个内存块有可能是malloc函数得到的,也有可能是通过相关的函数realloc得到的。同样也对异常情况进行了处理:
free(p)
- 当 p 为空指针时,函数不执行任何操作。
- 当 p 已经被释放之后,再次释放会出现乱七八糟的效果,其实就是double free。
- 除了被禁用(mallopt)的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间。
分配背后的系统调用
申请和释放内存时并不真正与系统交互,背后的系统调用主要是(s)brk以及mmap、munmap函数。
(s)brk
初始时,堆的起始地址start_brk以及堆的当前末尾brk指向同一地址。是否开启ASLR,两者的具体位置会有所不同:
- 不开,start_brk以及brk会指向data/bss段的结尾。
- 开启,start_brk以及brk也会指向同一位置,只是这个位置是在data/bss段结尾后的随机偏移处。
在不同的上下文里,“堆”的含义差别很大,分两层来理解。
第一层:操作系统层面的“堆” (Heap Segment)
这是从进程的虚拟内存布局角度来看的。
是什么:在 Linux 进程中,内存被划分为不同的段(Segment),其中有一个区域叫“堆段(Heap Segment)”。它位于数据段(BSS)之后,向高地址方向生长。
管理者:这个区域由操作系统内核管理,通过
brk和sbrk系统调用,可以调整堆段的“顶部边界”(即 program break)。特点:
这是进程最早获得的动态内存区域。
它是一段连续的虚拟地址空间。
进程启动时,堆段只有很小的初始大小,随着程序不断申请内存,堆段会逐渐扩大(
brk向上推)。
一句话总结:操作系统层面的“堆”,是进程虚拟地址空间中,一块可以被程序动态扩充和收缩的连续内存区域,通过
brk/sbrk来管理。
第二层:内存分配器层面的“堆” (Heap)
这是从ptmalloc2内存分配器的角度来看的。在 Glibc 中,malloc管理的内存并不局限于操作系统的那个“堆段”。
广义的堆:在
ptmalloc2的语境下,“堆”是一个更广义的概念,它指的是所有由内存分配器管理并提供给malloc使用的动态内存池。它包含两种来源:主分配区(main_arena)的堆:这直接对应操作系统层面的那个堆段,通过
brk/sbrk扩展。这就是我们传统意义上说的“堆”。非主分配区(thread arena)的堆:这些是通过
mmap系统调用从操作系统匿名映射来的内存区域。它们在虚拟地址空间中是独立的、不连续的,并不在传统的“堆段”内。
管理单位:在
ptmalloc2中,这些不同来源的内存块,被抽象成统一的heap_info结构来管理(非主分配区的每个堆段都有一个heap_info头部)。对于主分配区,因为它只有一个堆段,所以不需要heap_info结构。
一句话总结:
ptmalloc2中的“堆”,是内存分配器管理的“内存池”的泛称,它可能来自brk堆段,也可能来自mmap映射区域。
第三层:数据结构层面的“堆” (Chunk & Top Chunk)
更具体地说,在ptmalloc2的源码和运行机制中,“堆”还指代两个具体的数据结构实体:
Top Chunk:
每个 Arena(分配区)都有一个Top Chunk。它位于该 Arena 当前活跃堆段的最顶部(最高地址处)。
它是该 Arena 的“最后储备金”。当所有空闲链表(Bin)都无法满足分配请求时,
malloc会从 Top Chunk 中直接切割内存返回。当 Top Chunk 空间不足时,主分配区会通过
brk扩展堆段,非主分配区会通过mmap创建新的堆段,从而获得新的 Top Chunk。
Heap Segment(堆段):
对于非主分配区,每个通过
mmap映射来的连续内存区域,称为一个“堆段”。堆段的头部有一个
heap_info结构,记录了它的大小、属于哪个 Arena,以及指向前一个堆段的指针。一个非主分配区可以有多个堆段,它们通过
heap_info中的prev指针链接成一条单向链表。
三者关系图
+---------------------------------------------------------+ | 进程虚拟地址空间 | +---------------------------------------------------------+ | 代码段 | 数据段 | 堆段 (brk) | 内存映射段 (mmap) | 栈段 | +--------------------+----------+-------------------+-----+ | | | ▼ ▼ ▼ +-----------+ +-----------+ +--------------+ | main_arena| | thread_arena| | thread_arena| +-----------+ +-----------+ +--------------+ | | | ▼ ▼ ▼ +-----------+ +-----------+ +-----------+ | Top Chunk | | heap_info | | heap_info | | (brk堆段) | | (mmap堆段1)| | (mmap堆段2)| +-----------+ +-----------+ +-----------+ ▲ ▲ ▲ | | | └──────┬───────┴──────────────────┘ │ ▼ 所有堆段共同构成了 ptmalloc2 管理的“堆”
终极一句总结
“堆”在 Glibc 的语境下,是一个层叠的概念:
从内核看,它是通过
brk管理的一段连续虚拟内存区域(堆段)。从分配器看,它是
main_arena的brk堆段和所有thread_arena的mmap堆段的总集合。从数据结构看,它是每个 Arena 顶部的Top Chunk,以及构成这些 Top Chunk 的多个
heap_info堆段。