一、伙伴系统简介

Linux使用分页机制管理物理内存的,把所有的空闲页分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页块(2^0 ~ 2^10,页阶order从0-10)。最大可以申请1024个连续页,对应4MB大小的连续内存(待验证下)。每个页块的第一个页的物理地址是该块大小的整数倍。

Linux中的伙伴系统是以页面为最小单位分配的,工作机制如下:

当内核申请页阶为n的页块时,伙伴系统会先从块链表n中查找是否有空闲页;
如果没有则从大一阶的页块中查找(没找到继续往大页链表中找);
找到后,将大页阶进行对半切割,一半用于内存分配,另一半放到对应页阶的空闲链表中,同时设置伙伴标记。

二、数据结构

1、分页

struct page {
    //page结构体的标志,它决定页面是什么状态
    unsigned long flags;
    union {
        struct {
            //挂载上级结构的链表
            struct list_head lru;
            //用于文件系统,address_space结构描述上文件占用了哪些内存页面
            struct address_space *mapping;
            pgoff_t index;  
            unsigned long private;
        };
        //DMA设备的地址
        struct {
            dma_addr_t dma_addr;
        };
        //当页面用于内存对象时指向相关的数据结构 
        struct {   
            union {
                struct list_head slab_list;
                struct {  
                    struct page *next;
#ifdef CONFIG_64BIT
                    int pages; 
                    int pobjects;
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            //指向管理SLAB的结构kmem_cache
            struct kmem_cache *slab_cache;
            //指向SLAB的第一个对象
            void *freelist;   
            union {
                void *s_mem;  
                unsigned long counters;   
                struct {            
                    unsigned inuse:16;
                    unsigned objects:15;
                    unsigned frozen:1;
                };
            };
        };
        //用于页表映射相关的字段
        struct {
            unsigned long _pt_pad_1;   
            pgtable_t pmd_huge_pte; 
            unsigned long _pt_pad_2;
            union {
                struct mm_struct *pt_mm;
                atomic_t pt_frag_refcount;
            };
            //自旋锁
#if ALLOC_SPLIT_PTLOCKS
            spinlock_t *ptl;
#else
            spinlock_t ptl;
#endif
        };
        //用于设备映射
        struct {
            struct dev_pagemap *pgmap;
            void *zone_device_data;
        };
        struct rcu_head rcu_head;
    };
    //页面引用计数
    atomic_t _refcount;

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
    int _last_cpupid;
#endif
} _struct_page_alignment;

2、分区

Linux内核把属性相同物理内存页面归结到一个区中,使用命令可以查看当前机器上的分区:

# cat /proc/zoneinfo | grep Node
Node 0, zone      DMA
Node 0, zone    DMA32
Node 0, zone   Normal
Node 0, zone  Movable
Node 0, zone   Device

Linux内核用zone数据结构表示一个区,仅列出部分字段,重点关注:_watermark表示内存页面总量的水位线(有min, low, high三种状态,可以作为启动内存页面回收的判断标准),spanned_pages是该内存区总的页面数,以及free_area结构的数组(用于实现伙伴系统)。

free_area结构组成一个list_head链表数组,将具有相同迁移类型的page结构尽可能地分组,同一类型的所有相同order的page结构,就构成了一组page结构块。

内存分配的时候,会先按请求的migratetype从对应的page结构块中寻找,如果不成功,才会从其他migratetype的page结构块中分配,这样让内存页迁移更加高效,可以有效降低内存碎片。

enum migratetype {
    MIGRATE_UNMOVABLE, //不能移动的
    MIGRATE_MOVABLE,   //可移动和
    MIGRATE_RECLAIMABLE,
    MIGRATE_PCPTYPES,  //属于pcp list的
    MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
    MIGRATE_CMA,   //属于CMA区的
#endif
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,   
#endif
    MIGRATE_TYPES
};
//页面空闲链表头
struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long       nr_free;
};

struct zone {
    unsigned long _watermark[NR_WMARK];
    unsigned long watermark_boost;
    //预留的内存页面数
    unsigned long nr_reserved_highatomic;
    //内存区属于哪个内存节点 
#ifdef CONFIG_NUMA
    int node;
#endif
    struct pglist_data  *zone_pgdat;
    //内存区开始的page结构数组的开始下标 
    unsigned long       zone_start_pfn;
    
