基于Linux 5.10, 体系结构是aarch64
上文介绍了linux的物理内存页分配函数alloc_pages, alloc_pages是内核中常用的分配物理内存页面的函数,用于分配2^order 个连续的物理页。
alloc_pages最终会调用到伙伴系统的接口来完成实际物理页面的分配。
概述
1. 内存外碎片
内存外部碎片是指系统中无法利用的一些小的内存块,主要是由于内存在多次申请或者分配后处于离散分布的状态。
比如我需要申请16K bytes的连续物理内存,那么就需要4个4K bytes的页面,但是如果这4个页面的物理页帧号不连续,那么就无法分配16K bytes的内存,该情况就指的是内存外碎片。
2. 伙伴系统
2.1 主要思想
伙伴系统是一个能够“尽可能减少内存外碎片”的物理内存分配器。
伙伴系统支持连续物理页分配和释放,其主要思想是通过将物理内存划分成多个连续的块,然后以“块”作为基本单位进行分配。这些“块”的都是由一个或者多个连续的物理页组成,物理页的数量是2的n次幂(0 <= n <= MAX_ORDER)。
如下图所示:
数组 0 指向的链表就是 0 阶链表,他携带的内存块都是 1 (2 ^ 0)个页面,数组3指向的链表是3阶链表,它挂的都是 8(2 ^ 3)个页大小的内存块.
MAX_ORDER通常定义为11, 即内核管理的最大的连续空闲物理内存为2 ^ (11 - 1) = 4MB.
/* Free memory management - zoned buddy allocator. */
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
2.2 分裂合并的过程
当请求分配N个连续的物理页时,首先会去寻找一个合适大小的内存块,如果没有找到相匹配的空闲页,则将更为大的块分割成2个小块, 这两个小块就是“伙伴” 关系。 分割得到的小块可以继续分裂,直到能够得到一个大小合适的块响应请求的分配。
同样,当一个块被释放后,分配器会找到其伙伴块,如果该伙伴块也处于空闲的状态,那么就将这两个伙伴块进行合并,形成一个大一号的空闲块, 大号的空闲块也可以继续向上合并。
所以, 概括来说,伙伴系统减少内存外碎片靠的是:
(1) 小块内存在小块链表中分配,减少大块链表的污染
(2) 内存释放时会整合成大块内存,减少分配不到内存的可能。
2.3 伙伴算法示例
假设系统当前的页面如下, 一共有32个页面。红色的代表已分配,绿色的代表空闲。
假设此时order=0的链表上有6个节点;order=1的链表上有2个节点;order=2的链表为空;order=3的链表上有1个节点; 其他order的链表上为空。
空闲内存组织如下图:
现在请求4个地址连续的空闲物理块页面。
- 4 = 2 ^ 2, 所以在order=2的链表上找空闲的块
- order=2的链表上为空,所以需要从上一级的order上去找空闲的块
- order=3的链表上有一个空闲的块,但大小为8, 所以需要将该块分割成2个大小为4的块,一块用于内存的申请,还剩一块挂到order=2的链表上
分配完成后系统内存信息如下:(橙色的表示新分配的4个页面)
空闲内存组织如下图:
2.4 伙伴系统信息查看
当前系统的buddy状态可以通过 cat /proc/buddyinfo
命令查看。
cat /proc/buddyinfo
Node 0, zone DMA 23 15 4 5 2 3 3 2 3 1 0
Node 0, zone Normal 149 100 52 33 23 5 32 8 12 2 59
Node 0, zone HighMem 11 21 23 49 29 15 8 16 12 2 142
从左向右分别对应order0 ~ order10
内核实现
1. 数据结构
在Linux内存管理(五):描述物理内存中有介绍过,Zone的数据结构中free_area[MAX_ORDER]数组用于保存每一阶的空闲内存块链表。
struct zone {
...
struct free_area free_area[MAX_ORDER];
} ____cacheline_internodealigned_in_smp;
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
free_list: 用于连接包含大小相同的连续内存区域的页链表
nr_free: 该区域中空闲页表的数量
每个free_list链表上的各个元素, 都是通过struct page中的双链表成员变量来连接的。
migratetype是页面迁移类型。
NUMA架构中,支持内存在节点间移动,保持内存的均衡性,因此内存定义了以下几种的迁移类型,
enum migratetype {
MIGRATE_UNMOVABLE,
MIGRATE_MOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE, /* can't allocate from here */
#endif
MIGRATE_TYPES
};
migratetype | description |
---|---|
MIGRATE_UNMOVABLE | 不可移动, 核心内核分配的大部分页面都属于这一类。 |
MIGRATE_MOVABLE | 可移动,属于用户空间应用程序的页属于此类页面,它们是通过页表映射的,因此我们只需要更新页表项,并把数据复制到新位置就可以了,当然要注意,一个页面可能被多个进程共享,对应着多个页表项 |
MIGRATE_RECLAIMABLE | 可回收,不能直接移动,但是可以回收,因为还可以从某些源重建页面,比如映射文件的数据属于这种类别,kswapd会按照一定的规则,周期性的回收这类页面。 |
MIGRATE_PCPTYPES | 用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目。 |
MIGRATE_HIGHATOMIC | 某些情况,内核需要分配一个高阶的页面块而不能休眠.如果向具有特定可移动性的列表请求分配内存失败,这种紧急情况下可从MIGRATE_HIGHATOMIC中分配内存 |
MIGRATE_CMA | 预留一段的内存给驱动使用,但当驱动不用的时候,伙伴系统可以分配给用户进程用作匿名内存或者页缓存。而当驱动需要使用时,就将进程占用的内存通过回收或者迁移的方式将之前占用的预留内存腾出来,供驱动使用。 |
MIGRATE_ISOLATE | 不能从这个链表分配页框,因为这个链表专门用于NUMA结点移动物理内存页,将物理内存页内容移动到使用这个页最频繁的CPU。 |
数据结构的关系如下:
2. 申请页面
2.1 rmqueue
/*
* Allocate a page from the given zone. Use pcplists for order-0 allocations.
*/
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)
{
unsigned long flags;
struct page *page;
if (likely(order == 0)) { ---------------------------- (1)
/*
* MIGRATE_MOVABLE pcplist could have the pages on CMA area and
* we need to skip it when CMA area isn't allowed.
*/
if (!IS_ENABLED(CONFIG_CMA) || alloc_flags & ALLOC_CMA ||
migratetype != MIGRATE_MOVABLE) {
page = rmqueue_pcplist(preferred_zone, zone, gfp_flags,
migratetype, alloc_flags);
goto out;
}
}
/*
* We most definitely don't want callers attempting to
* allocate greater than order-1 page units with __GFP_NOFAIL.
*/
WARN_ON_ONCE((gfp_flags & __GFP_NOFAIL) && (order > 1));
spin_lock_irqsave(&zone->lock, flags);
do {
page = NULL;
/*
* order-0 request can reach here when the pcplist is skipped
* due to non-CMA allocation context. HIGHATOMIC area is
* reserved for high-order atomic allocation, so order-0
* request should skip it.
*/
if (order > 0 && alloc_flags & ALLOC_HARDER) { ----------------------- (2)
page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
if (page)
trace_mm_page_alloc_zone_locked(page, order, migratetype);
}
if (!page)
page = __rmqueue(zone, order, migratetype, alloc_flags); ---------------------- (3)
} while (page && check_new_pages(page, order)); -----------(4)
spin_unlock(&zone->lock);
if (!page)
goto failed;
__mod_zone_freepage_state(zone, -(1 << order),
get_pcppage_migratetype(page));
__count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
zone_statistics(preferred_zone, zone);
local_irq_restore(flags);
out:
/* Separate test+clear to avoid unnecessary atomics */
if (test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags)) { ------------------ (5)
clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags);
wakeup_kswapd(zone, 0, 0, zone_idx(zone));
}
VM_BUG_ON_PAGE(page && bad_range(zone, page), page);
return page;
failed:
local_irq_restore(flags);
return NULL;
}
(1) 第13行 - 24行,处理分配单个物理页面的情况(order = 0), 调用rmqueue_pcplist()
函数, 走的是PCP分配机制。
PCP即per_cpu_pages, 它是一个per-cpu变量,该变量中有一个单页面的链表,存放部分单个的物理页面,当系统需要单个物理页面时,直接从该per-cpu变量的链表中获取物理页面,这样能够做到更高的效率。
struct zone {
...
struct per_cpu_pageset __percpu *pageset;
} ____cacheline_internodealigned_in_smp;
struct per_cpu_pageset {
struct per_cpu_pages pcp;
...
};
struct per_cpu_pages {
int count; /* number of pages in the list */
int high; /* high watermark, emptying needed */
int batch; /* chunk size for buddy add/remove */
/* Lists of pages, one per migrate type stored on the pcp-lists */
struct list_head lists[MIGRATE_PCPTYPES];
};
count: 表示链表中页面的数量;
high: 表示当缓存的页面高于水位时就会回收页面到伙伴系统
batch: 表示每一次回收到伙伴系统的页面数量
(2) 第33 ~45 行, 如果order > 0 且alloc_flags & ALLOC_HARDER, 那么就调用__rmqueue_smallest
函数分割”块“,这里的migratetype是MIGRATE_HIGHATOMIC。ALLOC_HARDER表示 尽力分配,一般在gfp_mask设置了__GFP_ATOMIC时会使用。如果页面分配失败,则尽可能分配MIGRATE_HIGHATOMIC类型的空闲页面。
(3) 上面都没有分配到page, 那么就调用__rmqueue
函数分配内存。在__rmqueue
函数中首先也是调用__rmqueue_smallest
函数分割”块“, 如果__rmqueue_smallest
函数分配内存失败,就会调用__rmqueue_fallback
函数,fallback即备份的意思,该函数会从伙伴系统的备份空闲链表中借用内存。
备份空闲链表指的就是不同迁移类型的空闲链表。但是借用内存不会从同一个order的不同迁移类型的空闲链表查找,而是从MAX_ORDER-1的空闲链表查找是否有内存可以借用。
(4) 第48行, check_new_pages()
函数判断新分配出来的页面是否ok. 主要检查page的__mapcount是否为0,并且设置page的_refcount为0
(5) 第60~64行,这里主要是优化内存外碎片。如果&zone->flags设置了ZONE_BOOSTED_WATERMARK标志位,就会唤醒kswapd线程回收内存。ZONE_BOOSTED_WATERMARK在fallback流程里会被设置,说明此时页面分配器已经向备份空闲链表借用内存,有内存外碎片的可能。
2.2 __rmqueue_smallest
static __always_inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
int migratetype)
{
unsigned int current_order;
struct free_area *area;
struct page *page;
/* Find a page of the appropriate size in the preferred list */
for (current_order = order; current_order < MAX_ORDER; ++current_order) { ------- (1)
area = &(zone->free_area[current_order]);
page = get_page_from_free_area(area, migratetype);
if (!page)
continue;
del_page_from_free_list(page, zone, current_order);
expand(zone, page, order, current_order, migratetype); ------- (2)
set_pcppage_migratetype(page, migratetype);
return page;
}
return NULL;
}
(1) 从current order开始查找zone的空闲链表。如果当前的order中没有空闲对象,那么就会查找上一级order
(2) del_page_from_free_list
函数只会将空闲的对象摘出链表, 真正分配的功能在expand()
函数实现。
expand()
会将空闲链表上的页面块分配一部分后,将剩余的空闲部分挂在zone上更低order的页面块链表上。
static inline void expand(struct zone *zone, struct page *page,
int low, int high, int migratetype)
{
unsigned long size = 1 << high;
while (high > low) {
high--;
size >>= 1;
VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]);
/*
* Mark as guard pages (or page), that will allow to
* merge back to allocator when buddy will be freed.
* Corresponding page table entries will not be touched,
* pages will stay not present in virtual address space
*/
if (set_page_guard(zone, &page[size], high, migratetype))
continue;
add_to_free_list(&page[size], zone, high, migratetype);
set_buddy_order(&page[size], high);
}
}
这里的high就是current_order, 如果分配的页面块大于需求的页面块,那么就将order降一级, 最后通过add_to_free_list把剩余的空闲内存添加到低一级的空闲链表中。
总的申请页面流程如下:
3. 释放页面
释放页面的函数是free_page()
static inline void free_the_page(struct page *page, unsigned int order)
{
if (order == 0) /* Via pcp? */
free_unref_page(page);
else
__free_pages_ok(page, order, FPI_NONE);
}
void __free_pages(struct page *page, unsigned int order)
{
if (put_page_testzero(page))
free_the_page(page, order);
else if (!PageHead(page))
while (order-- > 0)
free_the_page(page + (1 << order), order);
}
和分配页面流程类似, 释放页面会分两种情况
(1) order = 0, free_unref_page()
释放单个页面。 在该函数中首先会调用local_irq_save()
关闭本地中断,因为中断可能会触发页面分配, pcp链表可能会被改变。free_unref_page_commit()
会释放单个页面到pcp链表中。
(2) order > 0,最终会调用到__free_one_page()
释放多个页面。__free_one_page()
既可以释放页面到伙伴系统,也可以处理空闲页面的合并。
static inline void __free_one_page(struct page *page,
unsigned long pfn,
struct zone *zone, unsigned int order,
int migratetype, fpi_t fpi_flags)
函数较长,说下主要思路。
如下图所示, A/B/C为相邻的内存,现在要释放内存A
- 内存A和内存B为相邻内存,我们判断这两块内存是否为buddy关系, 如果为buddy关系,就将这两块内存合并。
static inline bool page_is_buddy(struct page *page, struct page *buddy,
unsigned int order)
{
if (!page_is_guard(buddy) && !PageBuddy(buddy))
return false;
if (buddy_order(buddy) != order)
return false;
/*
* zone check is done late to avoid uselessly calculating
* zone/node ids for pages that could never merge.
*/
if (page_zone_id(page) != page_zone_id(buddy))
return false;
VM_BUG_ON_PAGE(page_count(buddy) != 0, buddy);
return true;
}
互为buddy关系需要满足几个条件:
(1) 内存B要在伙伴系统中
(2) 内存B要和内存A的order相同
(3) 内存B要和内存A的zone id要相同,也就是在同一个zone里。
- 满足条件后,内存A和内存B互为伙伴,我们将这两块内存合并,并将合并的内存A1 和 相邻内存C进行比较
但很显然,A1和C 不是buddy关系,因为order不一样。所以只能将A和B合并后的内存添加到order=2的空闲链表中。 - 以此类推,一直找到能合并的最大内存块。
参考资料
Linux 内核 Buddy 系统
奔跑吧linux内核4.1