内存管理¶
本文你会学到:
- 进程的虚拟内存布局(Text、Data、BSS、Heap、Stack)
malloc/free的使用与实现原理brk/sbrk底层机制与 program breakmmap匿名映射与文件映射mprotect修改内存保护、mlock内存锁madvise提示内核优化内存使用- 内存分析工具(
pmap、vmstat、top等)
程序的内存布局¶
进程虚拟地址空间¶
每个 Linux 进程看到的是一片连续的虚拟地址空间,内核和 MMU 负责将其映射到物理内存。对于一个典型的 C 程序,虚拟内存从低地址到高地址分为以下几个段:
graph TD
subgraph 高地址
STACK["栈(Stack)<br>局部变量、函数调用<br>向下增长"]
GAP[""]
HEAP["堆(Heap)<br>动态分配(malloc)<br>向上增长"]
end
subgraph 数据段
BSS["BSS 段<br>未初始化的全局/静态变量<br>(运行时清零)"]
DATA["Data 段<br>已初始化的全局/静态变量"]
end
subgraph 代码段
TEXT["Text 段<br>程序机器指令<br>只读"]
end
subgraph 固定
FIXED["0x00000000<br>保留(NULL 指针陷阱)"]
end
FIXED --> TEXT
TEXT --> DATA
DATA --> BSS
BSS --> HEAP
HEAP --> GAP
GAP --> STACK
classDef regular fill:transparent,stroke:#0288d1,color:#adbac7,stroke-width:1px
classDef highlight fill:transparent,stroke:#f57c00,color:#adbac7,stroke-width:2px
class HEAP,STACK highlight
class TEXT,DATA,BSS,FIXED,GAP regular
各段的作用:
| 段 | 存储内容 | 特点 |
|---|---|---|
| Text(代码段) | 程序的机器指令 | 只读,可共享,禁止修改 |
| Data(数据段) | 已初始化的全局变量和静态变量 | 编译时确定值,可读写 |
| BSS(Block Started by Symbol) | 未初始化的全局/静态变量 | 程序加载时由内核清零 |
| Heap(堆) | 运行时动态分配的内存 | 向高地址增长,由 brk/sbrk/mmap 管理 |
| Stack(栈) | 局部变量、函数参数、返回地址 | 向低地址增长,自动分配释放 |
可以通过以下命令查看可执行文件的段信息:
size 命令输出示例
运行时可以通过 /proc/PID/maps 查看进程实际的内存布局:
输出中的每一行对应一个虚拟内存区域,格式为:
权限字段含义:r=可读、w=可写、x=可执行、p=私有(Copy-on-Write)、s=共享。
堆内存分配¶
malloc/free 基础¶
C 程序在堆上分配内存最常用的接口是 malloc 函数族:
| malloc 函数族声明 | |
|---|---|
malloc 返回 void*,可以赋给任意类型的指针。分配的内存未经初始化:
| malloc 使用示例 | |
|---|---|
关键规则:
malloc失败返回NULL,必须检查返回值free(NULL)是安全的(什么都不做)- 释放后继续使用指针(dangling pointer)会导致未定义行为
- 禁止:多次释放同一块内存、释放非
malloc返回的指针
分配失败处理
calloc 和 realloc¶
calloc:为数组分配内存并自动初始化为 0:
realloc:调整已分配内存块的大小:
注意:realloc 可能会移动内存块,导致原指针失效。不要直接将返回值赋给原指针,否则 realloc 失败时原指针变为 NULL,造成内存泄漏。
alloca:栈上分配¶
alloca 从栈上分配内存,函数返回时自动释放,速度比 malloc 快得多:
限制:
- 不能用于函数参数列表中
- 栈溢出无法通过返回值检测(可能导致 SIGSEGV)
- 不可移植(非 POSIX 标准,但多数 UNIX 支持)
底层分配机制¶
brk/sbrk:调整 program break¶
堆的顶部边界称为 program break。初始时它与数据段末尾重合。分配堆内存本质上就是上移这个边界:
sbrk(0)返回当前 program break 位置(不改变它),用于监控堆大小brk和sbrk是 SUSv2 标记为 Legacy 的接口,SUSv3 已删除malloc内部使用sbrk来分配大块内存,但更倾向于使用mmap(见下文)
mmap 匿名映射¶
现代 malloc 实现(如 glibc 的 ptmalloc)对于大块内存分配(默认 >128 KB)会使用 mmap 创建匿名映射,而非调整 program break。这样做的好处是:
- 释放时可以直接
munmap归还给操作系统 - 不会造成堆碎片
- 不同大小的大块分配互不干扰
glibc ptmalloc 概述¶
glibc 的 malloc 实现(ptmalloc)采用以下策略:
| 分配大小 | 分配方式 | 释放行为 |
|---|---|---|
| 小内存(默认 ≤128 KB) | sbrk 调整堆 |
放入空闲列表,不归还 OS |
| 大内存(默认 >128 KB) | mmap 匿名映射 |
munmap 直接归还 OS |
可通过 mallopt 调整这一阈值:
虚拟内存控制¶
mprotect:修改内存保护¶
mprotect 修改一块虚拟内存区域上的访问权限:
addr必须是系统分页大小(通常 4096 字节)的整数倍prot是以下值的位或:PROT_NONE(拒绝访问)、PROT_READ、PROT_WRITE、PROT_EXEC- 违反保护会产生
SIGSEGV信号
mlock/mlockall:内存锁¶
内存锁防止指定内存被交换到磁盘。这对安全敏感数据(如密码)和实时应用很有用:
mlockall 的 flags 参数:
| 标志 | 含义 |
|---|---|
MCL_CURRENT |
锁定当前所有已映射的分页 |
MCL_FUTURE |
锁定将来所有映射的分页 |
非特权进程受 RLIMIT_MEMLOCK 限制(默认 8 个分页,约 32 KB):
| 查看内存锁限制 | |
|---|---|
内存锁的关键语义:
- 不可继承:
fork创建的子进程不继承父进程的内存锁 - exec 后失效:
exec会丢弃内存锁 - 不叠加:同一区域多次
mlock只需一次munlock即可解锁
madvise:建议后续内存使用模式¶
madvise 向内核提供内存使用模式的建议,帮助内核优化 I/O 和缓存行为:
| advice 值 | 含义 | 内核优化行为 |
|---|---|---|
MADV_NORMAL |
默认行为 | 适度预读 |
MADV_RANDOM |
随机访问 | 关闭预读,每次只取少量 |
MADV_SEQUENTIAL |
顺序访问一次 | 激进预读,访问后释放 |
MADV_WILLNEED |
即将访问 | 提前加载到内存 |
MADV_DONTNEED |
不再需要 | 丢弃(MAP_PRIVATE 下会丢失修改) |
mincore:检查内存驻留性¶
mincore 报告指定虚拟地址范围内的分页是否驻留在物理内存中:
addr必须分页对齐vec数组每个字节的最低有效位表示对应分页是否在内存中
内存映射¶
mmap:文件映射 vs 匿名映射¶
mmap 可以在进程虚拟地址空间创建两种类型的映射:
| 类型 | 用途 | 示例 |
|---|---|---|
| 文件映射 | 将文件内容映射到内存 | 文件 I/O、共享库加载 |
| 匿名映射 | 分配大块内存(不关联文件) | malloc 大内存、进程间共享内存 |
文件映射示例:
共享 vs 私有映射¶
| 标志 | 写操作影响 | 对其他进程可见 | 典型用途 |
|---|---|---|---|
MAP_PRIVATE |
Copy-on-Write,不写回文件 | 不可见 | 读取配置文件、调试 |
MAP_SHARED |
直接写回文件(或共享内存) | 可见 | 共享内存 IPC、文件 I/O |
graph LR
subgraph 进程A
A_PRIVATE["MAP_PRIVATE<br>写时复制副本"]
A_SHARED["MAP_SHARED<br>直接写入"]
end
subgraph 进程B
B_PRIVATE["MAP_PRIVATE<br>写时复制副本"]
B_SHARED["MAP_SHARED<br>直接写入"]
end
subgraph 物理内存
FILE["文件内容<br>(物理页)"]
PRIVATE_COPY["修改后的副本"]
end
A_PRIVATE -- 首次写 --> PRIVATE_COPY
B_PRIVATE -- 首次写 --> PRIVATE_COPY
A_SHARED -- 写 --> FILE
B_SHARED -- 写 --> FILE
FILE -- 读取 --> A_PRIVATE
FILE -- 读取 --> B_PRIVATE
classDef regular fill:transparent,stroke:#0288d1,color:#adbac7,stroke-width:1px
classDef orange fill:transparent,stroke:#f57c00,color:#adbac7,stroke-width:1px
classDef green fill:transparent,stroke:#388e3c,color:#adbac7,stroke-width:1px
class A_PRIVATE,B_PRIVATE,PRIVATE_COPY regular
class A_SHARED,B_SHARED orange
class FILE green
MAP_PRIVATE 的写时复制
写时复制意味着修改 MAP_PRIVATE 映射不会影响文件。内核会复制被修改的分页,两个进程各自持有独立副本。这是 fork 和共享库加载的核心机制。
实用工具¶
pmap:查看进程内存映射¶
输出中包含每个映射的区域地址、大小、权限、映射路径,可以直观了解进程的内存分布。
vmstat:虚拟内存统计¶
关键列:
| 字段 | 含义 |
|---|---|
swapd |
已使用的交换空间 (KB) |
free |
空闲内存 (KB) |
buff |
缓冲区缓存 (KB) |
cache |
页面缓存 (KB) |
si / so |
换入 / 换出速率 (KB/s) |
top/htop 内存列¶
| top 内存信息 | |
|---|---|
top 中内存相关的关键列:
| 列 | 含义 |
|---|---|
VIRT |
虚拟内存总量(包括未分配物理页的部分) |
RES |
常驻物理内存大小 (RSS) |
SHR |
共享内存大小 |
MEM% |
物理内存使用百分比 |
smem:更准确的内存统计¶
smem 报告 PSS(Proportional Set Size),比 RSS 更准确地反映共享内存的实际消耗:
OOM Killer¶
当系统内存耗尽时,Linux 内核会调用 OOM Killer(Out-Of-Memory Killer)选择并终止一个进程来释放内存。选择策略基于 oom_score(权重):
| OOM Killer 相关操作 | |
|---|---|
选型指南¶
| 需求 | 推荐 API | 原因 |
|---|---|---|
| 常规小内存分配 | malloc |
标准、易用、自动管理 |
| 数组分配需清零 | calloc |
自动初始化 0 |
| 调整已分配大小 | realloc |
避免手动复制 |
| 临时小内存 | alloca |
自动释放、速度最快 |
| 大块内存(>128 KB) | mmap 匿名映射 |
释放时立即归还 OS |
| 文件内容访问 | mmap 文件映射 |
简化 I/O、利用页面缓存 |
| 共享内存 IPC | mmap MAP_SHARED |
高性能、标准化 |
| 保护敏感数据 | mlock + mprotect |
防换出、防未授权访问 |