8. 进阶 - 物理内存分配

223 阅读9分钟

一、Buddy System

Buddy System(伙伴系统)是操作系统内核中管理物理内存页框(Page Frame) 的核心算法,专为解决外部碎片和大块连续物理内存分配问题而设计。

1. 关键数据结构

// 1. 内存区域描述符 (include/linux/mmzone.h)
struct zone {
    struct free_area    free_area[MAX_ORDER];  // 空闲区域数组(每个阶一个) MAX_ORDER:最大阶数(通常为 11),支持分配 $2^{10}$ = 1024 页(4MiB)。
    ...
};

// 2. 空闲块管理器 (include/linux/mmzone.h)
struct free_area {
    struct list_head    free_list[MIGRATE_TYPES]; // 按迁移类型分组的链表, MIGRATE_TYPES:迁移类型(如 MIGRATE_UNMOVABLE),用于抗碎片策略。
    unsigned long       nr_free;                 // 空闲块数量
};

// 3. 页框描述符 (include/linux/mm_types.h)
struct page {
    unsigned long flags;          // 状态标志(如是否空闲)
    struct list_head lru;         // 链接到 free_list 的指针
    ...
};

2. 内存分配(alloc_pages)

2.1 get_page_from_freelist

alloc_pages() → __alloc_pages() → 
        static struct page * get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags, const struct alloc_context *ac)

关键代码段解析

a. 区域遍历机制
for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx, ac->nodemask) //按节点/区域优先级遍历 zonelist

b. 水位检测(Watermark Check)
水位类型:
    ALLOC_WMARK_MIN:最低水位(紧急情况); ALLOC_WMARK_LOW:低水位(默认); ALLOC_WMARK_HIGH:高水位(积极回收)
    快速路径:zone_watermark_fast 使用每CPU缓存快速判断
mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
if (!zone_watermark_fast(zone, order, mark, ac->highest_zoneidx, alloc_flags))


c. 节点回收(Node Reclaim)
    触发条件:水位检查失败 && node_reclaim_mode 启用
    回收内容:页面缓存(Page Cache); Slab; 缓存非活动匿名页
ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
switch (ret) {
    case NODE_RECLAIM_NOSCAN: ...
    case NODE_RECLAIM_FULL: ...
    default: ...
}

d. 物理页分配核心
page = rmqueue(ac->preferred_zoneref->zone, zone, order, 
               gfp_mask, alloc_flags, ac->migratetype);

e. 高阶原子分配保留
    场景:原子分配(GFP_ATOMIC)且 order > 0
    机制:保留整个页块用于未来原子分配
if (unlikely(order && (alloc_flags & ALLOC_HARDER)))
    reserve_highatomic_pageblock(page, zone, order);
    
f. 延迟初始化处理
    背景:启动时延迟初始化页结构加速启动
    处理:首次访问时触发初始化
#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
    if (static_branch_unlikely(&deferred_pages)) {
        if (_deferred_grow_zone(zone, order))
            goto try_this_zone;
    }
#endif

2.2 rmqueue

static struct page * get_page_from_freelist()static inline struct page *rmqueue(struct zone *preferred_zone,
            struct zone *zone, unsigned int order,
            gfp_t gfp_flags, unsigned int alloc_flags,
            int migratetype)  

关键代码段解析

1. 单页分配快速路径(order=0)
    使用每CPU页缓存(pcplist)避免全局锁竞争
    时间复杂度 O(1),无锁操作
    占所有内存分配的 70%+(实证数据)
if (likely(order == 0)) {
    page = rmqueue_pcplist(preferred_zone, zone, gfp_flags,
                           migratetype, alloc_flags);
    goto out;
}

2. 高阶分配路径(order>0)
spin_lock_irqsave(&zone->lock, flags); // 获取区域锁
do {
    // 分配逻辑
} while (...);
spin_unlock(&zone->lock); // 释放锁

3. 分配优先级策略:
a. 高优先级原子分配(ALLOC_HARDER):
    使用 MIGRATE_HIGHATOMIC 保留区
    为原子分配(如中断处理)保留资源
if (order > 0 && alloc_flags & ALLOC_HARDER) {
    page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
}

b. CMA区域分配:
    条件:可移动类型 + CMA标志
    用于连续内存分配(如DMA缓冲区)
if (migratetype == MIGRATE_MOVABLE && alloc_flags & ALLOC_CMA) {
    page = __rmqueue_cma(zone, order, migratetype, alloc_flags);
}

c. 常规伙伴分配:
    核心函数 __rmqueue
    可能触发备用迁移类型回退
if (!page) {
    page = __rmqueue(zone, order, migratetype, alloc_flags);
}

3. 内存释放(free_pages)

free_pages() → __free_pages_ok() → free_one_page()

合并逻辑(核心函数 __free_one_page()):

