news 2026/10/2 14:58:33

CS-Base 图解 malloc:Linux 动态内存分配原理与 brk/mmap 实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CS-Base 图解 malloc:Linux 动态内存分配原理与 brk/mmap 实战解析
  • 文档
  • 教程
  • 知识库

【免费下载链接】CS-Base

图解计算机网络、操作系统、计算机组成、数据库,共 1000 张图 + 50 万字,破除晦涩难懂的计算机基础知识,让天下没有难懂的八股文!🚀 在线阅读:https://xiaolincoding.com

项目地址:https://gitcode.com/GitHub_Trending/cs/CS-Base
点击查看免费下载

本文是 CS-Base 仓库 《图解系统》 内存管理章节的核心内容,聚焦 C 标准库malloc()在 Linux 下的底层分配机制。你将通过两组可直接运行的 C 实验,亲手验证「malloc 分配的是虚拟内存」「malloc(1) 实际预分配 132KB」「free 后内存是否归还操作系统」等关键结论,并掌握用/proc/<pid>/maps、ps aux等工具观测进程内存分布的实战手段。读完后,你能系统讲清 brk() 与 mmap() 两条分配路径的差异、128KB 阈值的设计动机,以及 free() 仅凭一个地址就能准确释放内存的底层原理。

一、Linux 进程的内存分布:malloc 工作的舞台

在深入malloc()之前,先要看清它的活动空间——进程虚拟地址空间的分布结构。在 Linux 操作系统中,虚拟地址空间内部被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围不同:

  • 32位系统的内核空间占用1G,位于最高处,剩下的3G是用户空间;
  • 64位系统的内核空间和用户空间都是128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。

内核空间与用户空间之间有一道明确的分界线(权限隔离):

  • 进程在用户态时,只能访问用户空间内存;
  • 只有进入内核态后,才可以访问内核空间的内存。

虽然每个进程都各自拥有独立的虚拟内存,但每个虚拟内存中的内核地址,关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存,这也是系统调用能安全完成内核态工作的基础。关于虚拟地址与物理地址如何通过页表映射、MMU 如何完成地址转换,可参阅仓库文档 为什么要有虚拟内存?。

用户空间和内核空间的划分方式不同。以 32 位系统为例,用户空间内存从低到高分别是 6 种不同的内存段:

  • 程序文件段:包括二进制可执行代码;
  • 已初始化数据段:包括静态常量;
  • 未初始化数据段:包括未初始化的静态变量;
  • 堆段:包括动态分配的内存,从低地址开始向上增长;
  • 文件映射段:包括动态库、共享内存等,从低地址开始向上增长(增长方向跟硬件和内核版本有关);
  • 栈段:包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是8 MB,系统也提供了参数以便自定义大小。

在这 6 个内存段中,堆和文件映射段的内存是动态分配的。使用 C 标准库的malloc()或者mmap(),就可以分别在堆和文件映射段动态分配内存。这正是本文主角malloc()的活动舞台。

二、malloc 是如何分配内存的?

一个容易被误解的事实是:malloc()并不是系统调用,而是 C 库里的函数,用于动态分配内存。它底层再通过系统调用来向操作系统申请堆内存,一共有两种方式:

  • 方式一:通过brk()系统调用从堆分配内存。实现方式很简单,通过brk()函数将「堆顶」指针向高地址移动,获得新的内存空间。
  • 方式二:通过mmap()系统调用在文件映射区域分配内存。通过mmap()系统调用中「私有匿名映射」的方式,在文件映射区分配一块内存,也就是从文件映射区"偷"了一块内存。

什么场景下 malloc() 会通过 brk() 分配内存?又是什么场景下通过 mmap() 分配内存?

malloc()源码里默认定义了一个阈值(不同 glibc 版本的阈值定义不同):

  • 如果用户分配的内存小于 128 KB,则通过brk()申请内存;
  • 如果用户分配的内存大于 128 KB,则通过mmap()申请内存。

这个 128KB 阈值是理解 malloc 全部行为的分水岭:它决定了内存来自堆段还是文件映射段,也决定了free()释放后内存的去留,后文的所有实验都将围绕这个阈值展开。

三、malloc() 分配的是物理内存吗?

不是的,malloc()分配的是虚拟内存。

如果分配后的虚拟内存没有被访问,虚拟内存是不会映射到物理内存的,这样就不会占用物理内存。这一点在仓库文档 在 4GB 物理内存的机器上,申请 8G 内存会怎么样? 中有更极端的验证:在 64 位系统、物理内存仅 2GB 的机器上,程序可以连续申请 4 次 1GB 的虚拟内存而完全成功,因为只要不读写这些虚拟内存,操作系统就不会分配物理内存。

