之所以写这篇文章,是因为一个问题:
如果一个进程通过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内存即可。