while (order < MAX_ORDER - 1) {
    buddy_pfn = __find_buddy_pfn(pfn, order); // 计算伙伴页框号
    if (!page_is_buddy(page, buddy_pfn, order)) 
        break;
    
    list_del(&buddy_page->lru);              // 从链表移除伙伴
    pfn = min(pfn, buddy_pfn);                // 合并后的起始页框号
    order++;                                  // 阶数提升
}
add_to_free_area(pfn_to_page(pfn), &zone->free_area[order]); // 插入新块

二、Slab Alloctaor

Linux 内核里把“slab”当成一个总称,实际上有三种实现:SLAB、SLUB、SLOB。它们都位于“伙伴系统”之上,专门给内核对象做高速、低碎片的缓存分配。 三种分配器对比速查表 在这里插入图片描述

Slab Allocator 是 Linux 内核中用于高效管理内核对象的内存分配器,解决伙伴系统的两大痛点:

  • 小对象分配效率:伙伴系统最小分配单位是页(4KB),分配小对象浪费严重;
  • 频繁分配/释放开销:内核对象(如 task_struct)频繁创建销毁,初始化开销大;

1. 关键数据结构

1.1 kmem_cache - 缓存控制器

// include/linux/slub_def.h
struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  // 每CPU控制结构
    slab_flags_t flags;            // 标志位(调试、对齐等)
    unsigned int min_partial;      // 最小保留partial slab数量
    unsigned int size;             // 对象实际大小(含元数据)
    unsigned int object_size;      // 原始对象大小
    struct kmem_cache_order_objects oo; // 包含对象数和阶数
    
    // NUMA节点管理
    struct kmem_cache_node *node[MAX_NUMNODES];
    
    // 关键函数指针
    void (*ctor)(void *);          // 对象构造函数
    const char *name;              // 缓存名称
    struct list_head list;         // 全局缓存链表
    // ... 其他统计字段
};

1.2 kmem_cache_cpu - 每CPU快速通道

struct kmem_cache_cpu {
    void **freelist;       // 指向当前空闲对象链表
    struct page *page;     // 当前正在使用的slab页
#ifdef CONFIG_SLUB_CPU_PARTIAL
    struct page *partial;  // CPU本地partial列表
    unsigned int nr_partial; // partial列表计数
#endif
    int node;              // 当前slab所属NUMA节点
};

1.3 kmem_cache_node - 节点管理

struct kmem_cache_node {
    spinlock_t lock;                // 保护节点操作的锁
    unsigned long nr_partial;       // partial slab数量
    struct list_head partial;       // partial slab链表
#ifdef CONFIG_SLUB_DEBUG
    atomic_long_t nr_slabs;         // 总slab计数(调试)
    atomic_long_t total_objects;    // 对象总数(调试)
    struct list_head full;          // full slab链表(调试)
#endif
};

1.4 页元数据整合

// include/linux/mm_types.h
struct page {
    union {
        // SLUB专用字段
        struct {
            void *freelist;        // 页内空闲对象链表
            struct kmem_cache *slab_cache; // 所属缓存
            union {
                struct {
                    unsigned inuse:16;   // 使用中对象数
                    unsigned objects:15; // 总对象数
                    unsigned frozen:1;   // 冻结状态
                };
                void *pad; 
            };
        };
    };
};

2. 内存分配(slab_alloc)

// mm/slub.c
static __always_inline void *slab_alloc(
    struct kmem_cache *s, gfp_t gfpflags, unsigned long addr)
{
    struct kmem_cache_cpu *c;
    void *object;
    
    // 获取当前CPU的slab控制结构
    c = raw_cpu_ptr(s->cpu_slab);
    
    // 快速路径:直接从freelist分配
    object = c->freelist;
    if (unlikely(!object || !node_match(c, node))) 
        goto redo; // 慢速路径
    
    c->freelist = get_freepointer(s, object);
    return object;
    
redo:
    // 慢速路径处理
    return __slab_alloc(s, gfpflags, addr, c);
}

慢速路径分配(__slab_alloc)

static void *__slab_alloc(struct kmem_cache *s, ...)
{
    struct page *new;
    
    // 1. 检查本地partial列表
    if (kmem_cache_debug(s) && !alloc_debug_processing(s, page, addr))
        goto new_slab;
    
    // 2. 尝试从当前page分配
    if (unlikely(!node_match(c, node))) 
        deactivate_slab(s, page, c->freelist); // 释放当前slab
    
    // 3. 从CPU partial列表获取
    if (c->partial) {
        page = c->partial;
        c->partial = page->next;
        c->freelist = page->freelist;
        goto load_freelist;
    }
    
    // 4. 从节点partial列表获取
    spin_lock(&n->lock);
    if (n->partial) {
        page = list_first_entry(&n->partial);
        list_del(&page->lru);
        c->page = page;
        c->freelist = page->freelist;
        spin_unlock(&n->lock);
        goto load_freelist;
    }
    
new_slab:
    // 5. 分配新slab
    new = new_slab(s, gfpflags, node);
    c->page = new;
    c->freelist = new->freelist;
    return c->freelist;
}

