内存管理-sbrk()/brk()、mmap()/munmap()

本文深入探讨了内存管理中的sbrk()和brk()函数,它们用于动态内存分配,sbrk()方便分配,brk()利于释放。此外,还介绍了mmap()和munmap(),它们提供更高级的内存映射功能,并讨论了如何使用这些函数来管理内存,特别是在存储结构化数据如员工信息时。最后,提到了系统调用的概念及其对程序性能的影响。

1 sbrk()/brk()

sbrk()/brk()底层都维护了一个位置
sbrk(increment)
  当 increment 为正时,位置向后移动increment字节,同时返回移动之前的位置,相当于分配内存。
  当 increment 为负时,位置向前移动increment字节,相当于释放内存,其返回值没有实际意义。
  当 increment 为零时,不移动位置,只返回当前位置。
这里写图片描述

sbrk.c
#include <stdio.h>
#include <unistd.h>

int main(){
  void* p1 = sbrk(4);//分配4字节内存
  void* p2 = sbrk(4);//映射了一个内存页
  void* p3 = sbrk(4);  
  void* p4 = sbrk(4);
  printf("p1=%p,p2=%p,p3=%p,p4=%p\n",p1,p2,p3,p4);
  sbrk(-4);//释放4字节
  sbrk(-8);
  void* cur = sbrk(0);//返回当前位置
  printf("cur=%p\n",cur);
  printf("pid=%d\n",getpid());
  sbrk(4093);//4093+4 = 4097字节,超了
  printf(
<think>嗯,用户想要深入了解Linux系统中brkmmap的内存申请与释放机制。从引用内容来看,用户已经查阅了相关资料,但需要更系统的解释。用户可能正在学习Linux内存管理,或者是遇到相关开发问题需要原理支持。考虑到引用中包含了glibc实现细节和代码示例,用户的技术水平应该在中高级层次,需要深度机制说明而非基础概念。先拆解核心问题:1)brk/sbrk机制原理2)mmap/munmap机制原理3)两者差异对比4)释放机制的关键区别。需要特别注意用户提到的“申请与释放机制”这个关键词,意味着要强调内存操作的完整生命周期。引用[2]提供的重要信息是进程初始内存布局,这是理解brk的基础。引用[3]明确指出brk释放只需调整堆顶指针的特性,这个细节对理解glibc的free实现很关键。引用[4]展示的mmap函数原型需要结合其使用场景说明。要注意纠正引用[3]的技术表述:现代glibc的free实现其实同时维护了brkmmap两种释放策略,原文说释放用brk的说法不够准确。需要补充说明阈值128KB的分配策略转变,这个在引用[4]里有暗示但未明说。特别需要强调mmap的非连续特性与brk的连续性,这关系到内存碎片问题。用户可能没意识到:频繁小内存分配更适合brk,但大块或独立释放的需求必须用mmap。那些C代码示例(如引用[5]的malloc测试)可以转化为文字说明,避免代码块打断技术原理的连贯性。不过要保留关键参数:比如munmap必须完整释放mmap的整个区域,这点和brk的局部释放完全不同。最后结构化呈现时,把brkmmap分成独立的原理章节,但对比表格要前置,让用户先建立整体认知框架。内存布局图示很必要,但文字描述要确保能独立传达空间关系。</think> 好的,我们来详细解析一下 Linux 系统中 `brk`/`sbrk` 和 `mmap`/`munmap` 这两种核心的内存申请与释放机制,理解它们是深入理解 `malloc`/`free` 等库函数工作原理的基础。 ## 核心概念:虚拟内存与物理内存 在理解具体机制前,必须明确区分: 1. **虚拟内存 (Virtual Memory)**:进程视角看到的连续内存地址空间。每个进程拥有自己独立的虚拟地址空间(如 0 到 4GB/128TB)。 2. **物理内存 (Physical Memory)**:实际的 RAM 芯片。 3. **内存映射 (Memory Mapping)**:操作系统通过**页表 (Page Table)** 将进程的虚拟内存地址映射到物理内存地址或磁盘上的文件(交换空间/SWAP)。当进程访问虚拟地址时,硬件(MMU)根据页表将其转换为物理地址。 4. **按需调页 (Demand Paging)**:虚拟内存页最初只有映射关系,没有分配物理内存。只有当进程真正访问该页时,内核触发**缺页异常 (Page Fault)** 才会分配物理页帧并填充数据。 `brk` 和 `mmap` 都是用于**操作进程的虚拟内存布局**的系统调用,用于 **请求操作系统建立或修改虚拟地址到物理内存(或文件)的映射关系**。释放机制则是撤销这些映射关系。 --- ## 一、 brk / sbrk 机制 ### 核心思想 * 通过调整**程序间断点 (Program Break / `brk`) 的位置**来管理进程 **堆内存 (Heap)** 的大小。 * 程序间断点 (`brk`) 位于堆顶,初始位置紧挨着未初始化数据段(`.bss`)。`brk` 指针之前的内存(向上增长,低地址到高地址)是进程可用的堆内存空间。 * `brk`/`sbrk` 管理的是**单一片连续的虚拟内存区域**(堆)。下图展示了一个初始内存布局[^2]: ``` ... (低地址) | 代码段 (.text) | <- 只读程序代码 | 已初始化数据段 (.data)| <- 全局初始化变量 | 未初始化数据段 (.bss) | <- 全局未初始化变量 |-------------------| <- 初始 *brk* (&_edata)[^2] | 堆 (Heap) | <- brk/sbrk 操作区域 (向上增长) |-------------------| <- 当前 *brk* | (空白) | |-------------------| | 内存映射区域 | <- mmap 操作区域 (如库文件) |-------------------| | 栈 (Stack) | <- (向下增长) ... (高地址) ``` *(注:现代 Linux 中堆和内存映射区域(`mmap`)之间可能有其他段,且地址空间布局随机化(ASLR)会使位置偏移,但基本原理相同)* ### 1. 申请内存 (Allocation) * **`int brk(void *addr);`** * 将程序的间断点(`brk`)直接设置到指定的虚拟地址 `addr`。 * 如果 `addr` > 当前 `brk`,则**扩展堆空间**。扩展的区域仅仅是建立了**虚拟地址到“无内容”的映射**(此时物理页通常尚未分配)。 * 如果 `addr` < 当前 `brk`,则**收缩堆空间**(释放内存)。 * 成功返回 0,失败返回 -1。 * **`void *sbrk(intptr_t increment);`** * 将程序的间断点增加 `increment` 字节(可以为负值)。 * 本质是 `sbrk(0)` 获取当前 `brk` 值,然后调用 `brk(old_brk + increment)`。 * 成功返回**增加前**的旧 `brk` 值(即可用堆空间的起始地址),失败返回 `(void *)-1`。 * **如何分配物理内存?** * `brk`/`sbrk` 调用只负责**扩展堆的虚拟地址范围**。内核仅为扩展的虚拟地址建立页表项,标记为“未分配物理页”。 * 当进程**第一次读写**新分配堆内存区域的某个虚拟页时,触发**缺页异常 (Page Fault)**。 * 内核的缺页处理程序检测到是合法访问(在 `brk` 范围内),则分配一个**空闲的物理页帧 (Page Frame)**。 * 内核将缺页发生的虚拟页映射到新分配的物理页帧。 * 进程继续执行指令,就好像物理内存早已存在一样。 ### 2. 释放内存 (Deallocation) * 通过调用 `brk` 或 `sbrk`,将 `brk` 指针 **下调** 到之前某个分配的位置。 * **发生了什么?** * 内核将 `brk` 指针下调后所包含的地址范围(从新 `brk` 到旧 `brk`)的虚拟页,标记为**无效**或**未映射**。 * 如果这些被释放的虚拟页之前已经被映射到物理内存页帧: * 内核会**移除**虚拟页到物理页帧的映射关系。 * 内核会将包含数据的物理页帧(如果内容被修改过且未同步到后备存储)**回写到交换空间 (SWAP)**(如果是文件映射则回写到文件)。 * 最后,物理页帧被**标记为空闲**,可供其他进程或本进程后续分配使用。 * **关键限制:释放必须连续且从堆顶开始** * `brk`/`sbrk` **只能释放堆顶部连续的内存区域**。不能释放堆中间已分配的内存块。 * 这导致堆的内部**容易产生碎片 (Fragmentation)**。即使堆中有很多小的空闲内存块,如果它们不在堆顶,也无法通过 `brk` 回收给操作系统,只能由 `malloc`/`free` 等库管理复用。 ### 3. 特点总结 * **管理区域**:堆内存。 * **虚拟内存布局**:单一、**连续**的虚拟地址区域(堆)。 * **申请特点**:通过上移 `brk` 指针扩展连续的虚拟地址范围(物理页按需分配)。 * **释放特点**:通过下移 `brk` 指针收缩堆空间,**只能释放堆顶的连续内存**。 * **碎片问题**:内部碎片严重。只能回收堆顶空闲空间。 * **效率**:适合分配中、小型,生命周期不太短(减少频繁移动 `brk` 带来的开销)的对象。分配操作相对简单(移动指针),释放操作也简单(如果释放堆顶)。 * **锁开销**:操作单一 `brk` 指针通常需要使用全局锁,在多线程环境中扩展堆时可能成为瓶颈。 * **常见使用者**:C 标准库 (`glibc`) 中的 `malloc` 对于 **中小块内存** (通常小于阈值,如 128KB) 的底层分配器。 --- ## 二、 mmap / munmap 机制 ### 核心思想 * 直接在进程的虚拟地址空间中 **创建或删除独立的、新的内存映射区域**。 * 映射区域 **不限于堆区**,通常位于堆区和栈区之间的 **“内存映射区域”** (参考上方图示)。 * 映射对象可以是: * **匿名内存 (Anonymous Memory)**:不关联任何文件,内容初始化为零(常见于 `malloc` 分配大块内存)。 * **文件 (File)**:将文件的一部分内容映射到内存(文件映射)。访问这段内存等同于读写文件。常见于动态库加载 (`ld.so`)、`mmap` 文件 I/O、共享内存等。 ### 1. 申请内存 (Allocation) * **`void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);`** [^1][^4] * `addr`: 建议的映射起始虚拟地址(通常设为 `NULL`,由内核决定)。 * `length`: 映射区域的长度(字节)。 * `prot`: 保护位,如 `PROT_READ`, `PROT_WRITE`, `PROT_EXEC`。 * `flags`: 关键标志位: * `MAP_ANONYMOUS` (`MAP_ANON`): 创建**匿名映射**(不关联文件)。此时 `fd` 被忽略(通常设为 -1)。 * `MAP_PRIVATE`: 创建写时复制(COW)映射,进程写操作不影响原文件或其他映射。 * `MAP_SHARED`: 共享映射(文件映射时写操作会更新文件)。 * `MAP_FIXED`: 严格要求映射到 `addr` 指定的地址(使用需谨慎)。 * `fd`: 被映射文件的文件描述符(匿名映射设为 -1)。 * `offset`: 文件的起始偏移量(字节)。 * **返回值**:成功时返回映射区域的起始虚拟地址,失败返回 `MAP_FAILED` (`(void *)-1`)。 * **物理内存分配?** * 内核在**内存映射区域**为进程分配指定 `length` 大小的**连续虚拟地址空间**。 * 内核建立虚拟地址到“无内容”的映射(匿名映射)或到文件位置(文件映射)的映射。 * **按需分配物理页**:同样是第一次访问该区域的虚拟页时触发缺页中断分配物理页(匿名页初始化为零;文件页从文件中读取数据填充物理页)。 ### 2. 释放内存 (Deallocation) * **`int munmap(void *addr, size_t length);`** [^1] * `addr`: 要释放区域的起始地址(必须是 `mmap` 返回的地址)。 * `length`: 要释放区域的长度。 * **返回值**:成功返回 0,失败返回 -1。 * **发生了什么?** * 内核**解除**指定虚拟地址范围 (`[addr, addr + length - 1]`) 的映射关系。 * 对于之前映射了物理页帧的虚拟页: * 移除页表项。 * 如果页的内容被修改过(是脏页)且是私有写时复制的页,脏页会被丢弃(不写回)。 * 如果页是共享文件映射(`MAP_SHARED`)的脏页,内核可能根据需要将脏页数据写回文件(不一定立即写回,有缓冲和回写策略)。 * 物理页帧被释放(解除映射后其引用计数减一,变为零时被系统回收)。 * **关键优势:独立释放** * `munmap` **可以独立释放之前 `mmap` 分配的任何一个区域**,不论其位置(比如内存映射区域中间的一块)或大小。 * 这使得 **每个 `mmap` 分配的区域在生命周期和释放上都是独立的**,大大减少了碎片问题(外部碎片)。 ### 3. 特点总结 * **管理区域**:独立的内存映射区域(通常在堆和栈之间)。 * **虚拟内存布局**:创建**独立、不连续**的虚拟内存区域。 * **申请特点**:直接创建一个新的、指定大小的虚拟内存映射区域(物理页按需分配)。 * **释放特点**:通过 `munmap` 可以**独立释放任何一个映射区域**,无论其位置或大小。 * **碎片问题**:每个映射区域独立分配和释放,**有效减少外部碎片**。 * **效率**: * 建立和销毁映射有较高开销(涉及内核数据结构操作)。 * 适合分配**大块内存**(`glibc` `malloc` 大块阈值常为 128KB 或更高)或生命周期较长/独立的对象。 * 避免了对单一 `brk` 的争用锁,在某些多线程大内存分配场景下表现更好。 * **常见使用者**: * `glibc` `malloc` 用于分配**大块内存**。 * 显式文件 I/O (`mmap` 文件)。 * 动态库加载器 (`ld.so`) 加载共享库代码和数据。 * 进程间共享内存 (`MAP_ANONYMOUS | MAP_SHARED`)。 * 其他需要独立内存区域的高级内存分配器。 --- ## 三、对比总结表 | 特性 | brk / sbrk | mmap / munmap | | :--------------------- | :-------------------------------------------- | :-------------------------------------------- | | **管理区域** | 堆 | 独立内存映射区 | | **虚拟内存布局** | **单一连续区域** (堆) | **多个独立不连续区域** (映射段) | | **申请 (扩张虚拟地址)**| 上移 `brk` 指针 | 创建新映射区域 (`mmap`) | | **释放 (收缩虚拟地址)**| **只能下移 `brk` 释放堆顶内存** | **可独立释放任何区域** (`munmap`) | | **物理页分配时机** | 第一次访问 (缺页中断) | 第一次访问 (缺页中断) | | **碎片问题** | **严重内部碎片**, 无法释放堆中非顶部空闲区 | **较少外部碎片**, 区域独立分配释放 | | **分配/释放效率** | 分配/释放(堆顶时)开销较小 | 分配/释放(系统调用、管理结构)开销较大 | | **多线程争用** | 操作单一 `brk` 时存在锁争用 | 无争用(独立区域) | | **典型使用场景(`glibc`)** | **中小块内存分配** (< 阈值, 如128KB) | **大块内存分配** (>= 阈值)、文件映射、共享内存 | --- ## 四、与 `malloc`/`free` 的关系 C 标准库(如 `glibc`)的内存分配器 (`malloc`, `calloc`, `realloc`, `free`) 并不是操作系统原语,而是**构建在 `brk`/`sbrk` 和 `mmap`/`munmap` 之上的一套复杂的用户级管理库**。 1. **请求内存**: * 当程序调用 `malloc(size)` 时: * 如果 `size` **小于**一个特定阈值(例如 128KB),`glibc` 的分配器通常从它用 `brk` 管理的堆空间中分配内存。它在这个大的连续堆空间内部进行精细管理(处理小块分配、回收和复用),避免频繁调用 `sbrk` 扩张堆。 * 如果 `size` **大于等于**该阈值,`glibc` 通常会直接调用 `mmap` 创建一个**新的、独立的匿名映射区域**来满足这个大内存请求。这样 `free` 的时候可以直接调用 `munmap` 归还整个区域。 2. **释放内存**: * 当程序调用 `free(ptr)` 时: * 如果 `ptr` 是由 `mmap` 分配的(大内存块),那么 `glibc` 最终会调用 `munmap` 来释放整个映射区域。 * 如果 `ptr` 是位于 `brk` 管理的堆中的小块内存,`glibc` 的分配器会将其标记为空闲,并放入其内部管理的**空闲链表 (Free List)** 中,以供后续分配复用。它**并不会立即**调用 `brk` 来缩减堆大小给操作系统。 3. **堆缩减**: * `glibc` 的分配器有一个**内存整理**的机制(通常在 `free` 时触发)。如果它发现堆顶有一大块连续的空闲内存(称为 “top chunk” 的顶部空间)变得足够大,并且这块内存的大小超过了某个阈值,分配器**才可能**调用 `brk` 将堆顶指针下移,把这部分物理内存真正释放给操作系统。 ### 结论 `brk` 和 `mmap` 是 Linux 内核提供的两种最基础的虚拟内存操作机制。`malloc`/`free` 等库函数利用它们实现高级的内存管理策略。了解 `brk`(连续堆管理,只适合尾部释放)和 `mmap`(独立区域管理,可独立释放)的核心差异,是理解现代内存分配器行为的关键。`glibc` 的分配器正是根据大小阈值(小内存用`brk`+内部管理,大内存用`mmap`)来平衡效率和碎片问题的精妙实现[^1][^2][^3][^4]。 [^1]: 引用内容: Linux 虚拟内存操作函数声明与概述。 [^2]: 引用内容: 进程内存布局基础,_edata 与堆顶。 [^3]: 引用内容: brk/sbrkmmap/munmap 的简化使用特征对比。 [^4]: 引用内容: mmap 函数参数详解,重点在文件/匿名映射标志。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值