cma内存申请页迁移流程浅析

之所以写这篇文章,是因为一个问题:

如果一个进程通过mmap使用的文件页或者匿名页是位于CMA的内存区域中的,当驱动申请cma内存的时候,这些文件页或者匿名页能够被迁移走进而释放出对应的cma内存吗?

本文代码分析基于linux 4.19.195(真惭愧,linux大版本都7开头了,还在看4.19的内核)

cma内存申请,是通过cma_alloc()函数完成的,总体代码流程大致如下(忽略了大部分cma特有的细节,重点关注内存迁移/回收的动作):

c 复制代码
cma_alloc()->
	alloc_contig_range()->完成连续内存分配的核心函数
		start_isolate_page_range()将目标 pageblock 标记为 MIGRATE_ISOLATE
		__alloc_contig_migrate_range()->扫描并迁移指定范围内的所有页面
			while(pfn < end || !list_empty(&cc->migratepages))->
				isolate_migratepages_range()->
					for()->按 pageblock 粒度遍历扫描内存区域
						isolate_migratepages_block()->逐个页扫描和隔离,识别页面类型:对于文件页,基本直接加入迁移链表,对于匿名页,需要做一定的检查
							list_add(&page->lru, &cc->migratepages)待处理页面加入 cc->migratepages 链表,等待后续处理
				reclaim_clean_pages_from_list()->处理链表上的页面:干净文件页直接回收(无需迁移),脏文件页:暂时不回收,留给迁移流程
					shrink_page_list()
				migrate_pages()->处理需要迁移的页面(包括脏文件页等无法直接回收的页)
					unmap_and_move()->页面迁移的核心协调函数
						get_new_page()为新页面分配内存空间
						__unmap_and_move()->实际执行页面迁移操作
							try_to_unmap()解除页面映射
							move_to_new_page()->将旧页面数据迁移到新页面
								migrate_page()匿名页走这个分支,本质是memcpy
								mapping->a_ops->migratepage()文件页走这个分支,一般来说也会调用到migrate_page()
							remove_migration_ptes()修改页表映射,将所有引用指向新的页面地址,完成迁移
	test_pages_isolated()检查范围 [outer_start, end) 是否全部隔离/空闲
	isolate_freepages_range()验证通过后,从 Buddy 系统摘取空闲页

整个流程的逻辑很清晰,就是对对应区域的内存页,先做隔离(这里主要是针对空闲的页,即本来就在buddy里的页,避免再被从buddy里分配走),然后把那些已经分配出去的页进行回收(这里包含了页表的unmap---本质是做成migration的页表项,内容迁移,页表恢复三个步骤),最后再整体一次性给申请出来

对于能否对对应的匿名页/文件页做回收/迁移,主要有三处判断

第一处位于isolate_migratepages_block()中

c 复制代码
isolate_migratepages_block()
{
	if (PageCompound(page))
		goto isolate_fail;
	/*
		 * Migration will fail if an anonymous page is pinned in memory,
		 * so avoid taking lru_lock and isolating it unnecessarily in an
		 * admittedly racy check.
		 */
		if (!page_mapping(page) &&
			page_count(page) > page_mapcount(page))
			goto isolate_fail;
}

这里如果发现是复合页,就不会进行下去;而如果是匿名页,且page_count(page) > page_mapcount(page),也不会进行下去

第二处位于__unmap_and_move()中,完成unmap动作后,最终都是需要调用migrate_page()去完成具体的迁移操作,而函数migrate_page()会进一步判断能否对对应的匿名页/文件页做回收/迁移,具体逻辑在migrate_page_move_mapping()中

c 复制代码
migrate_page_move_mapping()
{
	//expected_count == 1
	if (!mapping) { //匿名页
		/* Anonymous page without mapping */
		if (page_count(page) != expected_count)
			return -EAGAIN;
	}
	//文件页
	expected_count += hpage_nr_pages(page) + page_has_private(page);
	if (page_count(page) != expected_count ||
		radix_tree_deref_slot_protected(pslot,
					&mapping->i_pages.xa_lock) != page) {
		xa_unlock_irq(&mapping->i_pages);
		return -EAGAIN;
	}
}