新slab创建(new_slab)

static struct page *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{
    struct page *page;
    void *start, *end;
    
    // 1. 从伙伴系统分配内存页
    page = alloc_slab_page(s, flags, node);
    
    // 2. 初始化slab元数据
    start = page_address(page);
    end = start + (PAGE_SIZE << oo_order(s->oo));
    
    // 3. 构建空闲链表
    last = start;
    for (i = 0; i < page->objects; i++) {
        void *object = last;
        set_freepointer(s, object, last + s->size); // 嵌入式空闲指针
        last += s->size;
    }
    set_freepointer(s, last - s->size, NULL); // 链表结束
    
    // 4. 初始化页状态
    page->freelist = start;
    page->inuse = 0;
    page->frozen = 1;
    
    return page;
}

3. 对象释放(slab_free)

static __always_inline void slab_free(
    struct kmem_cache *s, struct page *page, void *x, unsigned long addr)
{
    struct kmem_cache_cpu *c = this_cpu_ptr(s->cpu_slab);
    
    // 快速路径:释放到当前CPU的slab
    if (likely(page == c->page && c->node >= 0)) {
        set_freepointer(s, x, c->freelist); // 将对象插入空闲链表
        c->freelist = x;
        return;
    }
    
    // 慢速路径
    __slab_free(s, page, x, addr);
}

三、Buddy & Slab 总结

1. 差异性

在这里插入图片描述

2. 基本框架

在这里插入图片描述

3. 典型问题分析

Q:为什么需要两级分配器?

  • Buddy 单独工作:分配小对象时产生严重内部碎片(如分配 100B 需占用 4KB 页)。
  • Slab 单独工作:无法获取物理连续内存(Slab 依赖 Buddy 提供连续页)。
  • 协同优势:Buddy 提供大块连续页 → Slab 细分为小对象 → 兼顾外部碎片控制与小对象高效分配。 Q:kmalloc() 如何关联两者?
  • kmalloc(size) 使用通用 Slab 缓存(如 kmalloc-4k, kmalloc-8k)。
  • 若 size > 8KB(可配置),则回退到 Buddy 分配(相当于调用 alloc_pages)。 Q:何时直接使用 Buddy?
  • 需求明确为大块连续物理内存时(如设备 DMA 要求物理连续)。
  • 驱动中常见: // 申请 64KB 连续物理内存 page = alloc_pages(GFP_DMA | __GFP_ZERO, 4); // Order=4 (16页=64KB if 4KB/page) addr = page_address(page);

四、调试技巧

1. 查看各阶空闲块数量

cat /proc/buddyinfo
Node 0, zone      DMA      0      0      0      0      0      0      0      1      0      1      3 
Node 0, zone    DMA32  12315   5098   2232   1801    946    588    376    211     83     55      8 
Node 0, zone   Normal   1665   1293    718    424    131     79     38     22     10      5      2 
第 k 列表示阶数为 k 的空闲块数量(单位:$2^k$ 页)

2. 显示各迁移类型的详细分布

cat /proc/pagetypeinfo
Page block order: 9
Pages per block:  512

Free pages count per migrate type at order       0      1      2      3      4      5      6      7      8      9     10 
Node    0, zone      DMA, type    Unmovable      0      0      0      0      0      0      0      1      0      0      0 
Node    0, zone      DMA, type      Movable      0      0      0      0      0      0      0      0      0      1      3 
Node    0, zone      DMA, type  Reclaimable      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone      DMA, type   HighAtomic      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone      DMA, type      Isolate      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone    DMA32, type    Unmovable   1758    929    343    121     33      5      3      3      1      2      1 
Node    0, zone    DMA32, type      Movable   9166   4074   1587   1149    778    567    365    204     82     44      7 
Node    0, zone    DMA32, type  Reclaimable   1237     13    258    494    117      0      0      0      0      0      0 
Node    0, zone    DMA32, type   HighAtomic    154     82     44     37     18     16      8      4      0      9      0 
Node    0, zone    DMA32, type      Isolate      0      0      0      0      0      0      0      0      0      0      0 
Node    0, zone   Normal, type    Unmovable   1033    625    375    198     27      6      0      0      0      0      0 
Node    0, zone   Normal, type      Movable    157    346    289    200    100     71     38     22     10      5      2 
Node    0, zone   Normal, type  Reclaimable    482    253    114     43      0      0      0      0      0      0      0 
Node    0, zone   Normal, type   HighAtomic     73     41     36     28      6      2      0      0      0      0      0 
Node    0, zone   Normal, type      Isolate      0      0      0      0      0      0      0      0      0      0      0 

Number of blocks type     Unmovable      Movable  Reclaimable   HighAtomic      Isolate 
Node 0, zone      DMA            1            7            0            0            0 
Node 0, zone    DMA32           96         1259          158           15            0 
Node 0, zone   Normal          111          359           37            5            0