Linux内核内存管理月度总结:伙伴系统到slab的核心机制回顾

一、为什么重读内核内存管理

7月份我花了三周时间重新梳理Linux内核内存管理模块。
起因很有趣:团队在做性能优化时遇到了一个诡异的内存碎片问题。
排查到最后发现根因是伙伴系统(buddy system)的order分配行为。
那一刻我意识到,很多"玄学问题"的答案都写在教科书里。

所以我在7月系统地回顾了从伙伴系统到slab分配器的完整链路。
这篇文章是我一个月的学习笔记和验证实验的整理。
核心目标不是罗列知识,而是建立"物理内存→内核分配器→用户态"的完整心智模型。

二、伙伴系统:页面级的分配艺术

伙伴系统是内核最底层的物理页分配器。
它的核心思想是把物理内存按2的幂次(order)分组管理。
每一组叫做一个free_area,从order 0(4KB)到order 10(4MB)。

核心数据结构:

// include/linux/mmzone.h
struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long       nr_free;
};

struct zone {
    struct free_area    free_area[MAX_ORDER];
    // ...
    spinlock_t          lock;  /* 保护并发访问 */
};

伙伴系统的分配逻辑可以简化为:

  1. 计算请求大小对应的order。
  2. 在对应free_area[order]中查找空闲块。
  3. 如果没有,向order+1申请,然后分裂。
  4. 分配后将另一半放入低阶free_area。

释放时执行反向操作(合并):

// mm/page_alloc.c (简化)
static inline void __free_one_page(struct page *page, 
        unsigned long pfn, struct zone *zone, 
        unsigned int order)
{
    // 检查伙伴是否也空闲
    while (order < MAX_ORDER - 1) {
        buddy_pfn = __find_buddy_pfn(pfn, order);
        buddy = page + (buddy_pfn - pfn);
        
        if (!page_is_buddy(buddy, order))
            break;
        
        // 从当前free_list删除伙伴
        del_page_from_free_list(buddy, zone, order);
        // 合并为order+1的块
        combined_pfn = pfn & buddy_pfn;
        page = page + (combined_pfn - pfn);
        pfn = combined_pfn;
        order++;
    }
    add_to_free_list(page, zone, order);
}

碎片化类型与对策:

伙伴系统面临三种碎片:

碎片类型说明内核对策
外部碎片空闲页不连续内存规整(compaction)
内部碎片分配大于需求slab对象级细分
迁移碎片不可移动页阻断migrate_type分级

迁移类型(MIGRATE_TYPES)是反碎片的核心机制:

enum migratetype {
    MIGRATE_UNMOVABLE,   // 内核核心数据结构
    MIGRATE_MOVABLE,     // 用户态页面(可换出)
    MIGRATE_RECLAIMABLE, // slab等可回收内存
    MIGRATE_PCPTYPES,    // per-CPU页框
    MIGRATE_HIGHATOMIC,  // 高阶原子分配
    MIGRATE_CMA,         // 连续内存分配器
    MIGRATE_ISOLATE,     // 隔离中的页面
};

这个设计保证了:即使总空闲内存充足,
也不会因为可移动页散布而阻塞不可移动内存的分配。

三、Slab分配器:小而美的对象缓存

伙伴系统的最小分配单位是4KB(一页)。
但内核中大量的分配请求远小于一页。
比如:inode结构体约600字节,dentry约200字节。
如果每次都用伙伴系统分配一页,内部碎片会达到80%以上。

Slab分配器就是为了解决这个问题而生的。

它的核心思想是"对象缓存"(object caching):

// 创建专用缓存
struct kmem_cache *my_cache = kmem_cache_create(
    "my_struct",
    sizeof(struct my_struct),  // 对象大小
    0,                          // 对齐
    SLAB_HWCACHE_ALIGN,        // 硬件缓存行对齐
    NULL                        // 构造函数
);

// 分配和释放
struct my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
kmem_cache_free(my_cache, obj);

Slab分配器内部有三个层次:

第一层:kmem_cache — 缓存描述符

每个kmem_cache对应一种固定大小的对象类型。
内核启动时会创建两类缓存:

  • 专用缓存:为特定类型创建(如inode_cache、dentry_cache)。
  • 通用缓存:kmalloc使用的预定义大小缓存(8, 16, 32, ..., 8192字节)。

第二层:slab — 页面的管理者

每个slab管理一个或多个连续物理页。
slab有三种状态:

  • full:所有对象都在使用中。
  • partial:部分对象在使用中。
  • empty:所有对象都是空闲的。
struct slab {
    struct list_head    slab_list;   // 链接到缓存的partial/full/empty链表
    unsigned long       freelist[N]; // 空闲对象位图
    void               *s_mem;       // 第一个对象的地址
    unsigned int        inuse;       // 使用中的对象数
    unsigned int        free;        // 空闲对象数
};

第三层:per-CPU缓存 — 无锁快速路径

SLUB(当前默认实现)在每个CPU上维护一个本地对象池:

struct kmem_cache_cpu {
    void **freelist;    // 空闲对象链表
    struct page *page;  // 当前活跃的slab页
    int node;           // NUMA节点
    unsigned int offset;// 对象在页内的偏移
};

分配路径非常简洁:

// mm/slub.c (简化)
static void *slab_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node)
{
    struct kmem_cache_cpu *c = raw_cpu_ptr(s->cpu_slab);
    
    // 快速路径:从per-CPU freelist取
    void *object = c->freelist;
    if (likely(object)) {
        c->freelist = get_freepointer(s, object);
        return object;
    }
    
    // 慢速路径:需要从partial/新页面获取
    object = __slab_alloc(s, gfpflags, node, c);
    return object;
}

四、实践中的典型问题与诊断

问题一:内存碎片导致高阶分配失败

症状:系统有大量空闲内存,但order>=3的分配持续失败。

诊断工具:

# 查看各order的空闲块统计
cat /proc/buddyinfo

# 输出示例:
# Node 0, zone Normal  1024  512  256  128  64  32  16  8  4  2  1
#                      order0 ...                          order10

如果高阶区(order3+)的数字很小:

  • 触发内存规整:echo 1 > /proc/sys/vm/compact_memory
  • 或增加vm.min_free_kbytes:sysctl -w vm.min_free_kbytes=65536

问题二:Slab缓存膨胀

# 查看slab缓存统计
cat /proc/slabinfo

# 重点关注"active_objs / num_objs"比值
# 比值 << 1 表示大量空闲对象浪费内存

清理slab缓存:

echo 2 > /proc/sys/vm/drop_caches  # 回收dentry和inode

五、总结

核心技术提炼:

  1. 伙伴系统的本质:以2的幂次组织页面,通过分裂/合并实现高效物理页分配。
    关键参数:MAX_ORDER决定最大连续分配大小。
  2. MIGRATE_TYPES的反碎片策略:将页面按可迁移性分类,避免不可移动页阻断大块连续分配。
    实战中通过/proc/pagetypeinfo检查迁移类型分布。
  3. Slab的三层架构:kmem_cache(类型) → slab(页面) → per-CPU freelist(无锁)。
    SLUB是当前默认实现,用freelist指针链取代位图。
  4. 分配路径的两阶段:快速路径走per-CPU freelist(无锁),慢速路径走partial slab→新页分配。
    性能瓶颈通常在伙伴系统层的zone->lock争用。
  5. 诊断工具链:/proc/buddyinfo(碎片)、/proc/slabinfo(缓存膨胀)、/proc/pagetypeinfo(迁移类型分布)。
    三者结合可以定位90%的内存分配问题。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