只有在访问已分配的虚拟地址空间的时候,操作系统才会真正介入:CPU 通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,进程从用户态切换到内核态,由内核的 Page Fault Handler(缺页中断处理函数)处理——若此时有足够空闲物理内存,就直接分配物理内存并建立虚拟内存与物理内存之间的映射关系;若无空闲物理内存,则进入内存回收流程,回收仍不满足时才触发 OOM 机制。完整的缺页中断→内存回收→OOM 链路可参阅仓库文档 内存满了,会发生什么?。

值得注意的是,即便只是「申请虚拟内存」这个过程本身,也会消耗少量物理内存(例如内核保存虚拟内存数据结构所用内存),这在超大虚拟内存申请场景下会成为触发 OOM 的诱因,alloc_mem.md中对此有完整实验佐证。

四、malloc(1) 会分配多大的虚拟内存?——132KB 实测

malloc()在分配内存时,并不会老老实实按用户预期申请的字节数来分配空间,而是会预分配更大的空间作为内存池。具体预分配多大,跟 malloc 使用的内存管理器有关,这里以 malloc 默认的内存管理器Ptmalloc2来分析。

我们做个实验:申请 1 字节的内存,看看操作系统实际分配了多大的内存空间。先准备如下 C 代码:

#include <stdio.h> #include <malloc.h> int main() { printf("使用 cat /proc/%d/maps查看内存分配\n",getpid()); //申请 1 字节的内存 void *addr = malloc(1); printf("此 1 字节的内存起始地址:%x\n", addr); printf("使用 cat /proc/%d/maps查看内存分配\n",getpid()); //将程序阻塞,当输入任意字符时才往下执行 getchar(); //释放内存 free(addr); printf("释放了 1 字节的内存,但 heap 堆并不会释放\n"); getchar(); return 0; }

代码中两次getchar()分别把程序阻塞在「申请后」和「释放后」两个时间点,方便我们分两次观察内存分布。执行代码(原文实验环境使用的 glibc 库版本是2.17),在程序阻塞期间,通过/proc/<pid>/maps文件查看进程的内存分布情况,并用该 1 字节内存的起始地址过滤出内存地址范围:

[root@xiaolin ~]# cat /proc/3191/maps | grep d730 00d73000-00d94000 rw-p 00000000 00:00 0 [heap]

这里补充说明/proc/<pid>/maps输出各字段的含义:00d73000-00d94000是内存段的虚拟地址范围,rw-p表示该段可读(r)、可写(w)、不可执行(x 缺位)、私有(p),00000000是文件内的偏移,00:00是设备号,0是 inode 号(匿名映射为 0),行尾的[heap]标识该段为堆。

这个例子分配的内存小于 128 KB,所以是通过brk()系统调用向堆空间申请的内存,因此最右边有[heap]的标识。可以看到堆空间的内存地址范围是00d73000-00d94000,大小是132KB,这就说明了malloc(1) 实际上预分配了 132K 字节的内存。

细心的读者会发现:程序里打印的内存起始地址是d73010,而 maps 文件显示堆内存空间的起始地址是d73000,多出了0x10(16 字节)。这个 16 字节的差异是本文最后一个问题的伏笔,后面会专门展开。

五、free 释放内存,会归还给操作系统吗?

继续让上面的进程往下执行:调用free()释放那 1 字节内存后,再次查看堆内存,发现堆内存还是存在的,并没有归还给操作系统。

这是因为,与其把这 1 字节释放给操作系统,不如先缓存进 malloc 的内存池里——当进程再次申请 1 字节内存时可以直接复用,速度快了很多。当然,当进程退出后,操作系统会回收进程的所有资源。

但以上结论只针对 malloc 通过brk()方式申请的内存。如果 malloc 通过mmap()方式申请内存,free()释放内存后就会归还给操作系统。做个实验验证:通过 malloc 申请 128KB 字节内存,让 malloc 走 mmap 路径。

#include <stdio.h> #include <malloc.h> int main() { //申请 1 字节的内存 void *addr = malloc(128*1024); printf("此 128KB 字节的内存起始地址:%x\n", addr); printf("使用 cat /proc/%d/maps查看内存分配\n",getpid()); //将程序阻塞,当输入任意字符时才往下执行 getchar(); //释放内存 free(addr); printf("释放了 128KB 字节的内存,内存也归还给了操作系统\n"); getchar(); return 0; }

执行代码后查看进程的内存分布,可以发现最右边没有[heap]标志,说明这是通过 mmap 以匿名映射方式从文件映射区分配的匿名内存。释放掉这块内存后再查看该 128KB 内存的起始地址,会发现已经不存在了,说明归还给了操作系统。

于是「malloc 申请的内存,free 释放内存会归还给操作系统吗?」这个问题可以做个总结:

分配方式free 后内存去向原因
malloc 通过brk()方式申请的内存不归还操作系统,缓存在 malloc 的内存池中,待下次使用堆空间连续,缓存复用成本低
malloc 通过mmap()方式申请的内存归还操作系统,内存得到真正释放匿名映射与进程生命周期绑定

六、为什么不全部使用 mmap 来分配内存?

既然 mmap 释放后能真正归还内存,看起来"干净利落",为什么不全部用 mmap?原因在于性能:

  • 系统调用开销:向操作系统申请内存要执行系统调用,执行系统调用要进入内核态,之后再回到用户态,运行态的切换会耗费不少时间。如果都用 mmap 分配内存,等于每次分配都要执行系统调用;
  • 缺页中断开销:mmap 分配的内存每次释放时都归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态,第一次访问该虚拟地址时就会触发缺页中断。

也就是说,频繁通过 mmap 分配内存,不仅每次都会发生运行态的切换,还会发生缺页中断(第一次访问虚拟地址时),导致 CPU 消耗较大。

为了改进这两个问题,malloc 通过brk()系统调用在堆空间申请内存时,由于堆空间是连续的,可以直接预分配更大的内存作为内存池;内存释放时就缓存在内存池中,等下次再申请内存时,直接从内存池取出对应的内存块即可,而且这个内存块的虚拟地址与物理地址的映射关系可能还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,大大降低了 CPU 的消耗。

七、既然 brk 那么"高效",为什么不全部使用 brk 来分配?

brk 的内存池方案也有其代价——内存碎片。

考虑这样一个场景:连续申请了 10k、20k、30k 三片内存,如果 10k 和 20k 这两片被释放了,就变为了空闲内存空间。如果下次申请的内存小于 30k,就可以重用这个空闲内存空间;但如果下次申请的内存大于 30k,没有可用的空闲内存空间,就必须向操作系统申请,实际使用内存继续增大。

因此,随着系统频繁地 malloc 和 free,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致"内存泄露"。这种"泄露"现象使用valgrind 是无法检测出来的——因为内存确实被释放过,只是碎片无法被重新利用,属于堆内部的碎片化问题。

所以,malloc 实现中充分考虑了 brk 和 mmap 行为上的差异及优缺点,默认分配大块内存(大于 128KB)时才使用 mmap 分配内存空间:小块内存用 brk 走内存池减少系统调用,大块内存用 mmap 避免堆碎片堆积。

八、free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?

还记得前面那个伏笔吗?malloc 返回给用户态的内存起始地址(d73010)比进程的堆空间起始地址(d73000)多了 16 字节。

这个多出来的 16 字节就是保存该内存块的描述信息,其中就包含该内存块的大小。这样当执行free()函数时,free 会对传入进来的内存地址向左偏移 16 字节,然后从这个 16 字节的描述信息中分析出当前内存块的大小,自然就知道要释放多大的内存了。

这也是为什么free()只传一个地址就能完成精确释放:指针并非直接指向用户数据区,而是数据区起点,其前方的元数据区记录了块大小等管理信息。这种"用户指针前移、元数据前置"的设计在 Ptmalloc2 等主流分配器中是通用做法。

九、实战观测:用系统工具验证 malloc 的内存行为

结合本仓库的内存管理系列文档,可以形成一套完整的观测与排查手段:

  • 查看内存段分布:cat /proc/<pid>/maps | grep <地址前缀>可过滤出特定内存段,[heap]标识对应 brk 分配的堆,无标识的匿名段则来自 mmap;
  • 区分虚拟内存与物理内存:ps aux | grep <进程名>中的VSZ代表进程使用的虚拟内存大小,RSS代表进程使用的物理内存大小。申请不访问时,VSZ 很大而 RSS 很小,正是「malloc 分配虚拟内存」的直接证据(实验示例见 alloc_mem.md);
  • 大内存申请被拒:若 64 位系统上 malloc 大块虚拟内存返回Cannot allocate memory,可检查cat /proc/sys/vm/overcommit_memory(值为 0 表示试探式允许 overcommit,过大的单次申请会被内核判定不合理而拒绝;值为 1 表示 Always overcommit,来者不拒;值为 2 表示禁止 overcommit)。按需执行echo 1 > /proc/sys/vm/overcommit_memory可放开限制,详见 alloc_mem.md;
  • 缺页中断与回收:当访问 malloc 出的虚拟内存触发缺页中断且物理内存紧张时,内核会先做后台回收(kswapd)与直接内存回收,仍不足则触发 OOM Killer 杀进程,回收机制与 oom_score_adj 保护策略详见 mem_reclaim.md。

十、总结