第三处针对文件页,位于shrink_page_list()里的__remove_mapping()函数

c 复制代码
__remove_mapping()
{
	if (unlikely(PageTransHuge(page)) && PageSwapCache(page))
			refcount = 1 + HPAGE_PMD_NR;
		else
			refcount = 2;
	if (!page_ref_freeze(page, refcount))
		goto cannot_free;
}

可以看到,前两处地方都使用了page_count(page)来做判断;page_count()很简单,就是该page的引用计数,而第三处则使用了page_ref_freeze(),这个函数也是检查对该page的引用计数。让我们逐个分析。

第一处,尚未做unmap的动作,对于匿名页,我们要求page_count(page) == page_mapcount(page)。我们都知道,page_mapcount()就是该页被映射的次数。对page每做一次映射,该page的引用计数会加一。也就是说,如果两者相等,我们就能保证,通过unmap动作我们就能释放所有对该page的引用,或者说至少是没有人去pin住了对应的内存,这样就能够放心地去做迁移。

第二处,已经做了unmap的动作。对于匿名页,我们要求page_count(page) == 1,这里为1的原因是调用migrate_page_move_mapping()前,isolate_migratepages_block()流程已经get了一下该page,以避免在整个流程中该page被意外释放进而引发问题。此外,既然已经做了unmap,那正常来说应该是没有人在使用该page了,因而引用计数应该为1.

对于文件页,4.19内核都是用的4K页作为page cache,除去page_has_private(page)的情况(似乎是与buffer cache有关),那剩下的hpage_nr_pages(page)会返回1,因而对于文件页,我们要求page_count(page) == 2,即在page cache中带来的引用计数以及isolate_migratepages_block()流程已经get了一下该page。因而引用计数应该为2.

第三处,和第二处的逻辑是一样的,就不重复了。

好了,结合这三处的分析以及最开始对总体流程的把握,我们可以知道,其实对一般的匿名页及文件页,如果临时占用了cma的内存,在驱动真正需要cma内存的时候,基本都是可以通过迁移给挪开的,因为最重要且最关键的引用计数检查都是OK的。那么,会有什么情况下是无法迁移走的呢?本质上,无法迁移走的情况,基本都是通过增加page的引用计数实现的(也就是我们常说的pin住页的情况,驱动经常会调用get_user_page()等函数来实现pin住内存的效果)。那什么时候会去pin住内存呢?举个简单的例子,如果刚好设备通过dma写内存,那就需要pin住内存,保证这块内存不会被移动走,待dma完成后,再unpin内存即可。

相关推荐
未既2 小时前
linux以及docker修改文件描述符
linux·运维·docker
yuanmenghao2 小时前
Linux 性能实战 | 第 20 篇:trace-cmd 与 kernelshark 可视化分析 [特殊字符]
linux·python·性能优化
2401_873587822 小时前
Linux——传输层协议TCP
linux·网络·tcp/ip
嵌入小生0072 小时前
进程(2)---相关函数接口、消亡、exec函数族 | 嵌入式(Linux)
linux·c语言·嵌入式·进程·函数接口·exec函数族·进程的消亡
程序员一点2 小时前
第9章:软件包管理(DNF 与 RPM)
linux·运维·openeuler
@syh.2 小时前
【linux】进程间通信
linux
wdfk_prog2 小时前
EWMA、加权平均与一次低通滤波的对比与选型
linux·笔记·学习·游戏·ssh
longxibo2 小时前
【Ubuntu datasophon1.2.1 二开之六:解决CLICKHOUSE安装问题】
大数据·linux·clickhouse·ubuntu
何中应2 小时前
Jenkins如何注册为CentOS7的一个服务
linux·运维·jenkins·开发工具