Linux 内存管理

  • Linux 虚拟内存核心:进程看到的都是「虚拟地址」,与物理内存解耦 ,64 位系统下进程独享 128TB 虚拟地址空间,严格划分「用户态 + 内核态」,用户态分区规整,是内存管理的核心;
  • 堆 / 栈的本质区别:栈是「内核自动管理的小内存高速区」,堆是「程序员手动管理的大内存低速区」,二者生长方向、管理方式、效率、碎片特性天差地别;
  • mmap/brk 核心区别:都是 Linux 内核的内存申请系统调用,brk 操作「堆区」,mmap 操作「内存映射区」;brk 是「堆顶指针伸缩」,mmap 是「独立内存块映射」,内存归还效率、碎片控制是二者核心差异;
  • 内存池选 mmap 而非 malloc 的核心:malloc 是「glibc 封装的库函数」,有不可控的开销 + 碎片问题;mmap 是「内核系统调用」,内存可控、归还高效、碎片极少,完美匹配内存池的大块内存申请诉求,工业级内存池(tcmalloc/jemalloc/ 你的项目)均以 mmap 为核心申请方式。

一、Linux 进程的「虚拟内存空间布局」

Linux 是虚拟内存操作系统进程永远不会直接访问物理内存 ,进程看到的所有内存地址都是「虚拟地址」,由操作系统通过页表 (Page Table) 完成「虚拟地址 → 物理地址」的映射,内核负责维护映射关系。

虚拟内存对进程的核心意义:

  1. 地址空间隔离 :每个进程拥有独立的、完整的虚拟地址空间,进程 A 的虚拟地址和进程 B 无关,进程崩溃不会污染其他进程 / 内核,这是进程安全的根基;
  2. 物理内存复用:多个进程可以共享同一块物理内存(如共享库),物理内存不足时内核会通过「换页 (swap)」把闲置内存写到磁盘,充分利用物理内存;
  3. 地址空间规整:进程的虚拟地址空间被内核严格划分为固定区域,每个区域有明确用途,内存管理效率极高;

关键数字:x86_64(64 位 Linux) 进程虚拟地址空间总大小为 128TBx86(32 位) 为 4GB,目前服务器均为 64 位

64 位 Linux 虚拟内存空间「严格分区」

第一部分:内核态空间

地址范围:0x8000000000000000 ~ 0xFFFFFFFFFFFFFFFF

  • 所有进程共享同一份内核态虚拟地址空间,存放内核代码、内核数据、页表、缓冲区等;
  • 用户进程无法直接访问,只能通过「系统调用 (如 mmap/brk/read)」陷入内核态,由内核代为操作;
  • 内存池不涉及此区域,无需深入。

第二部分:用户态空间(低地址,约 64TB)

地址范围:0x0000000000000000 ~ 0x7FFFFFFFFFFFFF

用户态是进程实际使用的区域,严格按地址从低到高划分 6 个固定区域

  • 代码段(Text Segment) :地址最低,存放进程的二进制可执行代码(编译后的机器指令);只读、可共享,多个进程运行同一个程序时共享同一份代码段,节省内存。
  • 数据段(Data Segment) :存放已初始化的全局变量、静态变量 (如int a=10; static int b=20;);可读可写,进程启动时初始化,大小固定。
  • BSS 段(Block Started by Symbol) :存放未初始化的全局变量、静态变量 (如int a; static int b;);内核会在进程启动时将此区域全部置 0,可读可写,大小固定。
  • 堆(Heap) :【内存池弱关联】地址从低向高生长 (向上扩容),是进程的「动态内存区」,存放程序员手动申请的内存(malloc/brk申请的内存都在这里);大小不固定,可按需扩容 ,上限受物理内存 + 交换分区限制;堆的顶部由内核的「brk 指针」标记,堆的扩容本质就是移动 brk 指针。
  • 内存映射区(Memory Mapping Segment,mmap 区) :【内存池强关联・核心核心】地址在堆和栈之间的巨大空闲区域 (64 位下是 TB 级的空间,几乎无上限),是 Linux 内存管理的「黄金区域」;mmap 系统调用申请的内存全部在这里分配 ,是独立的、离散的内存块,无地址连续要求;共享库、动态链接库、匿名内存块、文件映射内存都存放在此
  • 栈(Stack) :地址从高向低生长 (向下扩容),是进程的「栈帧内存区」,存放函数的局部变量、函数参数、返回地址、寄存器上下文;内核自动管理,无需程序员干预 ,大小固定且极小 (Linux 默认栈大小为 8MB,可通过ulimit -s修改);函数调用时栈帧入栈,函数返回时栈帧出栈,内存自动释放。