回顾全文,malloc()的核心结论可以浓缩为以下几点:

  • malloc 是C 库函数而非系统调用,底层通过brk()(堆,小内存)或mmap()(文件映射区,大内存)两条路径向操作系统申请内存,默认分界阈值为128 KB(具体数值随 glibc 版本变化);
  • malloc 分配的是虚拟内存,未访问时不占物理内存;首次访问触发缺页中断后才建立虚拟到物理的映射;
  • malloc 会预分配更大的内存池(Ptmalloc2 下 malloc(1) 实测预分配 132KB),并以 16 字节的块描述信息记录内存块大小,这也是 free() 只凭地址即可精确释放的依据;
  • brk 分配的内存 free 后缓存在内存池不归还(换取快速复用、减少系统调用与缺页中断),但会造成堆碎片式"内存泄露"(valgrind 无法检测);mmap 分配的内存 free 后真正归还操作系统(避免碎片,但每次分配都伴随系统调用与首次访问缺页中断)。

理解了这两条路径的性能权衡,也就真正理解了 Linux 动态内存分配的设计取舍。进一步学习可继续阅读仓库内存管理章节的 为什么要有虚拟内存?、内存满了,会发生什么? 与 在 4GB 物理内存的机器上,申请 8G 内存会怎么样?。

  • 文档
  • 教程
  • 知识库

【免费下载链接】CS-Base

图解计算机网络、操作系统、计算机组成、数据库,共 1000 张图 + 50 万字,破除晦涩难懂的计算机基础知识,让天下没有难懂的八股文!🚀 在线阅读:https://xiaolincoding.com

项目地址:https://gitcode.com/GitHub_Trending/cs/CS-Base
点击查看免费下载

相关推荐

上一篇:LSP Plugins完全教程:如何搭建完整的数字音频工作站环境
下一篇:2025系统设计面试通关指南:从理论到实战的全方位资源手册

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 14:57:29

程序员如何入门AI量化投资:从策略回测到风险控制的完整路径

这两年&#xff0c;我身边越来越多程序员开始聊量化投资。有的深夜跑回测脚本&#xff0c;有的在讨论选股因子&#xff0c;还有人直接把“量化”写进简历转行做私募研究员。每次被问到“程序员搞量化是不是对的路”&#xff0c;我的回答都一样&#xff1a;技术上确实是量身定做…

作者头像 李华
网站建设 2026/10/2 14:55:59

SpringBoot高校社团纳新数字化平台:需求、设计到落地全解析

又到了一年一度毕设选题的时候。每年这个阶段&#xff0c;后台问得最多的就是“SpringBoot能做什么题目”“有没有不烂大街、又有实际意义的系统”。如果你正在为计算机毕业设计选题发愁&#xff0c;或者已经定了方向但不知道从哪下手&#xff0c;那我今天拆解的这套《基于Spri…

作者头像 李华
网站建设 2026/10/2 14:54:57

VGG迁移学习实战:卷积神经网络与珊瑚种类识别指南

简介&#xff1a;这是一份基于PyTorch的VGG卷积神经网络珊瑚种类识别项目&#xff0c;面向深度学习和图像分类初学者&#xff0c;也适合需要快速搭建完整CNN训练流程的开发者。代码仅用三个Python脚本实现&#xff0c;分别负责生成训练集txt、执行CNN训练以及提供PyQt界面&…

作者头像 李华
网站建设 2026/10/2 14:54:54

人体姿态识别实战:OpenPose源码训练、模型导出与摄像头部署全解析

简介&#xff1a;人体姿态估计是计算机视觉的核心方向之一&#xff0c;通过深度学习模型从图像或视频中定位人体关键点&#xff08;如关节、五官&#xff09;&#xff0c;生成骨架结构以描述动作状态。其实现通常依赖卷积神经网络提取特征&#xff0c;并输出关键点的热图表示&a…

作者头像 李华
网站建设 2026/10/2 14:54:37

南山口碑好的客家菜聚餐服务商挑选全攻略

什么是符合本地需求的客家菜聚餐服务&#xff0c;先建立基础认知客家菜是广东三大菜系之一&#xff0c;发源于客家民系的南迁聚居历史&#xff0c;自带浓郁的乡土烟火气&#xff0c;核心特点选料讲究本土新鲜食材&#xff0c;口味偏重原汁原味咸鲜适口&#xff0c;保留了不少传…

作者头像 李华
网站建设 2026/10/2 14:52:06

TK海外抢单源码实战:PHP+uniapp前后端分离与并发控制解析

简介&#xff1a;TikTok海外抢单源码是一套面向跨境TikTok接单场景的完整网站源码&#xff0c;采用前后端分离架构&#xff1a;前端基于uniapp&#xff08;Vue生态&#xff09;可跨端编译&#xff0c;后端使用PHP 7.2开发&#xff0c;配套MySQL 5.6数据库&#xff0c;内置指定派…

作者头像 李华