2015年12月16日星期三

KSM continued

地址空间共享

基于UNIX的OS通过forking创建子进程时,新建子进程的地址空间拷贝了forking process原有地址空间的内容,OS实际上使用了copt-on-write的形式将父进程的地址空间与子进程共享而不是去复制RAM中的内存内容。

文件系统cache共享

原先加载的文件通常会缓存在操作系统的文件缓存中(page cache)。文件可以被映射至内存中为了更容易和高效的非线性访问,比如mmap系统调用。多个映射同一文件的此类映射在文件缓存中是共享的。程序的二进制镜像和动态共享库通常是依赖这种机制实现的。这种机制的限制是仅能共享来自同一源对象的内存区域,依旧是拥有相同inode的内存区域。当文件被拷贝,两个文件不再在内存中共享。

虚拟机内存共享

semantic gap阻碍了虚拟机之间在宿主机上物理内存的共享。

Anonymous memory
匿名内存及组成此类内存的内存页在任意存储层级上没有自然表示,典型的例子是运行进程的堆栈空间。匿名内存内容不能在未明确写入后台存储中特定磁盘区域时替换出内存。
Named memory
命名内存页与其相反,它在存储栈上拥有一个自然表示,通常这是磁盘上的一个文件。此类文件的例子是文件映射至内存,指定映射共享库或进程的 文本段。命名页替换出内存时源文件内容未修改的直接丢弃即可,假如映射至内存的文件页发生过改动,修改应写回至原始文件。


page struct

_count 页框被虚拟页应用的次数
_mapcount 这个页框被几个页表项应用
mapping  上下文敏感的语义。命名内存的页框指向文件缓存中描述文件映射的数据结构,index描述这个页框在所表示的文件中的偏移。匿名内存情况下,mapping指向了一个链表,这个链表包含了所有的引用了此页框的虚拟内存空间VMA。

The address space of each task(process or thread) is described in memory descriptor.这个描述符由一系列内存域VMAs构成。每个VMA代表任务虚拟地址空间中的一段。
task_struct            进程
         mm_struct       地址空间
                  vm_area_struct        虚拟地址段

OS会为每个内存区域指定标识vm_flags。这些标识描述了这个内存域应当被如何使用。也决定了在映射时OS如何对应的PTE标志位。


KSM数据结构
Revere Mapping Item
rmap_item得名与它的功能是将合并页的物理页框号(Kpfn)转换为虚拟地址并按COW形式使用页框号。 rmap_item包含了如下信息:
  • 序列码(地址的低比特位)表示此页在过去被多么频繁的访问到
  • jhash2校验码,上次访问此页时根据页内容计算得到
  • state(同样编码在地址的低阶比特位)也反映了本项在哪个数据结构中(mm_slot,unstable tree, stable tree)
  • 指向它地址空间中下一项的指针
mm_slot 包含了提交到去重所有地址范围的有序链表。 应用程序可以从其拥有的地址空间中向mm_slot中增加新的范围,通过调用系统调用madvise以及MADV_MERGEABLE标志位。假如在同一MM中已经存在了可合并的区域,那么新内存区域会被添加到mm的rmap_list中,通过为域内的每页创建新的rmap_item并将它们插入到链表中。

KSM cursor:在每个系统中只存在这ksm_cursor结构体的单一实例。它保存了当前遍历所有mm_slots中rmap_item的线性扫描进程的信息。为此,它包含了当前进程指向mm_slot和rmap_item的指针以及要扫描的下一个地址。

以unstable tree为例,rmap_item如何组成树结构:

属于unstable_tree的rmap_item通过address域的地位的UNSTABLE_FLAG表征。

 树的索引是插入页的全部内容,为了在树中查找与页A相同的页,需将根节点的开始若干字节需要与页A的字节进行比较直至出现不同字节。若A中的异字节小于根节点,KSM继续沿左分支查找,反之查找右分支,重复之前的比较过程直至叶子节点。非稳定树中的页的引用不会受到写保护,也就是说那些由页内容决定的索引也会随时间发生改变,使得树发生混乱,造成有的结点不可达。

与unstable树相反,稳定树中的合并页被设置为COW也就是写保护的,稳定树中的页的内容保持它们被插入树时的内容,即使页已被unmapped。 去映射发生在写时拷贝引用页,状态的改变可以在扫描时检测到。过期的项会被移除出稳定树。

非稳定树与稳定树的另一个差异是,在非稳定树中发现多个项指向同一页内容是一个异常。而在稳定树中,正常的情形是,多个项共享同样的内容。由于不同的使用场景,两个额外域被用于引用rmap_item当它属于稳定树。









 rmap_item包含一个链表hlsit,将所有共享相同合并页框kpfn的rmap_item串联起来。

 Programs inform the kernel, that they want a certain address range to be deduplicated, using the madvise system call with the MADV_MERGEABLE flag. The OS forwards memory regions that are advised to be mergeable to KSM. The scanner
then inserts the VMA and issued address range to the mm_slot data structure right
before the current scan cursor. This way, the advised memory area has time to settle
down before the scan thread visits it for the first time. In its current implementation,
KSM only allows setting MADV_MERGEABLE on pages with 4 KiB page-size.

一段地址范围过后可以通过调用madvise系统调用MADV_UNMERGEABLE标志排除出去重扫描器。然后,该地址范围内的页的共享将被破坏,这段地址范围也会从mm_slot中清除。

KSM并不会增加mm_slot内页的引用计数器。KSM不需要再扫描对应页时锁定页,使用了page_cache_get_speculative()机制访问页却不引用页。

选择下一个页:KSM扫描的是虚拟页地址空间。KSM基于mm_slot数据结构中的顺序选择下个要扫描页。每个mm中的建议页安正地址增长的顺序被扫描。在唤醒间隔ksm_cursor存储当前扫描位置。假如这个接下来原定要扫描的虚拟页的反向重映射项不存在,那么这个位置就变成首次分配的新rmap_item。否则使用之前分配的rmap_item。在扫描过程中,扫描会跳过无效页。也就是它会忽略当前不在内存中的索引页,比如交换至底盘中的页。而且,KSM也不会访问已经位于stable树中的页,通过rmap_item中STABLE_FLAG标识。

Scan Rounds当扫描到最后一个mm_slot的最后一个memory area时,KSM完成一个整个扫描回合。它会通过置非稳定树根节点指针为NULL丢弃掉unstable tree。 然而,rmap_items仍然是通过它们所在mm_slot可达的并未释放掉。oldchecksum和整个stable tree保持不变。

扫描的过程是完全单线程的。






2015年12月14日星期一

Things about KSM

       虚拟机对于宿主机而言是个黑盒,所以即使明知虚拟机之间存在很多冗余数据,宿主并不清楚内容相同的页在内存中的位置。这被称为semantic gap。跨越这个鸿沟的一个解决方案是使用内存扫描器找到共享机会。内存扫描器周期性的扫描虚拟机内存以寻找所有内容相同的页以便于共享它们。
      
传统页共享
       在最先进的基于UNIX的操作中传统上使用两种方式节省空间:第一个是复制进程的地址空间在所有clones之间是cow共享的;第二是共享库通过mmap系统调用从磁盘映射,在所有的进程之间是只读共享的,于是每个库在系统内存中只存在一份。
mmap存在一个重要的限制:只有在页源自同一个文件(有相同的inode)时才能实现内存共享。只要文件被拷贝inode改变,那些文件的mmap会在OS的page cache中创建复制页。

虚拟化环境中的页共享
不幸的是,这两种方法都不能直接适用于虚拟化环境。在虚拟化环境中宿主并不了解客户机的内部状态。半虚拟化环境中客体机被修改以配合宿主协同工作,修改过的客体机会提供给宿主其内部状态相关的信息,以便于宿主可采取适当的措施实现不同虚拟机间以及虚拟机与宿主间的共享内存页。
KSM是通用的内存扫描器和相同内存合并器,它工作在匿名页上。KSM要扫描的内存首先要通过madvise系统调用被建议至KSM。建议过的内存页会被一页一页的线性的周期性的扫描。所有建议页的一个完整的扫描被定义为一个扫描回合scan round。对于每个要扫描的页会计算该页的校验和checksum。如果校验和从上次扫描至此时保持不变,那么这个页会被标记为潜在的共享候选页sharing candidate。这些备选页会被扫描以查看它们的内容是否与stable tree中的另一个页匹配,在稳定数中保存着所有当前已共享的页。假如在稳定树中没有这样的页,那么潜在的共享页会继续与不稳定树总的结点比较,在不稳定树中包含的是所有潜在sharing candidates。假如还是没有发现相同页,一个新的页就会被插入unstable tree里。假如发现匹配页,共享伙伴会从非稳定树中被移除并且两个页会被共享然后插入到稳定树中。
KSM有一个不变的扫描开销,一定数量的CPU总是被KSM内核进程使用。KSM周期性的唤醒,扫描固定数目的页然后进程再次挂起直至下次唤醒。KSM扫描线程是单发射的。内存扫描器总是要在节省内存与CPU使用量之间做折中。

KSM进程分成4个阶段:scan, compare, merge和unmerge。 KSM并不是为虚拟机专门设计的,实际上只要应用的内存是可合并的都能使用KSM。KSM的实现上只能合并匿名页而不适用命名页named pages。命名页有一个关联的backing file,例如通过mmap映射的文件。由于虚拟机的内存对于宿主而言是个黑盒,虚拟机的所有内存页对于宿主都是匿名页。因此,这些页可被KSM合并。

Scan阶段
每隔sleep_millisecs KSM就会扫描pages_to_scan个页。所有建议内存区域的完整扫描称为一个scan round。对于每个要扫描的虚拟页,反向映射项会被创建。使用反向映射项rmap_item保存该虚拟页的状态信息——页内容校验和,页是否已共享。每个虚拟页在系统中都有且只有一个rmap_item表示。KSM的扫描进程需要重用rmap_item当要重新扫描虚拟页时。每个被扫描的虚拟页都会通过KSM的比较程序与现存的共享候选页相比较以发现有相同内容的页。

Compare阶段
比较程序检查在系统中是否已存在与页相同内容的页。KSM使用两个红黑树结构作为查找结构:stable tree包含所有已合并页的指针,而unstable tree包含所有潜在的sharing candidate。虚拟页不像对象有representation,使用相应的rmap_item被用来向树中插入。在红黑树中使用页内容排序。在比较程序的开始,rmap_item可能处于三种状态中的一个:已共享,不稳定和易变。假如反向映射项是当前已共享的,不需要任何进一步的工作过程结束。假如反向映射项是不稳定,表明它已被标记为潜在的共享候选并且已经被插入了unstable tree。如果是易变volatile,它可能在将来称为共享候选页。
在比较过程的开始,将rmap_item从unstable tree中移除,假如反向映射项本不在非稳定树中,那么不做任何事。然后开始把它与稳定树进行比较,假如发现匹配,该页会被合并进稳定树并结束比较程序。不能在稳定树中找到匹配页,就转向非稳定树继续查找。假如存在匹配项,那么两页会被合并进稳定树并且相应项从非稳定树中移除。假如没找到匹配项,该共享备选页会被插入到非稳定树中。在完成整个扫描回合后,非稳定树会被flushed,同时清理了所有的潜在共享备选项。

Merge阶段
合并例程将若干相同内容的虚拟页映射至一个物理页 并且将所映射的物理页标记为cow。此过程将rmap_item插入稳定树。未映射的重复物理页被释放。由于稳定树中的结点依靠页内容索引,几个rmap_items会连接到同一个树结点上。为了解决这个问题,rmap_item会为每个物理页的反向映射项组织成单向链表。

Unmerge阶段
假如应用需要写一个当前已共享的页,操作系统会从MMU得到一个异常,异常处理程序会打破页共享使得进程得到一个专属的私有页的可读可写拷贝。另外,新得到的相应页会被标记为非共享的。它会在下次search/insert操作时从稳定树中被移除,同时设置其对应的rmap_item为易变的。假如它是最后一个共享伙伴,那么稳点树中的结点也要从树中移除。

KSM中的数据结构

KSM使用全局的扫描游标ksm_scan,在KSM进程休眠时存储扫描过程中状态信息。为了能够线性的扫描每一个虚拟内存区域,KSM使用rmap_item链表代表每个虚拟地址空间中的虚拟页状态。一个rmap_item包含了虚拟地址,校验和以及当前每个页的各自状态(volatile, unstable, stable)。rmap_item链表不是在内存建议至KSM时创建的,而是伴随着扫描过程实时构建起来的。最后被扫描的rmap_item的位置会在扫描游标结构体中被存储下来。从这个位置开始扫描链表寻找与记录的地址匹配的rmap_item。假如找不到匹配项,则会在链表尾端添加一个这个地址的新rmap_item项。每个虚拟地址空间的rmap_items组织成一个链表。KSM可以动态的增长advised内存空间。扫描游标结构体包含一个指向当前要扫描地址空间(mm_slot)的指针,虚拟地址,以及当前要扫描的rmap_item。每个地址空间都创建一个rmap_item链表然后存储到mm_slot里。ksm_scan数据结构也包含了扫描回合数信息,这个序列码可被用于判断一个rmap_item何时进行的最后一次比较。
KSM会在进程退出时清除属于该进程的虚拟内存区域VMA。 这个过程与线性扫描是并发进行的,KSM使用扫描游标避免竞争:属于已退出进程地址空间的rmap_item只有当它们不属于当前要扫描地址空间一部分的情况下才能被移除。








2015年12月10日星期四

Debug kernel with Qemu and GDB

The first step: download linux kernel and complie bzImage file.

      make menuconfig
      make bzImage -jN        N=cores numbers
then we get vmlinux under the linux-kernel directory, and under arch/x86/boot/ directory, there will be a bzImage file.

The second step: install qemu and gdb.
      sudo apt-get install qemu
Note: gdb can not be installed by apt-get install directly, because there is a bug when using  gdb remote connect to gdbserver in qemu:
      Remote g packet reply is too long.
To resolve this problem, we should modify the gdb source code. Download the gdb-7.10.tar.xz from http://ftp.gnu.org/gnu/gdb/, then modify the gdb/remote.c like this:

 6565   //if (buf_len > 2 * rsa->sizeof_g_packet)
 6566   //  error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
 6567
 6568   if (buf_len > 2 * rsa->sizeof_g_packet)
 6569     {
 6570       rsa->sizeof_g_packet = buf_len;
 6571
 6572       for (i = 0; i < gdbarch_num_regs (gdbarch); i++)
 6573     {
 6574       if (rsa->regs[i].pnum == -1)
 6575         continue;
 6576
 6577       if (rsa->regs[i].offset >= rsa->sizeof_g_packet)
 6578         rsa->regs[i].in_g_packet = 0;
 6579       else
 6580         rsa->regs[i].in_g_packet = 1;
 6581     }           
 6582    }
 After changing the source code, install gbd:
       ./configure
       make
       sudo make install
makeinfo problem can be solved: sudo apt-get install texinfo

If gdb installed successfully, check the gdb version:
       gdb -v


Then the last step, we can debug kernel with qemu and gdb.
invoke qemu:
       qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd /boot/initrd.img-2.6.32.68 -append "root=/dev/sda" -s -S
intialize gdb:
       gdb vmlinux
Connect gdb to qemu gdbserver port:
(gdb) target remote localhost:1234
(gdb) breakpoint where_you_want_to_break
(gdb) c
.....

The last thing, when compile the kernel, we should open the CONFIG_DEBUG_INFO option by selecting kernel hacking ----> Compile-time checks and compiler options  --->  [*] Compile the kernel with debug info.



Enjoy!









2015年11月19日星期四

kvm_mmu_page结构和用法解析

转载自刘宇涛的微博


KVM最新的内存虚拟化技术中,采用的是两级页表映射tdp (two-dimentional paging),客户虚拟机采用的是传统操作系统的页表,被称做guest page table (GPT),记录的是客户机虚拟地址(GVA)到客户机物理地址(GPA)的映射;而KVM维护的是第二级页表extended page table (EPT,注:AMD的体系架构中其被称为NPT,nested page table,在这篇文章中统一采用Intel的称法EPT),记录的是虚拟机物理地址(GPA)到宿主机物理地址(HPA)的映射。
在介绍主体内容之前,需要先统一下几个缩写(摘自KVM文档:linux/Documentation/virtual/kvm/mmu.txt):
  • pfn: host page frame number,宿主机中某个物理页的帧数
  • hpa: host physical address,宿主机的物理地址
  • hva: host virtual address,宿主机的虚拟地址
  • gfn: guest page frame number,虚拟机中某个物理页的帧数
  • gpa: guest physical address,虚拟机的物理地址
  • gva: guest virtual address,虚拟机的虚拟地址
  • pte: page table entry,指向下一级页表或者页的物理地址,以及相应的权限位
  • gpte: guest pte,指向GPT中下一级页表或者页的gpa,以及相应的权限位
  • spte: shadow pte,指向EPT中下一级页表或者页的hpa,以及相应的权限位
  • tdp: two dimentional paging,也就是我们所说的EPT机制
以上唯一需要解释的是spte,在这里被叫做shadow pte,如果不了解的话,会很容易和以前的shadow paging机制搞混。
KVM在还没有EPT硬件支持的时候,采用的是影子页表(shadow page table)机制,为了和之前的代码兼容,在当前的实现中,EPT机制是在影子页表机制代码的基础上实现的,所以EPT里面的pte和之前一样被叫做 shadow pte,这个会在之后进行详细的说明。

Hardware MMU

GVA->GPA

GPA->HPA

 


两级页表寻址 (tdp)

 

在上图中,包括guest CR3在内,算上PML4E、PDPTE、PDE、PTE,总共有5个客户机物理地址(GPA),这些GPA都需要通过硬件再走一次EPT,得到下一个页表页相对应的宿主机物理地址。
接下来,也就是这篇博文主要的关注点,给定一个GPA,如何通过EPT计算出其相对应的HPA呢?换句话说,如果发生一个EPT violation,即在客户虚拟机中发现某个GPA没有映射到相对应的HPA,那么在KVM这一层会进行什么操作呢?

EPT

 EPT/NPT functions:
  • The new layer to translate guest physical address to host physical address
  • Use EPT/NPT for all guest physical address accesses, including MMIO and guest page table walking
下图是EPT的总体结构:

 和传统的页表一样,EPT的页表结构也是分为四层(PML4、PDPT、PD、PT),EPT Pointer (EPTP)指向PML4的首地址,在没有大页(huge page)的情况下(大页会在以后的博文中说明,这篇博文不考虑大页的情况),一个gpa通过四级页表的寻址,得到相应的pfn,然后加上gpa最后12 位的offset,得到hpa,如下图所示:

物理页与页表页

在这个过程中,有两种不同类型的页结构:物理页(physical page)和页表页(MMU page)。物理页就是真正存放数据的页,而页表页,顾名思义,就是存放页表的页,而且存放的是EPT的页表。其中,第四级(level-4)页表,也就 是EPTP指向的那个页表,是所有MMU pages的根(root),它只有一个页,包含512(4096/8)个页表项(PML4E),每个页表项指向一个第三级(level-3)的页表页 (PDPT),类似的,每个PDPT页表页也是512个页表项指向下一级页表,直到最后一级(level-1)PT,PT中的每个页表项(PTE)指向的 是一个物理页的页帧(pfn)异或上相对应的access bits。
物理页和页表页除了功能和里面存储的内容不同外,它们被创建的方式也是不同的:
  • 物理页可以通过内核提供的__get_free_page来创建,该函数最后会通过底层的alloc_page来返回一段指定大小的内存区域。
  • 页表页则是从mmu_page_cache获得,该page cache是在KVM模块初始化vcpu的时候通过linux内核中的slab机制分配好作为之后MMU pages的cache使用的。
在KVM的代码实现中,每个页表页(MMU page)对应一个数据结构kvm_mmu_page。这个数据结构是理解整个EPT机制的关键,接下来的篇幅就主要围绕这个kvm_mmu_page进行分析。

ept violation处理流程

在引入这个数据结构之前,我们先来整体了解下在发生ept violation之后KVM是如何进行处理的:























handle_ept_violation最终会调用到arch/x86/kvm/mmu.c里面的tdp_page_fault。在该函数中,有两个大的步骤:
  • gfn_to_pfn:在这个过程中,通过gfn->memslot->hva->pfn这一系列步骤得到最后的pfn,这个过程以后会专门用一篇博客来描述;
  • __direct_map:这个函数所做的事情就是把上一步中得到的pfn和gfn的映射关系反映在EPT中,该过程是这篇博文介绍的重点。
顺便提一句,为什么这里叫direct_map呢,即这里的direct是什么意思呢?在我的理解中,这个direct和shadow是相对应的,direct是指在EPT的模式下进行映射,而shadow是在之前shadow paging的模式下进行映射,这主要反映在后面的kvm_mmu_get_page传参过程中(请参阅之后的介绍)。
__direct_map的主要逻辑如下:

static int __direct_map(args...)
{
  ...
  for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator) {
    if (iterator.level == level) {
      mmu_set_spte(vcpu, iterator.sptep, ACC_ALL,
             write, &emulate, level, gfn, pfn,
             prefault, map_writable);
      ...
      break;
    }

    if (!is_shadow_present_pte(*iterator.sptep)) {
      u64 base_addr = iterator.addr;

      base_addr &= PT64_LVL_ADDR_MASK(iterator.level);
      pseudo_gfn = base_addr >> PAGE_SHIFT;
      sp = kvm_mmu_get_page(vcpu, pseudo_gfn, iterator.addr,
                iterator.level - 1,
                1, ACC_ALL, iterator.sptep);

      link_shadow_page(iterator.sptep, sp, true);
    }
  }
  return emulate;
}

 这里的函数代码将映射的建立分成两种情况:

if (iterator.level == level) {
  mmu_set_spte(...);
  ...
}
 
else if (!is_shadow_present_pte(*iterator.sptep)) {
  kvm_mmu_get_page(...);
  link_shadow_page(...);
} 
 
简单来说,__direct_map这个函数是根据传进来的gpa进行计算,从第4级(level-4)页表页开始,一级一级地填写相应页表项,这些都是在for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator)这个宏定义里面实现的,这里不展开。这两种情况是这样子的:
  • 第一种情况是指如果当前页表页的层数(iterator.level)是最后一层的页表页(页表页属于level-1),那么直接通过调用mmu_set_spte(之后会细讲)设置页表项。
  • 第二种情况是指如果当前页表页A不是最后一层,而是中间某一层(leve-4, level-3, level-2),而且该页表项之前并没有初始化(!is_shadow_present_pte(*iterator.sptep)),那么需要调用kvm_mmu_get_page得到或者新建一个页表页B,然后通过link_shadow_page将其link到页表页A相对应的页表项中。

kvm_mmu_get_page

根据代码可能发生的前后关系,我们先来解释下第二种情况,即如何新建一个页表页,即之前所提到的kvm_mmu_page。
这是kvm_mmu_get_page的声明:
static struct kvm_mmu_page *kvm_mmu_get_page(struct kvm_vcpu *vcpu, gfn_t gfn,
        gva_t gaddr, unsigned level, int direct, unsigned access, u64 *parent_pte); 
 
首先解释下传进来的参数都是什么意思:
  • gaddr:产生该ept violation的gpa;
  • gfn:gaddr通过某些计算得到的gfn,计算的公式是(gaddr >> 12) & ~((1 << (level * 9)) - 1),这个会在之后进行解释;
  • level:该页表页对应的level,可能取值为3,2,1;
  • direct:在EPT机制下,该值始终为1,如果是shadow paging机制,该值为0;
  • access:该页表页的访问权限;
  • parent_pte:上一级页表页中指向该级页表页的页表项的地址。
下面举个例子来说明:
假设在__direct_map中,产生ept violation的gpa为0xfffff000,当前的level为3,这个时候,发现EPT中第3级的页表页对应的页表项为空,那么我们就需要创建一个第2级的页表页,然后将其物理地址填在第3级页表页对应的页表项中,那么传给kvm_mmu_get_page的参数很可能是这样子的:
  • gaddr:0xfffff000;
  • gfn: 0xc0000 (通过(0xfffff000 >> 12) & ~((1 << (3 - 1) * 9) - 1)得到);
  • level:2 (通过3 - 1得到);
  • direct:1;
  • access:7(表示可读、可写、可执行);
  • parent_pte:0xffff8800982f8018(这个是第3级页表页相应的页表项的宿主机虚拟地址hva);

struct kvm_mmu_page

接下来看看这个函数的返回值:struct kvm_mmu_page:


 以上是它的定义,该函数定义在arch/x86/include/asm/kvm_host.h中。那么它们分别是什么意思呢?
这里先有一个大概的解释(有几个域还不确定,之后会持续更新),等会儿我们会通过一个具体的例子来说明:

kvm_mmu_page子域 解释
link 将该页结构链接到kvm->arch.active_mmu_pages和invalid_list上,标注该页结构不同的状态
hash_link KVM中会为所有的mmu_page维护一个hash链表,用于快速找到对应的kvm_mmu_page实例,详见之后代码分析
gfn 通过kvm_mmu_get_page传进来的gfn,在EPT机制下,每个kvm_mmu_page对应一个gfn,shadow paging见gfns
role kvm_mmu_page_role结构,详见之后分析
spt 该kvm_mmu_page对应的页表页的宿主机虚拟地址hva
gfns 在shadow paging机制下,每个kvm_mmu_page对应多个gfn,存储在该数组中
unsync 用在最后一级页表页,用于判断该页的页表项是否与guest的翻译同步(即是否所有pte都和guest的tlb一致)
root_rount 用在第4级页表,标识有多少EPTP指向该级页表页
unsync_children 记录该页表页中有多少个spte是unsync状态的
parent_ptes 表示有哪些上一级页表页的页表项指向该页表页(之后会详细介绍)
mmu_valid_gen 该页的generation number,用于和kvm->arch.mmu_valid_gen进行比较,比它小表示该页是invalid的
unsync_child_bitmap 记录了unsync的sptes的bitmap,用于快速查找
write_flooding_count 在页表页写保护模式下,用于避免过多的页表项修改造成的模拟(emulation)
其中,role指向了一个union kvm_mmu_page_role结构,解释如下:
kvm_mmu_page_role子域 解释
level 该页表页的层级
cr4_pae 记录了cr4.pae的值,如果是direct模式,该值为0
quadrant 暂时不清楚
direct 如果是EPT机制,则该值为1,否则为0
access 该页表页的访问权限,参见之后的说明
invalid 表示该页是否有效(暂时不确定)
nxe 记录了efer.nxe的值(暂时不清楚什么作用)
cr0_wp 记录了cr0.wp的值,表示该页是否写保护
smep_andnot_wp 记录了cr4.smep && !cr0.wp的值(暂时不确定什么作用)

kvm_mmu_get_page源码分析

在了解了大部分子域的意义之后,我们来看下kvm_mmu_get_page的代码:

static struct kvm_mmu_page *kvm_mmu_get_page(...)
{
  ...
  role = vcpu->arch.mmu.base_role;
  role.level = level;
  role.direct = direct;
  if (role.direct)
    role.cr4_pae = 0;
  role.access = access;
  ...
  for_each_gfn_sp(vcpu->kvm, sp, gfn) {
    ...
    mmu_page_add_parent_pte(vcpu, sp, parent_pte);
    ...
    return sp;
  }
  ...
  sp = kvm_mmu_alloc_page(vcpu, parent_pte, direct);
  if (!sp)
    return sp;
  sp->gfn = gfn;
  sp->role = role;
  hlist_add_head(&sp->hash_link,
    &vcpu->kvm->arch.mmu_page_hash[kvm_page_table_hashfn(gfn)]);
  ...
  sp->mmu_valid_gen = vcpu->kvm->arch.mmu_valid_gen;
  init_shadow_page_table(sp);
  return sp;
} 
 
  • 一开始会初始化role,在EPT机制下,vcpu->arch.mmu.base_role最开始是被初始化为0的:

arch/x86/kvm/mmu.c
1
2
3
4
5
6
static void init_kvm_tdp_mmu(struct kvm_vcpu *vcpu)
{
    ...
    context->base_role.word = 0;
    ...
}

  • 然后调用for_each_gfn_sp查找之前已经使用过的kvm_mmu_page,该宏根据gfn的值在kvm_mmu_page结构中的hash_link进行,具体可参阅以下代码:

arch/x86/kvm/mmu.c
1
2
3
4
#define for_each_gfn_sp(_kvm, _sp, _gfn)  \  
  hlist_for_each_entry(_sp,   \
    &(_kvm)->arch.mmu_page_hash[kvm_page_table_hashfn(_gfn)], hash_link) \
      if ((_sp)->gfn != (_gfn)) {} else

  • 如果找到了,调用mmu_page_add_parent_pte,设置parent_pte对应的reverse map(reverse map一章会在之后对其进行详细的说明);
  • 如果该gfn对应的页表页不存在,则调用kvm_mmu_alloc_page:

arch/x86/kvm/mmu.c
1
2
3
4
5
6
7
8
9
10
11
12
static struct kvm_mmu_page *kvm_mmu_alloc_page(...)
{
  struct kvm_mmu_page *sp;

  sp = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_header_cache);
  sp->spt = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_cache);
  ...
  list_add(&sp->link, &vcpu->kvm->arch.active_mmu_pages);
  sp->parent_ptes = 0;
  mmu_page_add_parent_pte(vcpu, sp, parent_pte);
  return sp;
}

该函数调用mmu_memory_cache_alloc从之前分配好的mmu page的memory cache中得到一个kvm_mmu_page 
结构体实例,然后将其插入kvm->arch.active_mmu_pages中,同时调用mmu_page_add_parent_pte
函数设置parent pte对应的reverse map。 

一个例子

讲到这里,我们来看一个例子:

  

