【译】理解 Linux 中的内存分配 —— malloc、brk、sbrk、mmap 之间的关系,以及可调参数 vm.overcommit_memory
【译】理解 Linux 中的内存分配 —— malloc、brk、sbrk、mmap 之间的关系,以及可调参数 vm.overcommit_memory
作者:Aditya Pratap Singh
原文: understanding-memory-allocation-linux-relation-brk
作为一名嵌入式系统或内核开发的新手,你可能会好奇:当我们调用诸如 malloc() 这样的函数时,内存究竟是如何运作的?
让我们通过真实示例来深入分析 Linux 中的内存分配过程,并理解其背后的底层机制。
1. 探索虚拟内存布局
- 首先,让我们来看一下 Linux 是如何组织进程的虚拟内存的。对于任意一个进程,其内存布局都可以通过查看 /proc//maps 文件来了解,该文件清晰地展示了该进程的虚拟内存映射结构。
$ cat /proc/14799/maps 00400000-00402000 r-xp 00000000 00:45 41291688 vaflmalloc 00402000-00403000 r--p 00001000 00:45 41291688 vaflmalloc 00403000-00404000 rw-p 00002000 00:45 41291688 vaflmalloc 00404000-00425000 rw-p 00000000 00:00 0[heap] 007fb13676a000-007fb13676d000 rw-p 00000000 00:00 0 007fb13676d000-007fb136955000 r-xp 00000000 00:2b 30 /lib64/libc-2.31.so 007fb136955000-007fb136958000 r--p 001e7000 00:2b 30 /lib64/libc-2.31.so 007fb136958000-007fb136960000 rw-p 001ea000 00:2b 30 /lib64/libc-2.31.so 007fb136960000-007fb136964000 rw-p 00000000 00:00 0 007fb13697b000-007fb13697d000 rw-p 00000000 00:00 0 007fb13697d000-007fb1369a8000 r-xp 00000000 00:2b 17 /lib64/ld-2.31.so 007fb1369a8000-007fb1369a9000 r--p 0002a000 00:2b 17 /lib64/ld-2.31.so 007fb1369a9000-007fb1369ab000 rw-p 0002b000 00:2b 17 /lib64/ld-2.31.so 007ffd8e661000-007ffd8e686000 rw-p 00000000 00:00 0[stack] 007ffd8e741000-007ffd8e745000 r--p 00000000 00:00 0[vvar] 007ffd8e745000-007ffd8e747000 r-xp 00000000 00:00 0[vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0[vsyscall] $ grep -E "address.*sizes|la57" /proc/cpuinfo address sizes :46 bits physical, 48 bits virtual address sizes :46 bits physical, 48 bits virtual address sizes :46 bits physical, 48 bits virtual address sizes :46 bits physical, 48 bits virtual ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Physical addressing: 46 bits =2^46 =64 TB of physical memory Virtual addressing: 48 bits =2^48 =256 TB of virtual address space Virtual address space: 48 bits User space: 2^47 =128 TB (half of the 48-bit space) Kernel space: 2^47 =128 TB (the other half) Hardware: 48-bit virtual addressing CPU Kernel: 4-level page tables in use User space: 128 TB available - 下面我们来逐项解析看到的内容:
程序代码区域:00400000-00402000 r-xp
这里存放的是程序的可执行代码。
数据区域:00402000-00403000 r–p
包含已初始化的全局变量和静态变量。
堆区域:00404000-00425000 rw-p [heap]
这是动态内存分配的主要来源,例如通过 malloc() 分配的内存通常来自这里。
内存映射区域:007fb13676d000-007fb136955000 r-xp
用于共享库以及通过 mmap 映射的文件。
栈区域:007ffd8e661000-007ffd8e686000 rw-p [stack]
用于存放局部变量以及函数调用相关的信息。
特殊区域:[vvar]、[vdso]、[vsyscall]
这些是与 内核相关的特殊内存区域,用于加速系统调用和时间读取等操作。
- 最重要的一点是:你会注意到虚拟地址空间中存在大量的“空洞”。
这些空洞表示尚未使用的虚拟内存区域,在需要时,内核可以从这些区域中为进程分配新的内存。
Gaps in virtual address space (potential malloc areas): -------------------------------------------------- Start End Size -------------------------------------------------- 0x00000000001000 0x00000000400000 4092 KB 0x00000000425000 0x007fb13676a000 137108491540 KB <-- A massive gap of ~127 TB! 0x007fb136964000 0x007fb13697b000 92 KB 0x007fb1369ab000 0x007ffd8e661000 320205528 KB 0x007ffd8e686000 0x007ffd8e741000 748 KB 这些空隙正是内存分配可以映射到的位置,而它们的大小最终决定了单次能够分配的最大内存容量。
2. 内存分配是如何工作的
当你的程序调用 malloc() 时,通常会发生以下两种情况之一:
2.1 小块内存分配:堆(Heap)与 brk()
- 对于较小的内存分配请求,malloc() 会使用堆空间来完成分配。
- 下面我们通过一个实际示例,来看看这一过程是如何发生的。
// Allocating a small 10KB chunkvoid*ptr =malloc(10*1024);- 幕后发生了什么:
- malloc() 会先检查当前堆(heap)中是否存在可用的空闲空间。
- 如果可用空间不足,它会通过 brk() 系统调用 来扩展堆的边界。
- 接着,内存分配器会为该内存块添加必要的内部管理信息(元数据)。
- 最后,malloc() 返回一个指向可用内存区域的指针。
- 我们可以通过 strace 来观察这一过程:
$ strace -e brk ./our_program brk(NULL)= 0x403000 brk(0x424000)= 0x424000 第一次调用 brk(NULL) 用于获取当前的 program break(堆顶位置),而第二次调用则在此基础上将堆空间向上扩展了 132KB。
2.2 大块内存分配:直接使用 mmap()
- 对于较大的内存分配(通常认为是 >128KB,但我们的测试在本系统上显示阈值为 16MB),malloc() 会完全绕过堆(heap),转而直接使用 mmap() 来完成分配:
// Allocating a large 20MB chunk void *ptr = malloc(20 * 1024 * 1024);- strace 的输出显示:
$ strace -e mmap ./our_program mmap(NULL, 20971520, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)= 0x7f3700858000 这实际上是将内存直接映射到虚拟地址空间中的那些“空隙”位置。
3. 内存限制:物理内存(RAM)与虚拟内存
- 现在,让我们尝试分配一块远远超过物理内存容量的大型内存空间:
// Trying to allocate 100GBvoid*ptr =malloc(100ULL*1024*1024*1024);if(ptr)printf("Success!\n");elseprintf("Failed!\n");在我们这台拥有 45GB 内存的系统上,得到的结果是:
Failed!- 为什么会这样呢? 这是因为在默认情况下,Linux 采用了一种称为 “overcommit(内存超量分配)” 的内存分配策略,用来限制进程可分配的内存总量。 下面我们来查看一下该配置项的当前设置:
$ cat /proc/sys/vm/overcommit_memory 0- 当 overcommit 设置为 0(默认值) 时,Linux 通常会把可分配内存的上限控制在接近物理内存(RAM)大小的范围内。将 vm.overcommit_memory 设为 0(默认),意味着 Linux 会基于一种启发式(heuristic)策略来决定是否允许内存超量分配:它会尝试估算系统是否拥有足够的物理内存来满足内存请求。
这并不表示它会严格把分配额度限制在物理内存大小之内;相反,它会尽量确保应用程序不会使用超过物理内存可承受范围的内存。但在某些情况下,它仍然可能允许分配超过物理内存的内存空间。 - 当我们运行二分搜索(binary search)测试程序时,观察到的正是这种限制:
Maximum allocation found: 48672096256 bytes (45.33 GB, 0.04 TB)这与我们系统的物理内存容量几乎完全一致:
$ free -h total used free shared buff/cache available Mem: 45Gi 6.4Gi 41Gi 444Mi 1.5Gi 38Gi Swap: 0B 0B 0B 4. 通过 Overcommit 打破限制
- 让我们修改 overcommit 的设置,然后再尝试一次。
$ sysctl -w vm.overcommit_memory=1 vm.overcommit_memory =1现在,让我们再次运行内存分配测试程序:
Success!- 哇!现在我们竟然可以分配远远超过物理内存容量的内存了!这是怎么做到的?
当 overcommit 设置为 1 时,Linux 的策略会变得非常“乐观”。它会直接返回你所请求的虚拟内存地址,但在你真正访问这些内存之前,并不会实际为其预留对应的物理内存。 - 当我们在启用 overcommit 的情况下运行二分搜索测试时,得到的结果是:
Maximum allocation found: 76965814468608 bytes (71680.00 GB, 70.00 TB)高达 70 TB 的可分配内存——这超过了我们物理内存容量的 1,500 倍!
5. malloc() 与 mmap() 的极限对比
- 现在,让我们尝试一个更加极端的场景——不再使用 malloc(),而是直接调用 mmap()。我们的测试结果显示:
Testing maximum mmap allocation Attempting to mmap 76965813944320 bytes (70.00 TB)... SUCCESS: Mapped 76965813944320 bytes (70.00 TB) at address 0x39f4f39f9000 Attempting to mmap 82463372083200 bytes (75.00 TB)... SUCCESS: Mapped 82463372083200 bytes (75.00 TB) at address 0x34f4f39f9000 ... Attempting to mmap 137438953472000 bytes (125.00 TB)... SUCCESS: Mapped 137438953472000 bytes (125.00 TB) at address 0x2f4f39f9000 Attempting to mmap 142936511610880 bytes (130.00 TB)... FAILED: mmap failed with error: Cannot allocate memory - 太不可思议了! 使用直接的 mmap() 调用,我们最多可以分配 125 TB 的内存,而使用 malloc() 时却只能分配 70 TB。
但为什么会有这样的差异呢? - 让我们来看一下通过 strace 捕获到的实际系统调用:
# Direct mmap allocation of 125TB mmap(NULL, 137438953472000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)= 0x7f4db1b34000 # malloc trying to allocate 70TB mmap(NULL, 76965814472704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)= 0x39835adaf000 这些系统调用看起来完全一样,那么差异究竟在哪里呢?
6. 实践中的内存分配
- 基于我们的实验结果,下面总结了内存分配在实际运行中的表现:
6.1 元数据开销(Metadata Overhead)
- 每一次通过 malloc() 进行的内存分配,都会包含一定的内部管理开销。
- 在我们的测试中,这一开销被精确地测量了出来:
=== TEST 1: Checking Malloc Metadata Overhead === Requested Usable Overhead Overhead % -------------------------------------------------------------- 1624850.00% 1024103280.78% 102401024880.08% 10240010240880.01% 1048576105265640800.39% 104857601048984040800.04% 10485760010486168040800.00% 小块内存分配(< 128KB)仅有 8 字节的元数据开销,而较大的内存分配则会产生高达 4080 字节的开销!
6.2 使用 mmap() 的分配阈值
- 我们还精确确定了 malloc() 从使用堆(heap)切换为直接调用 mmap() 的临界点:
=== TEST 2: Examining Threshold Size for Direct mmap === Testing allocation strategy by size: Size Uses mmap? ------------------------- 65536 No 131072 No 262144 No 524288 No 1048576 No 4194304 No 16777216 Yes 33554432 Yes 在我们的系统上,当分配大小达到或超过 16MB 时,malloc() 就会改为使用 mmap() 来进行内存分配。
6.3 虚拟地址空间碎片化
- 限制我们最大可分配内存大小的最主要因素是虚拟地址空间的碎片化。当我们检查内存映射时,发现:
Largest available contiguous virtual address space: Size: 137438953472000 bytes (125.00 TB)- 这一数值与我们通过 mmap() 能够分配到的最大内存大小完全一致!这说明限制因素并不仅仅是元数据或管理开销,而是系统中可用的最大连续虚拟地址空间块本身。
- 相比之下,malloc() 更容易受到这种碎片化的影响,因为它需要维护自身的内部数据结构;而直接使用 mmap() 调用,则可以更高效地利用底层的虚拟内存映射布局。
7. 理解内存分配的最大限制
- 综合以上分析,我们得出了以下内存分配上限:
MethodOvercommit DisabledOvercommit Enabledmalloc()~45 GB~70 TBDirect mmap()~45 GB~125 TB - 决定这些上限的关键因素包括:
关闭 overcommit 时:
内存分配上限主要受限于物理内存容量(约 45 GB)。
启用 overcommit,使用 malloc() 时:
内存分配上限受限于 malloc() 的实现机制(约 70 TB)。
启用 overcommit,使用 mmap() 时:
内存分配上限主要受限于虚拟地址空间的碎片化程度(约 125 TB)。
这些结果解释了:为什么在不同的内存分配方式下,我们会观察到如此巨大的最大可分配内存差异。
8. 对嵌入式与内核开发的启示
- 对于嵌入式系统开发者或内核开发者而言,这些发现具有重要的实际意义:
资源受限环境:
在 RAM 资源有限的嵌入式系统中,建议将 overcommit 设置为 2,以防止在内存请求无法由物理内存实际支撑时仍然“成功”分配。
内存碎片化问题:
在长时间运行的系统中,虚拟地址空间碎片化可能会逐渐成为一个严重问题,从而不断降低单次可分配内存的最大尺寸。
自定义内存分配器:
在某些场景下,可能需要实现定制的内存分配器,以更好地满足特定需求,尤其是在内存分配模式可预测的情况下。
直接硬件交互:
在部分嵌入式系统中,可能需要使用 mmap() 并指定具体的物理地址,来直接映射物理内存区域以进行硬件交互。
内存池机制:
对于频繁分配固定大小内存的场景,可以考虑使用内存池(memory pool),以减少内存碎片并降低分配与释放的开销。
如本文对你有些许帮助,欢迎大佬支持我一下(点赞+收藏+关注、关注公众号等),您的支持是我持续创作的竭动力
支持我的方式