博客\

李群圣

2020-11-24

浏览

Linux下的内存交换

标签: 内存交换 , Linux , KVM

# Linux下的内存交换

# 摘要:

  内存交换是减缓内存使用压力常用的一种技术手段,KVM等hypervisor可以显式地通知宿主机系统是否允许进行内存交换,从而平衡运行时的效率和内存大小。本文的目的是通过概括内存交换的原理,介绍KVM中的内存交换机制,进而深化对内存管理的理解。

# 1. 内存交换

  内存交换是指当系统内存使用压力较大时,内核可以将一部分暂时不能运行(如受到阻塞)的进程内存页换出到外存中,释放这部分内存,供系统分配给新的进程或是将部分外存中的进程页换入。当程序需要访问被换出的内存时,内核又再次将换出的页重新载入,确保不会发生访问错误。内存页的换入与换出的时间较长,实际上是在以时间换空间。

  内存交换所换出的内存页分为两种,文件映射页(file-backed page)和匿名页(anonymous page)。在交换时,文件映射页直接通过文件进行读写,而匿名页包含了堆、栈、数据段等,不以文件的形式存在,无法与磁盘文件直接交换, 需要硬盘划分出额外的交换分区进行读写。

# 2. KVM中通知机制

  为了在虚拟机中实现内存交换,宿主机系统(Linux)需要有一套通知机制(notifier)来通知KVM哪一块内存进行了换入换出。

  在硬件辅助的虚拟化中,hypervisor将直接通过EPT页表进行虚拟机物理地址到宿主机物理地址的转换,而不需要经过宿主机的虚拟地址。以qemu为例,qemu本身具有自己的宿主机虚拟地址空间,其运行的虚拟机则有自己的物理地址空间,而Linux系统进行内存交换时,改变的是宿主机物理地址上的页面。因此,qemu本身的页面被换出时,再次访问该内存,Linux可以根据pte的内容,知道这个地址被换出到了外存,进而执行相应的换入操作;当虚拟机物理地址对应的宿主机物理地址所在的页面被换出时,再次访问该页面,将经由KVM通过EPT页表直接查询宿主机物理地址,不经Linux系统,即使是该内存被换出,KVM也会直接访问EPT中保存的原有地址空间。

  作为扩展,2008年Linux引入了MMU通知程序,本质上是通过回调函数的方式获取Linux系统的通知,当Linux系统中发生某些特定的事情时,就会调用这些回调函数。以Linux4.19中的KVM模块代码为例,它们定义在如下的结构中,其中release是当相关的mm_struct消失时,对KVM的最后一次回调:

static const struct mmu_notifier_ops kvm_mmu_notifier_ops = {

	.flags			= MMU_INVALIDATE_DOES_NOT_BLOCK,
	.invalidate_range_start	= kvm_mmu_notifier_invalidate_range_start,
	.invalidate_range_end	= kvm_mmu_notifier_invalidate_range_end,
	.clear_flush_young	= kvm_mmu_notifier_clear_flush_young,
	.clear_young		= kvm_mmu_notifier_clear_young,
	.test_young		= kvm_mmu_notifier_test_young,
	.change_pte		= kvm_mmu_notifier_change_pte,
	.release		= kvm_mmu_notifier_release,

};

  KVM可以通过如下代码来通知Linux系统,将其notifier注册到Linux系统中:

static int kvm_init_mmu_notifier(struct kvm *kvm)
{
	kvm->mmu_notifier.ops = &kvm_mmu_notifier_ops;
	return mmu_notifier_register(&kvm->mmu_notifier, current->mm);
}

  注册函数如下所示,参数mm是与给定的地址空间相关联的mm_struct结构,只有当这些地址空间发生变化时,Linux才会通知KVM,而与KVM无关的地址空间则没有必要进行统治,以此来提高notifier的效率。

int mmu_notifier_register(struct mmu_notifier *mn, struct mm_struct *mm)
{
	return do_mmu_notifier_register(mn, mm, 1);
}

# 3. 内存交换的实现

# 3.1 换出的时机

  内存换出的时机有两个:

  1. 内核唤醒kswapd内核线程进行慢速回收,并进行水位标记(watermark)控制(watermark)。

  以内存剩余量为水位的大小,内核中存在三个水位标记

  1) high:内存剩余较多,使用压力不大;

  2) low:当剩余内存减少到low水位时,表示当前内存压力较大,内核唤醒kswapd内核线程进行回收,直到再次上升到high水位;

  3)min:最小水位,内存减少到min水位时,内存严重不足,面临很大的使用压力,内核将会阻塞当前进程,并进行内存回收;小于min的内存非特殊情况不会被使用。

  1. 通过drop_cache进行回收;慢速回收需要内存不足时开始换出,drop_cache则可以主动发起回收。

# 3.2 swap cache

  内存与硬盘之间存在着swap cache,但是与cpu与内存之间的cache不同。swap cache一方面通过缓存的方式将交换过程与文件系统相关联,使得我们可以通过文件系统抽象接口完成交换;另一方面成为换入换出过程中的共享资源,在换出过程中,其page frame是在swap cache中供进程访问,通过锁机制可以达到同步效果,防止某一进程进行换出,而另一个进程进行换入的情况。

# 3.3 内存的换出

  以匿名页的换出为例,文件映射页类似,换出过程如下:

  1)首先检查匿名页是否在Swap cache中,如果不在Swap cache中,将该页加入到Swap cache中;

  2)内核通过反向映射,找到该匿名页对应的所有PTE页表,并解除映射关系,与Swap分区中的新页面重新建立映射关系;

  3)映射完成之后,如果脏页已经回刷完成,则内核将匿名页从Swap cache中删除,该页的引用数量降为0,可以被内核回收利用,至此完成了从内存到硬盘的页面交换。

/*
 * shrink_page_list() returns the number of reclaimed pages
 */
static unsigned long shrink_page_list(struct list_head *page_list,
				      struct pglist_data *pgdat,
				      struct scan_control *sc,
				      enum ttu_flags ttu_flags,
				      struct reclaim_stat *stat,
				      bool force_reclaim)
{
	...
	while (!list_empty(page_list)) {
		...

		/*
		 * 是匿名页但不在swap cache中,将该页加入到swap cache中
		 */
		if (PageAnon(page) && PageSwapBacked(page)) {
			if (!PageSwapCache(page)) {
				...

				// add_to_swap()为匿名页分配swap空间,并将该页添加到swap cache中
				if (!add_to_swap(page)) {
					...
				}

				may_enter_fs = 1;

				/* Adding to swap updated mapping */
				mapping = page_mapping(page);
			}
		} else if (unlikely(PageTransHuge(page))) {
			...
		}

		/*
		 * 尝试解除所有映射,try_to_unmap通过反向映射查找该页的所有PTE并解除映射
		 */
		if (page_mapped(page)) {
			enum ttu_flags flags = ttu_flags | TTU_BATCH_FLUSH;

			if (unlikely(PageTransHuge(page)))
				flags |= TTU_SPLIT_HUGE_PMD;
			if (!try_to_unmap(page, flags)) {
				nr_unmap_fail++;
				goto activate_locked;
			}
		}

		if (PageDirty(page)) {
			...
			// pageout函数将该页写回交换分区

			switch (pageout(page, mapping, sc)) {
			...
			case PAGE_SUCCESS:
				...
				mapping = page_mapping(page);
			case PAGE_CLEAN:
				; /* try to free the page below */
			}
		}
		...

		if (PageAnon(page) && !PageSwapBacked(page)) {
			...
		} else if (!mapping || !__remove_mapping(mapping, page, true))  // 调用__remove_mapping将page->_count清0,加入到free_page中,释放该页
			goto keep_locked;
		...
	return nr_reclaimed;
}

# 3.4 内存的换入

  当被换出的页面再次被访问时,触发page fault异常处理,内核通过在换出时写入的页表内容,将换出的内存页重新换入。在linux中,调用do_swap_page函数完成页面换入操作。

  换入的过程如下:

  1)查找swap cache中是否存在所查找的页面,如果存在,则根据swap cache引用的内存页,重新映射并更新页表;如果不存在,则分配新的内存页,并添加到swap cache的引用中,更新内存页内容完成后,更新页表。

  2)换入操作结束后,对应swap area的页引用减一,当减少到0时,代表没有任何进程引用了该页,可以进行回收。

linux-4.19/mm/memory.c

/*
 * We enter with non-exclusive mmap_sem (to exclude vma changes,
 * but allow concurrent faults), and pte mapped but not yet locked.
 * We return with pte unmapped and unlocked.
 *
 * We return with the mmap_sem locked or unlocked in the same cases
 * as does filemap_fault().
 */
vm_fault_t do_swap_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page = NULL, *swapcache;
	struct mem_cgroup *memcg;
	swp_entry_t entry;
	pte_t pte;
	int locked;
	int exclusive = 0;
	vm_fault_t ret = 0;

	if (!pte_unmap_same(vma->vm_mm, vmf->pmd, vmf->pte, vmf->orig_pte))
		goto out;

	entry = pte_to_swp_entry(vmf->orig_pte);  //根据pte返回swap空间的入口entry
	
	...
	
	page = lookup_swap_cache(entry, vma, vmf->address);  // 在swap cache中寻找entry对应的page
	swapcache = page;

	if (!page) {
		struct swap_info_struct *si = swp_swap_info(entry);

		if (si->flags & SWP_SYNCHRONOUS_IO &&
			...
			}
		} else {
			/*
			 * 如果在cache中找不到page,则在swap area中查找,分配新的内存页并从swap area中读入;
			 */
			page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,
						vmf);
			swapcache = page;
		}
		...
		
		ret = VM_FAULT_MAJOR;
		count_vm_event(PGMAJFAULT);
		count_memcg_event_mm(vma->vm_mm, PGMAJFAULT);
	} else if (PageHWPoison(page)) {
		...
	}

	locked = lock_page_or_retry(page, vma->vm_mm, vmf->flags);  // 给page加锁
	
	...

	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
			&vmf->ptl);  // 获取一个pte的entry,重新建立映射
	
	...

	pte = mk_pte(page, vma->vm_page_prot);
	
	...

	/* ksm created a completely new copy */
	if (unlikely(page != swapcache && swapcache)) {
		page_add_new_anon_rmap(page, vma, vmf->address, false);
		mem_cgroup_commit_charge(page, memcg, false, false);
		lru_cache_add_active_or_unevictable(page, vma);
	} else {
		do_page_add_anon_rmap(page, vma, vmf->address, exclusive);
		mem_cgroup_commit_charge(page, memcg, true, false);
		activate_page(page);
	}
	...
	swap_free(entry);	// 减少该swap area的entry上的引用计数
	...
	return ret;
}

【版权声明】Copyright © 2021 openEuler Community。本文由openEuler社区首发,欢迎遵照 CC-BY-SA 4.0 协议规定转载。转载时敬请在正文注明并保留原文链接和作者信息。
【免责声明】本文仅代表作者本人观点,与本网站无关。本网站对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文仅供读者参考,由此产生的所有法律责任均由读者本人承担。