在上图中,我们假设需要映射gpa(0xfffff000)到其相对应的hpa(0x42faf000)。
另外,对于每一个MMU page,我们都列出了其相对应的kvm_mmu_page对应的页结构中几个比较关键的域的值。
对于gpa为0xfffff000的地址,其gfn为0xfffff,我们将其用二进制表示出来,并按照EPT entry的格式进行分割:


比如,对于EPT pointer指向的第4级(level-4)页表页,它的role.level为4,它的sp->spt为该页表页的hva值0xffff8800982f9000。另外,对于最高层级的页表页来说,它的sp->gfn为0,表示gfn为0的地址可以通过寻址找到该页表页。而由于ept entry中第4段的index为0,所以该页表页的第1个页表项(PML4E)指向了下一层的页表页。
同样的,对于第3级(level-3)页表页,它的role.level为3,sp->spt为该页表页的hva值0xffff8800982f8000。由上图可知,在ept entry中,它的上一层(即第4段)的index值为0,所以其sp->gfn也是0,同样表示gfn为0的地址可以通过寻址找到该页表页。另外,在该层的页表页中,其parent_ptes填的是上一层的页表页中指向该页表页的页表项的地址,即第4级页表页的第一个页表项的地址0xffff8800982f9000,而在ept entry中,由于第3段的index为3,所以该页表页的第3个页表项(PDPTE)指向了下一层的页表页。
以此类推,到第2级(level-2)页表页,前面几项都和之前是类似的,而对于sp->gfn来说,由于它的上一层(第3层)的index值为3,那么通过计算公式(gaddr >> 12) & ~((1 << (level * 9)) - 1)可以得到以下的值:

将其转化为十六进制数,即可得到0xc0000,表示gfn为0xc0000的地址在寻址过程中会找到该页表页。而它的parent_ptes就指向了第3层页表页中第3个页表项的地址0xffff8800982f8018,ept entry中第2段的index 0xfff 表示它最后一项页表项(PDE)指向了下一级的页表页。
类似的,可以算出第1级页表页的sp->gfn为0xffe00,parent_ptes为0xffff880060db7ff8,同时,它的最后一个页表项(PTE)指向了真正的hpa0x42faf000。
 到此为止,gpa被最终映射为hpa,并放映在EPT中,于是下次客户虚拟机应用程序访问该gpa的时候就不会再发生ept violation了。


reverse map

似乎讲到这里就该结束了?
确实,基本上这篇博文的内容就要接近尾声了,只是还有那么一小点内容,关于reverse map。
如果你倒回去看会发现,我们还有两个很重要的函数没有展开:
  • mmu_page_add_parent_pte
  • mmu_set_spte
这两个函数是干什么的呢?其实它们都和reverse map有关。
首先,对于低层级(level-3 to level-1)的页表页结构kvm_mmu_page,我们需要设置上一级的相应的页表项地址,然后通过mmu_page_add_parent_pte设置其parent_pte的reverse map:

arch/x86/kvm/mmu.c
1
2
3
4
5
6
7
static void mmu_page_add_parent_pte(...)
{
  if (!parent_pte)
    return;

  pte_list_add(vcpu, parent_pte, &sp->parent_ptes);
}
另外一点,我说过,页分为两类,物理页和页表页,但是我之前没有说的一点是,页表页本身也被分为两类,高层级(level-4 to level-2)的页表页,和最后一级(level-1)的页表页。
对于高层级的页表页,我们只需要调用link_shadow_page,将页表项的值和相应的权限位直接设置上去就好了,但是对于最后一级的页表项,我们除了设置页表项对应的值之外,还需要做另一件事,rmap_add:

arch/x86/kvm/mmu.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static void mmu_set_spte(...)
{
  ...

  if (set_spte(vcpu, sptep, pte_access, level, gfn, pfn, speculative,
        true, host_writable)) {
    ...
  }
  ...
  if (is_shadow_present_pte(*sptep)) {
    if (!was_rmapped) {
      rmap_count = rmap_add(vcpu, sptep, gfn);
      ...
    }
  }

  ...
}

static int rmap_add(struct kvm_vcpu *vcpu, u64 *spte, gfn_t gfn)
{
  ...
  sp = page_header(__pa(spte));
  kvm_mmu_page_set_gfn(sp, spte - sp->spt, gfn);
  rmapp = gfn_to_rmap(vcpu->kvm, gfn, sp->role.level);
  return pte_list_add(vcpu, spte, rmapp);
}
可以看到,不管是mmu_page_add_parent_pte,还是mmu_set_spte调用的rmap_add,最后都会调用到pte_list_add。
那么问题来了,这货是干嘛的呢?

翻译成中文的话,reverse map被称为反向映射,在上面提到的两个反向映射中,第一个叫parent_ptes,记录的是页表页和指向它的页表项对应的映射,另一个是每个gfn对应的反向映射rmap,记录的是该gfn对应的spte。
我们举rmap为例,给定一个gfn,我们怎么找到其对应的rmap呢?
  • 首先,我们通过gfn_to_memslot得到这个gfn对应的memory slot(这个机制会在以后的博文中提到);
  • 通过得到的slot和gfn,算出相应的index,然后从slot->arch.rmap数组中取出相应的rmap:

arch/x86/kvm/mmu.c
1
2
3
4
5
6
7
8
static unsigned long *__gfn_to_rmap(gfn_t gfn, int level,
            struct kvm_memory_slot *slot)
{
  unsigned long idx;

  idx = gfn_to_index(gfn, slot->base_gfn, level);
  return &slot->arch.rmap[level - PT_PAGE_TABLE_LEVEL][idx];
}
有了gfn对应的rmap之后,我们再调用pte_list_add将这次映射得到的spte加到这个rmap中

arch/x86/kvm/mmu.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int pte_list_add(struct kvm_vcpu *vcpu, u64 *spte,
      unsigned long *pte_list)
{
  struct pte_list_desc *desc;
  int i, count = 0;

  if (!*pte_list) {
    rmap_printk("pte_list_add: %p %llx 0->1\n", spte, *spte);
    *pte_list = (unsigned long)spte;
  } else if (!(*pte_list & 1)) {
    rmap_printk("pte_list_add: %p %llx 1->many\n", spte, *spte);
    desc = mmu_alloc_pte_list_desc(vcpu);
    desc->sptes[0] = (u64 *)*pte_list;
    desc->sptes[1] = spte;
    *pte_list = (unsigned long)desc | 1;
    ++count;
  } else {
    rmap_printk("pte_list_add: %p %llx many->many\n", spte, *spte);
    desc = (struct pte_list_desc *)(*pte_list & ~1ul);
    while (desc->sptes[PTE_LIST_EXT-1] && desc->more) {
      desc = desc->more;
      count += PTE_LIST_EXT;
    }
    if (desc->sptes[PTE_LIST_EXT-1]) {
      desc->more = mmu_alloc_pte_list_desc(vcpu);
      desc = desc->more;
    }
    for (i = 0; desc->sptes[i]; ++i)
      ++count;
    desc->sptes[i] = spte;
  }
  return count;
}
看到这里你可能还是一头雾水,rmap到底是什么,为什么加一个rmap的项要那么复杂?
好吧,其实我的理解是这样的:
  • 首先,rmap就是一个数组,这个数组的每个项都对应了这个gfn反向映射出的某个spte的地址;
  • 其次,由于大部分情况下一个gfn对应的spte只有一个,也就是说,大部分情况下这个数组的大小是1;
  • 但是,这个数组也可能很大,大到你也不知道应该把数组的大小设到多少合适;
  • 所以,总结来说,rmap是一个不确定大小,但是大部分情况下大小为1的数组。
那么,怎么做?
我想说,这是一个看上去很完美的设计!
由于spte的地址只可能是8的倍数(自己想为什么),所以其第一位肯定是0,那么我们就利用这个特点:
  • 我们用一个unsigned long *来表示一个rmap,即上文中的pte_list;
  • 如果这个pte_list为空,则表示这个rmap之前没有创建过,那么将其赋值,即上文中0->1的情况;
  • 如果这个pte_list不为空,但是其第一位是0,则表示这个rmap之前已经被设置了一个值,那么需要将这个pte_list的值改为某个struct pte_list_desc的地址,然后将第一位设成1,来表示该地址并不是单纯的一个spte的地址,而是指向某个struct pte_list_desc,这是上文中1->many的情况;
  • 如果这个pte_list不为空,而且其第一位是1,那么通过访问由这个地址得到的struct pte_list_desc,得到更多的sptes,即上文中many->many的情况。
struct pte_list_desc结构定义如下:

arch/x86/kvm/mmu.c
1
2
3
4
struct pte_list_desc {
    u64 *sptes[PTE_LIST_EXT];
    struct pte_list_desc *more;
};
它是一个单链表的节点,每个节点都存有3个spte的地址,以及下一个节点的位置。
好了,最后一个问题,rmap到底有什么用?
当然,信息总归是有用的,特别是这些和映射相关的信息。
举个例子吧,假如操作系统需要进行页面回收或换出,如果宿主机需要把某个客户机物理页换到disk,那么它就需要修改这个页的物理地址gpa对应的spte,将其设置成不存在。
那么这个该怎么做呢?
当然,你可以用软件走一遍ept页表,找到其对应的spte。但是,这样太慢了!这个时候你就会想,如果有一个gfn到spte的反向映射岂不方便很多!于是,reverse map就此派上用场。
这里最后说一点,如果说有这么一个需求:宿主机想要废除当前客户机所有的MMU页结构,那么如何做最快呢?
当然,你可以从EPTP开始遍历一遍所有的页表页,处理掉所有的MMU页面和对应的映射,但是这种方法效率很低。
如果你还记得之前kvm_mmu_page结构里面的mmu_valid_gen域的话,你就可以通过将kvm->arch.mmu_valid_gen加1,那么当前所有的MMU页结构都变成了invalid,而处理掉页结构的过程可以留给后面的过程(如内存不够时)再处理,这样就可以加快这个过程。
而当mmu_valid_gen值达到最大时,可以调用kvm_mmu_invalidate_zap_all_pages手动废弃掉所有的MMU页结构。

--------------------------------------------------------------------------------------------------------------------------------
The End.