    atomic_long_t       managed_pages;
    //内存区总的页面数
    unsigned long       spanned_pages;
    //内存区存在的页面数
    unsigned long       present_pages;
    //内存区名字
    const char      *name;
    //挂载页面page结构的链表
    struct free_area    free_area[MAX_ORDER];
    //内存区的标志
    unsigned long       flags;
    /*保护free_area的自旋锁*/
    spinlock_t      lock;
};

3、内存节点

NUMA体系架构:物理内存是分布式的,由多个计算节点组成,每个CPU核都会各自的本地内存,CPU在访问本地内存的时候就比较快,访问其他CPU核内存的时候就比较慢,这种体系结构被称为Non-Uniform Memory Access(NUMA,非一致内存访问架构)。

在NUMA系统中,当Linux内核收到内存分配的请求时,它会优先从发出请求的CPU本地或邻近的内存node中寻找空闲内存,这种方式被称作local allocation,local allocation能让接下来的内存访问相对底层的物理资源是local的。

linux针对NUMA进行抽象,用内存节点来表示。每个node由一个或多个zone组成,每个zone又由若干page frames组成。

enum {
    ZONELIST_FALLBACK,
#ifdef CONFIG_NUMA
    ZONELIST_NOFALLBACK,
#endif
    MAX_ZONELISTS
};
struct zoneref {
    struct zone *zone;//内存区指针
    int zone_idx;     //内存区对应的索引
};
struct zonelist {
    struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
//zone枚举类型 从0开始
enum zone_type {
#ifdef CONFIG_ZONE_DMA
    ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
    ZONE_DMA32,
#endif
    ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
    ZONE_HIGHMEM,
#endif
    ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
    ZONE_DEVICE,
#endif
    __MAX_NR_ZONES

};
//定义MAX_NR_ZONES为__MAX_NR_ZONES 最大为6
DEFINE(MAX_NR_ZONES, __MAX_NR_ZONES);
//内存节点
typedef struct pglist_data {
    //定一个内存区数组,最大为6个zone元素
    struct zone node_zones[MAX_NR_ZONES];
    //两个zonelist,一个是指向本节点的的内存区,另一个指向由本节点分配不到内存时可选的备用内存区。
    struct zonelist node_zonelists[MAX_ZONELISTS];
    //本节点有多少个内存区
    int nr_zones; 
    //本节点开始的page索引号
    unsigned long node_start_pfn;
    //本节点有多少个可用的页面 
    unsigned long node_present_pages;
    //本节点有多少个可用的页面包含内存空洞 
    unsigned long node_spanned_pages;
    //节点id
    int node_id;
    //交换内存页面相关的字段
    wait_queue_head_t kswapd_wait;
    wait_queue_head_t pfmemalloc_wait;
    struct task_struct *kswapd; 
    //本节点保留的内存页面
    unsigned long       totalreserve_pages;
    //自旋锁
    spinlock_t      lru_lock;
} pg_data_t;

各数据结构之间的关系如下所示:

pglist_data {              // 对应内存节点
    zone {                 // 对应逻辑分区
        free_area {
            page;          // 对应物理页    
            page_list;
        }
        free_area;
        free_area_list;
    }
    zone;
    zonelise;
}

三、分配过程

在Linux物理内存页面管理中,连续且相同大小的pages就可以表示成伙伴。

物理页面分配过程:首先要找到内存节点,接着找到内存区,然后合适的空闲链表,最后在其中找到页的page结构,完成物理内存页面的分配。

1、调用入口:alloc_pages

alloc_pages接口调用过程(最终使用rmqueue进行页面分配):

alloc_pages                        // 内核态可以直接调用该接口进行物理页面分配
    alloc_pages_current
        __alloc_pages_nodemask     // 参数order,表示分配页面数量为2^order页
            // step1:初始化参数:找出要分配内存区、候选内存区以及内存区中空闲链表的migratetype类型
            prepare_alloc_pages
                gfp_zone           // 待分配内存区
                node_zonelist    
                gfp_migratetype    // 计算迁移类型,优先从相同的迁移类型的page结构体中分配,减少内存碎片
                
