【操作系统】内存分配之伙伴系统
一、伙伴系统简介
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