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
