ARM64 KVM工作流程分析

发布时间:2024-11-29 23:35

利用数据分析优化工作流程效率 #生活技巧# #组织技巧# #工作流程优化#

基本介绍

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

guest执行WFI/WFE指令。guest访问特权级的系统寄存器。内存相关地址访问退出,这又分两种情况。一种是相关内存地址未分配物理内存,需要进入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 }

1234567891011121314151617

        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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364

        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 }

12345678910111213141516171819202122232425

        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 }

1234567891011121314151617181920212223

         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); }; 12345678910111213

        前面几个字段(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 }

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100

        当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 }

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263

        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 }

12345678910111213141516171819202122232425262728293031

        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 }

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111

        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函数处理的。

网址:ARM64 KVM工作流程分析 https://www.yuejiaxmz.com/news/view/317557

相关内容

工作流程优化:提升工作效率与流程标准化.pptx
流程优化:优化工作流程、提高工作效率的第一步
工作流程优化.docx
优化工作流程图
自动化工作流程
工作流中流程自动化优化的作用
移动应用与系统:构建现代数字生态的基石在当今这个高度数字化的社会中,移动应用与操作系统已成为我们日常生活不可或缺的一部分。它们不仅改变了我们的沟通方式,还重塑了我们的工作、学习和娱乐模式。本文将深入探讨移动应用开发的基础、移动操作系统的功能以及这两者如何共同塑造了我们的数字世界。
优化工作流程等方式
如何优化工作流程?企业流程优化的十大原则
工作流程优化与改善.docx

随便看看