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 申请大块内存做池,自主管理小块内存 」,这也是「内存池」的本质 ------池化内存,减少系统调用,控制碎片,提升效率

相关推荐
大树881 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠1 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
LDR0061 小时前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术1 小时前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript
码云数智-园园1 小时前
C++20 Modules 模块详解
java·开发语言·spring
霸道流氓气质1 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush41 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 小时前
Linux 11 动态监控指令top
linux
swordbob2 小时前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio
源分享2 小时前
Java线程同步的多种实现方法(非常详细)
java·开发语言·jvm