文章说明:
Linux内核版本:5.0
架构:ARM64
参考资料及图片来源:《奔跑吧Linux内核》
Linux 5.0内核源码注释仓库地址:
页面迁移机制支持两大类内存页面:
页面迁移的本质是将页面的内容迁移到新的页面。这个过程中会分配新页面,将旧页面的内容复制到新页面,断开旧页面的映射关系,并把映射关系映射到新页面,最后释放旧页面。页面迁移的整个流程图如下所示:
为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程。
页面迁移(page migration)在Linux内核的主函数是migrate_pages()
函数:
// 页面迁移的主函数
// from: 将要迁移页面的链表
// get_new_page: 申请新内存的页面的函数指针
// put_new_page: 迁移失败时释放目标页面的函数指针
// private: 传递给 get_new_page 的参数
// mode:迁移模式
// reason: 迁移的原因
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)
{
...
rc = unmap_and_move(get_new_page, put_new_page,
private, page, pass > 2, mode,
reason);
....
}
migrate_pages()->unmap_and_move()
:
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)
{
...
// 分配一个新的页面
newpage = get_new_page(page, private);
if (!newpage)
return -ENOMEM;
if (page_count(page) == 1) {
...
// 刚分配的页面需要调用 put_new_page() 回调函数
if (put_new_page)
put_new_page(newpage, private);
...
}
// 尝试迁移页面到新分配的页面中
rc = __unmap_and_move(page, newpage, force, mode);
if (rc == MIGRATEPAGE_SUCCESS)
set_page_owner_migrate_reason(newpage, reason);
out:
// 若返回值不等于 -EAGAIN,说明可能迁移没成功
if (rc != -EAGAIN) {
...
}
// 若返回值等于 MIGRATEPAGE_SUCCESS,说明迁移成功,释放页面
if (rc == MIGRATEPAGE_SUCCESS) {
...
// 处理迁移没成功的情况,把页面重新添加到可移动的页面里。释放刚才新分配的页面
} else {
....
}
return rc;
}
migrate_pages()->unmap_and_move()->__unmap_and_move()
:
// page:被迁移的页面
// newpage: 迁移页面的目的地
// force: 表示是否强制迁移。在 migrate_pages() 中,当尝试次数大于 2 时,会设置为 1(0 表示强制迁移)
// mode: 迁移模式
static int __unmap_and_move(struct page *page, struct page *newpage,
int force, enum migrate_mode mode)
{
...
// __PageMovable() 函数用于判断这个页面是否属于非 LRU 页面,它是通过 page 数据结构中的 mapping 成员
// 是否设置了 PAGE_MAPPING_MOVABLE 标志位来判断的
bool is_lru = !__PageMovable(page);
// trylock_page() 尝试给页面加锁,返回 true 则表示当前进程已经成功获取锁
if (!trylock_page(page)) {
// 如果尝试获取也锁不成功
// 若满足 !force || mode == MIGRATE_ASYNC,则直接忽略这个页面,因为这种情况下没有必要睡眠等待页面释放锁
if (!force || mode == MIGRATE_ASYNC)
goto out;
// 如过当前进程设置了 PF_MEMALLOC 标志位,表示当前进程可能处于直接内存压缩的内核路径上,通过睡眠等待页锁是
// 是不安全的,所以直接忽略该页面
if (current->flags & PF_MEMALLOC)
goto out;
// 其他情况下只能等待页锁的释放
lock_page(page);
}
// 处理正在回写的页面,即设置了 PG_writeback 标志位的页面
if (PageWriteback(page)) {
// 只有当页面迁移的模式为 MIGRATE_ASYNC 或者 MIGRAIE_SYNC_LIGHT 且设置强制迁移(force=1)
// 时,才会等待这个页面回写完成,否则直接忽略该页面,该页面不会被迁移
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 可能被释放的特殊情况,因为接下来 try_to_unmap() 函数运行完成时,
// page->_mapcount 会变成 0。在页面迁移的过程中,我们无法知道 anon_vma 数据结构是否被释放了
if (PageAnon(page) && !PageKsm(page))
// page_get_anon_vma() 增加 anon_vma->refcount 引用计数防止其被其他进程释放
anon_vma = page_get_anon_vma(page);
// 尝试给 newpage 申请锁
if (unlikely(!trylock_page(newpage)))
goto out_unlock;
// 若这个页面属于非 LRU 页面
if (unlikely(!is_lru)) {
// move_to_new_page() 函数中会通过驱动程序注册 migratepage() 函数来进行页面迁移
rc = move_to_new_page(newpage, page, mode);
goto out_unlock_both;
}
// 接下来的代码用于处理传统的 LRU 页面
if (!page->mapping) {
// 处理一个特殊情况。当一个交换缓存页面从交换分区被读取之后,它会被添加到 LRU 链表里,我们把它当作
// 一个交换缓存页面。但是它还没有设置 RMAP,因此 page->mapping 为空。若调用 try_to_unmap() 可能
// 会触发内核岩机,因此这里做特殊处理,并跳转到 out_unlock_both 标签处。
VM_BUG_ON_PAGE(PageAnon(page), page);
if (page_has_private(page)) {
try_to_free_buffers(page);
goto out_unlock_both;
}
// page_mapped() 判断该页面的 _mapcount 是否大于或等于 0,若大于或等于 0,说明有用户 PTE 映射该页面
} else if (page_mapped(page)) {
VM_BUG_ON_PAGE(PageAnon(page) && !PageKsm(page) && !anon_vma,
page);
// 对于有用户态进程地址空间映射的页面,调用 try_to_unmap() 解除页面所有映射的用户 PTE
try_to_unmap(page,
TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);
page_was_mapped = 1;
}
// 对于已经解除完所有用户 PTE 映射的页面,调用 move_to_new_page() 把它们迁移到新分配的页面
if (!page_mapped(page))
rc = move_to_new_page(newpage, page, mode);
if (page_was_mapped)
// 迁移页表
// 对于迁移页面失败的情况,调用 remove_migration_ptes() 删除迁移的 PTE
remove_migration_ptes(page,
rc == MIGRATEPAGE_SUCCESS ? newpage : page, false);
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:
if (rc == MIGRATEPAGE_SUCCESS) {
// 对于非 LRU 页面,调用 put_page() 把 newpage 的 _refcount 减 1
if (unlikely(!is_lru))
put_page(newpage);
// 对于传统 LRU 页面,把 newpage 添加到 LRU 链表中
else
putback_lru_page(newpage);
}
return rc;
}
migrate_pages()->unmap_and_move()->__unmap_and_move()->move_to_new_page()
:
// 用于迁移旧页面到新页面中
static int move_to_new_page(struct page *newpage, struct page *page,
enum migrate_mode mode)
{
...
// 判断页面是否属于传统的的 LRU 页面。通过 page 数据结构中的 mapping 成员
// 是否设置了 PAGE_MAPPING_MOVABLE 标志位来判断的
bool is_lru = !__PageMovable(page);
...
// 返回页面的 mapping
mapping = page_mapping(page);
if (likely(is_lru)) {
// 若页面属于传统的 LRU 链表的页面,按以下几种情况处理
// 若 mapping 为空,说明该页面是匿名页面但是没有分配交换缓存,那么调用 migrate_page() 函数来迁移页面
if (!mapping)
rc = migrate_page(mapping, newpage, page, mode);
// 该页面实现了 migratepage(),那么直接调用 mapping->a_ops->migratepage() 来迁移页面
else if (mapping->a_ops->migratepage)
rc = mapping->a_ops->migratepage(mapping, newpage,
page, mode);
// 其他情况下
else
rc = fallback_migrate_page(mapping, newpage,
page, mode);
// 对于页面属于非 LRU 页面的情况,直接调用驱动程序为这个页面注册的 migratepage() 来迁移页面
} else {
...
rc = mapping->a_ops->migratepage(mapping, newpage,
page, mode);
WARN_ON_ONCE(rc == MIGRATEPAGE_SUCCESS &&
!PageIsolated(page));
}
// 处理迁移成功的情况
if (rc == MIGRATEPAGE_SUCCESS) {
...
}
out:
return rc;
}
migrate_pages()->unmap_and_move()->__unmap_and_move()->remove_migration_ptes()->remove_migration_pte()
:
static bool remove_migration_pte(struct page *page, struct vm_area_struct *vma,
unsigned long addr, void *old)
{
...
// page_vma_mapped_walk() 遍历页表,通过虚拟地址找到对应的 PTE
while (page_vma_mapped_walk(&pvmw)) {
...
// 根据新页面和 vma 属性来生成一个 PTE
pte = pte_mkold(mk_pte(new, READ_ONCE(vma->vm_page_prot)));
...
if (PageHuge(new)) {
...
} else{
// 把新生成的 PTE 的内容写回到原来映射的页面中,完成 PTE 的迁移,这样用户进程地址空间就可以
// 通过原来的 PTE 访问新页面
set_pte_at(vma->vm_mm, pvmw.address, pvmw.pte, pte);
// 把新页面添加到 RMAP 系统中
if (PageAnon(new))
page_add_anon_rmap(new, vma, pvmw.address, false);
else
page_add_file_rmap(new, false);
}
...
// 更新相应的高速缓存
update_mmu_cache(vma, pvmw.address, pvmw.pte);
}
return true;
}
migrate_pages()->unmap_and_move()->__unmap_and_move()->move_to_new_page()->migratepage()
:
struct address_space_operations {
...
int (*migratepage) (struct address_space *,
struct page *, struct page *, enum migrate_mode);
...
};
migratepage()
方法会迁移旧页面的内容到新页面中,并且设置page对应的成员和属性。驱动实现的migratepage()
方法在完成页面迁移之后需要显式地调用__ClearPageMovable()
函数清除PAGE_MAPPING_MOVABLE
标志位。如果迁移页面不成功,返回-EAGAIN
,那么根据页面迁移机制会重试一次。若返回其他错误值,那么根据页面迁移机制就会放弃这个页面。