补充:data+BSS合称「静态区」,生命周期与进程一致,进程退出才释放。

核心重点:内存映射区是内存池「大块内存申请」的唯一目标,也是 mmap 的核心操作区!

  • 堆和 mmap 区之间有巨大的空闲虚拟地址空间,所以 mmap 申请的内存几乎没有上限,而堆的扩容会受限于这个空闲区;
  • 内存池的「小内存块」是在mmap 申请的大块内存中切分的,而非直接在堆上申请,这是内存池低碎片的核心;
  • 栈的地址向下生长,堆向上生长,二者相向而行,但中间的 mmap 区足够大,永远不会相遇。

二、进程的「堆 (Heap)」和「栈 (Stack)」的核心区别

栈是「内核自动管理的高速小内存」,堆是「程序员手动管理的低速大内存」,二者是「极致效率」与「极致灵活」的对立,无好坏之分,各司其职。

1. 内存生长方向不同
  • 栈:从高地址 → 低地址 向下生长 ,栈顶指针%rsp(寄存器)永远指向栈的最顶端,函数调用时栈帧压栈,指针向低地址移动;
  • 堆:从低地址 → 高地址 向上生长 ,堆顶由内核的brk指针标记,堆扩容时brk指针向高地址移动。
2. 内存的管理主体不同
  • 栈:内核自动分配、自动释放,无需程序员干预。函数调用时内核为函数分配栈帧,函数执行完毕内核自动回收栈帧内存,无内存泄漏风险;
  • 堆:程序员手动分配、手动释放 ,通过malloc/mmap/brk申请,free/munmap释放。忘记释放会造成内存泄漏,释放后重复使用会造成野指针,释放顺序错误会造成内存错乱。
3. 内存大小限制不同(天壤之别)
  • 栈:大小固定,且极小 ,Linux 默认栈大小为8MB,超出栈大小会触发「栈溢出 (Stack Overflow)」,进程直接崩溃(如递归过深、局部数组过大);栈的大小是硬限制,无法扩容;
  • 堆:大小无硬性上限 ,理论上受物理内存 + 交换分区的总大小限制,64 位系统下堆的可用空间是 TB 级的,可通过brk/mmap无限扩容(直到物理内存耗尽)。
4. 内存分配 / 释放的效率不同(极致差距)
  • 栈:效率极致,CPU 寄存器级操作,分配释放仅需移动栈顶指针,无任何内核开销、无任何内存查找,耗时为「纳秒级」;栈是 CPU 缓存的优先缓存区,访问速度极快;
  • 堆:效率低下,系统调用级操作,分配释放需要内核在物理内存中查找空闲块、更新页表、维护内存链表,耗时为「微秒级」;堆内存不在 CPU 缓存的优先区,访问速度慢于栈。
内存碎片问题不同(内存池核心痛点)
  • 栈:完全无内存碎片,栈的内存是连续的栈帧,分配释放严格遵循「先进后出 (LIFO)」,内存块不会离散,永远规整;
  • 堆:极易产生严重的内存碎片 ,堆的内存是随机分配、随机释放的,频繁malloc/free会导致堆中出现大量「无法复用的小空闲块」,即内存碎片;碎片过多会导致「内存泄漏」「内存耗尽但无大块可用内存」,这是内存池要解决的核心问题
6. 内存初始化方式不同
  • 栈:未初始化,栈上的局部变量默认是「随机的脏数据」(栈中残留的旧数据),必须手动初始化后使用;
  • 堆:malloc申请的堆内存是未初始化的脏数据calloc申请的堆内存会被内核置 0;mmap申请的匿名内存块默认被内核置 0。
7. 内存共享性不同
  • 栈:完全私有,栈是线程独占的内存区,每个线程有自己独立的栈,其他线程 / 进程无法访问;
  • 堆:进程内共享,同一个进程的所有线程共享同一份堆空间,线程间可通过堆内存通信;进程间可通过「共享内存 (mmap 实现)」共享堆内存。
8. 内存申请的粒度不同
  • 栈:细粒度小内存申请,栈帧的大小是固定的(函数的局部变量 + 参数),每次申请的内存都是 KB 级的小内存;
  • 堆:粗粒度大内存申请,程序员通常申请 MB 级的大内存,适合存放大型数据结构(数组、链表、对象池)。
9. 异常风险不同
  • 栈:仅存在「栈溢出」一种风险,触发即崩溃,无其他隐患;
  • 堆:风险繁多,内存泄漏、野指针、内存越界、双重释放、内存碎片,任何一个都会导致进程崩溃或数据错乱,是 C/C++ 程序的主要 bug 来源。
10. 底层支撑不同
  • 栈:由内核的「线程栈管理机制」支撑,无需系统调用,纯用户态操作;
  • 堆:由内核的「内存管理子系统」支撑,申请释放需要brk/mmap/munmap系统调用,需要陷入内核态。

栈:下生长、内核管、小而快、无碎片、自动回收;

堆:上生长、程序员管、大而慢、多碎片、手动回收。

三、mmap 和 brk 的核心区别

malloc不是系统调用,只是 glibc 的库函数 ,而malloc的底层,就是封装了 Linux 内核的两个核心内存申请系统调用:brk/sbrkmmap

二者都是 Linux 内核提供的内存申请 / 释放的系统调用 ,返回值都是「申请到的虚拟内存首地址」,失败返回MAP_FAILED(-1),均为「用户态→内核态」的陷门操作,有内核开销,但比 malloc 的开销更可控。

1. brk/sbrk 系统调用:操作「堆区」的内存申请
cpp 复制代码
#include <unistd.h>
// 核心功能:修改进程的堆顶指针brk,扩展/收缩堆的大小
void *brk(void *addr);
void *sbrk(intptr_t increment); // increment>0:扩容堆,<0:缩容堆

核心原理

  • brk 是「堆区的唯一入口」,内核为每个进程维护一个brk指针,指向堆的最顶端
  • 调用brk(addr)时,内核将堆顶指针移动到addr地址,指针移动的区间就是申请到的堆内存
  • sbrk(n)是 brk 的封装,直接让堆顶指针「向后移动 n 字节」,返回移动前的堆顶地址;
  • 本质:brk 是对堆区的「整块伸缩」,申请的内存是「连续的、与原堆区拼接的」,属于堆的一部分
2. mmap 系统调用:操作「内存映射区」的内存申请
cpp 复制代码
#include <sys/mman.h>
// 核心功能:在内存映射区,创建一块独立的、匿名的内存映射区(无文件关联)
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length); // 释放mmap申请的内存

// 申请size字节的匿名、可读写、私有内存块(内存池标准用法)
void* p = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

核心原理

  • mmap 的核心是「内存映射」,在进程的内存映射区(堆和栈之间) 申请一块独立的、离散的、与堆区无关联的虚拟内存块
  • 内存池用的是「匿名映射 (MAP_ANONYMOUS)」,无文件关联,申请的内存是纯内存块;
  • 释放时调用munmap,直接释放整块内存,无任何残留;
  • 本质:mmap 是「独立内存块的映射」,申请的内存是离散的、独立的,属于内存映射区,与堆区完全解耦
区别 1:申请的内存「归属区域不同」
  • brk:申请的内存属于 进程堆区,是堆的一部分,内存地址与原堆区连续;
  • mmap:申请的内存属于 进程内存映射区,是独立的内存块,地址与堆区、栈区均无关联,离散分布。
区别 2:内存「归还内核的效率 / 能力不同」
  • brk:申请的内存,释放后很难归还给操作系统 !brk 扩容堆后,若释放堆中的「中间内存块」,内核不会将这部分内存归还给系统,只会标记为「空闲」,供后续 malloc 复用;只有当堆顶的内存块被释放 ,且堆顶指针可以向前移动时,这部分内存才会归还给系统。这种特性导致:频繁 brk 申请释放会造成「堆内存膨胀」,内存只进不出,物理内存被持续占用,最终内存泄漏
  • mmap:申请的内存,释放后立即归还给操作系统 !mmap 申请的是独立的内存块,调用munmap(addr, size)后,内核会立即将这块内存的虚拟地址映射关系删除,物理内存直接归还给系统,进程的虚拟内存占用会立刻减少,无任何内存残留,无内存膨胀问题
区别 3:内存碎片问题的严重程度不同
  • brk:极易产生严重的内存碎片!brk 申请的是连续的堆内存,频繁的申请、释放会让堆中充满「无法复用的小空闲块」,碎片会越积越多,最终导致「堆内存耗尽但无大块可用内存」;
  • mmap:几乎无内存碎片问题 !mmap 申请的是独立的、大块的内存块,内存池会在这块内存中自主切分、管理小内存块,释放时是「整块释放」,内核层面无碎片,内存池内部的碎片可通过 SizeClass 对齐策略控制(你之前的 SizeClass 就是干这个的)。
