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 字节内存的起始地址过滤出内存地址范围[rootxiaolin ~]# 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 缺位、私有p00000000是文件内的偏移00:00是设备号0是 inode 号匿名映射为 0行尾的[heap]标识该段为堆。这个例子分配的内存小于 128 KB所以是通过brk()系统调用向堆空间申请的内存因此最右边有[heap]的标识。可以看到堆空间的内存地址范围是00d73000-00d94000大小是132KB这就说明了malloc(1) 实际上预分配了 132K 字节的内存。细心的读者会发现程序里打印的内存起始地址是d73010而 maps 文件显示堆内存空间的起始地址是d73000多出了0x1016 字节。这个 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点击查看免费下载相关推荐ctf-wiki 堆利用入门ptmalloc2 堆概述与 glibc 底层内存分配机制brk/mmap 全解析ctf wiki 堆利用入门ptmalloc2 堆概述与 glibc 底层内存分配机制brk/mmap 全解析 导读 本文是 ctf wiki 堆利用系列文档网络安全教程mmap-go 内存映射实战Go 可移植 mmap 库的 API 详解与平台实现原理mmap go 内存映射实战Go 可移植 mmap 库的 API 详解与平台实现原理 导读 mmap go 是一个为 Go 语言设计的可移植内存映射memo后端云原生容器编排微服务CS-Base 项目中的 Redis 内存优化bigkey 处理与内存碎片图解CS Base 项目中的 Redis 内存优化bigkey 处理与内存碎片图解 Redis 作为高性能的内存数据库在处理大规模数据时经常面临内存管理挑战。本文档教程知识库上一篇LSP Plugins完全教程如何搭建完整的数字音频工作站环境下一篇2025系统设计面试通关指南从理论到实战的全方位资源手册创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →