Linux下的内存交换
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 换出的时机
内存换出的时机有两个:
- 内核唤醒kswapd内核线程进行慢速回收,并进行水位标记(watermark)控制(watermark)。
以内存剩余量为水位的大小,内核中存在三个水位标记
1) high:内存剩余较多,使用压力不大;
2) low:当剩余内存减少到low水位时,表示当前内存压力较大,内核唤醒kswapd内核线程进行回收,直到再次上升到high水位;
3)min:最小水位,内存减少到min水位时,内存严重不足,面临很大的使用压力,内核将会阻塞当前进程,并进行内存回收;小于min的内存非特殊情况不会被使用。
- 通过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;
}