Linux内存管理:(八)页面迁移

文章说明:

  • Linux内核版本:5.0

  • 架构:ARM64

  • 参考资料及图片来源:《奔跑吧Linux内核》

  • Linux 5.0内核源码注释仓库地址:

    zhangzihengya/LinuxSourceCode_v5.0_study (github.com)

1. 可迁移页面

页面迁移机制支持两大类内存页面:

  • 传统LRU页面,如匿名页面和文件映射页面
  • 非LRU页面,如zsmalloc或者virtio-balloon页面,以virtio-balloon页面为例,它也有页面迁移的需求,之前的做法是在virtio-balloon驱动中进行迁移操作和相应的逻辑。如果其他的驱动也想做类似的页面迁移,那么它们就不能复用与virtio-balloon驱动相关的代码,必须重新写一套代码,这样会造成很多代码的重复和冗余。为了解决这个问题,内存管理的页面迁移机制提供相应的接口来支持这些非LRU页面的迁移。

2. 页面迁移流程

页面迁移的本质是将页面的内容迁移到新的页面。这个过程中会分配新页面,将旧页面的内容复制到新页面,断开旧页面的映射关系,并把映射关系映射到新页面,最后释放旧页面。页面迁移的整个流程图如下所示:

Linux内存管理:(八)页面迁移_第1张图片

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程。

页面迁移(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,那么根据页面迁移机制会重试一次。若返回其他错误值,那么根据页面迁移机制就会放弃这个页面。

你可能感兴趣的:(Linux内存管理篇,linux)