区别 4:内存申请的「粒度 / 上限」不同
  • brk:适合小内存、连续内存的申请,申请的内存是堆的延伸,地址连续,但堆的大小受限于「堆和 mmap 区之间的空闲空间」,无法申请超大内存;
  • mmap:适合大内存、离散内存的申请,内存映射区是 TB 级的空间,几乎无上限,内存池的「大块内存申请」(如 1MB/4MB/64MB)全部用 mmap,完美适配。
区别 5:内存初始化方式不同
  • brk:申请的堆内存是「未初始化的脏数据」,内核不会做任何清零操作,内存中是之前的残留数据;
  • mmap:申请的匿名内存块 (MAP_ANONYMOUS) 会被内核自动置 0,内存是干净的,无需手动初始化,内存池使用时更安全。
区别 6:内存访问的灵活性不同
  • brk:申请的内存是连续的,只能按「堆顶伸缩」的方式扩容,无法申请离散的内存块;
  • mmap:可申请任意大小、任意数量的离散内存块,内存地址无连续要求,内存池可按需申请多块内存,灵活管理。
  • 开销:brk 的系统调用开销略小于 mmap(少了页表映射的步骤),但差距极小;
  • 共享性:mmap 可通过MAP_SHARED实现进程间内存共享,brk 无此能力;
  • 适用场景:brk 适合小内存频繁申请,mmap 适合大内存一次性申请。
    brk:堆区伸缩、连续内存、归还困难、碎片严重、适合小内存;

mmap:映射区独立块、离散内存、归还即时、无碎片、适合大内存。

四、内存池项目中,Linux 下为什么用「mmap」而不是直接用「malloc」?

malloc 不是系统调用,是 glibc 库的封装函数!glibc 的 malloc 底层做了这些事:

  1. 申请「小内存(默认 < 128KB)」:调用brk扩容堆区,从堆中分配内存;
  2. 申请「大内存(默认≥128KB)」:调用mmap申请匿名内存块;
  3. malloc 会维护一个「内存缓存池」,释放的内存不会立即归还给内核,而是缓存起来供后续复用;这也是 malloc 的核心缺陷来源 ------封装带来了便利,但牺牲了「可控性、效率、碎片控制」
原因 1:mmap 申请的内存「释放后立即归还给内核」,解决 malloc 的「内存膨胀 / 内存泄漏」痛点

这是内存池选用 mmap 的第一硬性原因 ,也是 malloc 的致命缺陷

  • malloc 的缓存机制:free 释放的内存,malloc 不会归还给内核,而是缓存到自己的空闲链表中,供后续 malloc 复用;这会导致进程的「虚拟内存占用只增不减」,即使内存池释放了内存,物理内存也不会被回收,造成内存膨胀,长期运行的服务器会因内存耗尽崩溃;
  • mmap 的优势:调用munmap释放内存后,内核立即回收物理内存,进程的虚拟内存占用立刻减少,无内存膨胀、无内存泄漏风险,完美匹配服务器「长期运行、高并发」的场景。
原因 2:mmap 几乎无内存碎片,malloc 的堆碎片问题无法解决

内存池的核心设计目标之一就是解决内存碎片问题,而 malloc 的碎片问题是「先天性的、无法根治」:

  • malloc 的小内存用 brk 申请,堆区的频繁申请释放会产生大量内存碎片,malloc 的内置碎片整理算法效率低下,碎片会越积越多;
  • mmap 申请的是「大块独立内存块」,内存池在这块内存中自主切分、管理小内存块 (通过你的 SizeClass 对齐策略),释放时是整块释放,内核层面无碎片;内存池内部的碎片可通过对齐策略、空闲链表管理控制,碎片率极低
原因 3:mmap 的内存申请「可控性极致」,malloc 的封装带来不可控的额外开销

内存池是高性能组件,对内存的申请、释放、对齐、管理有极致的可控性要求,而 malloc 的封装会带来「不可控的黑盒开销」:

  • malloc 的额外开销:malloc 会在每个申请的内存块头部,额外分配「内存控制块 (meta-data)」(如块大小、空闲标记、链表指针),占用 4~16 字节的内存;频繁申请小内存时,这些控制块的开销会被无限放大,内存利用率暴跌;
  • mmap 的可控性:mmap 申请的是「纯净的内存块」,无任何额外控制块,内存池可以自主设计元信息(如空闲链表节点、SizeClass 标记),按需分配,内存利用率 100% 可控;同时 mmap 的申请参数可自定义(对齐、权限),完美匹配内存池的 SizeClass 对齐策略。
