1. 概述
Contiguous Memory Allocator, CMA
,连续内存分配器,用于分配连续的大块内存。 平时设备不用时又要把内存给系统用,最大化利用内存。
CMA分配器
,会Reserve一片物理内存区域:
- 设备驱动不用时,内存Buddy系统将该区域用于分配和管理Moveable类型页面;
- 设备驱动使用时,用于连续内存分配,此时已经分配的Moveable页面需要进行迁移;
此外,CMA分配器
还可以与DMA子系统
集成在一起,使用DMA的设备驱动程序无需使用单独的CMA API
。
本文通过奔跑吧linux的qemu进行实际演示,内核版本:kernel5.0
2. CMA数据结构
2.1 struct cma
使用struct cma来描述一个CMA区域:
c
struct cma {
//CMA区域物理地址的起始page frame number(页帧号)
unsigned long base_pfn;
//CMA区域的页面数量
unsigned long count;
//描述cma区域页面的分配情况,1表示已分配,0为空闲。
unsigned long *bitmap;
//表示bitmap中一个bit所代表的页面数量(2^order_per_bit)
unsigned int order_per_bit;
struct mutex lock;
#ifdef CONFIG_CMA_DEBUGFS
struct hlist_head mem_head;
spinlock_t mem_head_lock;
#endif
const char *name;
};
cma模块使用bitmap来管理其内存的分配,0表示free,1表示已经分配。具体内存管理的单位和struct cma中的order_per_bit成员相关,如果order_per_bit等于0,表示按照一个一个page来分配和释放,如果order_per_bit等于1,表示按照(2^1)个page组成的block来分配和释放,以此类推。
3. CMA主要流程分析
3.1 CMA初始化流程
系统初始化过程需要先创建CMA区域,创建方法有:dts的reserved memory或者通过命令行参数。
- 通过dts的reserved memory方式,物理内存的描述放置在dts中配置,比如:
c
linux,cma {
compatible = "shared-dma-pool";
/*alloc-ranges = <0x0 0x00000000 0x0 oxffffffff>;*/
reusable;
alignment = <0x0 400000>;
size = <0x0 0x800000>;
linux,cma-default;
};
- linux,cma:为CMA 区域名称。
- compatible:须为"shared-dma-pool"。
- resuable:表示 cma 内存可被 buddy 系统使用。
- size:表示cma区域的大小,单位为字节
- alignment:指定 CMA 区域的地址对齐大小。
- linux,cma-default:属性表示当前 cma 内存将会作为默认的cma pool 用于cma 内存的申请。
QEMU 内置了多款的开发板,其中 QEMU Virt 开发板模拟一款通用的 ARM 开发板,包括内存布局、中断分配、 CPU 配置、时钟配置等信息,这些信息目前都实现在 QEMU 的源代码中,具体文件是 QEMU 源代码: hw/arm/virt.c。所以无法通过修改dts进行实际演示,于是通过第二种方法。
- 通过命令行参数
可以通过在启动kernel时传到kernel command参数,格式如下:
less
cma=nn[MG]@[start[MG][-end[MG]]]
- nn[MG]: 为cma区域大小单位是M或者G,例如启动一个64M大小的CMA为:cma=64MB
- 如果需要指定特定物理地址范围,则后续需要跟@标识符
- @标识符后面start为起始物理地址
- end为结束物理地址,start和end都非必选项。
在qemu run_debian_arm64.sh中修改kernel_arg="noinitrd nokaslr cma=8MB"
在/proc/meminfo中可查看cma内存的大小:
在系统启动过程中,内核对上面描述的dtb文件进行解析,从而完成内存信息注册,调用流程为:
c
setup_arch
arm64_memblock_init
early_init_fdt_scan_reserved_mem
__reserved_mem_init_node
// drivers/of/of_reserved_mem.c
static int __init __reserved_mem_init_node(struct reserved_mem *rmem)
{
extern const struct of_device_id __reservedmem_of_table[];
const struct of_device_id *i;
//遍历__reservedmem_of_table section中的内容
for (i = __reservedmem_of_table; i < &__rmem_of_table_sentinel; i++) {
reservedmem_of_init_fn initfn = i->data;
const char *compat = i->compatible;
//检查到dts中有compatible匹配(CMA这里为"shared-dma-pool")
if (!of_flat_dt_is_compatible(rmem->fdt_node, compat))
continue;
//进一步执行对应的initfn,此时initfn是rmem_cma_setup()
if (initfn(rmem) == 0) {
pr_info("initialized node %s, compatible id %s\n",
rmem->name, compat);
return 0;
}
}
return -ENOENT;
}
RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);
#define RESERVEDMEM_OF_DECLARE(name, compat, init) \
_OF_DECLARE(reservedmem, name, compat, init, reservedmem_of_init_fn)
#define _OF_DECLARE(table, name, compat, fn, fn_type) \
static const struct of_device_id __of_table_##name \
__used __section(__##table##_of_table) \
= { .compatible = compat, \
.data = (fn == (fn_type)NULL) ? fn : fn }
__reserved_mem_init_node会遍历__reservedmem_of_table section中的内容,检查到dts中有compatible匹配(CMA这里为"shared-dma-pool")就进一步执行对应的initfn。通过RESERVEDMEM_OF_DECLARE定义的都会被链接到__reservedmem_of_table这个section段中,最终会调到使用RESERVEDMEM_OF_DECLARE定义的函数,如下rmem_cma_setup:
c
//用dtb中解析出来的地址信息来初始化CMA,用来创建和初始化struct cma
tatic int __init rmem_cma_setup(struct reserved_mem *rmem)
{
phys_addr_t align = PAGE_SIZE << max(MAX_ORDER - 1, pageblock_order);
phys_addr_t mask = align - 1;
unsigned long node = rmem->fdt_node;
struct cma *cma;
int err;
if (!of_get_flat_dt_prop(node, "reusable", NULL) ||
of_get_flat_dt_prop(node, "no-map", NULL))
return -EINVAL;
if ((rmem->base & mask) || (rmem->size & mask)) {
pr_err("Reserved memory: incorrect alignment of CMA region\n");
return -EINVAL;
}
//cma_init_reserved_mem 从保留内存块里面获取一块地址为base,大小为size的内存
err = cma_init_reserved_mem(rmem->base, rmem->size, 0, rmem->name, &cma);
if (err) {
pr_err("Reserved memory: unable to setup CMA region\n");
return err;
}
/* Architecture specific contiguous memory fixup. */
dma_contiguous_early_fixup(rmem->base, rmem->size);
//如果dts指定了linux,cma-default,则将dma_contiguous_set_default指向这个CMA区域,
//使用dma_alloc_contiguous从CMA分配内存时,默认会从该区域划分。
//dma_contiguous_default_area = cma;
if (of_get_flat_dt_prop(node, "linux,cma-default", NULL))
dma_contiguous_set_default(cma);
rmem->ops = &rmem_cma_ops;
rmem->priv = cma;
pr_info("Reserved memory: created CMA memory pool at %pa, size %ld MiB\n",
&rmem->base, (unsigned long)rmem->size / SZ_1M);
return 0;
}
c
int __init cma_init_reserved_mem(phys_addr_t base, phys_addr_t size,
unsigned int order_per_bit,
const char *name,
struct cma **res_cma)
{
struct cma *cma;
phys_addr_t alignment;
/* Sanity checks */
if (cma_area_count == ARRAY_SIZE(cma_areas)) {
pr_err("Not enough slots for CMA reserved regions!\n");
return -ENOSPC;
}
if (!size || !memblock_is_region_reserved(base, size))
return -EINVAL;
/* ensure minimal alignment required by mm core */
alignment = PAGE_SIZE <<
max_t(unsigned long, MAX_ORDER - 1, pageblock_order);
/* alignment should be aligned with order_per_bit */
if (!IS_ALIGNED(alignment >> PAGE_SHIFT, 1 << order_per_bit))
return -EINVAL;
if (ALIGN(base, alignment) != base || ALIGN(size, alignment) != size)
return -EINVAL;
/*
* Each reserved area must be initialised later, when more kernel
* subsystems (like slab allocator) are available.
*/
cma = &cma_areas[cma_area_count];
if (name) {
cma->name = name;
} else {
cma->name = kasprintf(GFP_KERNEL, "cma%d\n", cma_area_count);
if (!cma->name)
return -ENOMEM;
}
cma->base_pfn = PFN_DOWN(base);
cma->count = size >> PAGE_SHIFT;
cma->order_per_bit = order_per_bit;
*res_cma = cma;
cma_area_count++;
totalcma_pages += (size / PAGE_SIZE);
return 0;
}
执行到此, CMA和其它的保留内存是一样的,都是放在 memblock.reserved 中,这部分保留内存一样没能被 Buddy 系统用到。前面讲过为了提升内存利用率,还需要将CMA这部分内存标记后归还给 Buddy系统,供 Buddy作为可移动页面提供给APP或内核内存申请,由cma_init_reserved_areas来实现。
c
//mm/cma.c
static int __init cma_init_reserved_areas(void)
{
int i;
for (i = 0; i < cma_area_count; i++) {
int ret = cma_activate_area(&cma_areas[i]);
if (ret)
return ret;
}
return 0;
}
core_initcall(cma_init_reserved_areas);
static int __init cma_activate_area(struct cma *cma)
{
//cma_bitmap_maxno计算Bitmap需要多少内存
int bitmap_size = BITS_TO_LONGS(cma_bitmap_maxno(cma)) * sizeof(long);
unsigned long base_pfn = cma->base_pfn, pfn = base_pfn;
//i变量表示该CMA eara有多少个pageblock
unsigned i = cma->count >> pageblock_order;
struct zone *zone;
//CMA区域由一个bitmap来管理所有page的状态,一个bit位代表2^order_per_bit个page
cma->bitmap = kzalloc(bitmap_size, GFP_KERNEL);
if (!cma->bitmap)
return -ENOMEM;
WARN_ON_ONCE(!pfn_valid(pfn));
zone = page_zone(pfn_to_page(pfn));
//遍历该CM区域中的所有的pageblock
do {
unsigned j;
//确保CMA区域中的所有page都是在一个zone内
base_pfn = pfn;
for (j = pageblock_nr_pages; j; --j, pfn++) {
WARN_ON_ONCE(!pfn_valid(pfn));
if (page_zone(pfn_to_page(pfn)) != zone)
goto not_in_zone;
}
//最终调用init_cma_reserved_pageblock,以pageblock为单位进行处理,设置migrate type为MIGRATE_CMA,将页面添加到伙伴系统中并更新zone管理的页面总数。
init_cma_reserved_pageblock(pfn_to_page(base_pfn));
} while (--i);
mutex_init(&cma->lock);
return 0;
not_in_zone:
pr_err("CMA area %s could not be activated\n", cma->name);
kfree(cma->bitmap);
cma->count = 0;
return -EINVAL;
}
/* Free whole pageblock and set its migration type to MIGRATE_CMA. */
void __init init_cma_reserved_pageblock(struct page *page)
{
unsigned i = pageblock_nr_pages;
struct page *p = page;
do {
//将页面已经设置的reserved标志位清除掉
__ClearPageReserved(p);
//设置page的_refcount为0
set_page_count(p, 0);
} while (++p, --i);
//将migratetype设置为MIGRATE_CMA
set_pageblock_migratetype(page, MIGRATE_CMA);
//循环调用__free_pages函数,将CMA区域中所有的页面都释放到buddy系统中,每次不超过2^MAX_ORDER - 1个page
if (pageblock_order >= MAX_ORDER) {
i = pageblock_nr_pages;
p = page;
do {
//设置page的_refcount为1
set_page_refcounted(p);
__free_pages(p, MAX_ORDER - 1);
p += MAX_ORDER_NR_PAGES;
} while (i -= MAX_ORDER_NR_PAGES);
} else {
set_page_refcounted(page);
__free_pages(page, pageblock_order);
}
//更新伙伴系统管理的内存数量
adjust_managed_page_count(page, pageblock_nr_pages);
}
void adjust_managed_page_count(struct page *page, long count)
{
atomic_long_add(count, &page_zone(page)->managed_pages);
totalram_pages_add(count);
#ifdef CONFIG_HIGHMEM
if (PageHighMem(page))
totalhigh_pages_add(count);
#endif
}
执行到此,后续这部分CMA内存就可以为buddy所申请。在伙伴系统中migratetype为movable并且分配flag带CMA,可以从CMA分配内存。
c
// mm/page_alloc.c
static __always_inline struct page *
__rmqueue(struct zone *zone, unsigned int order, int migratetype,
unsigned int alloc_flags)
{
struct page *page;
retry:
page = __rmqueue_smallest(zone, order, migratetype);
if (unlikely(!page)) {
//从CMA分配内存
if (migratetype == MIGRATE_MOVABLE)
page = __rmqueue_cma_fallback(zone, order);
if (!page && __rmqueue_fallback(zone, order, migratetype,
alloc_flags))
goto retry;
}
trace_mm_page_alloc_zone_locked(page, order, migratetype);
return page;
}
static __always_inline struct page *__rmqueue_cma_fallback(struct zone *zone,
unsigned int order)
{
return __rmqueue_smallest(zone, order, MIGRATE_CMA);
}
/*
* Go through the free lists for the given migratetype and remove
* the smallest available page from the freelists
*/
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) {
area = &(zone->free_area[current_order]);
page = list_first_entry_or_null(&area->free_list[migratetype],
struct page, lru);
if (!page)
continue;
list_del(&page->lru);
rmv_page_order(page);
area->nr_free--;
expand(zone, page, order, current_order, area, migratetype);
set_pcppage_migratetype(page, migratetype);
return page;
}
return NULL;
}
3.2 CMA分配流程
c
//mm/cma.c
struct page *cma_alloc(struct cma *cma, size_t count, unsigned int align,
bool no_warn)
{
unsigned long mask, offset;
unsigned long pfn = -1;
unsigned long start = 0;
unsigned long bitmap_maxno, bitmap_no, bitmap_count;
size_t i;
struct page *page = NULL;
int ret = -ENOMEM;
if (!cma || !cma->count)
return NULL;
pr_debug("%s(cma %p, count %zu, align %d)\n", __func__, (void *)cma,
count, align);
if (!count)
return NULL;
//bitmap中的page已经按order_per_bit对齐,align_order大于order_per_bit则需要按照align_order对齐,这里返回align_order对齐的mask
mask = cma_bitmap_aligned_mask(cma, align);
//base_pfn按align_order对齐后,按照order_per_bit的offset
offset = cma_bitmap_aligned_offset(cma, align);
//根据CMA区域的页面数量,获取bitmap最大的可用bit数
bitmap_maxno = cma_bitmap_maxno(cma);
//将申请的page count按order_per_bit向上对齐,计算要多少个bit
bitmap_count = cma_bitmap_pages_to_bits(cma, count);
//要申请的bit count大于最大可用bit,不能满足要求
if (bitmap_count > bitmap_maxno)
return NULL;
//在位图中查找下一个连续的bitmap_count个零位区域的偏移量(offset)
for (;;) {
mutex_lock(&cma->lock);
bitmap_no = bitmap_find_next_zero_area_off(cma->bitmap,
bitmap_maxno, start, bitmap_count, mask,
offset);
//没有找到满足条件的零位区域,则返回大于等于bitmap_maxno的值
if (bitmap_no >= bitmap_maxno) {
mutex_unlock(&cma->lock);
break;
}
//将要分配的页面的对应bitmap先置位为1,表示已经分配了
bitmap_set(cma->bitmap, bitmap_no, bitmap_count);
mutex_unlock(&cma->lock);
pfn = cma->base_pfn + (bitmap_no << cma->order_per_bit);
mutex_lock(&cma_mutex);
//使用alloc_config_range来进行内存回收与迁移,最后将这块干净的连续内存返回给调用者使用
ret = alloc_contig_range(pfn, pfn + count, MIGRATE_CMA,
GFP_KERNEL | (no_warn ? __GFP_NOWARN : 0));
mutex_unlock(&cma_mutex);
if (ret == 0) {
page = pfn_to_page(pfn);
break;
}
//分配失败则清除掉bitmap
cma_clear_bitmap(cma, pfn, count);
if (ret != -EBUSY)
break;
pr_debug("%s(): memory range at %p is busy, retrying\n",
__func__, pfn_to_page(pfn));
/* try again with a bit different memory target */
start = bitmap_no + mask + 1;
}
trace_cma_alloc(pfn, page, count, align);
......
pr_debug("%s(): returned %p\n", __func__, page);
return page;
}
继续看cma_alloc流程的alloc_config_range要干哪些事情:
简而言之,目的就是想从一块"脏的"连续内存块(已经被各种类型的内存使用),得到一块干净的连续内存块,要么是回收掉,要么是迁移走,最后将这块干净的连续内存返回给调用者使用,如下图:
c
int alloc_contig_range(unsigned long start, unsigned long end,
unsigned migratetype, gfp_t gfp_mask)
{
unsigned long outer_start, outer_end;
unsigned int order;
int ret = 0;
/*
* struct compact_control结构体:
* .nr_migratepages: 要迁移的页面数
* .order: 要迁移的页面的页框数的指数(即2的幂),初始值为 -1
* .zone: 起始页框所属的内存区域,由 start 所指向的页框计算而来
* .mode: 内存迁移的模式,这里是同步模式(MIGRATE_SYNC)
* .ignore_skip_hint: 是否忽略跳过提示。这里设置为 true
* .no_set_skip_hint: 是否禁止设置跳过提示。这里设置为 true
* .gfp_mask: GFP 内存分配标志,由当前进程的 GFP 上下文 gfp_mask 决定
*/
struct compact_control cc = {
.nr_migratepages = 0,
.order = -1,
.zone = page_zone(pfn_to_page(start)),
.mode = MIGRATE_SYNC,
.ignore_skip_hint = true,
.no_set_skip_hint = true,
.gfp_mask = current_gfp_context(gfp_mask),
};
INIT_LIST_HEAD(&cc.migratepages);
//将目标内存块的pageblock 的迁移类型由MIGRATE_CMA 变更为 MIGRATE_ISOLATE。
//因为buddy系统不会从 MIGRATE_ISOLATE 迁移类型的pageblock 分配页面,可以防止在cma分配过程中,这些页面又被人从Buddy分走。
ret = start_isolate_page_range(pfn_max_align_down(start),
pfn_max_align_up(end), migratetype, 0);
if (ret)
return ret;
//将目标内存块已使用的页面进行迁移处理,迁移过程就是将页面内容复制到其他内存区域,并更新对该页面的引用
ret = __alloc_contig_migrate_range(&cc, start, end);
if (ret && ret != -EBUSY)
goto done;
ret =0;
//回收lru链表
lru_add_drain_all();
//回收per-cpu pages,回收过程需要先将放在PCP缓存页面再归还给Buddy。
drain_all_pages(cc.zone);
order = 0;
outer_start = start;
while (!PageBuddy(pfn_to_page(outer_start))) {
if (++order >= MAX_ORDER) {
outer_start = start;
break;
}
outer_start &= ~0UL << order;
}
if (outer_start != start) {
order = page_order(pfn_to_page(outer_start));
if (outer_start + (1UL << order) <= start)
outer_start = start;
}
/* Make sure the range is really isolated. */
if (test_pages_isolated(outer_start, end, false)) {
pr_info_ratelimited("%s: [%lx, %lx) PFNs busy\n",
__func__, outer_start, end);
ret = -EBUSY;
goto done;
}
/* Grab isolated pages from freelists. */
outer_end = isolate_freepages_range(&cc, outer_start, end);
if (!outer_end) {
ret = -EBUSY;
goto done;
}
/* Free head and tail (if any) */
if (start != outer_start)
free_contig_range(outer_start, start - outer_start);
if (end != outer_end)
free_contig_range(end, outer_end - end);
done:
//pageblock的迁移类型从 MIGRATE_ISOLATE 恢复为 MIGRATE_CMA
undo_isolate_page_range(pfn_max_align_down(start),
pfn_max_align_up(end), migratetype);
return ret;
}
3.2.1 start_isolate_page_range函数
c
//mm/page_isolation.c
//将目标内存块的pageblock 的迁移类型由MIGRATE_CMA 变更为 MIGRATE_ISOLATE。
//#define pageblock_nr_pages (1UL << pageblock_order)
//非HUGETLB pageblock_order = MAX_ORDER - 1
int start_isolate_page_range(unsigned long start_pfn, unsigned long end_pfn, unsigned migratetype, int flags)
{
unsigned long pfn;
unsigned long undo_pfn;
struct page *page;
BUG_ON(!IS_ALIGNED(start_pfn, pageblock_nr_pages));
BUG_ON(!IS_ALIGNED(end_pfn, pageblock_nr_pages));
for (pfn = start_pfn;
pfn < end_pfn;
pfn += pageblock_nr_pages) {
//判断页框号是否有效,返回第一个有效的page
page = __first_valid_page(pfn, pageblock_nr_pages);
if (page &&
set_migratetype_isolate(page, migratetype, flags)) {
undo_pfn = pfn;
goto undo;
}
}
return 0;
undo:
for (pfn = start_pfn;
pfn < undo_pfn;
pfn += pageblock_nr_pages) {
struct page *page = pfn_to_online_page(pfn);
if (!page)
continue;
unset_migratetype_isolate(page, migratetype);
}
return -EBUSY;
}
static inline struct page *
__first_valid_page(unsigned long pfn, unsigned long nr_pages)
{
int i;
for (i = 0; i < nr_pages; i++) {
struct page *page;
//给定的pfn是否有效
if (!pfn_valid_within(pfn + i))
continue;
//只有当页面处于在线状态时,才返回有效页框号的页面
page = pfn_to_online_page(pfn + i);
if (!page)
continue;
return page;
}
return NULL;
}
#define pfn_valid_within(pfn) pfn_valid(pfn)
int pfn_valid(unsigned long pfn)
{
//左移操作符 `<<` 来将页框号 `pfn` 转换为对应的物理地址 `addr`
phys_addr_t addr = pfn << PAGE_SHIFT;
if ((addr >> PAGE_SHIFT) != pfn)
return 0;
#ifdef CONFIG_SPARSEMEM
//页框号对应的内存区域号是否超出了系统支持的内存区域数量 `NR_MEM_SECTIONS`
if (pfn_to_section_nr(pfn) >= NR_MEM_SECTIONS)
return 0;
if (!valid_section(__nr_to_section(pfn_to_section_nr(pfn))))
return 0;
#endif
//检查给定的物理地址是否属于内存映射区域
return memblock_is_map_memory(addr);
}
EXPORT_SYMBOL(pfn_valid);
static int set_migratetype_isolate(struct page *page, int migratetype, int isol_flags)
{
struct zone *zone;
unsigned long flags, pfn;
struct memory_isolate_notify arg;
int notifier_ret;
int ret = -EBUSY;
zone = page_zone(page);
spin_lock_irqsave(&zone->lock, flags);
//如果指定页面已经被标记为隔离状态,则跳转到标签 `out`
if (is_migrate_isolate_page(page))
goto out;
//将页面转换为页框号,并赋值给 `pfn` 变量
pfn = page_to_pfn(page);
arg.start_pfn = pfn;
arg.nr_pages = pageblock_nr_pages;
arg.pages_found = 0;
//函数通知内存隔离事件,并根据返回值进行相应的处理,将结果保存在 `notifier_ret` 变量中
notifier_ret = memory_isolate_notify(MEM_ISOLATE_COUNT, &arg);
notifier_ret = notifier_to_errno(notifier_ret);
if (notifier_ret)
goto out;
//检查是否存在不可移动的页面
if (!has_unmovable_pages(zone, page, arg.pages_found, migratetype, flags))
ret = 0;
out:
//如果 `ret` 为零,则更新相关的内存状态信息,并释放空闲页块
if (!ret) {
unsigned long nr_pages;
int mt = get_pageblock_migratetype(page);
//设置为MIGRATE_ISOLATE
set_pageblock_migratetype(page, MIGRATE_ISOLATE);
zone->nr_isolate_pageblock++;
//将范围内的空闲页面移动到请求类型的空闲列表中
//list_move(&page->lru,&zone->free_area[order].free_list[migratetype]);
nr_pages = move_freepages_block(zone, page, MIGRATE_ISOLATE, NULL);
//vmstat统计,将zone中NR_FREE_PAGES与NR_FREE_CMA_PAGES内存区域的空闲页面数量减少nr_pages
__mod_zone_freepage_state(zone, -nr_pages, mt);
}
spin_unlock_irqrestore(&zone->lock, flags);
if (!ret)
//当系统需要申请内存时,优先从PCP缓存拿,用完了再从buddy批发。
//释放时也优先放回该PCP缓存,缓存满了再放回buddy系统。
//将所有 CPU 上的每个 per-cpu 页面重新放回伙伴分配器
drain_all_pages(zone);
return ret;
}
3.2.2 __alloc_contig_migrate_range函数
将目标内存块已使用的页面进行迁移处理,迁移过程就是将页面内容复制到其他内存区域,并更新对该页面的引用。
c
/* [start, end) must belong to a single zone. */
static int __alloc_contig_migrate_range(struct compact_control *cc,
unsigned long start, unsigned long end)
{
/* This function is based on compact_zone() from compaction.c. */
unsigned long nr_reclaimed;
unsigned long pfn = start;
unsigned int tries = 0;
int ret = 0;
//CPU 的所有类型的pagevec放到对应类型的lru链表中
migrate_prep();
while (pfn < end || !list_empty(&cc->migratepages)) {
if (fatal_signal_pending(current)) {
ret = -EINTR;
break;
}
if (list_empty(&cc->migratepages)) {
cc->nr_migratepages = 0;
//隔离要分配区域已经被Buddy使用的page,存放到cc的链表中,返回的是最后扫描并处理的页框号。
//这里隔离主要是防止后续迁移过程,page被释放或者被LRU回收路径使用。
pfn = isolate_migratepages_range(cc, pfn, end);
if (!pfn) {
ret = -EINTR;
break;
}
tries = 0;
} else if (++tries == 5) {
ret = ret < 0 ? ret : -EBUSY;
break;
}
//对于干净的文件页,直接回收即可
nr_reclaimed = reclaim_clean_pages_from_list(cc->zone,
&cc->migratepages);
cc->nr_migratepages -= nr_reclaimed;
//该函数是页面迁移在内核态的主要接口,它把可移动的物理页迁移到一个新分配的页面。
ret = migrate_pages(&cc->migratepages, alloc_migrate_target,
NULL, 0, cc->mode, MR_CONTIG_RANGE);
}
if (ret < 0) {
putback_movable_pages(&cc->migratepages);
return ret;
}
return 0;
}
migrate_prep()函数
c
//CPU 的所有类型的pagevec放到对应类型的lru链表中
int migrate_prep(void)
{
//清除LRU列表,以便页面可以被隔离
lru_add_drain_all();
return 0;
}
void lru_add_drain(void)
{
lru_add_drain_cpu(get_cpu());
put_cpu();
}
//每个cpu有一个lru_pvecs(DEFINE_PER_CPU(struct lru_pvecs, lru_pvecs)) , lru_pvecs中有5个不同的pagevec,
//这5条链表分别是LRU_INACTIVE_ANON,LRU_ACTIVE_ANON,LRU_INACTIVE_FILE,LRU_ACTIVE_FILE,LRU_UNEVICTABLE
//每个pagevec可以暂存PAGEVEC_SIZE(15)个page。内核在不同地方将需要添加的不同类型页pagevec_add 到 pagevec中。
//将CPU 的所有类型的pagevec放到对应类型的lru链表中。
//要么"cpu"是当前CPU,并且已经禁用了抢占;要么"cpu"正在热拔插,已经失效。
void lru_add_drain_cpu(int cpu)
{
//lru_add_pvec:原来不属于lru链表的,新加入进来的页
struct pagevec *pvec = &per_cpu(lru_add_pvec, cpu);
if (pagevec_count(pvec))
//Add the passed pages to the LRU
__pagevec_lru_add(pvec);
//lru_rotate_pvecs:非活动页并且在非活动lru链表中,将这些页移动到非活动lru链表的末尾
pvec = &per_cpu(lru_rotate_pvecs, cpu);
if (pagevec_count(pvec)) {
unsigned long flags;
local_irq_save(flags);
pagevec_move_tail(pvec);
local_irq_restore(flags);
}
//lru_deactivate_file_pvecs:活动lru链表中的页,会强制清除PG_activate和PG_referenced,
//并加入到非活动lru链表的链表表头中,这些页一般从活动lru链表中的尾部拿出来的
pvec = &per_cpu(lru_deactivate_file_pvecs, cpu);
if (pagevec_count(pvec))
pagevec_lru_move_fn(pvec, lru_deactivate_file_fn, NULL);
pvec = &per_cpu(lru_lazyfree_pvecs, cpu);
if (pagevec_count(pvec))
pagevec_lru_move_fn(pvec, lru_lazyfree_fn, NULL);
activate_page_drain(cpu);
}
void __pagevec_lru_add(struct pagevec *pvec)
{
pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
}
//对每个pvec中的每个page执行move_fn
static void pagevec_lru_move_fn(struct pagevec *pvec,
void (*move_fn)(struct page *page, struct lruvec *lruvec, void *arg),
void *arg)
{
int i;
struct pglist_data *pgdat = NULL;
struct lruvec *lruvec;
unsigned long flags = 0;
//每个pvec中的每个page
for (i = 0; i < pagevec_count(pvec); i++) {
struct page *page = pvec->pages[i];
struct pglist_data *pagepgdat = page_pgdat(page);
if (pagepgdat != pgdat) {
if (pgdat)
spin_unlock_irqrestore(&pgdat->lru_lock, flags);
pgdat = pagepgdat;
spin_lock_irqsave(&pgdat->lru_lock, flags);
}
lruvec = mem_cgroup_page_lruvec(page, pgdat);
(*move_fn)(page, lruvec, arg);
}
if (pgdat)
spin_unlock_irqrestore(&pgdat->lru_lock, flags);
release_pages(pvec->pages, pvec->nr);
pagevec_reinit(pvec);
}
static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
void *arg)
{
enum lru_list lru;
int was_unevictable = TestClearPageUnevictable(page);
VM_BUG_ON_PAGE(PageLRU(page), page);
//设置page的LRU标志位
SetPageLRU(page);
smp_mb();
//可回收的page
if (page_evictable(page)) {
lru = page_lru(page);
update_page_reclaim_stat(lruvec, page_is_file_cache(page),
PageActive(page));
if (was_unevictable)
count_vm_event(UNEVICTABLE_PGRESCUED);
} else {
//不可回收的page
lru = LRU_UNEVICTABLE;
ClearPageActive(page);
SetPageUnevictable(page);
if (!was_unevictable)
count_vm_event(UNEVICTABLE_PGCULLED);
}
//添加page到lru链表
add_page_to_lru_list(page, lruvec, lru);
trace_mm_lru_insertion(page, lru);
}
static __always_inline void add_page_to_lru_list(struct page *page,
struct lruvec *lruvec, enum lru_list lru)
{
//更新lru size,添加page到lru链表
update_lru_size(lruvec, lru, page_zonenum(page), hpage_nr_pages(page));
list_add(&page->lru, &lruvec->lists[lru]);
}
isolate_migratepages_range()函数
c
//mm/compaction.c
//隔离要分配区域已经被Buddy使用的page,存放到cc的链表中,返回的是最后扫描并处理的页框号。
//这里隔离主要是防止后续迁移过程,page被释放或者被LRU回收路径使用。
unsigned long
isolate_migratepages_range(struct compact_control *cc, unsigned long start_pfn,
unsigned long end_pfn)
{
unsigned long pfn, block_start_pfn, block_end_pfn;
/* Scan block by block. First and last block may be incomplete */
pfn = start_pfn;
block_start_pfn = pageblock_start_pfn(pfn);
if (block_start_pfn < cc->zone->zone_start_pfn)
block_start_pfn = cc->zone->zone_start_pfn;
block_end_pfn = pageblock_end_pfn(pfn);
for (; pfn < end_pfn; pfn = block_end_pfn,
block_start_pfn = block_end_pfn,
block_end_pfn += pageblock_nr_pages) {
block_end_pfn = min(block_end_pfn, end_pfn);
if (!pageblock_pfn_to_page(block_start_pfn,
block_end_pfn, cc->zone))
continue;
//将单个页块pageblock中所有可迁移页面隔离
//隔离给定范围 [low_pfn, end_pfn) 中所有可迁移的页面。这个范围被期望位于同一个页块内。
//如果有致命信号待处理,返回 0,否则返回第一个未扫描的页面的 PFN,
//它可能小于、等于或大于 end_pfn。
//页面被隔离到 cc->migratepages 列表(不需要为空),并相应地更新 cc->nr_migratepages 字段。
pfn = isolate_migratepages_block(cc, pfn, block_end_pfn, ISOLATE_UNEVICTABLE);
if (!pfn)
break;
if (cc->nr_migratepages == COMPACT_CLUSTER_MAX)
break;
}
return pfn;
}
reclaim_clean_pages_from_list()函数
c
//mm/vmscan.c
//对于干净的文件页,直接回收即可
unsigned long reclaim_clean_pages_from_list(struct zone *zone,
struct list_head *page_list)
{
struct scan_control sc = {
.gfp_mask = GFP_KERNEL,
.priority = DEF_PRIORITY,
.may_unmap = 1,
};
unsigned long ret;
struct page *page, *next;
LIST_HEAD(clean_pages);
list_for_each_entry_safe(page, next, page_list, lru) {
if (page_is_file_cache(page) && !PageDirty(page) &&
!__PageMovable(page)) {
ClearPageActive(page);
list_move(&page->lru, &clean_pages);
}
}
//回收page,returns the number of reclaimed pages
ret = shrink_page_list(&clean_pages, zone->zone_pgdat, &sc,
TTU_IGNORE_ACCESS, NULL, true);
list_splice(&clean_pages, page_list);
mod_node_page_state(zone->zone_pgdat, NR_ISOLATED_FILE, -ret);
return ret;
}
migrate_pages()函数
该函数是页面迁移在内核态的主要接口,它把可移动的物理页迁移到一个新分配的页面。
系统要使用CMA区域的内存,内存上的页面必须是可迁移的,这样子当设备要使用CMA时页面才能迁移走,那么哪些页面可以迁移呢?有两种类型:
-
LRU上的页面,LRU链表上的页面为用户进程地址空间映射的页面,如匿名页和文件页,都是从buddy分配器migrate type为movable的pageblock上分来的。
-
非LRU上,但是是movable页面。非LRU的页面通常是为kernel space分配的page,要实现迁移需要驱动实现page->mapping->a_ops中的相关方法。比如我们常见的zsmalloc内存分配器的页面就支持迁移。
如下图,migrate_pages()无非是要分配一个新的页面,断开旧页面的映射关系,重新简历映射到新的页面,并且要拷贝旧页面的内容到新页面、新页面的struct page属性要和旧页面设置得一样,最后释放旧的页面。
c
//mm/migrate.c
//caller: migrate_pages(&cc->migratepages, alloc_migrate_target, NULL, 0, cc->mode, MR_CONTIG_RANGE);
/*
* from: 准备迁移页面的链表
* get_new_page:申请新页面函数的指针
* putnew_page:释放新页面函数的指针
* private:传给get_new_page的参数,CMA这里没有使用到传NULL
* mode:迁移模式,CMA的迁移模式会设置为MIGRATE_SYNC。迁移过程可能发生阻塞,若需要迁移某个page正在writeback或locked会等待它完成
* reason:迁移原因,记录是什么功能触发了迁移的行为。因为内核许多路径都需要用migrate_pages来迁移比如还有内存规整、热插拔等。
* CMA传递的为MR_CONTIG_RANG,表示调用alloc_contig_range()分配连续内存。
migrate_mode迁移模式主要有以下几个:
- MIGRATE_ASYNC //异步迁移,过程中不会发生阻塞。
- MIGRATE_SYNC_LIGHT //轻度同步迁移,允许大部分的阻塞操作,唯独不允许脏页的回写操作。
- MIGRATE_SYNC //同步迁移,迁移过程会发生阻塞,若需要迁移的某个page正在writeback或被locked会等待它完成
- MIGRATE_SYNC_NO_COPY //同步迁移,但不等待页面的拷贝过程。页面的拷贝通过回调migratepage(),过程可能会涉及DMA
migrate reason 用于说明迁移原因:
- MR_COMPACTION //内存规整导致的迁移
- MR_MEMORY_FAILURE //当内存出现硬件问题(ECC校验失败等)时触发的页面迁移。 参考memory-failure.c
- MR_MEMORY_HOTPLUG //内存热插拔导致的迁移
- MR_SYSCALL //应用层主动调用migrate_pages()或move_pages()触发的迁移。
- MR_MEMPOLICY_MBIND //调用mbind系统调用设置memory policy时触发的迁移
- MR_NUMA_MISPLACED //numa balance触发的页面迁移(node之间)
- MR_CONTIG_RANGE //调用alloc_contig_range()为CMA或HugeTLB分配连续内存时触发的迁移(和compact相关)。
*/
int migrate_pages(struct list_head *from, new_page_t get_new_page,
free_page_t put_new_page, unsigned long private,
enum migrate_mode mode, int reason)
{
int retry = 1;
int nr_failed = 0;
int nr_succeeded = 0;
int pass = 0;
struct page *page;
struct page *page2;
//获取当前进程是否允许将页写到swap
int swapwrite = current->flags & PF_SWAPWRITE;
int rc;
//如果当前进程不支持将页写到swap,要强制其支持
if (!swapwrite)
current->flags |= PF_SWAPWRITE;
for(pass = 0; pass < 10 && retry; pass++) {
retry = 0;
//遍历 from链表,对每个page调用unmap_and_move来实现迁移处理。page2是page在from中的下一个页
list_for_each_entry_safe(page, page2, from, lru) {
retry:
cond_resched();
if (PageHuge(page))
rc = unmap_and_move_huge_page(get_new_page,
put_new_page, private, page,
pass > 2, mode, reason);
else
//调用get_new_page分配一个新页面,然后使用__unmap_and_move迁移页面到这个新分配的页面中
rc = unmap_and_move(get_new_page, put_new_page,
private, page, pass > 2, mode,
reason);
//返回值处理
switch(rc) {
case -ENOMEM:
if (PageTransHuge(page) && !PageHuge(page)) {
lock_page(page);
rc = split_huge_page_to_list(page, from);
unlock_page(page);
if (!rc) {
list_safe_reset_next(page, page2, lru);
goto retry;
}
}
nr_failed++;
goto out;
case -EAGAIN:
retry++;
break;
case MIGRATEPAGE_SUCCESS:
nr_succeeded++;
break;
default:
nr_failed++;
break;
}
}
}
nr_failed += retry;
rc = nr_failed;
out:
//统计
if (nr_succeeded)
count_vm_events(PGMIGRATE_SUCCESS, nr_succeeded);
if (nr_failed)
count_vm_events(PGMIGRATE_FAIL, nr_failed);
trace_mm_migrate_pages(nr_succeeded, nr_failed, mode, reason);
//恢复PF_SWAPWRITE标记
if (!swapwrite)
current->flags &= ~PF_SWAPWRITE;
return rc;
}
c
static ICE_noinline int unmap_and_move(new_page_t get_new_page,
free_page_t put_new_page,
unsigned long private, struct page *page,
int force, enum migrate_mode mode,
enum migrate_reason reason)
{
int rc = MIGRATEPAGE_SUCCESS;
struct page *newpage;
if (!thp_migration_supported() && PageTransHuge(page))
return -ENOMEM;
//get_new_page分配一个新页面,具体见alloc_migrate_target()
newpage = get_new_page(page, private);
if (!newpage)
return -ENOMEM;
//如果页的refcount == 1,说明此页必定是非文件页而且没有进程映射了此页,此页可以直接释放掉(???)
if (page_count(page) == 1) {
/* page was freed from under us. So we are done. */
ClearPageActive(page);
ClearPageUnevictable(page);
if (unlikely(__PageMovable(page))) {
lock_page(page);
if (!PageMovable(page))
__ClearPageIsolated(page);
unlock_page(page);
}
if (put_new_page)
put_new_page(newpage, private);
else
put_page(newpage);
goto out;
}
//将page取消映射,并把page的数据复制到newpage中、
rc = __unmap_and_move(page, newpage, force, mode);
if (rc == MIGRATEPAGE_SUCCESS)
set_page_owner_migrate_reason(newpage, reason);
out:
if (rc != -EAGAIN) {
//已迁移的页面将删除所有引用并将被释放。未迁移的页面将保留其引用并将被恢复。
list_del(&page->lru);
if (likely(!__PageMovable(page)))
mod_node_page_state(page_pgdat(page), NR_ISOLATED_ANON +
page_is_file_cache(page), -hpage_nr_pages(page));
}
//如果迁移成功,则释放在隔离期间占用的引用。否则,将页面恢复到正确的列表,除非我们想重试。
if (rc == MIGRATEPAGE_SUCCESS) {
put_page(page);
if (reason == MR_MEMORY_FAILURE) {
//有意将 PG_HWPoison 设置在刚释放的页面上。虽然这听起来有些奇怪,但目前这是 HWPoison 标志的工作方式。
if (set_hwpoison_free_buddy_page(page))
num_poisoned_pages_inc();
}
} else {
//迁移不成功,把旧页还原
if (rc != -EAGAIN) {
if (likely(!__PageMovable(page))) {
putback_lru_page(page);
goto put_new;
}
lock_page(page);
if (PageMovable(page))
putback_movable_page(page);
else
__ClearPageIsolated(page);
unlock_page(page);
put_page(page);
}
put_new:
//迁移不成功,将新页放回到空闲页链表中,上面put_new_page为NULL,则调用put_page
if (put_new_page)
put_new_page(newpage, private);
else
put_page(newpage);
}
return rc;
}
c
static __always_inline int __PageMovable(struct page *page)
{
return ((unsigned long)page->mapping & PAGE_MAPPING_FLAGS) ==
PAGE_MAPPING_MOVABLE;
}
static int __unmap_and_move(struct page *page, struct page *newpage,
int force, enum migrate_mode mode)
{
int rc = -EAGAIN;
int page_was_mapped = 0;
struct anon_vma *anon_vma = NULL;
bool is_lru = !__PageMovable(page); (???为什么moveable取反就是在lru上)
//尝试获取old page的页面锁PG_locked
if (!trylock_page(page)) {
//force == 0 或者 对于MIGRATE_ASYNC模式的为异步迁移 拿不到锁就直接跳过此页面。
if (!force || mode == MIGRATE_ASYNC)
goto out;
//force == 1 && mode != MIGRATE_ASYNC
//表示当前进程正在进行内存分配操作
if (current->flags & PF_MEMALLOC)
goto out;
//CMA迁移模式为MIGRATE_SYNC,这里一定使用lock_page一定要等到锁。
lock_page(page);
}
//处理正在回写的页面,根据迁移模式判断是否等待页面回写完成。
//MIGRATE_SYNC_LIGHT和MIGRATE_ASYNC不等待,cma迁移模式为MIGRATE_SYNC,会调用wait_on_page_writeback()函数等待页面回写完成。
if (PageWriteback(page)) {
switch (mode) {
case MIGRATE_SYNC:
case MIGRATE_SYNC_NO_COPY:
break;
default:
rc = -EBUSY;
goto out_unlock;
}
if (!force)
goto out_unlock;
wait_on_page_writeback(page);
}
//对于匿名页,为了防止迁移过程anon_vma数据结构被释放了,需要使用page_get_anon_vma增加anon_vma->refcount引用计数。
if (PageAnon(page) && !PageKsm(page))
anon_vma = page_get_anon_vma(page);
//获取new page的页面锁PG_locked,正常情况都能获取到。
if (unlikely(!trylock_page(newpage)))
goto out_unlock;
//判断这个页面是否属于非LRU页面
if (unlikely(!is_lru)) {
//如果页面为非LRU页面,则通过调用move_to_new_page来处理,该函数会回调驱动的miratepage函数来进行页面迁移。
rc = move_to_new_page(newpage, page, mode);
goto out_unlock_both;
}
//通过page_mapped()判断是否有用户PTE映射了该页面。
//如果有则调用try_to_unmap(),通过反向映射机制解除old page所有相关的PTE。
if (!page->mapping) {
VM_BUG_ON_PAGE(PageAnon(page), page);
if (page_has_private(page)) {
try_to_free_buffers(page);
goto out_unlock_both;
}
} else if (page_mapped(page)) {
/* Establish migration ptes */
VM_BUG_ON_PAGE(PageAnon(page) && !PageKsm(page) && !anon_vma,
page);
//try to remove all page table mappings to a page
try_to_unmap(page,
TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);
page_was_mapped = 1;
}
//调用move_to_new_page,拷贝old page的内容和struct page属性数据到new page。
//对于LRU页面 move_to_new_page是通过调用migrate_page做了两件事:复制struct page的属性和页面内容。
if (!page_mapped(page))
rc = move_to_new_page(newpage, page, mode);
//对页表进行迁移:remove_migration_ptes通过反向映射机制建立new page到进程的映射关系。
if (page_was_mapped)
remove_migration_ptes(page,
rc == MIGRATEPAGE_SUCCESS ? newpage : page, false);
//迁移完成释放old、new页面的PG_locked,当然对于匿名页我们也要put_anon_vma减少的anon_vma->refcount引用计数
out_unlock_both:
unlock_page(newpage);
out_unlock:
/* Drop an anon_vma reference if we took one */
if (anon_vma)
put_anon_vma(anon_vma);
unlock_page(page);
out:
//如果迁移成功,则减少新页面的引用计数,但不会释放页面,因为新页面的所有者已增加引用计数器。
//如果该页面是LRU页面,还将将页面添加到LRU列表中。
if (rc == MIGRATEPAGE_SUCCESS) {
if (unlikely(!is_lru))
put_page(newpage);
else
putback_lru_page(newpage);
}
return rc;
}
如果有page
被映射到多个虚拟地址,可以通过Rmap Walk机制
来遍历所有的VMA
,并最终调用回调函数来取消映射。
c
/*
* rmap_walk_control: 用于控制特定需求的 rmap 遍历
*
* arg: 传递给 rmap_one() 和 invalid_vma()
* rmap_one: 对已映射页面的每个 vma 上执行
* done: 用于检查遍历终止条件
* anon_lock: 通过优化方式获取 anon_lock,而不是使用默认设置
* invalid_vma: 用于跳过不感兴趣的 vma
*/
struct rmap_walk_control {
void *arg;
bool (*rmap_one)(struct page *page, struct vm_area_struct *vma,
unsigned long addr, void *arg);
int (*done)(struct page *page);
struct anon_vma *(*anon_lock)(struct page *page);
bool (*invalid_vma)(struct vm_area_struct *vma, void *arg);
};
/**
* try_to_unmap - 尝试将页面的所有页表映射删除
* @page: 要取消映射的页面
* @flags: action and flags
*
* 尝试删除映射到此页面的所有页表项,调用者必须持有页面锁。
*/
bool try_to_unmap(struct page *page, enum ttu_flags flags)
{
struct rmap_walk_control rwc = {
.rmap_one = try_to_unmap_one, //完成页表映射移除工作
.arg = (void *)flags,
.done = page_mapcount_is_zero,
.anon_lock = page_lock_anon_vma_read,
};
if ((flags & (TTU_MIGRATION|TTU_SPLIT_FREEZE))
&& !PageKsm(page) && PageAnon(page))
rwc.invalid_vma = invalid_migration_vma;
if (flags & TTU_RMAP_LOCKED)
rmap_walk_locked(page, &rwc);
else
//完成页表映射移除工作,内部调用try_to_unmap_one()
rmap_walk(page, &rwc);
return !page_mapcount(page) ? true : false;
}
c
//mm/migrate.c
//非LRU和LRU页面都是通过move_to_new_page来复制页面
static int move_to_new_page(struct page *newpage, struct page *page,
enum migrate_mode mode)
{
struct address_space *mapping;
int rc = -EAGAIN;
bool is_lru = !__PageMovable(page);
VM_BUG_ON_PAGE(!PageLocked(page), page);
VM_BUG_ON_PAGE(!PageLocked(newpage), newpage);
mapping = page_mapping(page);
//是lru页面
if (likely(is_lru)) {
//回调驱动的miratepage函数来进行页面迁移
if (!mapping)
//复制struct page的属性和页面内容
rc = migrate_page(mapping, newpage, page, mode);
else if (mapping->a_ops->migratepage)
//大多数页面都有映射,大多数文件系统都提供了 migratepage 回调函数
//匿名页面是交换空间的一部分,其也有自己的 migratepage 回调函数
rc = mapping->a_ops->migratepage(mapping, newpage,
page, mode);
else
rc = fallback_migrate_page(mapping, newpage,
page, mode);
} else {
//对于非LRU页面,它可能会在隔离步骤后被释放。在这种情况下,我们不应该尝试迁移。
VM_BUG_ON_PAGE(!PageIsolated(page), page);
if (!PageMovable(page)) {
rc = MIGRATEPAGE_SUCCESS;
__ClearPageIsolated(page);
goto out;
}
//迁移
rc = mapping->a_ops->migratepage(mapping, newpage,
page, mode);
WARN_ON_ONCE(rc == MIGRATEPAGE_SUCCESS &&
!PageIsolated(page));
}
//在释放页面之前,需要清除旧的 pagecache 页面的 `page->mapping` 指针。
//但是,为了统计目的,需要保留页面的 `PageAnon` 标志不变。
if (rc == MIGRATEPAGE_SUCCESS) {
if (__PageMovable(page)) {
VM_BUG_ON_PAGE(!PageIsolated(page), page);
//进行页面迁移操作时,需要在 `page_lock`(页面锁)下清除 `PG_movable` 标志位。
//这样可以确保在页面正在被处理的过程中,任何compactor都不会尝试迁移此页面。
__ClearPageIsolated(page);
}
//对于匿名(anonymous)页面和可移动的(movable)页面,其 `page->mapping` 指针会在 `free_pages_prepare` 中进行清除操作,因此在这里不需要再次重置。
//这样可以保持页面标记为匿名页面(PageAnon),以确保其正常工作。
if (!PageMappingFlags(page))
page->mapping = NULL;
}
out:
return rc;
}
int migrate_page(struct address_space *mapping,
struct page *newpage, struct page *page,
enum migrate_mode mode)
{
int rc;
BUG_ON(PageWriteback(page)); /* Writeback must be complete */
//先检查page的refcount是否符合预期,符合后之后会复制页面的映射数据,比如page->index、page->mapping以及PG_swapbacked
rc = migrate_page_move_mapping(mapping, newpage, page, mode, 0);
if (rc != MIGRATEPAGE_SUCCESS)
return rc;
if (mode != MIGRATE_SYNC_NO_COPY)
//page页面内容的复制
migrate_page_copy(newpage, page);
else
//migrate_page_states用来复制页面的flag,如PG_dirty,PG_XXX等标志位,也是属于struct page属性的复制。
migrate_page_states(newpage, page);
return MIGRATEPAGE_SUCCESS;
}
3.3 CMA释放流程
cma_release释放CMA内存的代码很简单,就是把页面重新free给Buddy,清除掉cma的bitmap分配标识:
c
/**
* cma_release() - release allocated pages
* @cma: Contiguous memory region for which the allocation is performed.
* @pages: Allocated pages.
* @count: Number of allocated pages.
*
* 此函数释放由 alloc_cma() 分配的内存。
* 当提供的页面不属于连续区域时,返回 false;否则返回 true。
*/
bool cma_release(struct cma *cma, const struct page *pages, unsigned int count)
{
unsigned long pfn;
if (!cma || !pages)
return false;
pr_debug("%s(page %p)\n", __func__, (void *)pages);
pfn = page_to_pfn(pages);
if (pfn < cma->base_pfn || pfn >= cma->base_pfn + cma->count)
return false;
VM_BUG_ON(pfn + count > cma->base_pfn + cma->count);
//把页面重新free给Buddy
free_contig_range(pfn, count);
//清除掉cma的bitmap分配标识
cma_clear_bitmap(cma, pfn, count);
trace_cma_release(pfn, pages, count);
return true;
}