            // step2:优先尝试快速分配(不会处理页面合并与回收)
            get_page_from_freelist
                for_next_zone_zonelist_nodemask    // 遍历所有的候选内存区
                    wmark_pages
                    zone_watermark_fast            // 检查内存区的水位线
                    zone_watermark_ok              // 根据分配的order数量判断内存区的水位线是否满足要求
                        rmqueue                    // 检查通过之后,执行内存页面分配

            // or step3:快速分配失败后,进入慢速分配路径(进行页面回收,重新分配)
            __alloc_pages_slowpath
                get_page_from_freelist             // 再次调用快速分配接口,大概率失败
                __alloc_pages_direct_reclaim       // 尝试直接回收内存并且再分配内存页面(移动内存页面,进行内存碎片整理)
                __alloc_pages_direct_compact       // 尝试直接压缩内存并且再分配内存页面
                should_reclaim_retry               // 是否需要重试回收
                should_compact_retry               // 是否需要重试压缩
                __alloc_pages_may_oom              // 回收、压缩失败后,调用OOM,杀死进程,回收内存页面

2、分配函数:rmqueue

rmqueue接口调用过程(实际完成分配任务):

rmqueue
    if (order == 0)             // 大部分场景下,分配1个页面
        rmqueue_pcplist         // 从pcplist中分配
    else 
        __rmqueue_smallest      // 从free_area中分配(失败后,重复执行)

// 请求分配单个页面
/*
  PCP (Per CPU PageSet Allocator), 用于从每个 CPU 的物理页集合中分配物理页的内存管理器。  
  PCP分配器在每个ZONE区间上为每个CPU提供了两个物理页链表,每个链表上维护了一个定数量的单个可用物理页。  
  当系统请求分配当个物理页的时候,内存管理子系统就将这个任务让PCP内存管理器去完成,而不是交给 Buddy 内存管理器。
*/
rmqueue_pcplist
    this_cpu_ptr(zone->pageset)->pcp        // 获取当前CPU下的pcp,得到list上的page结构
    __rmqueue_pcplist
        while (check_new_pcp(page))         // 遍历page链表
            if (list_empty(list))
                rmqueue_bulk                // 如果list为空,则从内存区分配一个页面到pcp中
            else
                list_first_entry            // 否则,摘取链表上的第一个page结构,分配成功

// 请求分配多个页面
__rmqueue_smallest
    get_page_from_free_area                 // 根据order,获取free_area中对应migratetype为下标的free_list中的page
    del_page_from_free_list                 // 将page从free_list中摘除,待进行分割
    expand                                  // 分割一组页,伙伴算法的核心
    set_pcppage_migratetype                 // 配置页面属性

3、核心分配过程 expend

    expand                                  // 分割一组页,伙伴算法的核心
        while (high > low)                  // 当前页阶high > 待分配页阶low时,需要对半切割当前页
            high--;
            size >>= 1;
            if (set_page_guard(zone, &page[size], high, migratetype))     // 将其中一半标记为保护页,放入page中
                continue;
            add_to_free_list(&page[size], zone, high, migratetype);       // 将另一半page加入对应的free_area中
            set_buddy_order(&page[size], high);                           // 设置伙伴

参考:
PCP Allocator