原因 4:mmap 适合「大块内存申请」,malloc 的大内存申请有阈值限制

内存池项目中,核心逻辑是「预申请大块内存,切分复用小内存」,这正是 mmap 的强项:

  • 内存池的标准做法:一次性用 mmap 申请「1MB/4MB/64MB」的大块内存,然后在这块内存中切分成 8B/16B/32B 等定长小内存块(你的 SizeClass),供业务层申请;
  • malloc 的问题:malloc 的大内存阈值是 glibc 硬编码的(默认 128KB),无法自定义;申请超过阈值的内存时才会调用 mmap,申请小内存时用 brk,碎片化严重;且 malloc 的大块内存申请会被缓存,无法及时归还内核。
原因 5:mmap 的内存是「匿名映射且自动置 0」,更安全,malloc 的内存是脏数据
  • mmap 申请的匿名内存块,内核会自动将内存内容置 0,内存池分配的内存是干净的,无需手动初始化,避免了脏数据导致的程序 bug;
  • malloc 申请的堆内存是未初始化的脏数据,需要手动调用 memset 置 0,增加了额外的开销,也容易因忘记初始化导致数据错乱。
原因 6:mmap 的内存申请「无地址连续限制」,malloc 的堆内存是连续的,易触顶
  • brk 申请的堆内存是连续的,堆的扩容会受限于「堆和 mmap 区之间的空闲空间」,当堆扩容到一定程度后,会无法继续申请内存;
  • mmap 申请的内存是离散的,内存映射区是 TB 级的空间,几乎无上限,内存池可以按需申请多块内存,拼接成更大的内存池,无触顶风险。
原因 7:malloc 的线程安全锁开销大,mmap 的线程安全可控
  • glibc 的 malloc 是线程安全的,但为了保证线程安全,malloc 在申请释放内存时会加「全局互斥锁」,高并发场景下,多个线程竞争 malloc 的锁会导致性能暴跌
  • mmap 的线程安全由内存池自主控制:内存池用「自旋锁」保护空闲链表,锁的粒度极小,临界区极短,高并发下的性能远超 malloc 的全局锁。
原因 8:mmap 可实现「内存池的地址空间隔离」,提升稳定性
  • 内存池用 mmap 申请的多块内存,是离散的独立内存块,不同的内存块之间地址隔离;即使某块内存出现越界,也只会污染当前块,不会影响其他内存块和进程的核心区域;
  • malloc 的堆内存是连续的,内存越界会直接污染堆的其他区域,导致进程崩溃,稳定性差。
工业级内存池的「实际混用策略」
  • 大块内存(≥128KB) :用mmap申请,释放时munmap归还给内核,核心逻辑,无碎片、无膨胀;
  • 小块内存(<128KB) :在 mmap 申请的大块内存中切分复用,通过 SizeClass 对齐策略管理,空闲链表维护,无需再次调用系统调用;
  • 极致小内存(如 8B/16B):用内存池的 slab 分配器管理,无任何系统调用开销,效率远超 malloc;

结论:内存池的核心是「用 mmap 申请大块内存做池,自主管理小块内存 」,这也是「内存池」的本质 ------池化内存,减少系统调用,控制碎片,提升效率

相关推荐
m0_748252382 小时前
Ruby 数据类型概述
开发语言·mysql·ruby
珠穆峰2 小时前
linux清理缓存命令“echo 3 > /proc/sys/vm/drop_caches”
java·linux·缓存
天天睡大觉2 小时前
Python学习9
开发语言·python·学习
2301_797312262 小时前
学习Java39天
开发语言·python·学习
cllsse2 小时前
堡垒机下载安装
运维
Xの哲學2 小时前
Linux自旋锁深度解析: 从设计思想到实战应用
linux·服务器·网络·数据结构·算法
zzxxlty2 小时前
kafka C++ 和 java端计算分区ID不一致排查
java·c++·kafka
Reenrr2 小时前
C++学习
开发语言·c++·学习
晚风吹长发2 小时前
深入理解Linux中用户缓冲区,文件系统及inode
linux·运维·算法·链接·缓冲区·inode