ARM64 KVM工作流程分析

基本介绍

        在kvm虚拟机正常的运行的过程中,guest os正常执行,就好像它在一个物理机上执行一样。当guest执行到敏感指令时,就需要KVM来进行敏感指令的模拟和IO的处理了。因此不严格的可以说KVM的工作(对系统进行虚拟化)主要就是通过对guest的退出进行处理来完成的。本文就讲述ARM64平台KVM对guest的退出处理,主要集中在内核KVM模块的代码分析,涉及到应用层qemu的IO处理,则本文没有涉及。
        ARM64 KVM的guest的退出大概有三种情况:

  1. guest执行WFI/WFE指令。
  2. guest访问特权级的系统寄存器。
  3. 内存相关地址访问退出,这又分两种情况。一种是相关内存地址未分配物理内存,需要进入host分配内存。另一种是该地址是一个IO地址,这种情况要退出到host进行IO模拟。

        上述三种情况guest都会执行敏感指令,从而产生EL2异常,CPU执行模式自动切换到EL2,程序跳转到EL2的el1_sync异常入口,从而KVM开始执行。在CPU切换到EL2模式时,CPU的相关寄存器比如ESR_EL2会记录异常产生的原因,如果是地址访问异常,寄存器还会记录产生异常的地址及要访问的寄存器,KVM可以查询相关的寄存器从而进行分别处理。

WFI/WFE指令模拟

        当操作系统空闲,没有任务要处理时,idle线程就会被调度执行,一般情况下idle线程都会调用WFI/WFI指令进入一个低功耗状态。在物理环境下可以让CPU进入低功耗状态,但是在虚拟环境下,显然不能直接让CPU进入低功耗状态,因为可能host或其他虚拟机还有任务需要处理。所以KVM要对WFI/WFE指令进行模拟,主要是阻塞guest进程一段时间,时间允许的情况下调度其他vcpu执行。下面进行代码分析。
        KVM处理退出的函数是handle_exit函数,该函数中如果判断退出原因是WFI/WFE指令,则调用kvm_handle_wfx函数进行模拟,该函数代码如下:

 74 static int kvm_handle_wfx(struct kvm_vcpu *vcpu, struct kvm_run *run)
 75 {
 76         if (kvm_vcpu_get_hsr(vcpu) & HSR_WFI_IS_WFE) {
 77                 trace_kvm_wfx(*vcpu_pc(vcpu), true);
 78                 vcpu->stat.wfe_exit_stat++;
 79                 kvm_vcpu_on_spin(vcpu, vcpu_mode_priv(vcpu)); //WFE模拟函数
 80         } else {
 81                 trace_kvm_wfx(*vcpu_pc(vcpu), false);
 82                 vcpu->stat.wfi_exit_stat++;
 83                 kvm_vcpu_block(vcpu); //WFI模拟函数
 84                 kvm_clear_request(KVM_REQ_UNHALT, vcpu);
 85         }
 86 
 87         kvm_skip_instr(vcpu, kvm_vcpu_trap_il_is32bit(vcpu));
 88 
 89         return 1;
 90 }

        WFE指令的模拟(kvm_vcpu_on_spin函数,该函数较简单,没有贴出代码一行行分析),是把guest进程挂起直到下次再调度到guest进程,则进入guest执行。为了优化实现,具体的做法并不是将该vcpu进程直接挂起到进程队列,而是把CPU让给同一虚拟机的其他CPU(yield_to函数),为了公平起见,会按照顺序让渡给vm中的vcpu,下次再让渡时从上次停止让渡的vcpu向下遍历。并不是每个CPU都能被yield_to,必须是之前被调度走的(vcpu->preempted=true)且runable的。
        WFI指令模拟,调用kvm_vcpu_block直到接收到一个到guest的IRQ或FIQ。

