【译】理解 Linux 中的内存分配 —— malloc、brk、sbrk、mmap 之间的关系,以及可调参数 vm.overcommit_memory

【译】理解 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);
  • 幕后发生了什么:
  1. malloc() 会先检查当前堆(heap)中是否存在可用的空闲空间。
  2. 如果可用空间不足,它会通过 brk() 系统调用 来扩展堆的边界。
  3. 接着,内存分配器会为该内存块添加必要的内部管理信息(元数据)。
  4. 最后,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),以减少内存碎片并降低分配与释放的开销。

如本文对你有些许帮助,欢迎大佬支持我一下(点赞+收藏+关注、关注公众号等),您的支持是我持续创作的竭动力
支持我的方式

Read more

web前端JS—基本语法

一、引入方式 1、内部脚本:将代码定义在HTML页面里面 * 将JS定义在<script></script>之间 * 可以在html里面的任意位置放置任意数量的<script></script> * 一般放置在<body>元素的底部,改善显示速度 <script> console.log('页面加载时执行'); function localFunction() { return '内部函数'; } </script> 2、外部脚本:额外定义一个.js文件,引入到HTML里面 * 只能包含js文件,不包含&

By Ne0inhk
【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题 在开发 Web 应用时,尤其是集成了 Unity WebGL 内容的页面,遇到一个问题:当 Unity WebGL 渲染内容嵌入到一个 Tab 中时,切换 Tab 后画面会变黑,直到用户点击黑屏区域,才会恢复显示。 这个问题通常是因为 Unity 渲染在 Tab 切换时被暂停或未能获得焦点所致。 在本文中,我们将介绍如何在使用 Layui 框架时,通过监听 Tab 切换事件并强制 Unity WebGL 渲染恢复,来解决这一问题。 1. 问题描述 当 Unity WebGL 内容嵌入到页面中的多个

By Ne0inhk
从安装到实测:基于 Claude Code + GLM-4.7 的前端生成与评测实战

从安装到实测:基于 Claude Code + GLM-4.7 的前端生成与评测实战

目录 引言 一、命令行使用 Claude Code(安装与配置) 步骤一:安装 Claude Code(命令行) 步骤二:配置蓝耘MaaS平台 步骤三:常见排查 二、编码工具中使用 claude-code:三个端到端案例(含提示与实测评价) 案例 1:交互式个人血压记录网页 — 前端端到端生成 案例 2:Web 双人对战小游戏(Joy-Con 风格) 案例 3:前端可视化组件生成 三、补充建议(快速 checklist) 总结 引言 近一年来,代码生成类工具逐渐从“写几行示例代码”走向“完整功能交付”,但真正落到工程实践时,很多工具仍停留在 Demo 阶段:要么跑不起来,

By Ne0inhk
Flutter for OpenHarmony:web 拥抱 Web 标准的桥梁(Wasm GC 与 DOM 互操作) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:web 拥抱 Web 标准的桥梁(Wasm GC 与 DOM 互操作) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 随着 Flutter 3.x 全面拥抱 Wasm(WebAssembly),Dart 团队推出了全新的 package:web 来取代老旧的 dart:html。 package:web 是基于最新的 JS Interop 机制构建的,它不仅性能更好,而且兼容 Wasm GC 标准。 虽然这个库通过名字看是为 “Web” 平台的,但对于 OpenHarmony 开发者来说,了解它有着特殊的意义: 1. 混合开发:鸿蒙原生支持 ArkWeb (WebView),在 Flutter 中通过 JS互操作与 Web 页面交互是常见需求。 2.

By Ne0inhk