本文将前面内存规整流程中一个重要函数__unmap_and_move进行详细分析,该函数定义如下
static int __unmap_and_move(struct page *page, struct page *newpage,
int force, enum migrate_mode mode)
该函数作用是进行单页的迁移操作:将page页的内容和状态迁移到新页newpage中去,主要实现思路如下所示(具体处理细节参考下面的源码分析):
1.先对被迁移的页page上锁,置位PG_locked标志。(异步模式拿不到锁则直接跳过该页面的迁移工作).
2.再判断被迁移页page是否正在回写,若在回写则根据迁移模式来判断是否等待被迁移页的回写:
a.MIGRATE_SYNC_LIGHT和MIGRATE_ASYNC不等待
b.MIGRATE_ASYNC等待
2.接着新建一个页表项entry1,这个页表项entry类似于swap页的页表项,其指向的页不在物理内存中,但是页表项entry1的偏移
量是page页对应的页框号.
3.紧接着通过反向映射找到所有映射了被迁移页page的进程页表项,找到后将这些进程映射的页表项都用页表项entry1替换。(在 这以后若有进程想通过虚拟地址访问被迁移页page,都会触发缺页中断,并在缺页处理流程中因等待page的页锁而进入等待状
态,只有page迁移完成后释放其页锁,这些进程才会被再次唤醒,再次唤醒通过同样的虚拟地址去访问物理页时映射的页由page
变为newpage)
4.然后获取待迁入页newpage的页面锁PG_locked (newpage正常情况下页锁不会获取失败,因为该页隔离前为空闲页)
5.最后将被迁移页page的数据和参数复制到新页newpage中,再新建一个页表项entry2,entry2是常规页表项数据,其偏移量是
newppage对应的物理页框号,新页表项entry2创建完后用它将之前所有映射了被迁移页page的进程页表项替换,释放锁,唤醒
等待的进程。
到此page到newpage的页迁移流程执行完毕
介绍该函数实现细节前先对函数参数做一个详细分析:
-
page:待迁移页的页描述符,该页的迁移类型只能是MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE和MIGRATE_CMA中的一种(内存规整中是通过迁移页扫描器隔离到cc->migratepages链表中的页)。
-
newpage:page准备移入的页,该页为空闲页,通常从伙伴系统中取出被隔离到一个特定的区域。(内存规整是通过其空闲页扫描器将空闲页隔离到cc->freepages链表中)
-
force:表示迁移的紧急程度,若if判断为true表示迫切需要进行page页的迁移。
-
mod:迁移模式
enum migrate_mode { /* *异步模式: *内存碎片整理最常用的模式(默认初始是异步模式),在此模式中不会进行阻塞(但是时间片到了可以进行主动调 *度),也就是此种模式不会对文件页进行处理,文件页用于映射文件数据使用,这种模式也是对整体系统压力较小 *的模式(尝试移动的页都是MIGRATE_MOVABLE和MIGRATE_CMA类型的页框)。 */ MIGRATE_ASYNC, /* *轻同步模式: *当异步模式整理不了更多内存时,有两种情况下会使用轻同步模式再次整理内存:1.明确表示分配的不是透明大 *页的情况下;2.当前进程是内核线程的情况下。这个模式中允许大多数操作进行阻塞(比如隔离了太多页,需要阻 *塞等待一段时间,设备繁忙,会等待一小会,锁繁忙,会阻塞直到拿到锁为止)。这种模式会处理匿名页和文件 *页,但是不会对脏文件页执行回写操作,而当处理的页正在回写时,也不会等待其回写结束。 */ MIGRATE_SYNC_LIGHT, /* *同步模式: *所有操作都可以进行阻塞,并且会等待处理的页回写结束,并会对文件页、匿名页进行回写到磁盘,所以导致最 *耗费系统资源,对系统造成的压力最大。它会在三种情况下发生: * 1.从cma中分配内存时; * 2.调用alloc_contig_range()尝试分配一段指定了开始页框号和结束页框号的连续页框时; * 3.通过写入1到sysfs中的/vm/compact_memory文件手动实现同步内存碎片整理。 *同步模式会增加推迟计数器阀值,并且在同步模式下,会设置好compact_control,让同步模式时忽略 *pageblock的PB_migrate_skip标记 */ MIGRATE_SYNC, //同步迁移,但不等待页面的拷贝过程。页面的拷贝通过回调migratepage(),过程可能会涉及DMA MIGRATE_SYNC_NO_COPY, };
函数代码细节分析:
static int __unmap_and_move(struct page *page, struct page *newpage,
int force, enum migrate_mode mode)
{
//迁移执行的状,本函数返回值,默认表示try again(迁移失败后希望再次尝试)
int rc = -EAGAIN;
int page_was_mapped = 0;
struct anon_vma *anon_vma = NULL;
bool is_lru = !__PageMovable(page);
/*
*(1) 获取迁移页page的锁(PG_locked置位),该操作非常关键,因为按照要求在page的迁移过程
* 所有映射了page的进程若要访问page,会通过特殊操作将这些进程加入到一个等待队列中等待
* 着。当该页的迁移执行完后,释放掉该锁,才从上面的等待队将中将那些等待page锁释放的进
* 程唤醒。释放锁前page页的内容和状态数据都迁移到newpage中去了。所以这些进程唤醒后通
* 过原先虚拟地址访问的页由page变为了newpage
*(2)在page迁移过程中,让所有访问迁移页page的进程都处于等待获取page锁状态的方法是:
* (a)首先是获取page的锁
* (b)在迁移page的流程中,函数会先创建一个特殊的页表项(该页表项的状态位设置为页表项
* 指向的物理页不在内存中,而页表项的偏移量设置为page页对应的页框号pfn。(类似swap
* 类型的页表项,只是swap类型的页表项的偏移量是匿名页所在swap分区页槽的索引)
* (c)当某一进程通过虚拟地址访问page物理页时,遇到该特殊的页表项,则会触发缺页中断,而
* 在异常中断处理流程中该特殊页表项也会引导进程进入到一个特殊的处理流程中,该特殊流程会让当前进程因获
*取到到page的页锁而处于等待状态
*/
if (!trylock_page(page)) {
/*
*在异步非紧急的迁移流程中必须要在此获取到page的锁,否立即返回-EAGAIN,该次页迁移失败。
*为了防止后面lock_page(page)获取锁,可能会导致阻塞等待
*/
if (!force || mode == MIGRATE_ASYNC)
goto out;
/*
* It's not safe for direct compaction to call lock_page.
* For example, during page readahead pages are added locked
* to the LRU. Later, when the IO completes the pages are
* marked uptodate and unlocked. However, the queueing
* could be merging multiple pages for one bio (e.g.
* mpage_readpages). If an allocation happens for the
* second or third page, the process can end up locking
* the same page twice and deadlocking. Rather than
* trying to be clever about what pages can be locked,
* avoid the use of lock_page for direct compaction
* altogether.
*/
if (current->flags & PF_MEMALLOC)
goto out;
//同步和轻同步的情况下,都有可能会为了拿到这个锁而阻塞在这
lock_page(page);
}
if (PageWriteback(page)) {//若该页正在回写
/*
* Only in the case of a full synchronous migration is it
* necessary to wait for PageWriteback. In the async case,
* the retry loop is too short and in the sync-light case,
* the overhead of stalling is too much
*/
if (mode != MIGRATE_SYNC) {//异步或轻同步模式页迁移不等待页回写,返回迁移失败(设备繁忙)
rc = -EBUSY;
goto out_unlock;
}
if (!force)//非紧急模式下也结束迁移,该返回值告诉上层函数可以再次尝试该页的迁移
goto out_unlock;//page的页锁还未释放
//同步迁移模式下,阻塞,等待页回写完成
wait_on_page_writeback(page);
}
/*
* By try_to_unmap(), page->mapcount goes down to 0 here. In this case,
* we cannot notice that anon_vma is freed while we migrates a page.
* This get_anon_vma() delays freeing anon_vma pointer until the end
* of migration. File cache pages are no problem because of page_lock()
* File Caches may use write_page() or lock_page() in migration, then,
* just care Anon page here.
*
* Only page_get_anon_vma() understands the subtleties of
* getting a hold on an anon_vma from outside one of its mms.
* But if we cannot get anon_vma, then we won't need it anyway,
* because that implies that the anon page is no longer mapped
* (and cannot be remapped so long as we hold the page lock).
*
*/
if (PageAnon(page) && !PageKsm(page))//非ksm匿名页进入该代码块
//获取匿名页page反向映射中的struct anon_vma指针(page->mapping指向)
anon_vma = page_get_anon_vma(page);
/*
* Block others from accessing the new page when we get around to
* establishing additional references. We are usually the only one
* holding a reference to newpage at this point. We used to have a BUG
* here if trylock_page(newpage) fails, but would like to allow for
* cases where there might be a race with the previous use of newpage.
* This is much like races on refcount of oldpage: just don't BUG().
*获取待迁入页newpage的页锁,失败的话本次迁移失败,返回-EAGAIN,希望尝试对page进行迁移
*/
if (unlikely(!trylock_page(newpage)))
goto out_unlock;
/*
*被迁移页page是从非lru链表中获取,特殊情况:就是被迁移页page迁移前在被进程使用,但未在lru链表中,
*大概率是PAGE_MAPPING_MOVABLE状态的页.对于上述特殊情况,规整进程直接调用move_to_new_page完成迁移工作
*/
if (unlikely(!is_lru)) {
rc = move_to_new_page(newpage, page, mode);
goto out_unlock_both;//page和newpage的页都未释放
}
/*
* Corner case handling:
* 1. When a new swap-cache page is read into, it is added to the LRU
* and treated as swapcache but it has no rmap yet.
* Calling try_to_unmap() against a page->mapping==NULL page will
* trigger a BUG. So handle it here.
* 2. An orphaned page (see truncate_complete_page) might have
* fs-private metadata. The page can be picked up due to memory
* offlining. Everywhere else except page reclaim, the page is
* invisible to the vm, so the page can not be migrated. So try to
* free the metadata, so the page can be freed.
*page->mapping为空该页可能是:
*(1)page->private为空,该页加入到swapcache中且进行过unmap操作,这种页能被迁移但不需要进行unmap操作。
*(2)若page->private不为空,该页指向一个buffer_head链表(网上说是日志缓冲区使用的页,不太清楚),该页
* 不能进行迁移,解锁,返回
*/
if (!page->mapping) {
VM_BUG_ON_PAGE(PageAnon(page), page);
//若日志缓存区使用的页,释放该页所有的buffer_head,然后free该页到cpu高速缓冲区pcp链表中。
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);
/*
*unmap迁移页page
*(1)创建一个特殊的页表项,页表项状态为表示其指向的物理页不在内存中,页表项的偏移量为
* page页页框号pfn.
*(2)通过反向映射将映射了page的所有进程对应的页表项都替换成刚创建的特殊页表项。
* 这样处理后,以前映射了page页的进程,若通过虚拟地址访问page页,都会因为等待page页的页锁而处阻塞状态
*/
try_to_unmap(page,
TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);
page_was_mapped = 1;
}
if (!page_mapped(page))
//将page中的内容copy到newpage,包括页描述符数据,内存数据等
rc = move_to_new_page(newpage, page, mode);
if (page_was_mapped)
/*
*(1).若page和newpage数据拷贝成功,将通过反向映射将以前映射了page所有进程对应的
* pte页表项的偏移量替换成newpage对应的物理页框号pfn,并置位页表项对应bit,表
* 示其指向的物理页在内存中
*(2)若数据拷贝失败,则将所有映射了旧页的进程页表项再重新映射到旧页上,主要是将页表项
* 中表示该映射对应的物理页在内存中的bit置位,当然本次迁移失败。
*/
remove_migration_ptes(page,
rc == MIGRATEPAGE_SUCCESS ? newpage : page, false);
//释放lock的页锁
out_unlock_both:
unlock_page(newpage);
out_unlock:
/* Drop an anon_vma reference if we took one */
if (anon_vma)//解除anon_vma应用计数
put_anon_vma(anon_vma);
unlock_page(page);
out:
/*
* If migration is successful, decrease refcount of the newpage
* which will not free the page because new page owner increased
* refcounter. As well, if it is LRU page, add the page to LRU
* list in here.
*/
if (rc == MIGRATEPAGE_SUCCESS) {
if (unlikely(__PageMovable(newpage)))
put_page(newpage);//newpage->cout--
else
putback_lru_page(newpage);//添加newpage到zone的lru链表中
}
return rc;
}
在上述函数中有两个很重要的函数解除页映射函数try_to_unmap和页数据拷贝函数move_to_new_page,try_to_unmap与页的反向映射有关,参考以前介绍的反向映射相关文档。下面对move_to_new_page函数做一个简单的源码分析。
move_to_new_page函数就是在页迁移的过程中将旧页的参数和数据拷贝到新页中。
/*
* Move a page to a newly allocated page
* The page is locked and all ptes have been successfully removed.
*
* The new page will have replaced the old page if this function
* is successful.
*
* Return value:
* < 0 - error code
* MIGRATEPAGE_SUCCESS - success
*/
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);
/*
*获取被迁移页page的mapping判断页类型,注意到这里时,映射了此页的进程虽然已经对此页进行了unmap操作,
*但进程对应的页表项被设置为了指向page(而不是newpage)的swp_entry_t,所以若以前page有对应进程映射,
*即使被unmap过,现在page->mapping不为空
*/
mapping = page_mapping(page);
if (likely(is_lru)) {//被迁移页以前在lru链表上
//如果mapping为空,被迁移页page原先是被加入到了swapcache中且进行过unmap操作的匿名页,则此处执行默认的migrate_page()
if (!mapping)
//使用默认迁移函数,直接复制
rc = migrate_page(mapping, newpage, page, mode);
/*
* 下面两个判断是处理被迁移page的mapping不为null的情况,page可能是文件页,页可能是匿名页。
*(1)对于文件页mapping指向page关联文件的文件系统变量struct address_space。大多数的文件系统都会提供特
* 定迁移页的回调函数(mapping->a_ops->migratepage())。若不提供则用fallback_migrate_page函数进行页
* 内容迁移
*(2)对于匿名页:有部分匿名页是属于swap space,它对应也会提供特定的页迁移回调函
* 数(mapping->a_ops->migratepage()),而对于普通的有进程页表项映射匿名页通过
* fallback_migrate_page页迁移。
*/
else if (mapping->a_ops->migratepage)
/*
* Most pages have a mapping and most filesystems
* provide a migratepage callback. Anonymous pages
* are part of swap space which also has its own
* migratepage callback. This is the most common path
* for page migration.
*加入到address_space中的页,不使用默认迁移函数,而是使用address_space中的迁移函数,主要会更新旧页在
* address_space中对应slot,让其指向新页(有些匿名页是swap space的一部分页拥有自己的迁移回调函数)。
*/
rc = mapping->a_ops->migratepage(mapping, newpage,
page, mode);
else
/*
*此处是mapping指向的文件系统没有提够页迁移的回调函数时,通过fallback_migrate_page函数来迁移页内
*容,实质还是调用migrate_page,但会做一些判断
*/
rc = fallback_migrate_page(mapping, newpage,
page, mode);
} else {//被迁移页原先未在lru链表上,属于PAGE_MAPPING_MOVABLE类型页特殊处理
/*
* In case of non-lru page, it could be released after
* isolation step. In that case, we shouldn't try migration.
*/
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));
}
/*
* When successful, old pagecache page->mapping must be cleared before
* page is freed; but stats require that PageAnon be left as PageAnon.
*/
if (rc == MIGRATEPAGE_SUCCESS) {//page页成功迁移到newpage
if (__PageMovable(page)) {//特殊的PAGE_MAPPING_MOVABLE类型页迁移成功后,清除page原先的隔离
VM_BUG_ON_PAGE(!PageIsolated(page), page);
/*
* We clear PG_movable under page_lock so any compactor
* cannot try to migrate this page.
*/
__ClearPageIsolated(page);
}
/*
* Anonymous and movable page->mapping will be cleard by
* free_pages_prepare so don't reset it here for keeping
* the type to work PageAnon, for example.
*大概意思是被迁移页page若是匿名页或movable类型的页,在页内容被成功
*迁移到newpage后,出了本函数,后续会通过free_pages_prepare去释放page
*此处就不去重置page的mapping
*/
if (!PageMappingFlags(page))
page->mapping = NULL;
}
out:
return rc;
}
在单页的迁移过程中有很多重要的函数下面列出来,有兴趣的可以结合linux开源源码进行分析理解:
1.int try_to_unmap(struct page *page, enum ttu_flags flags)
对此页进行反向映射,对每一个映射了此页的进程页表进行处理,注意若flags中有TTU_MIGRATION标志,代表着这次反向映 射是为了页面迁移而进行的,而有TTU_IGNORE_MLOCK标志,也代表着内存碎片整理是可以对mlock在内存中的页框进行的
2.将被迁移页page的参数和数据复制到新页newpage中:
a.对于普通匿名页:int migrate_page(struct address_space *mapping,
struct page *newpage, struct page *page,
enum migrate_mode mode)
b.对于文件系统提供页特定页迁移函数的文件页:page->mapping->a_ops->migratepage(mapping,
newpage, page, mode)
c.对于文件系统为提供特定页迁移函数的文件:
static int fallback_migrate_page(struct address_space *mapping,
struct page *newpage, struct page *page, enum migrate_mode mode)
3.void remove_migration_ptes(struct page *old, struct page *new, bool locked)
新页newpage进行一次反向映射(新页已经从旧页中复制好了,新的的反向映射效果和旧页的反向映射效果一模一样),然后将
所有被修改为swap类型的进程页表项都重新设置为映射了新页的页表项