2149 void kvm_vcpu_block(struct kvm_vcpu *vcpu)
2150 {
2151         ktime_t start, cur;
2152         DECLARE_SWAITQUEUE(wait);
2153         bool waited = false;
2154         u64 block_ns;
2155 
2156         start = cur = ktime_get();
2157         if (vcpu->halt_poll_ns) {
2158                 ktime_t stop = ktime_add_ns(ktime_get(), vcpu->halt_poll_ns);
2159 
2160                 ++vcpu->stat.halt_attempted_poll;
2161                 do {
2162                         /*
2163                          * This sets KVM_REQ_UNHALT if an interrupt
2164                          * arrives.
2165                          */
2166                         if (kvm_vcpu_check_block(vcpu) < 0) {
2167                                 ++vcpu->stat.halt_successful_poll;
2168                                 if (!vcpu_valid_wakeup(vcpu))
2169                                         ++vcpu->stat.halt_poll_invalid;
2170                                 goto out;
2171                         }
2172                         cur = ktime_get();
2173                 } while (single_task_running() && ktime_before(cur, stop));
2174         }
2176         kvm_arch_vcpu_blocking(vcpu);
2177 
2178         for (;;) {
2179                 prepare_to_swait(&vcpu->wq, &wait, TASK_INTERRUPTIBLE);
2180 
2181                 if (kvm_vcpu_check_block(vcpu) < 0)
2182                         break;
2183 
2184                 waited = true;
2185                 schedule();
2186         }
2187 
2188         finish_swait(&vcpu->wq, &wait);
2189         cur = ktime_get();
2190 
2191         kvm_arch_vcpu_unblocking(vcpu);
2192 out:
2193         block_ns = ktime_to_ns(cur) - ktime_to_ns(start);
2194 
2195         if (!vcpu_valid_wakeup(vcpu))
2196                 shrink_halt_poll_ns(vcpu);
2197         else if (halt_poll_ns) {
2198                 if (block_ns <= vcpu->halt_poll_ns)
2199                         ;
2200                 /* we had a long block, shrink polling */
2201                 else if (vcpu->halt_poll_ns && block_ns > halt_poll_ns)
2202                         shrink_halt_poll_ns(vcpu);
2203                 /* we had a short halt and our poll time is too small */
2204                 else if (vcpu->halt_poll_ns < halt_poll_ns &&
2205                         block_ns < halt_poll_ns)
2206                         grow_halt_poll_ns(vcpu);
2207         } else
2208                 vcpu->halt_poll_ns = 0;
2209 
2210         trace_kvm_vcpu_wakeup(block_ns, waited, vcpu_valid_wakeup(vcpu));
2211         kvm_arch_vcpu_block_finish(vcpu);
2212 }
2213 EXPORT_SYMBOL_GPL(kvm_vcpu_block);`C

        kvm_vcpu_block的功能是让vcpu线程阻塞,简单的做法就是调用schedule程序让渡CPU直到下次调度回来检查有没有达到VCPU的唤醒条件(vcpu接收到IRQ/FIQ/singal)。但是kvm在调度出去后再调度回来可能会经历一个较长的时间,如果要等待的事件很快到来,kvm_vcpu_block其实应该等待较少的时间。这里有个优化实现,kvm_vpu_block在调用schedule调度走之前,先用一小段时间(vcpu->halt_poll_ns)循环poll事件是否到来,如果到来就结束block,然后返回,否则就schedule.
        2157~2176,循环探测事件是否到来,如果到来就结束block,然后返回。
        2178~2186,schedule让出CPU
        2195~2210,poll的时间长短不是固定的,而是根据block时间的长短来动态伸缩,最开始设为0。这段代码就是根据每次block的时间伸缩poll的时间的代码。

guest os访问系统寄存器模拟

        guest os访问CPU的系统寄存器(非通用寄存器)的指令也是特权指令,CPU捕获到该指令后会自动切换到EL2,从而进入KVM执行。KVM的handle_exit函数判断guest退出的原因是访问系统寄存器,则会调用kvm_handle_sys_reg函数来处理。

1912 int kvm_handle_sys_reg(struct kvm_vcpu *vcpu, struct kvm_run *run)
1913 {      
1914         struct sys_reg_params params;
1915         unsigned long esr = kvm_vcpu_get_hsr(vcpu);
1916         int Rt = kvm_vcpu_sys_get_rt(vcpu);
1917         int ret;
1918        
1919         trace_kvm_handle_sys_reg(esr);
1920        
1921         params.is_aarch32 = false;
1922         params.is_32bit = false;
1923         params.Op0 = (esr >> 20) & 3;
1924         params.Op1 = (esr >> 14) & 0x7;
1925         params.CRn = (esr >> 10) & 0xf;
1926         params.CRm = (esr >> 1) & 0xf;
1927         params.Op2 = (esr >> 17) & 0x7;
1928         params.regval = vcpu_get_reg(vcpu, Rt);
1929         params.is_write = !(esr & 1);
1930 
1931         ret = emulate_sys_reg(vcpu, &params);
1932 
1933         if (!params.is_write)
1934                 vcpu_set_reg(vcpu, Rt, params.regval);
1935         return ret;
1936 }

        guest访问系统寄存器的指令是MRS(读)指令和MSR(写)指令,在guest执行MRS、MSR访问系统寄存器时会触发EL2的sync异常(0x18),并且ESR寄存器的ISS域保存了具体的指令信息。
        1916行,kvm_vcpu_sys_get_rt从guest退出原因信息中取出ESR.ISS.Rt域(bit5~9)的值,该值为该触发异常的指令要中使用的通用寄存器。
        这段代码主要是初始化一个struct sys_reg_params 结构体,该结构体记录要模拟的指令信息(比如是否是读或写,指令的操作码等),该结构体作为参数传递给指令模拟函数。其中1928行,regval是从vcpu结构体中取出当前vcpu中Rt寄存器的值。
        1931行,调用 emulate_sys_reg函数进行指令模拟。
        1934行,指令模拟完后,如果是要读寄存器,则把模拟的寄存器的值存入vcpu结构体中,当进入guest时,该值就会被恢复到目标寄存器,从而模拟完成。

1873 static int emulate_sys_reg(struct kvm_vcpu *vcpu,
1874                            struct sys_reg_params *params)
1875 {
1876         size_t num;
1877         const struct sys_reg_desc *table, *r;
1878 
1879         table = get_target_table(vcpu->arch.target, true, &num);
1880 
1881         /* Search target-specific then generic table. */
1882         r = find_reg(params, table, num);
1883         if (!r)
1884                 r = find_reg(params, sys_reg_descs, ARRAY_SIZE(sys_reg_descs));
1885 
1886         if (likely(r)) {
1887                 perform_access(vcpu, params, r);
1888         } else {
1889                 kvm_err("Unsupported guest sys_reg access at: %lx\n",
1890                         *vcpu_pc(vcpu));
1891                 print_sys_reg_instr(params);
1892                 kvm_inject_undefined(vcpu);
1893         }
1894         return 1;
1895 }

         struct sys_reg_desc结构体里存储了怎么模拟一个指令的信息:

struct sys_reg_desc {
        u8      Op0;
        u8      Op1;
        u8      CRn;
        u8      CRm;
        u8      Op2;
        bool (*access)(struct kvm_vcpu *, struct sys_reg_params *,  const struct sys_reg_desc *); 
        void (*reset)(struct kvm_vcpu *, const struct sys_reg_desc *); 
        int reg;
        u64 val;
        int (*get_user)(struct kvm_vcpu *vcpu, const struct sys_reg_desc *rd,  const struct kvm_one_reg *reg, void __user *uaddr);
        int (*set_user)(struct kvm_vcpu *vcpu, const struct sys_reg_desc *rd,   const struct kvm_one_reg *reg, void __user *uaddr);
};

        前面几个字段(Op0,Op1,CRn,CRm,Op2)存放了要模拟的指令的相关编码。
        access字段是具体的模拟函数。
        reset字段是vcpu初始化时调用的函数。
        reg是该指令访问的寄存器在寄存器表中的索引。
        value是一个寄存器值,一般是初始值。
        ,get_user/set_user函数是应用层程序调用ioctl获取和设置寄存器值时调用的函数。
        每个待模拟的指令都有一个sys_reg_desc结构体,他们放在数组(table)中。系统中有两个sys_reg_desc表,一个是系统启动时调用core_initcall初始化的,每个不同的CPU target类型都有一个表,另外一个是通用的sys_reg_descs表。
        1882行,先查找CPU target相关的sys_reg_desc表,如果找不到该指令的模拟程序,则跳入通用sys_reg_descs表查找。
        1887行,如果找到相关模拟指令的sys_reg_desc,则perform_access是调用具体执行指令模拟的access函数执行具体模拟操作。
        1892行,如果没有找到模拟函数,则向guest注入一个未定义指令异常,这是通过向vcpu中的相关寄存器赋值来实现的。vcpu.elr_el1=vcpu.pc, vcpu.pc=异常向量地址(是通过vcpu.vbar计算得到),esr_el1为计算出的异常向量掩码,vcpu.spsr=vcpu.cpsr。vcpu.cpsr=PSTATE_FAULT_BITS_64。当切换回guest时,el2.elr和el2.esr则直接恢复的是异常向量的地址和状态,然后跳入guest的el1异常向量执行。
        由此可见访问每个系统寄存器都有特定的模拟函数,上述分析过程主要是跳入特定指令的模拟函数执行。由于每个特定系统寄存器都有特定函数模拟,代码比较多就不一一列举了。大部分的系统寄存器的模拟是从vcpu结构体中取出或保存一个虚拟的系统寄存器的值。

IO访问、内存访问模拟

        guest os访问IO的指令(访问某一个IO地址)是敏感指令,会产生异常切换到EL2模式,访问普通内存空间的指令分两种情况,一种是已经未其分配了物理内存,则非敏感指令,guest正常访问内存执行程序,但是当guest访问的内存地址未映射到物理内存时就变成了敏感指令,需要跳入到KVM去分配物理内存。KVM判断guest退出的原因是地址访问时,会调用kvm_handle_guest_abort函数就行执行。那么怎么区分是普通内存访问还是IO内存访问呢,这是通过产生异常的IPA地址来确定的,guest的内存(IPA)地址范围是由qemu分配的,他们都会被注册进KVM,KVM通过查找IPA是否在注册的地址范围内来确定是否是普通内存的。

1467 int kvm_handle_guest_abort(struct kvm_vcpu *vcpu, struct kvm_run *run)
1468 {
1469         unsigned long fault_status;
1470         phys_addr_t fault_ipa;
1471         struct kvm_memory_slot *memslot;
1472         unsigned long hva;
1473         bool is_iabt, write_fault, writable;
1474         gfn_t gfn;
1475         int ret, idx;
1476 
1477         fault_status = kvm_vcpu_trap_get_fault_type(vcpu);
1478 
1479         fault_ipa = kvm_vcpu_get_fault_ipa(vcpu);
1480         is_iabt = kvm_vcpu_trap_is_iabt(vcpu);
1481 
1482         /* Synchronous External Abort? */
1483         if (kvm_vcpu_dabt_isextabt(vcpu)) {
1484                 /*
1485                  * For RAS the host kernel may handle this abort.
1486                  * There is no need to pass the error into the guest.
1487                  */
1488                 if (!handle_guest_sea(fault_ipa, kvm_vcpu_get_hsr(vcpu)))
1489                         return 1;
1490 
1491                 if (unlikely(!is_iabt)) {
1492                         kvm_inject_vabt(vcpu);
1493                         return 1;
1494                 }
1495         }
1496 
1497         trace_kvm_guest_fault(*vcpu_pc(vcpu), kvm_vcpu_get_hsr(vcpu),
1498                               kvm_vcpu_get_hfar(vcpu), fault_ipa);
1499 
1500         /* Check the stage-2 fault is trans. fault or write fault */
1501         if (fault_status != FSC_FAULT && fault_status != FSC_PERM &&
1502             fault_status != FSC_ACCESS) {
1503                 kvm_err("Unsupported FSC: EC=%#x xFSC=%#lx ESR_EL2=%#lx\n",
1504                         kvm_vcpu_trap_get_class(vcpu),
1505                         (unsigned long)kvm_vcpu_trap_get_fault(vcpu),
1506                         (unsigned long)kvm_vcpu_get_hsr(vcpu));
1507                 return -EFAULT;
1508         }
1509 
1510         idx = srcu_read_lock(&vcpu->kvm->srcu);
1511 
1512         gfn = fault_ipa >> PAGE_SHIFT;
1513         memslot = gfn_to_memslot(vcpu->kvm, gfn);
1514         hva = gfn_to_hva_memslot_prot(memslot, gfn, &writable);
1515         write_fault = kvm_is_write_fault(vcpu);
1516         if (kvm_is_error_hva(hva) || (write_fault && !writable)) {
1517                 if (is_iabt) {
1518                         /* Prefetch Abort on I/O address */
1519                         kvm_inject_pabt(vcpu, kvm_vcpu_get_hfar(vcpu));
1520                         ret = 1;
1521                         goto out_unlock;
1522                 }
1523 
1524                 /*
1525                  * Check for a cache maintenance operation. Since we
1526                  * ended-up here, we know it is outside of any memory
1527                  * slot. But we can't find out if that is for a device,
1528                  * or if the guest is just being stupid. The only thing
1529                  * we know for sure is that this range cannot be cached.
1530                  *
1531                  * So let's assume that the guest is just being
1532                  * cautious, and skip the instruction.
1533                  */
1534                 if (kvm_vcpu_dabt_is_cm(vcpu)) {
1535                         kvm_skip_instr(vcpu, kvm_vcpu_trap_il_is32bit(vcpu));
1536                         ret = 1;
1537                         goto out_unlock;
1538                 }
1539 
1540                 /*
1541                  * The IPA is reported as [MAX:12], so we need to
1542                  * complement it with the bottom 12 bits from the
1543                  * faulting VA. This is always 12 bits, irrespective
1544                  * of the page size.
1545                  */
1546                 fault_ipa |= kvm_vcpu_get_hfar(vcpu) & ((1 << 12) - 1);
1547                 ret = io_mem_abort(vcpu, run, fault_ipa);
1548                 goto out_unlock;
1549         }
1550 
1551         /* Userspace should not be able to register out-of-bounds IPAs */
1552         VM_BUG_ON(fault_ipa >= KVM_PHYS_SIZE);
1553 
1554         if (fault_status == FSC_ACCESS) {
1555                 handle_access_fault(vcpu, fault_ipa);
1556                 ret = 1;
1557                 goto out_unlock;
1558         }
1559 
1560         ret = user_mem_abort(vcpu, fault_ipa, memslot, hva, fault_status);
1561         if (ret == 0)
1562                 ret = 1;
1563 out_unlock:
1564         srcu_read_unlock(&vcpu->kvm->srcu, idx);
1565         return ret;
1566 }

        当guest产生ABORT异常到EL2时,host处理这两个异常的函数是kvm_handle_guest_abort函数。
        当guest产生的el2异常是ABORT异常几乎可以肯定是stag2阶段地址转换表项缺失引起的,这意味着要么guest需要更多内存(host要为其分配一个内存页,类似页故障);要么意味着guest访问IO memory,这一般交给userspace的qemu来模拟IO操作。究竟是哪种情况是通过引起异常的IPA地址来区分的,如果该IPA地址内存范围被userspace的qemu注册为stand RAM的,则说明不是访问IO memory,我们需为guest分配更多内存。
        1477行,从vcpu.fault.esr_el2寄存器中取出故障状态,也就是ISS域,里面存储ARBORT异常的细节信息。
        1479行,从vcpu.fault.hpfar中获取引起异常的IPA地址。
        1480行,判断是指令异常还是数据异常。
        1483~1495行,如果是外部同步异常,如果支持RAS,host能够处理该异常,则不需要注入该异常给guest,否则,为guest注入VSE异常。这是通过简单设置HCR寄存器的VSE位来实现的。
        1500~1508,错误处理,如果出现异常错误掩码(不是CPU设计引入的),则出错返回。
        1512~1515,根据IPA地址的页帧号找到其对应的memory_slot。然后通过该槽把IPA转换为qemu进程的虚拟地址(hv),注意如果是stand ram,则可以顺利转换为hv。否则,则为IO memory访问,hv被赋值为KVM_HVA_ERR_BAD。
        1516~1549,如果hv=KVM_HVA_ERR_BAD或者在只读地址上写操作,则要么是指令ABORT,要么是I/O memory访问。对应指令ABORT,则向guest注入一个指令ABORT异常(通过给vcpu相关寄存器赋值)。如果是I/Omemory访问,则分两种情况,如果指令是cache维护指令,则直接跳过该条指令,什么也不做(I/Omeoory是非cache的,这里是假定guest有些愚蠢的代码,才会出现这种情况);剩下的就是正常的IO操作指令了,调用io_mem_abort函数进行模拟。第1546行是把IPA与guest va的低12位相与,得到完整的IPA地址。
        1554~1558行,handle_access_fault处理Access Flag异常。Access Flag标识该内存页是否允许访问,如果设为0,则不被允许访问。当Access Flag为0(stag2页表)而guest访问该页时,会触发Access Flag异常,简单的调用mkyoung函数把Access Flag设为1,以后就可以正常访问了。
        1550行,到这里说明guest需要分配更多的内存,user_mem_abort函数处理该要求。

155 int io_mem_abort(struct kvm_vcpu *vcpu, struct kvm_run *run,
156                  phys_addr_t fault_ipa)
157 {
158         unsigned long data;
159         unsigned long rt;
160         int ret;
161         bool is_write;
162         int len;
163         u8 data_buf[8];
164 
165         /*
166          * Prepare MMIO operation. First decode the syndrome data we get
167          * from the CPU. Then try if some in-kernel emulation feels
168          * responsible, otherwise let user space do its magic.
169          */
170         if (kvm_vcpu_dabt_isvalid(vcpu)) {
171                 ret = decode_hsr(vcpu, &is_write, &len);
172                 if (ret)
173                         return ret;
174         } else {
175                 kvm_err("load/store instruction decoding not implemented\n");
176                 return -ENOSYS;
177         }
178 
179         rt = vcpu->arch.mmio_decode.rt;
180 
181         if (is_write) {
182                 data = vcpu_data_guest_to_host(vcpu, vcpu_get_reg(vcpu, rt),
183                                                len);
184 
185                 trace_kvm_mmio(KVM_TRACE_MMIO_WRITE, len, fault_ipa, &data);
186                 kvm_mmio_write_buf(data_buf, len, data);
187 
188                 ret = kvm_io_bus_write(vcpu, KVM_MMIO_BUS, fault_ipa, len,
189                                        data_buf);
190         } else {
191                 trace_kvm_mmio(KVM_TRACE_MMIO_READ_UNSATISFIED, len,
192                                fault_ipa, NULL);
193 
194                 ret = kvm_io_bus_read(vcpu, KVM_MMIO_BUS, fault_ipa, len,
195                                       data_buf);
196         }
197 
198         /* Now prepare kvm_run for the potential return to userland. */
199         run->mmio.is_write      = is_write;
200         run->mmio.phys_addr     = fault_ipa;
201         run->mmio.len           = len;
202 
203         if (!ret) {
204                 /* We handled the access successfully in the kernel. */
205                 if (!is_write)
206                         memcpy(run->mmio.data, data_buf, len);
207                 vcpu->stat.mmio_exit_kernel++;
208                 kvm_handle_mmio_return(vcpu, run);
209                 return 1;
210         }
211 
212         if (is_write)
213                 memcpy(run->mmio.data, data_buf, len);
214         vcpu->stat.mmio_exit_user++;
215         run->exit_reason        = KVM_EXIT_MMIO;
216         return 0;
217 }

        170~177行,通过ABORT异常的esr_el2.iss.HSR_ISV位判断是否记录下了该次访问的细节信息,如果没有记录下来,则无法模拟,报错退出。否则调用decode_hsr解码具体的细节信息。
        179~196行,尝试在内核层模拟该IO请求(内核模拟的设备),内核会对一些设备进行模拟,这些设备按照模拟的地址空间存放在不同的数组中MMIO设备存放在KVM_MMIO_BUS数组,每个设备有读写模拟函数,这里通过kvm_io_bus_read/write函数调用具体的读写模拟接口。
        198~216,如果内核没有模拟该IO操作,则返回应用层(return 0)模拟,把相关的操作参数存放在kvm_run.mmio结构体中。如果内核模拟完,则不进入应用层(return 1),直接再此进入guest循环。

123 static int decode_hsr(struct kvm_vcpu *vcpu, bool *is_write, int *len)
124 {
125         unsigned long rt;
126         int access_size;
127         bool sign_extend;
128 
129         if (kvm_vcpu_dabt_iss1tw(vcpu)) {
130                 /* page table accesses IO mem: tell guest to fix its TTBR */
131                 kvm_inject_dabt(vcpu, kvm_vcpu_get_hfar(vcpu));
132                 return 1;
133         }
134 
135         access_size = kvm_vcpu_dabt_get_as(vcpu);
136         if (unlikely(access_size < 0))
137                 return access_size;
138 
139         *is_write = kvm_vcpu_dabt_iswrite(vcpu);
140         sign_extend = kvm_vcpu_dabt_issext(vcpu);
141         rt = kvm_vcpu_dabt_get_rd(vcpu); 
142 
143         *len = access_size;
144         vcpu->arch.mmio_decode.sign_extend = sign_extend;
145         vcpu->arch.mmio_decode.rt = rt;
146         
147         /*
148          * The MMIO instruction is emulated and should not be re-executed
149          * in the guest.
150          */
151         kvm_skip_instr(vcpu, kvm_vcpu_trap_il_is32bit(vcpu));
152         return 0;
153 }

        129~133行,如果是由S1PTW引起的异常,说明使用的stag1的页表地址不对,给guest注入异常,告诉guest修改其TTBR。
        剩下的代码记录访问细节信息,包括访问大小,是写还是读,以及访问的寄存器,其中sign_extend代表byte或半字访问是否是符号扩展的。细节信息保存在vcpu.arch.mmio_decode中。
        最后一行是vcpu的pc寄存器跳过一个指令,因为该次的访问指令将被模拟,不需要再执行。

1282 static int user_mem_abort(struct kvm_vcpu *vcpu, phys_addr_t fault_ipa,
1283                           struct kvm_memory_slot *memslot, unsigned long hva,
1284                           unsigned long fault_status)
1285 {
1286         int ret;
1287         bool write_fault, writable, hugetlb = false, force_pte = false;
1288         unsigned long mmu_seq;
1289         gfn_t gfn = fault_ipa >> PAGE_SHIFT;
1290         struct kvm *kvm = vcpu->kvm;
1291         struct kvm_mmu_memory_cache *memcache = &vcpu->arch.mmu_page_cache;
1292         struct vm_area_struct *vma;
1293         kvm_pfn_t pfn;
1294         pgprot_t mem_type = PAGE_S2;
1295         bool logging_active = memslot_is_logging(memslot);
1296         unsigned long flags = 0;
1297 
1298         write_fault = kvm_is_write_fault(vcpu);
1299         if (fault_status == FSC_PERM && !write_fault) {
1300                 kvm_err("Unexpected L2 read permission error\n");
1301                 return -EFAULT;
1302         }
1303 
1304         /* Let's check if we will get back a huge page backed by hugetlbfs */
1305         down_read(&current->mm->mmap_sem);
1306         vma = find_vma_intersection(current->mm, hva, hva + 1);
1307         if (unlikely(!vma)) {
1308                 kvm_err("Failed to find VMA for hva 0x%lx\n", hva);
1309                 up_read(&current->mm->mmap_sem);
1310                 return -EFAULT;
1311         }
1312 
1313         if (vma_kernel_pagesize(vma) == PMD_SIZE && !logging_active) {
1314                 hugetlb = true;
1315                 gfn = (fault_ipa & PMD_MASK) >> PAGE_SHIFT;
1316         } else {
1326                 if ((memslot->userspace_addr & ~PMD_MASK) !=
1327                     ((memslot->base_gfn << PAGE_SHIFT) & ~PMD_MASK))
1328                         force_pte = true;
1329         }
1330         up_read(&current->mm->mmap_sem);
1331 
1332         /* We need minimum second+third level pages */
1333         ret = mmu_topup_memory_cache(memcache, KVM_MMU_CACHE_MIN_PAGES,
1334                                      KVM_NR_MEM_OBJS);
1335         if (ret)
1336                 return ret;
1337 
1338         mmu_seq = vcpu->kvm->mmu_notifier_seq;
1348         smp_rmb();
1349 
1350         pfn = gfn_to_pfn_prot(kvm, gfn, write_fault, &writable);
1351         if (pfn == KVM_PFN_ERR_HWPOISON) {
1352                 kvm_send_hwpoison_signal(hva, vma);
1353                 return 0;
1354         }
1355         if (is_error_noslot_pfn(pfn))
1356                 return -EFAULT;
1357 
1358         if (kvm_is_device_pfn(pfn)) {
1359                 mem_type = PAGE_S2_DEVICE;
1360                 flags |= KVM_S2PTE_FLAG_IS_IOMAP;
1361         } else if (logging_active) {
1362                 /*
1363                  * Faults on pages in a memslot with logging enabled
1364                  * should not be mapped with huge pages (it introduces churn
1365                  * and performance degradation), so force a pte mapping.
1366                  */
1367                 force_pte = true;
1368                 flags |= KVM_S2_FLAG_LOGGING_ACTIVE;
1369 
1370                 /*
1371                  * Only actually map the page as writable if this was a write
1372                  * fault.
1373                  */
1374                 if (!write_fault)
1375                         writable = false;
1376         }
1377 
1378         spin_lock(&kvm->mmu_lock);
1379         if (mmu_notifier_retry(kvm, mmu_seq))
1380                 goto out_unlock;
1381 
1382         if (!hugetlb && !force_pte)
1383                 hugetlb = transparent_hugepage_adjust(&pfn, &fault_ipa);
1385         if (hugetlb) {
1386                 pmd_t new_pmd = pfn_pmd(pfn, mem_type);
1387                 new_pmd = pmd_mkhuge(new_pmd);
1388                 if (writable) {
1389                         new_pmd = kvm_s2pmd_mkwrite(new_pmd);
1390                         kvm_set_pfn_dirty(pfn);
1391                 }
1392                 coherent_cache_guest_page(vcpu, pfn, PMD_SIZE);
1393                 ret = stage2_set_pmd_huge(kvm, memcache, fault_ipa, &new_pmd);
1394         } else {
1395                 pte_t new_pte = pfn_pte(pfn, mem_type);
1396 
1397                 if (writable) {
1398                         new_pte = kvm_s2pte_mkwrite(new_pte);
1399                         kvm_set_pfn_dirty(pfn);
1400                         mark_page_dirty(kvm, gfn);
1401                 }
1402                 coherent_cache_guest_page(vcpu, pfn, PAGE_SIZE);
1403                 ret = stage2_set_pte(kvm, memcache, fault_ipa, &new_pte, flags);
1404         }
1405 
1406 out_unlock:
1407         spin_unlock(&kvm->mmu_lock);
1408         kvm_set_pfn_accessed(pfn);
1409         kvm_release_pfn_clean(pfn);
1410         return ret;
1411 }

        1306~1310,根据guest访问的IPA地址对应于qemu进程hv地址找到管理该内存段的vma结构。
        1312~1315,判断是否使用hugepage,如果host使用hugepage,且目前没有正在记录脏页,则hugetlb=true。
        1333行, 该行主要是扩充kvm_mmu_memory_cache以保证在后面设置stage2页表时有足够的页来建立页表。
        1350行,gfn_to_pfn_prot函数从host的qemu进程的页表中找到hv对应的物理页帧号,并返回该页帧号。(这个函数保证被换出的页必定被换入?)
        1383行,判断能否使用透明巨页功能。
        1385~1391行,如果使用巨页或透明巨页,则设置stage2页表PMD页表项为block页
        1394~1404行,设置普通页的stag2页表。
        可以看出普通内存模拟主要就是找到物理页帧号设置stag2阶段的页表为虚拟机分配物理内存。其中分配物理内存找出物理页帧号主要是由gfn_to_pfn_prot函数处理的。

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值