linux内存管理
数据结构
物理内存(Physical Memory)
- 定义:物理内存是计算机硬件实际存在的内存芯片(如DRAM),直接由CPU通过物理地址访问。
- 特点:
- 容量有限,地址空间连续(从0到最大物理地址)。
- 由操作系统直接管理,分配方式包括连续分配(如分段)或非连续分配(如分页)。
- 进程无法直接使用物理地址(避免安全问题和管理复杂度)。
虚拟内存(Virtual Memory)
- 定义:虚拟内存是操作系统为每个进程提供的抽象内存空间,通过地址转换机制映射到物理内存或磁盘(如交换空间)。
- 特点:
- 每个进程拥有独立的虚拟地址空间(如32位系统为4GB),隔离且安全。
- 支持分页(Page)或分段(Segment)机制,将虚拟地址划分为固定/可变大小的单元。
- 允许部分数据驻留在磁盘(按需加载),实现内存扩展和多任务隔离。
虚拟内存与物理内存的关联
(1)页表(Page Table)
- 作用:存储虚拟页到物理页帧的映射关系,由操作系统维护。
- 结构:
- 多级页表(如x86的4级页表)节省空间,支持稀疏地址空间。
- 页表项(PTE)包含物理页帧号、权限位(读/写/执行)、有效位(是否在内存)等。
- 地址转换:
- CPU访问虚拟地址 → MMU通过页表查询物理地址 → 访问内存。
- 若页不在内存(缺页异常),触发操作系统从磁盘加载。
(2)TLB(Translation Lookaside Buffer)
- 作用:缓存频繁使用的页表项,加速地址转换。
- 特点:
- 硬件实现的高速缓存(通常为64-128条目)。
- 命中时直接获取物理地址,未命中时查页表(可能触发TLB重填)。
(3)MMU(Memory Management Unit)
- 作用:硬件单元(集成在CPU中)负责虚拟到物理地址的转换。
- 工作流程:
- CPU发出虚拟地址 → MMU优先查询TLB。
- TLB未命中 → MMU自动遍历页表(多级页表需多次访存)。
- 转换成功后更新TLB,并访问物理内存。
- 若页表项无效(缺页)或权限不足,触发异常由操作系统处理。
内存分配
物理内存
Buddy System
作用:管理大块连续物理内存(通常以页为单位)
数据结构 : 空闲链表数组:每个数组元素对应一种块大小(如 1, 2, 4, 8 页),存储该大小的空闲块链表。
算法步骤
- 分配
找到满足大小的最小块(2n≥请求大小)。
若对应链表为空,递归向上分割更大的块。 - 释放
检查伙伴块是否空闲,若空闲则合并为更大的块。
时间复杂度:分配和释放均为 O(1)。
Slab
板坯(Slab), 将内存划分为固定大小的“板块”,用于高效管理对象缓存。
作用:高效管理内核对象(如 task_struct
)的内存分配。
数据结构
- Slab Cache:每个缓存对应一种对象类型,包含多个 Slab。
- Slab:一个连续的内存块,包含多个对象和元数据(如空闲对象链表)。
算法步骤
- 分配:从 Slab 的空闲对象链表中取出一个对象。
- 释放:将对象重新加入空闲链表,供后续重用。
优势:避免频繁初始化对象,减少内存碎片。
对比
特性 | 伙伴系统(Buddy System) | SLAB分配器 |
---|---|---|
主要用途 | 管理大块连续物理内存(通常以页为单位) | 管理小对象内存(如内核数据结构的频繁分配/释放) |
分配粒度 | 以页(如4KB)为基数,按2的幂次方分配(如4KB、8KB、16KB) | 以对象(如几十字节到几KB)为单位,精确匹配请求大小 |
典型场景 | 用户进程的匿名内存、文件缓存、大块DMA缓冲区 | 内核数据结构(如task_struct、inode、sk_buff等) |
碎片问题 | 外部碎片(通过合并相邻空闲块缓解) | 内部碎片(因对象固定大小可能浪费部分空间) |
虚拟内存
分为用户态和内核态,在32位上分别为3G:1G
kmalloc
-
本质:
- 内核提供的函数,用于在内核态动态分配连续的虚拟内存(映射到物理内存)。
- 底层依赖伙伴系统(大块)或SLAB/SLUB(小块)。
-
特点:
-
物理连续:默认返回物理地址连续的内存(适用于DMA操作)。
-
大小限制:通常限制为单个页框的倍数(如4KB的整数倍)。
void *kmalloc(size_t size, gfp_t flags); // 如 GFP_KERNEL(可睡眠)、GFP_ATOMIC(原子上下文)
-
malloc
-
本质:
- C库(如glibc)提供的函数,分配用户态虚拟内存,可能不连续物理内存。
- 底层通过
brk
或mmap
系统调用向内核申请内存。
-
特点:
-
虚拟连续:返回的地址在用户态虚拟空间连续,物理内存可能不连续。
-
灵活大小:可分配任意大小(由C库的内存池管理)。
void *malloc(size_t size); // 如 malloc(1024)
-
对比
维度 | kmalloc (内核态) | malloc (用户态) |
---|---|---|
运行层级 | 内核态(Ring 0) | 用户态(Ring 3) |
底层实现 | 基于 SLAB分配器(小对象)或 伙伴系统(大块) | 基于 ptmalloc/jemalloc 等库,底层通过 brk /mmap 系统调用向内核申请内存 |
内存来源 | 直接管理物理内存(可能涉及DMA、高端内存等区域) | 操作虚拟内存,由内核分配物理页并映射到进程地址空间 |
分配粒度 | 通常按 字节对齐(如GFP_KERNEL 标志控制) | 按 页(4KB)或更小单位(依赖库实现) |
失败行为 | 可能直接panic(如GFP_ATOMIC 失败) | 返回NULL ,进程可处理 |
并发安全 | 需显式处理锁/原子上下文(如GFP_ATOMIC ) | 由库内部实现线程安全(如ptmalloc 的arena机制) |
使用场景 | 内核数据结构(如sk_buff 、task_struct ) | 用户进程的堆内存(如数组、对象) |
内存回收
基于内存交换的速率
回收策略
页面置换
作用:选择不常用的内存页写入交换空间(Swap)。
数据结构
- 活跃链表(Active List):存放最近被访问的页。
- 非活跃链表(Inactive List):存放候选回收的页。
算法步骤
- 新访问的页加入活跃链表尾部。
- 定期将活跃链表中较旧的页移动到非活跃链表。
- 优先回收非活跃链表中的页。
优化:通过“二次机会”策略避免频繁置换常用页。
常见
算法 | 核心思想 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
FIFO | 置换最早进入内存的页面 | 实现简单 | Belady异常(缺页率可能随帧增加而升高) | 早期系统,教学示例 |
LRU | 置换最久未使用的页面 | 符合局部性原理,缺页率低 | 实现复杂(需硬件支持或近似算法) | 通用场景(理论最优) |
Clock | 近似LRU,使用访问位轮询 | 开销低于LRU | 可能误判活跃页面 | 实际系统(如Linux) |
LFU | 置换使用频率最低的页面 | 适合长期热点数据 | 难以适应访问模式变化 | 数据库缓存 |
Optimal | 置换未来最长时间不使用的页面(理论) | 缺页率最低(理想情况) | 无法实际实现(需预知未来) |
平台
平台 | 页面置换算法 | 核心机制 | 特点 |
---|---|---|---|
Windows | 工作集 + 时钟算法(WS-Clock) | - 基于工作集模型(活跃页面优先保留) - 近似LRU的时钟算法(二次机会置换) | - 动态调整工作集大小 - 支持优先级内存分配(如前台进程优先) |
Linux | Clock(二次机会) + 交换优先级 | - 全局时钟扫描(kswapd 内核线程) - 支持多种置换策略(/proc/sys/vm/swappiness 调节) | - 支持NUMA优化 - 支持压缩内存(zswap)替代部分磁盘交换 |
macOS | 混合策略(FIFO + 压缩) | - FIFO为主,结合内存压缩技术 - 优先压缩非活跃内存(Jetsam机制回收内存) | - 对iOS/macOS统一优化 - 激进 |
内存压缩
作用:压缩内存页以减少物理内存占用。
数据结构
- 压缩池:存储压缩后的内存页。
算法
- 将候选页压缩后存入压缩池。
- 当需要访问时,解压缩并恢复为原始页。
优势:延迟触发交换操作,提高性能。
空间交换
作用:将不常用的内存页写入磁盘,释放物理内存。
数据结构
- 交换分区(Swap Partition):磁盘上的一个独立分区,专门用于交换空间。
- 交换文件(Swap File):磁盘上的一个文件,作为交换空间使用。
- 交换缓存(Swap Cache)记录已写入交换空间的内存页的状态(如脏页、访问时间等)。
- 页表项(PTE): 记录内存页是否在交换空间中,以及交换空间中的位置。
算法
- 页面选择:使用改进的 LRU 算法选择不常用的内存页作为候选。
- 写入交换空间:将选中的内存页写入交换分区或交换文件。更新页表项,标记该页在交换空间中。
- 读回内存:当进程访问已交换的页时,触发缺页异常(Page Fault)。从交换空间中读回该页,更新页表项,标记为在物理内存中。
优势:扩展内存容量,防止系统因内存不足而崩溃。
OOM Killer
作用:在内存严重不足时终止进程。
算法
- 计算进程的
oom_score
(基于内存占用、优先级等)。 - 选择得分最高的进程终止。
策略:牺牲少数进程,防止系统崩溃。
对比
操作系统通常按以下顺序逐级触发更激进的回收机制,形成「内存回收漏斗」:
回收策略 | 触发条件 | 开销 | 影响范围 |
---|---|---|---|
页面置换(LRU) | 内存水位低于low 阈值(由/proc/sys/vm/lowmem_reserve_ratio 定义) | 低(仅扫描) | 少数不活跃进程 |
内存压缩 | 置换后仍无法满足需求,或/proc/sys/vm/compaction_proactiveness 配置主动压缩 | 中(CPU计算) | 可压缩的匿名页/文件页 |
空间交换 | 压缩后内存仍不足,或/proc/sys/vm/swappiness 设置倾向交换(默认60,0禁用交换) | 高(磁盘I/O) | 匿名页(堆、栈等) |
OOM Killer | 交换空间耗尽且/proc/sys/vm/panic_on_oom=0 (不直接panic) | 致命(杀进程) | 占用内存最多的进程 |
压力指标
内存水位
-
定义:内核为每个内存区域(Zone)预设三个阈值,通过
/proc/zoneinfo
查看:
grep -A10 "Node 0" /proc/zoneinfo | grep -E "min|low|high"
high
:理想水位,无需回收。low
:后台回收(kswapd)启动阈值。min
:直接回收(同步回收)阈值,可能阻塞进程。
-
触发策略:
- 可用内存 <
low
→ 启动页面置换(异步回收)。 - 可用内存 <
min
→ 强制同步回收(可能触发压缩/交换)。
- 可用内存 <
页面活跃度
- 指标来源:
/proc/meminfo
中的Active
/Inactive
内存统计。- 内核维护的LRU链表(活跃 vs 非活跃)。
- 决策逻辑:
- 非活跃页面比例高 → 优先回收非活跃页(低开销)。
- 活跃页面占比过高 → 需加大扫描压力或触发压缩。
交换倾向
- 指标:
/proc/sys/vm/swappiness
(默认60,范围0-100)。 - 作用:
- 值越高,越倾向于交换匿名页(如进程堆内存)。
- 值越低,越倾向于回收文件页(如缓存)。
- 特殊场景:
swappiness=0
:除非极端情况,否则禁用交换(适合数据库)。
缺页率
-
监控命令:
sar -B 1 # 查看pgpgin/pgpgout(页换入/换出)
-
高压标志:
- **主要缺页(Major Faults)**激增 → 表明频繁从磁盘加载页,需加速回收。
策略选择
页面置换
- 触发条件:
- 内存水位 <
low
且 非活跃页充足。
- 内存水位 <
- 策略特点:
- 低开销,仅扫描LRU链表并标记可回收页。
- 优先回收干净文件页(无需I/O)。
内存压缩
- 触发条件:
- 非活跃文件页不足 或
swappiness
较低。
- 非活跃文件页不足 或
- 依赖配置:
- 内核启用
CONFIG_ZSWAP
,通过/sys/kernel/mm/zswap
调整压缩比。
- 内核启用
空间交换
- 触发条件:
- 内存水位 <
min
且 文件页回收无效。 swappiness > 0
时,匿名页成为候选。
- 内存水位 <
- 关键限制:
- 交换分区/文件未用尽(通过
free -h
查看Swap
行)。
- 交换分区/文件未用尽(通过
OOM Killer
- 终极条件:
- 可用内存 ≈ 0 且 交换空间耗尽 且 回收失败。
- 选择牺牲者:
- 根据
oom_score
(综合内存占用、进程优先级等)。
- 根据
实战
堆栈
代码
#include <iostream>
#include <cstdlib>
#include <vector>// 函数展示栈分配的变量地址
void stackAllocationDemo() {int a = 1;int b = 2;int c = 3;std::cout << "Stack allocated variables addresses:\n";std::cout << "&a: " << &a << " (value: " << a << ")\n";std::cout << "&b: " << &b << " (value: " << b << ")\n";std::cout << "&c: " << &c << " (value: " << c << ")\n";// 计算地址差值std::cout << "Difference between &a and &b: "<< reinterpret_cast<char*>(&b) - reinterpret_cast<char*>(&a) << " bytes\n";std::cout << "Difference between &b and &c: "<< reinterpret_cast<char*>(&c) - reinterpret_cast<char*>(&b) << " bytes\n";
}// 函数展示堆分配的变量地址
void heapAllocationDemo() {// 分配不同大小的内存块void* p1 = malloc(16);void* p2 = malloc(32);void* p3 = malloc(64);void* p4 = malloc(128);void* p5 = malloc(256);std::cout << "\nHeap allocated memory addresses:\n";std::cout << "p1 (16 bytes): " << p1 << "\n";std::cout << "p2 (32 bytes): " << p2 << "\n";std::cout << "p3 (64 bytes): " << p3 << "\n";std::cout << "p4 (128 bytes): " << p4 << "\n";std::cout << "p5 (256 bytes): " << p5 << "\n";// 计算地址差值std::cout << "\nAddress differences:\n";std::cout << "p2 - p1: " << (static_cast<char*>(p2) - static_cast<char*>(p1)) << " bytes\n";std::cout << "p3 - p2: " << (static_cast<char*>(p3) - static_cast<char*>(p2)) << " bytes\n";std::cout << "p4 - p3: " << (static_cast<char*>(p4) - static_cast<char*>(p3)) << " bytes\n";std::cout << "p5 - p4: " << (static_cast<char*>(p5) - static_cast<char*>(p4)) << " bytes\n";// 释放内存free(p1);free(p2);free(p3);free(p4);free(p5);
}int main() {stackAllocationDemo();heapAllocationDemo();return 0;
}
结果
Stack allocated variables addresses:
&a: 0x7fffcc3701dc (value: 1)
&b: 0x7fffcc3701e0 (value: 2)
&c: 0x7fffcc3701e4 (value: 3)
Difference between &a and &b: 4 bytes
Difference between &b and &c: 4 bytesHeap allocated memory addresses:
p1 (16 bytes): 0x5556a9d942c0
p2 (32 bytes): 0x5556a9d942e0
p3 (64 bytes): 0x5556a9d94310
p4 (128 bytes): 0x5556a9d94360
p5 (256 bytes): 0x5556a9d943f0//实际的地址差值比实际分配大小多16bytes,实际上是存放的元数据,表示当前内存块的大小
Address differences:
p2 - p1: 32 bytes
p3 - p2: 48 bytes
p4 - p3: 80 bytes
p5 - p4: 144 bytes