Skip to content

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/&lt;pid>/maps
    }
}

面试追问方向

  • 伙伴系统的分配和释放是如何工作的? 提示:分裂和合并。
  • Slab分配器和伙伴系统是什么关系? 提示:Slab基于伙伴系统,管理更小的对象。
  • 什么是内存碎片?Linux是如何处理内存碎片的? 提示:内存整理(compaction)。
  • kmalloc和vmalloc的区别是什么? 提示:kmalloc物理连续,vmalloc虚拟连续。

基于 VitePress 构建