Linux内存管理:伙伴系统与Slab分配器
你有没有想过,当你执行free -h时看到的那几行数字是什么意思? 为什么Linux总能用完所有内存,却又不会OOM?
答案在伙伴系统和Slab分配器这两个精妙的内存管理机制里。
Linux内存管理架构
┌─────────────────────────────────────────────────────────────┐
│ Linux内存管理层次 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户空间 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 进程虚拟地址空间 │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ 用户态地址转换(页表) │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ↓ │
│ 内核空间 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 伙伴系统(物理内存分配器) │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ Slab分配器(对象缓存) │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ 页面分配(buddy system) │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ↓ │
│ 物理内存(DRAM) │
│ │
└─────────────────────────────────────────────────────────────┘伙伴系统(Buddy System)
基本思想
伙伴系统将物理内存按2的幂次方分成块,每块称为一个「伙伴」。 当需要分配N个页面时,找一个2^k大小的块(2^(k-1) < N ≤ 2^k)。
┌─────────────────────────────────────────────────────────────┐
│ 伙伴系统示意图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 初始状态(8个页面,2^3): │
│ ┌───────────────────────────────────────────┐ │
│ │ 8 │ │
│ └───────────────────────────────────────────┘ │
│ │
│ 分配2个页面(2^1): │
│ ┌───────────────────────┬───────────────────────┐ │
│ │ 2 │ 2 │ │
│ └───────────────────────┴───────────────────────┘ │
│ │
│ 分配1个页面(2^0): │
│ ┌───────────────┬───────────────┬───────────────┬─────────┐│
│ │ 1 │ 1 │ 2 │ ││
│ └───────────────┴───────────────┴───────────────┴─────────┘│
│ │
│ 释放第一个1: │
│ ┌───────────────┬───────────────┬───────────────────────┐│
│ │ 1 │ 1 │ 2 │ │
│ └───────────────┴───────────────┴───────────────────────┘│
│ │
│ 伙伴合并(两个1的伙伴可以合并成2): │
│ ┌───────────────────────────────┬───────────────────────┐│
│ │ 2 │ 2 │ │
│ └───────────────────────────────┴───────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────┘伙伴系统的数据结构
c
// 伙伴系统的空闲链表数组
struct free_area {
struct list_head free_list[MIGRATE_TYPES]; // 按迁移类型分类
unsigned long nr_free; // 空闲页数量
};
struct zone {
// 每个order对应一条链表
struct free_area free_area[MAX_ORDER]; // MAX_ORDER通常是11或10
// order 0: 1页
// order 1: 2页
// order 2: 4页
// order 3: 8页
// ...
// order 10: 1024页
};分配和释放
c
// 分配页面
struct page* alloc_pages(gfp_t gfp_mask, unsigned int order) {
// gfp_mask: 分配标志(GFP_KERNEL, GFP_ATOMIC等)
// order: 2^order个页面
// 1. 从对应order的空闲链表中查找
// 2. 如果没有,尝试从更大的order分裂
// 3. 继续向上查找直到找到
// 分配时会分裂大的块
// 例如:需要4KB(1页),但只有8KB(2页)
// 则分裂8KB为两个4KB,返回一个
}
// 释放页面
void __free_pages(struct page* page, unsigned int order) {
// 1. 放入对应order的空闲链表
// 2. 检查伙伴块是否也空闲
// 3. 如果伙伴也空闲,合并成更大的块
// 4. 递归向上合并
}伙伴系统的优点和缺点
优点:
- 分配和释放都是O(1)(在链表操作意义上)
- 碎片较少(只有内部碎片)
- 伙伴可以合并,有效利用外部碎片
缺点:
- 只能分配2的幂次方大小的内存
- 对小内存分配有浪费(内部碎片)Slab分配器
为什么需要Slab?
伙伴系统只适合分配整页内存,但内核需要分配各种大小的对象(如task_struct、inode)。
┌─────────────────────────────────────────────────────────────┐
│ Slab vs 伙伴系统 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 伙伴系统: │
│ - 最小单位:1页(4KB) │
│ - 适合大块内存分配 │
│ - 分配3KB?需要分配1页,浪费1KB │
│ │
│ Slab分配器: │
│ - 最小单位:字节 │
│ - 基于伙伴系统,划分成多个Slab │
│ - 每个Slab包含多个相同大小的对象 │
│ - 高效管理常用对象的分配 │
│ │
└─────────────────────────────────────────────────────────────┘Slab的结构
┌─────────────────────────────────────────────────────────────┐
│ Slab结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Slab(一个或多个页面) │ │
│ │ │ │
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐│ │
│ │ │对象│ │对象│ │对象│ │空闲│ │空闲│ │空闲│ │... ││ │
│ │ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘│ │
│ │ │ │
│ │ 管理结构: │ │
│ │ - free链表:指向空闲对象 │ │
│ │ - inuse:已分配对象数量 │ │
│ │ - objects:总对象数 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Slab缓存
c
// Slab缓存结构
struct kmem_cache {
// 对象属性
size_t object_size; // 对象大小
size_t size; // Slab中每个对象的大小(含对齐)
// Slab管理
struct list_head slabs_full; // 满的Slab
struct list_head slabs_partial; // 部分使用的Slab
struct list_head slabs_free; // 完全空闲的Slab
// 分配和释放函数
void (*ctor)(void*); // 构造函数
void (*dtor)(void*); // 析构函数
};常用缓存
bash
# 查看Slab缓存信息
cat /proc/slabinfo
# 常用缓存名称
# - task_struct: 进程/线程对象
# - vm_area_struct: 虚拟内存区域
# - anon_vma: 反向映射
# - filp: 文件描述符
# - dentry: 目录项
# - inode_cache: inode对象
# 格式化显示
slabtop # 实时显示slab使用情况Slab分配器的工作流程
c
// kmalloc: Slab分配器的接口
void* kmalloc(size_t size, gfp_t flags) {
// 1. 将size对齐到2的幂次
// 2. 查找对应大小的Slab缓存
// 3. 从缓存的free链表取出一个对象
// 例如:kmalloc(100, GFP_KERNEL)
// → 找到128字节的缓存(128 > 100)
// → 从对应Slab取出一个对象
}
// kfree: 释放对象
void kfree(const void* objp) {
// 1. 确定对象属于哪个Slab缓存
// 2. 放回缓存的free链表
}内存分配API
用户空间malloc vs 内核kmalloc
c
// 用户空间
void* malloc(size_t size);
void free(void* ptr);
// 内核空间
void* kmalloc(size_t size, gfp_t flags); // 基于Slab
void* __get_free_pages(gfp_t flags, unsigned int order); // 基于伙伴系统
void kfree(const void* ptr);
void free_pages(unsigned long addr, unsigned int order);分配标志(gfp_t)
c
// GFP_KERNEL: 常规分配,可能阻塞(睡眠)
void* ptr = kmalloc(100, GFP_KERNEL);
// GFP_ATOMIC: 原子分配,不阻塞(中断处理)
void* ptr = kmalloc(100, GFP_ATOMIC);
// GFP_NOIO: 不允许I/O操作
void* ptr = kmalloc(100, GFP_NOIO);
// GFP_NOFS: 不允许文件系统操作
void* ptr = kmalloc(100, GFP_NOFS);
// __GFP_HIGHMEM: 允许使用高端内存
void* ptr = kmalloc(100, GFP_HIGHUSER);内存碎片化与解决方案
碎片类型
内部碎片:分配的内存大于实际需要的
外部碎片:内存可用但分散
伙伴系统:几乎没有外部碎片,但有内部碎片
Slab:内部碎片较少,可以管理多种大小内存 compaction
bash
# 手动触发内存整理
echo 1 > /proc/sys/vm/compact_memory
# 查看内存区域
cat /proc/buddyinfo
# 示例输出:
# Node 0, zone DMA free 2 2 1 1 1 0 1 0 1
# Node 0, zone DMA32 free 12345 6789 2345 1234 567 234 123 67 34
# Node 0, zone Normal free 45678 23456 12345 6789 3456 1789 945 534 289
# order: 0 1 2 3 4 5 6 7 8 9
# 页面数: 2^0 2^1 2^2 2^3 2^4 2^5 2^6 2^7 2^8 2^9实际案例:JVM的内存分配
java
public class JVMMemoryAllocation {
public static void main(String[] args) {
// JVM内存结构(在Linux内存管理之上)
//
// JVM堆 → 伙伴系统分配的大内存
// TLAB → 线程本地分配缓冲区
// Metaspace → 操作系统内存映射
// JVM使用mmap分配堆
// -Xms2g -Xmx4g
// 内部按代划分:Eden、Survivor、Old Gen
// 每个代的内存来自伙伴系统的大页面
// 查看JVM进程的内存映射
// cat /proc/<pid>/maps
}
}面试追问方向
- 伙伴系统的分配和释放是如何工作的? 提示:分裂和合并。
- Slab分配器和伙伴系统是什么关系? 提示:Slab基于伙伴系统,管理更小的对象。
- 什么是内存碎片?Linux是如何处理内存碎片的? 提示:内存整理(compaction)。
- kmalloc和vmalloc的区别是什么? 提示:kmalloc物理连续,vmalloc虚拟连续。
