01 · QUESTION
무엇을 확인할 것인가
guest register, stage-2 translation, virtual interrupt와 host task state의 owner가 vCPU run 전후에 어떻게 바뀌는가?
userspace VMM이 KVM_RUN을 호출하면 KVM core가 vCPU를 load하고 pending request/interrupt를 처리한 뒤 architecture enter_guest를 실행한다. hardware exit는 reason을 decode해 kernel에서 처리하거나 exit structure로 userspace에 반환한다.
host userspace, KVM ioctl context, guest execution, hypervisor exception과 host interrupt context를 나눈다. preemption과 migration은 CPU-local virtualization register owner를 바꾸므로 vcpu_load/put 경계를 지켜야 한다.
02 · CONTRACT
공통 규약과 아키텍처별 구현
| architecture | 핵심 mechanism | 실패 형태 | 확인할 상태 |
|---|---|---|---|
| arm64 | EL2 VHE/nVHE와 stage-2 VTTBR_EL2 | VMID wrap flush 누락, hyp mapping lifetime 오류 또는 virtual timer offset 불일치는 guest memory leak, hyp fault와 시간 점프를 만든다. | HCR_EL2, VTTBR_EL2/VMID, ESR_EL2, FAR/HPFAR, guest PC/PSTATE, GIC LR와 kvm_request bitmap을 본다. |
| x86-64 | VMX VMCS 또는 SVM VMCB와 EPT/NPT | invalid VMCS field는 VM-entry failure를 만들고 stale EPT/VPID는 guest가 이전 mapping을 계속 사용하게 한다. | exit_reason/qualification, guest linear/physical address, EPTP/VPID, interrupt-window flag, VMCS instruction error와 requests를 본다. |
| RISC-V | H-extension VS/VU state와 HGATP two-stage translation | VMID generation, guest timer delta 또는 virtual instruction emulation 오류가 stale mapping, guest hang과 잘못된 time을 만든다. | HSTATUS, HGATP/VMID, HTVAL/HTINST, SCAUSE/SEPC, VS CSR, virtual interrupt bits와 request bitmap을 확인한다. |
03 · DIAGRAMS
세 그림으로 먼저 읽기
arm64
- mechanism
- EL2 VHE/nVHE와 stage-2 VTTBR_EL2
- state
- KVM은 guest EL1 register, timer, GIC virtualization state를 vCPU context에 보관한다. VTTBR_EL2가 VMID와 stage-2 root를 선택하고 HCR_EL2가 trap policy를 정한다. nVHE는 별도 hyp address space와 stack을 사용한다.
- checkpoint
- HCR_EL2, VTTBR_EL2/VMID, ESR_EL2, FAR/HPFAR, guest PC/PSTATE, GIC LR와 kvm_request bitmap을 본다.
x86-64
- mechanism
- VMX VMCS 또는 SVM VMCB와 EPT/NPT
- state
- VMCS/VMCB에 guest/host control register, segment, RIP/RSP와 intercept policy를 둔다. EPTP/NCR3가 second-level translation root를 고르고 posted interrupt/APICv가 일부 interrupt를 exit 없이 주입한다.
- checkpoint
- exit_reason/qualification, guest linear/physical address, EPTP/VPID, interrupt-window flag, VMCS instruction error와 requests를 본다.
RISC-V
- mechanism
- H-extension VS/VU state와 HGATP two-stage translation
- state
- HS-mode KVM이 VS-level CSR, guest GPR/FP/vector와 AIA interrupt state를 vCPU에 저장한다. HGATP가 VMID와 guest-stage root를 선택하며 HFENCE.GVMA/VVMA가 guest translation cache를 무효화한다.
- checkpoint
- HSTATUS, HGATP/VMID, HTVAL/HTINST, SCAUSE/SEPC, VS CSR, virtual interrupt bits와 request bitmap을 확인한다.
04 · SOURCE
Linux 6.18.37 원본 코드와 주요 구문 해설
소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.
arm64 · Linux 6.18.37
EL2 VHE/nVHE와 stage-2 VTTBR_EL2
KVM은 guest EL1 register, timer, GIC virtualization state를 vCPU context에 보관한다. VTTBR_EL2가 VMID와 stage-2 root를 선택하고 HCR_EL2가 trap policy를 정한다. nVHE는 별도 hyp address space와 stack을 사용한다.
원본 코드: arch/arm64/kvm/arm.c:1157-1251
1157 * @vcpu: The VCPU pointer
1158 *
1159 * This function is called through the VCPU_RUN ioctl called from user space. It
1160 * will execute VM code in a loop until the time slice for the process is used
1161 * or some emulation is needed from user space in which case the function will
1162 * return with return value 0 and with the kvm_run structure filled in with the
1163 * required data for the requested emulation.
1164 */
1165int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
1166{
1167 struct kvm_run *run = vcpu->run;
1168 int ret;
1169
1170 if (run->exit_reason == KVM_EXIT_MMIO) {
1171 ret = kvm_handle_mmio_return(vcpu);
1172 if (ret <= 0)
1173 return ret;
1174 }
1175
1176 vcpu_load(vcpu);
1177
1178 if (!vcpu->wants_to_run) {
1179 ret = -EINTR;
1180 goto out;
1181 }
1182
1183 kvm_sigset_activate(vcpu);
1184
1185 ret = 1;
1186 run->exit_reason = KVM_EXIT_UNKNOWN;
1187 run->flags = 0;
1188 while (ret > 0) {
1189 /*
1190 * Check conditions before entering the guest
1191 */
1192 ret = kvm_xfer_to_guest_mode_handle_work(vcpu);
1193 if (!ret)
1194 ret = 1;
1195
1196 if (ret > 0)
1197 ret = check_vcpu_requests(vcpu);
1198
1199 /*
1200 * Preparing the interrupts to be injected also
1201 * involves poking the GIC, which must be done in a
1202 * non-preemptible context.
1203 */
1204 preempt_disable();
1205
1206 kvm_nested_flush_hwstate(vcpu);
1207
1208 if (kvm_vcpu_has_pmu(vcpu))
1209 kvm_pmu_flush_hwstate(vcpu);
1210
1211 local_irq_disable();
1212
1213 kvm_vgic_flush_hwstate(vcpu);
1214
1215 kvm_pmu_update_vcpu_events(vcpu);
1216
1217 /*
1218 * Ensure we set mode to IN_GUEST_MODE after we disable
1219 * interrupts and before the final VCPU requests check.
1220 * See the comment in kvm_vcpu_exiting_guest_mode() and
1221 * Documentation/virt/kvm/vcpu-requests.rst
1222 */
1223 smp_store_mb(vcpu->mode, IN_GUEST_MODE);
1224
1225 if (ret <= 0 || kvm_vcpu_exit_request(vcpu, &ret)) {
1226 vcpu->mode = OUTSIDE_GUEST_MODE;
1227 isb(); /* Ensure work in x_flush_hwstate is committed */
1228 if (kvm_vcpu_has_pmu(vcpu))
1229 kvm_pmu_sync_hwstate(vcpu);
1230 if (unlikely(!irqchip_in_kernel(vcpu->kvm)))
1231 kvm_timer_sync_user(vcpu);
1232 kvm_vgic_sync_hwstate(vcpu);
1233 local_irq_enable();
1234 preempt_enable();
1235 continue;
1236 }
1237
1238 kvm_arch_vcpu_ctxflush_fp(vcpu);
1239
1240 /**************************************************************
1241 * Enter the guest
1242 */
1243 trace_kvm_entry(*vcpu_pc(vcpu));
1244 guest_timing_enter_irqoff();
1245
1246 ret = kvm_arm_vcpu_enter_exit(vcpu);
1247
1248 vcpu->mode = OUTSIDE_GUEST_MODE;
1249 vcpu->stat.exits++;
1250 /*
1251 * Back from guest주요 구문 해설
원본 95줄은 그대로 표시하며, 검토한 46개 구문에 설명을 붙였습니다.
int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)사용자 공간의 KVM_RUN 요청을 받아 지정 vCPU의 실행·중단·재실행을 관리하는 arm64 함수입니다. 이전 이탈 작업을 마무리하고 게스트 진입 준비와 이탈 처리를 반복합니다.
struct kvm_run *run = vcpu->run;사용자 공간과 공유하는 kvm_run 자료를 가리킵니다. 이전 이탈 이유와 MMIO 결과를 읽고 이번 실행의 이탈 이유를 기록하는 창구입니다.
int ret;KVM_RUN의 진행 여부와 최종 반환값을 담을 정수 ret를 선언합니다. 이 함수의 실행 루프에서는 양수가 게스트 실행 계속, 0이 사용자 공간 처리로 반환, 음수가 오류를 뜻합니다.
if (run->exit_reason == KVM_EXIT_MMIO) {이전 실행이 사용자 공간의 MMIO 처리 때문에 멈췄는지 확인합니다. 그 경우 사용자 공간이 돌려준 결과를 먼저 반영해야 중단됐던 게스트 명령을 올바르게 이어갈 수 있습니다.
ret = kvm_handle_mmio_return(vcpu);이전 MMIO 종료에 대해 사용자 공간이 돌려준 데이터를 게스트 레지스터에 반영하고, 에뮬레이션한 명령을 다시 실행하지 않도록 게스트 PC를 진행시킵니다. 완료한 처리는 반복하지 않으며 이 버전의 함수는 1을 반환합니다.
if (ret <= 0)MMIO 후처리 결과가 0 또는 음수여서 게스트 실행을 계속할 수 없는지 검사합니다. 다만 v6.18.37의 kvm_handle_mmio_return은 모든 반환 경로에서 1을 주므로 현재 구현에서는 이 조건이 참이 되지 않습니다.
return ret;위 MMIO 후처리 검사에 걸린 결과값을 그대로 KVM_RUN 호출자에게 반환하는 문장입니다. 현재 버전의 후처리 함수는 항상 1을 반환하므로 이 조기 반환은 실행되지 않습니다.
vcpu_load(vcpu);이 vCPU를 현재 호스트 CPU에서 실행할 수 있도록 불러오고 스케줄링 시 상태를 전환할 준비를 합니다. 이후 실행 루프가 물리 CPU의 KVM 상태를 이 vCPU에 맞춰 다루게 됩니다.
if (!vcpu->wants_to_run) {이번 요청에서 실제 게스트 실행을 허용하지 않는 상태인지 확인합니다. 실행 취소가 요청되었다면 -EINTR를 준비하고 vCPU 상태를 정리한 뒤 반환합니다.
ret = -EINTR;vcpu->wants_to_run이 거짓이면 실행 요청이 중단되었음을 -EINTR로 기록합니다. 다음 goto out이 이미 적재한 vCPU 상태를 정리한 뒤 호출자에게 오류를 전달하게 합니다.
goto out;불러온 vCPU를 정리하는 공통 out 경로로 이동합니다. 게스트 명령을 실행하지 않는 조기 종료에서도 vCPU 자원을 불러오기 전 상태로 돌립니다.
kvm_sigset_activate(vcpu);사용자가 KVM 실행용 신호 마스크를 지정했다면 현재 스레드에 적용합니다. 게스트 실행 중 어떤 호스트 신호로 KVM_RUN을 중단할지 정하며, 실행을 마치면 원래 신호 마스크로 복구합니다.
ret = 1;게스트 실행 루프에 들어가도록 ret를 1로 둡니다. 여기서 1은 일반적인 성공 반환값 0과 달리 계속 실행하라는 내부 상태값입니다.
run->exit_reason = KVM_EXIT_UNKNOWN;이전 KVM_RUN의 이탈 이유를 UNKNOWN으로 초기화합니다. 이번 실행에서 실제로 발생한 이탈 이유를 새로 기록할 수 있게 합니다.
run->flags = 0;공유 run 영역의 결과 플래그를 비웁니다. 이전 실행에서 설정된 표시가 이번 실행 결과에 남지 않도록 합니다.
while (ret > 0) {ret가 양수인 동안 진입 준비와 게스트 실행을 반복합니다. 처리 결과가 0이면 사용자 공간에 맡길 작업이 있고, 음수이면 오류이므로 반복을 끝냅니다.
ret = kvm_xfer_to_guest_mode_handle_work(vcpu);게스트로 들어가기 전에 호스트 스레드의 대기 작업을 처리합니다. 정상적으로 끝나면 0이고 시그널 때문에 중단해야 하면 -EINTR을 반환하며 KVM 종료 이유도 갱신합니다.
if (!ret)게스트 진입 전 호스트 작업 처리가 오류 없이 끝났는지 검사합니다. 이 함수의 성공값 0을 실행 루프가 사용하는 계속 상태로 바꾸기 위한 조건입니다.
ret = 1;호스트 작업 처리가 성공했으므로 ret를 계속 실행을 뜻하는 1로 변환합니다. 앞의 helper가 돌려준 0을 그대로 두면 아래 실행 루프가 종료되어 버립니다.
if (ret > 0)앞선 호스트 작업 처리에서 중단 사유가 없을 때만 vCPU 요청 처리를 진행합니다. 음수 오류를 새 요청 처리 결과로 덮어쓰지 않기 위한 조건입니다.
ret = check_vcpu_requests(vcpu);이 vCPU에 대기 중인 reset·sleep·가상 인터럽트 상태·PMU·중첩 가상화 등의 요청을 처리합니다. 계속 실행할 수 있으면 1, 사용자 공간으로 나가야 하면 0, VM 종료 등 오류이면 음수를 ret에 받습니다.
preempt_disable();커널 선점을 막아 게스트 진입 준비 도중 다른 태스크로 교체되지 않게 합니다. VGIC와 PMU처럼 현재 물리 CPU에 연결된 상태를 설정한 뒤 진입 여부를 다시 검사하는 구간입니다.
kvm_nested_flush_hwstate(vcpu);중첩 가상화를 사용하는 vCPU에 처리 대기 중인 SError가 있으면 그 표시를 지우고 가상 SError를 주입할 상태를 만듭니다. 이 버전의 함수는 중첩 가상화 레지스터 전체를 복사하는 함수가 아닙니다.
if (kvm_vcpu_has_pmu(vcpu))이 vCPU에 가상 PMU가 제공되는 경우에만 진입 전 PMU 상태를 맞춥니다. PMU가 없는 게스트에는 성능 카운터 오버플로·IRQ 처리가 필요 없습니다.
kvm_pmu_flush_hwstate(vcpu);PMU의 오버플로 상태에 맞춰 게스트 PMU 인터럽트 상태를 갱신합니다. 호스트 코드를 실행하는 동안 생긴 카운터 오버플로도 게스트로 돌아가기 전에 반영하려는 처리이며, 가상 PMU가 있는 vCPU에서 수행합니다.
local_irq_disable();현재 CPU의 일반 IRQ를 막아 가상 인터럽트 상태를 하드웨어와 맞추는 도중 호스트 IRQ가 끼어들지 않게 합니다. 바로 앞 선점 금지와 함께 게스트 진입 직전의 준비 구간을 보호합니다.
kvm_vgic_flush_hwstate(vcpu);게스트에 보낼 가상 인터럽트의 소프트웨어 상태를 VGIC 하드웨어 상태에 반영합니다. 중첩 게스트에 가상 EL2 예외를 먼저 주입해야 하면 진입을 취소하게 할 요청을 만들 수도 있습니다.
kvm_pmu_update_vcpu_events(vcpu);VHE를 쓰지 않고 PMUv3를 지원하면, 현재 CPU에서 호스트·게스트별로 계수할 PMU 이벤트 비트마스크를 vCPU에 복사합니다. 게스트 진입 때 카운터를 올바르게 전환하기 위한 정보이며, 복사 중 IRQ가 이 정보를 바꾸지 않도록 IRQ를 끈 뒤 실행합니다. 해당 조건이 아니거나 성능 이벤트 지원이 없는 빌드에서는 이 복사를 하지 않습니다.
smp_store_mb(vcpu->mode, IN_GUEST_MODE);vCPU의 mode를 IN_GUEST_MODE로 기록하고 메모리 순서를 보장합니다. 이 값을 본 다른 CPU는 필요하면 vCPU를 깨우거나 실행을 중단시킬 수 있으며, 현재 CPU는 뒤에서 새 요청이 생겼는지 다시 검사합니다.
if (ret <= 0 || kvm_vcpu_exit_request(vcpu, &ret)) {앞에서 실행 중단이 결정됐거나 마지막 요청 검사에서 게스트 진입을 미뤄야 하면 준비한 하드웨어 상태를 되돌리는 분기로 갑니다. kvm_vcpu_exit_request는 재검사만 필요한 경우 ret를 유지하고, 사용자 공간으로 나갈 때는 ret도 바꿉니다.
vcpu->mode = OUTSIDE_GUEST_MODE;이번 게스트 진입을 취소했으므로 mode를 호스트 실행 상태로 되돌립니다. 다른 CPU가 이 vCPU를 게스트 안에서 실행 중이라고 판단하지 않도록 합니다.
isb(); /* Ensure work in x_flush_hwstate is committed */앞서 수행한 PMU·VGIC 등의 하드웨어 상태 반영이 명령 흐름에 적용된 뒤 되읽도록 ISB를 실행합니다. 진입을 취소하더라도 준비와 정리 사이의 순서는 지켜야 합니다.
if (kvm_vcpu_has_pmu(vcpu))가상 PMU가 있는 vCPU만 취소 경로의 PMU 상태 동기화를 수행합니다. 없는 경우에는 바로 타이머·VGIC 정리로 진행합니다.
kvm_pmu_sync_hwstate(vcpu);게스트 진입을 취소하는 경로에서도 PMU 오버플로와 가상 IRQ 상태를 다시 맞춥니다. 앞서 진입을 준비했다가 호스트 작업으로 돌아가는 사이 PMU 상태가 빠지지 않도록 하는 정리입니다.
if (unlikely(!irqchip_in_kernel(vcpu->kvm)))인터럽트 컨트롤러를 사용자 공간이 담당하는 VM인지 확인합니다. 이 경우 타이머 IRQ의 재허용 처리가 필요하며, 커널 irqchip을 쓰는 VM에는 그 사용자 irqchip용 처리를 적용하지 않습니다.
kvm_timer_sync_user(vcpu);사용자 공간이 인터럽트 컨트롤러를 담당하는 경우, 가상 타이머 상태에 따라 호스트의 가상 타이머 IRQ 마스크를 조정합니다. 이 helper는 타이머 레지스터를 사용자 메모리에 복사하는 함수가 아닙니다.
kvm_vgic_sync_hwstate(vcpu);준비해 둔 VGIC 하드웨어 상태를 소프트웨어의 가상 인터럽트 상태에 다시 반영합니다. 게스트 진입을 취소한 뒤에도 list register와 pending·active 목록의 일관성을 유지합니다.
local_irq_enable();게스트 진입 취소에 필요한 PMU·타이머·VGIC 정리를 끝냈으므로 현재 CPU의 일반 IRQ를 다시 허용합니다. 이어 선점도 허용하고 실행 루프에서 대기 작업을 다시 확인합니다.
preempt_enable();진입 준비 중 막았던 선점을 다시 허용합니다. 하드웨어 상태 정리가 끝난 이 경로에서는 다른 호스트 태스크가 실행될 수 있어야 합니다.
continue;진입 준비를 되돌렸으므로 이번에는 게스트를 실행하지 않고 반복문의 조건을 다시 확인합니다. ret가 양수면 요청을 재처리하고, 0 이하이면 루프를 끝냅니다.
kvm_arch_vcpu_ctxflush_fp(vcpu);현재 스레드의 FP/SIMD 상태가 CPU 레지스터에 올라와 있지 않다면 호스트의 FP 소유 표시를 비웁니다. 진입 준비 중 커널이 FP를 사용했을 가능성을 반영하는 상태 표시 갱신이며, 이 함수 자체가 모든 FP 레지스터를 저장·복원하지는 않습니다.
trace_kvm_entry(*vcpu_pc(vcpu));게스트가 다음에 실행할 PC를 KVM 진입 trace 이벤트에 남깁니다. 뒤의 실제 게스트 실행과 이탈 이벤트를 연결하여 vCPU 동작을 추적할 수 있습니다.
guest_timing_enter_irqoff();IRQ가 꺼진 상태에서 게스트 실행 시간의 회계를 시작합니다. 이 줄 자체가 게스트 명령으로 분기하는 것은 아니며, 실제 게스트 진입·이탈은 뒤의 kvm_arm_vcpu_enter_exit가 담당합니다.
ret = kvm_arm_vcpu_enter_exit(vcpu);EL2 entry를 통해 guest를 실행하고 다음 trap/interrupt에서 host로 돌아온다.
vcpu->mode = OUTSIDE_GUEST_MODE;게스트 실행 함수에서 돌아온 뒤 mode를 호스트 실행 상태로 바꿉니다. 원격 CPU가 보내는 vCPU 요청의 판단 기준도 이에 맞춰집니다.
vcpu->stat.exits++;게스트에서 호스트로 나온 횟수를 vCPU 통계에 하나 더합니다. MMIO·예외 등 구체적인 이탈 원인 판정과는 별도의 전체 이탈 계수입니다.
x86-64 · Linux 6.18.37
VMX VMCS 또는 SVM VMCB와 EPT/NPT
VMCS/VMCB에 guest/host control register, segment, RIP/RSP와 intercept policy를 둔다. EPTP/NCR3가 second-level translation root를 고르고 posted interrupt/APICv가 일부 interrupt를 exit 없이 주입한다.
원본 코드: arch/x86/kvm/x86.c:11924-12024
11924 */
11925 if (vcpu->arch.mp_state == KVM_MP_STATE_INIT_RECEIVED &&
11926 !kvm_apic_init_sipi_allowed(vcpu))
11927 return -EINVAL;
11928
11929 return kvm_x86_call(vcpu_pre_run)(vcpu);
11930}
11931
11932int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
11933{
11934 struct kvm_queued_exception *ex = &vcpu->arch.exception;
11935 struct kvm_run *kvm_run = vcpu->run;
11936 u64 sync_valid_fields;
11937 int r;
11938
11939 r = kvm_mmu_post_init_vm(vcpu->kvm);
11940 if (r)
11941 return r;
11942
11943 vcpu_load(vcpu);
11944 kvm_sigset_activate(vcpu);
11945 kvm_run->flags = 0;
11946 kvm_load_guest_fpu(vcpu);
11947
11948 kvm_vcpu_srcu_read_lock(vcpu);
11949 if (unlikely(vcpu->arch.mp_state == KVM_MP_STATE_UNINITIALIZED)) {
11950 if (!vcpu->wants_to_run) {
11951 r = -EINTR;
11952 goto out;
11953 }
11954
11955 /*
11956 * Don't bother switching APIC timer emulation from the
11957 * hypervisor timer to the software timer, the only way for the
11958 * APIC timer to be active is if userspace stuffed vCPU state,
11959 * i.e. put the vCPU into a nonsensical state. Only an INIT
11960 * will transition the vCPU out of UNINITIALIZED (without more
11961 * state stuffing from userspace), which will reset the local
11962 * APIC and thus cancel the timer or drop the IRQ (if the timer
11963 * already expired).
11964 */
11965 kvm_vcpu_srcu_read_unlock(vcpu);
11966 kvm_vcpu_block(vcpu);
11967 kvm_vcpu_srcu_read_lock(vcpu);
11968
11969 if (kvm_apic_accept_events(vcpu) < 0) {
11970 r = 0;
11971 goto out;
11972 }
11973 r = -EAGAIN;
11974 if (signal_pending(current)) {
11975 r = -EINTR;
11976 kvm_run->exit_reason = KVM_EXIT_INTR;
11977 ++vcpu->stat.signal_exits;
11978 }
11979 goto out;
11980 }
11981
11982 sync_valid_fields = kvm_sync_valid_fields(vcpu->kvm);
11983 if ((kvm_run->kvm_valid_regs & ~sync_valid_fields) ||
11984 (kvm_run->kvm_dirty_regs & ~sync_valid_fields)) {
11985 r = -EINVAL;
11986 goto out;
11987 }
11988
11989 if (kvm_run->kvm_dirty_regs) {
11990 r = sync_regs(vcpu);
11991 if (r != 0)
11992 goto out;
11993 }
11994
11995 /* re-sync apic's tpr */
11996 if (!lapic_in_kernel(vcpu)) {
11997 if (kvm_set_cr8(vcpu, kvm_run->cr8) != 0) {
11998 r = -EINVAL;
11999 goto out;
12000 }
12001 }
12002
12003 /*
12004 * If userspace set a pending exception and L2 is active, convert it to
12005 * a pending VM-Exit if L1 wants to intercept the exception.
12006 */
12007 if (vcpu->arch.exception_from_userspace && is_guest_mode(vcpu) &&
12008 kvm_x86_ops.nested_ops->is_exception_vmexit(vcpu, ex->vector,
12009 ex->error_code)) {
12010 kvm_queue_exception_vmexit(vcpu, ex->vector,
12011 ex->has_error_code, ex->error_code,
12012 ex->has_payload, ex->payload);
12013 ex->injected = false;
12014 ex->pending = false;
12015 }
12016 vcpu->arch.exception_from_userspace = false;
12017
12018 if (unlikely(vcpu->arch.complete_userspace_io)) {
12019 int (*cui)(struct kvm_vcpu *) = vcpu->arch.complete_userspace_io;
12020 vcpu->arch.complete_userspace_io = NULL;
12021 r = cui(vcpu);
12022 if (r <= 0)
12023 goto out;
12024 } else {주요 구문 해설
원본 101줄은 그대로 표시하며, 검토한 62개 구문에 설명을 붙였습니다.
if (vcpu->arch.mp_state == KVM_MP_STATE_INIT_RECEIVED &&사용자 공간이 vCPU를 INIT 수신 후 SIPI 대기 상태로 설정했는지 검사합니다. 다음 조건과 함께, 시작 인터럽트를 받을 수 없는 vCPU가 그런 상태에 놓인 모순을 거릅니다.
!kvm_apic_init_sipi_allowed(vcpu))앞줄의 INIT_RECEIVED 상태 검사에 이어, 이 vCPU가 INIT/SIPI 시작 절차를 사용할 수 있는지도 확인합니다. 시작 인터럽트를 허용하지 않는 vCPU가 그 상태를 요청하면 잘못된 입력으로 보고 -EINVAL을 반환합니다.
return -EINVAL;INIT/SIPI를 받을 수 없는데 그 시작 절차를 기다리는 잘못된 vCPU 상태이므로 -EINVAL을 반환합니다.
return kvm_x86_call(vcpu_pre_run)(vcpu);VMX 또는 SVM 등 선택된 x86 가상화 구현의 실행 전 검사를 호출하고 결과를 반환합니다. 공통 MP 상태 검사 뒤 하드웨어 구현별 제약을 추가로 확인합니다.
int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)x86의 KVM_RUN을 처리하는 함수입니다. FPU·신호·공유 레지스터와 이전 I/O 결과를 준비한 뒤 vCPU 실행으로 이어집니다.
struct kvm_queued_exception *ex = &vcpu->arch.exception;vCPU에 대기 중인 예외 기록을 가리킵니다. 아래에서 사용자 공간이 넣은 예외를 중첩 게스트에 직접 주입할지 L1로 이탈시킬지 판단할 때 사용합니다.
struct kvm_run *kvm_run = vcpu->run;사용자 공간과 공유하는 실행 자료를 가리킵니다. 플래그·CR8·레지스터 동기화 요청·이탈 이유를 이 자료로 주고받습니다.
u64 sync_valid_fields;이 VM에서 공유 run 영역으로 동기화할 수 있는 레지스터 종류의 64비트 마스크입니다. 지원하지 않는 사용자 요청 비트를 걸러낼 기준이 됩니다.
int r;MMU 준비, 레지스터 반영, vCPU 실행 등의 결과를 보관하는 반환값입니다. 음수 오류와 정상적인 사용자 공간 이탈을 공통 종료 경로에 전달합니다.
r = kvm_mmu_post_init_vm(vcpu->kvm);VM의 MMU 사후 준비를 수행합니다. 이 버전에서는 NX huge page 완화 기능에 필요한 큰 페이지 복구 작업을 VM당 한 번 시작하며, 시작에 실패하면 게스트를 실행하지 않고 오류를 반환합니다.
if (r)MMU 사후 준비 함수가 실패했는지 확인합니다. 오류가 있으면 vCPU를 불러오거나 게스트 실행을 시작하지 않습니다.
return r;MMU 준비에서 받은 오류를 그대로 사용자 공간에 돌려줍니다. 아직 실행 문맥을 불러오기 전이라 뒤의 공통 실행 정리는 필요하지 않습니다.
vcpu_load(vcpu);vCPU를 현재 physical CPU에 load해 VMCS/VMCB와 per-CPU owner를 준비한다.
kvm_sigset_activate(vcpu);KVM 실행용으로 지정된 신호 마스크를 현재 호스트 스레드에 적용합니다. KVM_RUN이 실행되는 동안 허용할 신호를 선택하고, 반환할 때 기존 마스크로 되돌리는 준비입니다.
kvm_run->flags = 0;이번 KVM_RUN의 결과 플래그를 0으로 초기화합니다. 이전 실행의 표시가 새 실행 결과로 보이지 않게 합니다.
kvm_load_guest_fpu(vcpu);호스트 태스크와 게스트의 FPU 상태 저장 영역을 전환해 게스트 FP/SIMD 실행을 준비합니다. PKRU는 다른 순서로 다뤄야 하므로 이 전환에 포함하지 않습니다.
kvm_vcpu_srcu_read_lock(vcpu);VM의 SRCU 읽기 구간에 들어가 반환된 토큰을 vCPU에 저장합니다. 실행 준비 중 읽는 공유 KVM 자료가 동시에 교체·해제되지 않도록 수명을 보호합니다.
if (unlikely(vcpu->arch.mp_state == KVM_MP_STATE_UNINITIALIZED)) {vCPU가 아직 INIT을 받아 시작 준비를 마치지 않은 상태인지 확인합니다. 이런 vCPU는 일반 게스트 실행 대신 시작 이벤트를 기다리는 경로로 보냅니다.
if (!vcpu->wants_to_run) {초기화 대기 중이라도 사용자가 이번 실행을 취소했으면 대기하지 않습니다. -EINTR로 종료하여 취소 요청에 즉시 응답합니다.
r = -EINTR;실행 의사가 없는 요청을 인터럽트된 시스템 호출로 보고 -EINTR를 설정합니다.
goto out;초기화 대기조차 하지 않고 공통 종료로 이동합니다. 이미 준비한 FPU·신호·vCPU 문맥은 그 경로에서 정리합니다.
kvm_vcpu_srcu_read_unlock(vcpu);아직 시작되지 않은 vCPU를 기다리게 하기 전에 SRCU 읽기 구간을 끝냅니다. 오랫동안 잠들 수 있는 대기 때문에 다른 CPU의 SRCU 정리 작업을 붙잡지 않게 합니다.
kvm_vcpu_block(vcpu);시작 준비가 끝나지 않은 vCPU를 깨울 이벤트나 신호가 올 때까지 대기시킵니다. 게스트 명령을 실행하는 호출이 아니라, 초기화 대기 상태에서 호스트 CPU를 계속 소비하지 않게 하는 처리입니다.
kvm_vcpu_srcu_read_lock(vcpu);대기에서 돌아온 뒤 KVM 공유 자료를 다시 살펴보기 위해 SRCU 읽기 보호를 재개합니다. 새 토큰은 뒤에서 이 읽기 구간을 끝낼 때 사용합니다.
if (kvm_apic_accept_events(vcpu) < 0) {대기 후 APIC의 INIT/SIPI 등 이벤트를 수용하는 과정에서 게스트 실행을 계속할 수 없는 결과가 생겼는지 확인합니다. 이 경우 설정된 이탈 결과를 사용자 공간으로 돌립니다.
r = 0;APIC 이벤트 처리기가 준비한 사용자 공간 이탈을 반환하도록 KVM_RUN의 반환값을 0으로 설정합니다. 이후 run의 이탈 이유가 세부 결과를 전달합니다.
goto out;APIC 이벤트 처리 결과를 가지고 공통 종료 경로로 이동합니다. 여기서 곧바로 게스트 명령을 실행하지 않습니다.
r = -EAGAIN;초기화 대기에서 돌아온 기본 결과를 재시도 요청인 -EAGAIN으로 정합니다. 다음 KVM_RUN에서 갱신된 vCPU 상태로 다시 실행을 시도하게 합니다.
if (signal_pending(current)) {현재 호스트 스레드에 처리할 신호가 도착했는지 확인합니다. 신호가 있으면 단순 재시도 대신 신호 중단으로 결과를 바꿉니다.
r = -EINTR;호스트 신호 때문에 중단된 실행이므로 반환값을 -EINTR로 바꿉니다.
kvm_run->exit_reason = KVM_EXIT_INTR;공유 run 영역에도 신호 등에 의한 실행 중단임을 KVM_EXIT_INTR로 기록합니다.
++vcpu->stat.signal_exits;신호로 종료한 KVM 실행 횟수를 통계에 더합니다. 게스트 내부 예외 횟수를 세는 값은 아닙니다.
goto out;초기화 대기 경로의 결과가 정해졌으므로 공통 종료로 갑니다. 같은 호출에서 아래 일반 실행 준비로 이어지지 않습니다.
sync_valid_fields = kvm_sync_valid_fields(vcpu->kvm);이 VM이 KVM_RUN의 공유 영역으로 동기화할 수 있는 레지스터 종류의 비트마스크를 얻습니다. 보호된 VM에는 0을 반환하므로 사용자가 그 경로로 게스트 레지스터를 읽고 쓰지 못하게 합니다.
if ((kvm_run->kvm_valid_regs & ~sync_valid_fields) ||사용자가 요청한 레지스터 읽기 동기화 비트 중 VM이 지원하지 않는 종류가 있는지 확인합니다. 다음 줄의 쓰기 요청 검사와 합쳐 잘못된 비트마스크를 거릅니다.
(kvm_run->kvm_dirty_regs & ~sync_valid_fields)) {사용자 공간이 수정했다고 표시한 레지스터 종류에도 지원 범위 밖의 비트가 있는지 검사합니다. 읽기나 쓰기 어느 쪽이든 잘못되면 -EINVAL로 거부합니다.
r = -EINVAL;지원하지 않는 레지스터 동기화 요청이므로 잘못된 인자를 뜻하는 -EINVAL을 설정합니다.
goto out;유효하지 않은 레지스터 비트마스크로 vCPU 상태를 바꾸지 않고 공통 종료로 이동합니다.
if (kvm_run->kvm_dirty_regs) {사용자가 수정한 레지스터가 하나라도 있으면 shared run 영역의 값을 vCPU에 반영합니다. dirty 비트가 없으면 불필요한 동기화를 건너뜁니다.
r = sync_regs(vcpu);사용자가 dirty로 표시한 일반 레지스터·시스템 레지스터·이벤트 상태를 공유 run 영역에서 vCPU에 반영합니다. 적용한 비트를 지우며, 시스템 상태나 이벤트 값이 유효하지 않으면 -EINVAL로 실행을 중단합니다.
if (r != 0)레지스터 값을 반영하는 과정에서 유효성 검사 등에 실패했는지 확인합니다. 실패한 상태로 게스트 실행을 진행하지 않습니다.
goto out;레지스터 동기화 오류를 유지한 채 공통 종료로 이동합니다.
if (!lapic_in_kernel(vcpu)) {로컬 APIC를 사용자 공간이 에뮬레이션하는 vCPU인지 확인합니다. 이 경우 사용자가 전달한 CR8 값을 다시 받아 인터럽트 우선순위를 맞춥니다.
if (kvm_set_cr8(vcpu, kvm_run->cr8) != 0) {run 영역의 CR8을 vCPU에 설정하고 유효성을 확인합니다. CR8은 태스크 우선순위와 연결되므로 허용되지 않는 값이면 게스트를 실행하지 않습니다.
r = -EINVAL;사용자 공간이 준 CR8 값이 유효하지 않아 -EINVAL을 설정합니다.
goto out;잘못된 CR8을 사용한 실행을 막고 공통 종료로 갑니다.
if (vcpu->arch.exception_from_userspace && is_guest_mode(vcpu) &&사용자 공간에서 넣은 예외가 있고 현재 L2 중첩 게스트를 실행 중인지 확인합니다. L1이 그 예외를 가로채려는지도 이어서 검사합니다.
kvm_x86_ops.nested_ops->is_exception_vmexit(vcpu, ex->vector,VMX/SVM의 중첩 가상화 연산으로 이 예외 벡터가 L1로의 VM 이탈 대상인지 물어봅니다. 예외를 L2에 바로 주입할지 결정하는 검사입니다.
ex->error_code)) {예외의 error code도 전달하여 중첩 이탈 조건을 판정합니다. 벡터뿐 아니라 오류의 세부 속성까지 L1의 가로채기 정책에 반영합니다.
kvm_queue_exception_vmexit(vcpu, ex->vector,L1이 가로채는 예외라면 직접 게스트 예외 대신 중첩 VM 이탈을 대기열에 넣습니다. 이탈 원인으로 사용할 예외 벡터를 넘깁니다.
ex->has_error_code, ex->error_code,예외 error code의 존재 여부와 값을 함께 넘깁니다. 오류 코드가 없는 예외에 임의의 값을 있는 것처럼 보고하지 않게 합니다.
ex->has_payload, ex->payload);예외의 추가 payload가 있는지와 그 값을 VM 이탈 기록에 전달합니다. L1이 원래 예외 정보를 잃지 않고 이탈을 처리하도록 합니다.
ex->injected = false;예외를 L1로의 이탈로 바꿨으므로 L2에 직접 주입된다는 표시를 지웁니다.
ex->pending = false;같은 예외가 L2에 다시 주입되지 않도록 직접 주입 대기 표시도 지웁니다.
vcpu->arch.exception_from_userspace = false;사용자 공간에서 온 예외의 변환 검토를 마쳤으므로 출처 표시를 초기화합니다. 다음 실행에서 같은 입력을 새 요청처럼 다시 처리하지 않게 합니다.
if (unlikely(vcpu->arch.complete_userspace_io)) {이전 이탈에서 사용자 공간에 맡긴 I/O 완료 함수가 남아 있는지 확인합니다. 있으면 게스트 재진입 전에 먼저 그 작업을 끝냅니다.
int (*cui)(struct kvm_vcpu *) = vcpu->arch.complete_userspace_io;대기 중인 I/O 완료 함수 주소를 지역 함수 포인터에 보관합니다. 아래에서 vCPU의 포인터를 비우더라도 이번 완료 함수를 호출할 수 있게 합니다.
vcpu->arch.complete_userspace_io = NULL;vCPU의 완료 함수 포인터를 먼저 비웁니다. 이전 I/O가 완료 중이라는 사실을 반영하고 같은 콜백이 다음 실행에서 중복 호출되지 않게 합니다.
r = cui(vcpu);이전 KVM_RUN이 사용자 공간에 맡긴 I/O의 완료 콜백을 실행합니다. 사용자 공간이 제공한 결과로 중단됐던 처리를 끝낸 뒤, 반환값이 계속 실행을 허용하는지 확인합니다.
if (r <= 0)I/O 완료 결과가 0 이하이면 게스트 실행을 계속하지 않습니다. 0은 준비된 사용자 공간 이탈, 음수는 오류를 뜻하는 경로입니다.
goto out;완료 콜백의 결과를 보존하고 공통 종료로 이동합니다.
} else {완료 콜백이 없다면 대기 중인 PIO/MMIO도 없어야 하는 경로입니다. 다음 경고 검사로 내부 상태의 불일치를 확인합니다.
RISC-V · Linux 6.18.37
H-extension VS/VU state와 HGATP two-stage translation
HS-mode KVM이 VS-level CSR, guest GPR/FP/vector와 AIA interrupt state를 vCPU에 저장한다. HGATP가 VMID와 guest-stage root를 선택하며 HFENCE.GVMA/VVMA가 guest translation cache를 무효화한다.
원본 코드: arch/riscv/kvm/vcpu.c:856-950
856 trap->scause = csr_read(CSR_SCAUSE);
857 trap->stval = csr_read(CSR_STVAL);
858
859 vcpu->arch.last_exit_cpu = vcpu->cpu;
860 guest_state_exit_irqoff();
861 kvm_riscv_vcpu_swap_in_host_state(vcpu);
862}
863
864int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
865{
866 int ret;
867 struct kvm_cpu_trap trap;
868 struct kvm_run *run = vcpu->run;
869
870 if (!vcpu->arch.ran_atleast_once)
871 kvm_riscv_vcpu_setup_config(vcpu);
872
873 /* Mark this VCPU ran at least once */
874 vcpu->arch.ran_atleast_once = true;
875
876 kvm_vcpu_srcu_read_lock(vcpu);
877
878 switch (run->exit_reason) {
879 case KVM_EXIT_MMIO:
880 /* Process MMIO value returned from user-space */
881 ret = kvm_riscv_vcpu_mmio_return(vcpu, vcpu->run);
882 break;
883 case KVM_EXIT_RISCV_SBI:
884 /* Process SBI value returned from user-space */
885 ret = kvm_riscv_vcpu_sbi_return(vcpu, vcpu->run);
886 break;
887 case KVM_EXIT_RISCV_CSR:
888 /* Process CSR value returned from user-space */
889 ret = kvm_riscv_vcpu_csr_return(vcpu, vcpu->run);
890 break;
891 default:
892 ret = 0;
893 break;
894 }
895 if (ret) {
896 kvm_vcpu_srcu_read_unlock(vcpu);
897 return ret;
898 }
899
900 if (!vcpu->wants_to_run) {
901 kvm_vcpu_srcu_read_unlock(vcpu);
902 return -EINTR;
903 }
904
905 vcpu_load(vcpu);
906
907 kvm_sigset_activate(vcpu);
908
909 ret = 1;
910 run->exit_reason = KVM_EXIT_UNKNOWN;
911 while (ret > 0) {
912 /* Check conditions before entering the guest */
913 ret = kvm_xfer_to_guest_mode_handle_work(vcpu);
914 if (ret)
915 continue;
916 ret = 1;
917
918 kvm_riscv_gstage_vmid_update(vcpu);
919
920 ret = kvm_riscv_check_vcpu_requests(vcpu);
921 if (ret <= 0)
922 continue;
923
924 preempt_disable();
925
926 /* Update AIA HW state before entering guest */
927 ret = kvm_riscv_vcpu_aia_update(vcpu);
928 if (ret <= 0) {
929 preempt_enable();
930 continue;
931 }
932
933 local_irq_disable();
934
935 /*
936 * Ensure we set mode to IN_GUEST_MODE after we disable
937 * interrupts and before the final VCPU requests check.
938 * See the comment in kvm_vcpu_exiting_guest_mode() and
939 * Documentation/virt/kvm/vcpu-requests.rst
940 */
941 vcpu->mode = IN_GUEST_MODE;
942
943 kvm_vcpu_srcu_read_unlock(vcpu);
944 smp_mb__after_srcu_read_unlock();
945
946 /*
947 * We might have got VCPU interrupts updated asynchronously
948 * so update it in HW.
949 */
950 kvm_riscv_vcpu_flush_interrupts(vcpu);주요 구문 해설
원본 95줄은 그대로 표시하며, 검토한 55개 구문에 설명을 붙였습니다.
trap->scause = csr_read(CSR_SCAUSE);게스트 실행에서 돌아온 직후 SCAUSE를 읽어 trap의 종류를 보관합니다. 뒤에서 호스트 상태를 복원하거나 다른 trap을 처리하기 전에 VM 이탈 원인을 남기는 순서입니다.
trap->stval = csr_read(CSR_STVAL);STVAL에 담긴 trap 부가 정보를 같은 trap 기록에 저장합니다. 원인에 따라 오류 주소나 명령 관련 정보 등이 될 수 있으며, SCAUSE와 함께 게스트 이탈을 해석할 때 사용합니다.
vcpu->arch.last_exit_cpu = vcpu->cpu;이번 게스트 이탈이 발생한 호스트 CPU 번호를 저장합니다. 다음 실행에서 다른 CPU로 옮겨졌는지 판단하고 G-stage TLB 상태를 정리하는 데 사용합니다.
guest_state_exit_irqoff();IRQ가 꺼진 상태에서 문맥 추적에 게스트 실행이 끝났음을 알립니다. RCU와 호스트 문맥 회계를 호스트 실행 상태로 돌리는 단계이며, 게스트에서 빠져나오는 기계어 분기는 이미 앞에서 끝났습니다.
kvm_riscv_vcpu_swap_in_host_state(vcpu);게스트용으로 바꿨던 SCOUNTEREN·SENVCFG 등의 제어 상태를 저장하면서 호스트 값을 복원합니다. 지원되는 구성에서는 SSTATEEN0도 함께 바꿔, 이후 호스트 코드가 게스트의 접근 허용 설정으로 실행되지 않게 합니다.
int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)RISC-V vCPU의 KVM_RUN 요청을 처리합니다. 이전 MMIO·SBI·CSR 이탈을 완료하고 가상 인터럽트와 주소 변환 상태를 준비한 뒤 게스트 진입을 반복합니다.
int ret;MMIO·SBI·CSR 후처리 결과와 이후 KVM_RUN 실행 루프의 상태를 저장할 ret를 선언합니다. 같은 변수라도 후처리 단계의 0은 성공, 실행 루프의 양수는 계속 실행을 뜻하므로 단계별로 읽어야 합니다.
struct kvm_cpu_trap trap;게스트 이탈 때 발생한 trap 원인과 부가 정보를 담는 지역 구조체입니다. 호스트 상태 복원 뒤에도 이탈 당시 정보를 해석할 수 있게 합니다.
struct kvm_run *run = vcpu->run;사용자 공간과 공유하는 run 자료를 가리킵니다. 이전 이탈에 대한 응답을 읽고 이번 실행 결과를 돌려주는 데 사용합니다.
if (!vcpu->arch.ran_atleast_once)이 vCPU가 한 번도 실행되지 않았는지 확인합니다. 처음 실행할 때만 설정된 ISA 확장에 맞는 제어 상태를 구성합니다.
kvm_riscv_vcpu_setup_config(vcpu);vCPU를 처음 실행할 때만 게스트에게 허용한 ISA 확장에 맞춰 초기 제어 상태를 구성합니다. 예를 들어 타이머·캐시 관리 명령·페이지 속성 확장의 허용 비트를 정하고 디버깅 설정도 반영합니다.
vcpu->arch.ran_atleast_once = true;초기 실행 설정을 마쳤음을 기록합니다. 이후 KVM_RUN에서는 이미 진행된 게스트 상태를 초기값으로 덮지 않게 합니다.
kvm_vcpu_srcu_read_lock(vcpu);KVM 공유 자료를 읽는 SRCU 구간을 시작하고 토큰을 저장합니다. 이전 이탈 작업의 완료 처리와 이번 실행 준비가 공유 자료의 해제와 겹쳐도 안전하게 읽도록 합니다.
switch (run->exit_reason) {이전 KVM_RUN이 어떤 이유로 사용자 공간에 돌아갔는지에 따라 완료 처리를 선택합니다. 이번 게스트 실행의 새 trap 원인을 분류하는 switch는 아닙니다.
case KVM_EXIT_MMIO:사용자 공간이 맡아 처리한 MMIO 결과를 게스트에 반영해야 하는 경우입니다. 중단됐던 메모리 접근을 먼저 완료합니다.
ret = kvm_riscv_vcpu_mmio_return(vcpu, vcpu->run);사용자 공간이 에뮬레이션한 MMIO 읽기 결과를 게스트의 대상 레지스터에 반영하고 PC를 다음 명령으로 진행시킵니다. 성공·중복 처리 완료는 0, 지원하지 않는 전송 크기는 -EOPNOTSUPP를 반환합니다.
break;MMIO 완료 처리를 마치고 공통 결과 검사로 진행합니다.
case KVM_EXIT_RISCV_SBI:이전 이탈에서 사용자 공간에 위임한 RISC-V SBI 호출의 반환값을 반영하는 분기입니다.
ret = kvm_riscv_vcpu_sbi_return(vcpu, vcpu->run);사용자 공간에서 처리한 SBI 결과 두 값을 게스트 a0·a1에 기록하고 PC를 ECALL 다음으로 옮깁니다. 이미 처리한 응답은 반복 적용하지 않으며 이 함수의 정상 반환값은 0입니다.
break;SBI 완료 처리를 마치고 다른 이탈 종류의 처리를 건너뜁니다.
case KVM_EXIT_RISCV_CSR:사용자 공간이 에뮬레이션한 CSR 접근의 결과를 게스트 레지스터 상태에 반영하는 분기입니다.
ret = kvm_riscv_vcpu_csr_return(vcpu, vcpu->run);사용자 공간의 CSR 에뮬레이션 결과를 게스트 명령의 목적 레지스터에 넣고 PC를 다음 명령으로 진행시킵니다. rd가 x0이면 값을 버리며 완료 결과로 0을 반환합니다.
break;CSR 접근 완료 처리를 끝내고 공통 실행 준비로 이어집니다.
default:MMIO·SBI·CSR 완료가 필요하지 않은 이전 이탈 이유는 별도의 완료 작업 없이 성공 상태로 진행합니다.
ret = 0;직전 종료 이유가 MMIO·SBI·CSR 후처리 대상이 아니면 ret를 0으로 둡니다. 이 단계에서는 추가 후처리 없이 다음 실행 준비로 진행할 수 있다는 뜻입니다.
break;별도 완료 작업이 없는 경우의 switch 분기를 끝냅니다.
if (ret) {이전 사용자 공간 에뮬레이션의 후처리가 0이 아닌 오류를 반환했는지 검사합니다. 오류가 있으면 vCPU 실행 준비를 계속하지 않고 SRCU 잠금을 푼 뒤 반환합니다.
kvm_vcpu_srcu_read_unlock(vcpu);이전 MMIO·SBI·CSR 접근을 완료하는 과정에서 오류가 났으므로 SRCU 읽기 보호를 끝냅니다. 이어 오류를 반환하되 읽기 구간을 열린 채로 남기지 않습니다.
return ret;앞의 MMIO·SBI·CSR 후처리 오류를 KVM_RUN 호출자에게 전달합니다. 바로 위에서 SRCU 읽기 구간을 끝냈으며 아직 새 게스트 실행을 시작하지 않은 상태입니다.
if (!vcpu->wants_to_run) {사용자가 이번 KVM_RUN에서 게스트 실행을 취소했는지 확인합니다. 취소 상태이면 SRCU 보호를 풀고 -EINTR로 돌아갑니다.
kvm_vcpu_srcu_read_unlock(vcpu);실행 요청이 취소되어 -EINTR로 돌아가기 전에 SRCU 토큰을 반환합니다. 게스트에 들어가지 않는 조기 반환에서도 공유 자료의 읽기 보호를 정확히 마무리합니다.
return -EINTR;실행하지 말라는 요청에 응답하여 -EINTR을 반환합니다. 게스트에서 발생한 RISC-V trap 오류를 뜻하는 반환값은 아닙니다.
vcpu_load(vcpu);vCPU를 현재 호스트 CPU의 KVM 실행 문맥에 연결합니다. 물리 CPU에 종속된 상태를 준비하고 스케줄링에 따른 vCPU 상태 전환을 사용할 수 있게 합니다.
kvm_sigset_activate(vcpu);이번 KVM_RUN에 지정된 호스트 신호 마스크를 적용합니다. 게스트 실행 중 어떤 신호가 vCPU 실행을 중단할 수 있는지 정하며, 함수에서 나갈 때 기존 마스크로 복구합니다.
ret = 1;실행 루프를 시작하도록 ret에 양수 1을 넣습니다. 이전 후처리의 성공값 0과 구분하여 이제부터는 양수가 게스트 실행 계속을 나타냅니다.
run->exit_reason = KVM_EXIT_UNKNOWN;이번 실행의 이탈 이유를 UNKNOWN으로 초기화합니다. 이전 이탈의 완료 처리를 끝낸 뒤 새 결과를 받을 준비입니다.
while (ret > 0) {ret가 양수인 동안 게스트 진입 준비와 실행을 반복합니다. 0의 사용자 공간 처리 요청이나 음수 오류가 생기면 루프를 종료합니다.
ret = kvm_xfer_to_guest_mode_handle_work(vcpu);게스트 진입 전에 호스트 스레드에서 처리해야 할 대기 작업을 수행합니다. 성공은 0이며, 시그널로 중단해야 하면 -EINTR과 KVM_EXIT_INTR을 남깁니다.
if (ret)호스트 작업 처리에서 오류가 나면 현재 반복의 나머지를 건너뜁니다. 이어 while 조건이 음수 ret를 검사하므로 게스트에 들어가지 않고 루프를 끝내게 됩니다.
continue;호스트의 대기 작업 처리 결과 때문에 지금 진입할 수 없으면 실행 루프의 조건을 다시 확인합니다. ret가 계속 실행을 허용할 때만 다음 반복을 수행합니다.
ret = 1;호스트 작업 처리가 정상 종료했으므로 실행 계속 상태인 1을 넣습니다. helper의 성공값 0과 실행 루프의 종료 조건이 충돌하지 않게 바꾸는 단계입니다.
kvm_riscv_gstage_vmid_update(vcpu);현재 VM의 G-stage 주소 변환용 VMID 세대가 유효한지 확인하고 필요하면 새 VMID를 배정합니다. 번호를 재사용하는 세대 전환에서는 다른 CPU의 게스트 TLB도 정리하여 서로 다른 VM의 변환 결과가 섞이지 않게 합니다.
ret = kvm_riscv_check_vcpu_requests(vcpu);vCPU의 sleep·reset, HGATP 갱신, 명령 캐시·TLB fence, steal-time 기록 등의 대기 요청을 처리합니다. 계속 가능하면 1을 주고 dirty-ring 처리를 위해 사용자 공간으로 나가야 하면 0을 줍니다.
if (ret <= 0)vCPU 요청 처리 결과가 실행 계속 상태가 아니면 이번 게스트 진입을 건너뜁니다. 현재 구현에서 dirty-ring 때문에 0을 받으면 다음 while 검사에서 종료합니다.
continue;vCPU 요청 처리 후 즉시 진입할 수 없으면 이번 진입 준비를 건너뜁니다. 반복 조건의 ret 값이 재시도 또는 종료를 결정합니다.
preempt_disable();AIA 상태와 게스트 진입 준비를 다루는 동안 현재 호스트 태스크의 선점을 막습니다. 준비한 상태가 속한 물리 CPU에서 이어서 진입 여부를 결정하도록 합니다.
ret = kvm_riscv_vcpu_aia_update(vcpu);현재 물리 CPU에서 게스트 인터럽트를 받을 수 있도록 AIA/IMSIC 하드웨어 상태를 갱신합니다. CPU 이동 시 interrupt file을 옮기는 처리도 포함하며 계속 가능하면 1, 진입 실패나 오류이면 0 또는 음수를 반환합니다.
if (ret <= 0) {AIA 준비 결과가 0 또는 음수이면 게스트 진입을 중단합니다. 이 구간은 선점을 막아 둔 상태이므로 다음 줄에서 선점을 다시 허용한 뒤 루프 종료 검사로 돌아갑니다.
preempt_enable();AIA 갱신 결과 때문에 이번 게스트 진입을 계속할 수 없으면 선점을 다시 허용합니다. 바로 반복문의 처음으로 돌아가므로, 진입 준비용 선점 금지가 다음 반복에 누적되지 않게 합니다.
continue;AIA 상태 갱신 뒤 진입을 계속할 수 없어 선점을 다시 허용한 다음, 실행 루프의 처음으로 돌아갑니다.
local_irq_disable();현재 CPU의 일반 IRQ를 막아 게스트 진입 직전의 상태 확인과 인터럽트 반영을 보호합니다. 뒤에서 mode를 공개하고 마지막 요청 검사를 마칠 때까지 호스트 IRQ의 개입을 제한합니다.
vcpu->mode = IN_GUEST_MODE;최종 요청 검사 직전에 vCPU를 게스트 진입 상태로 공개합니다. 실제 게스트 명령은 아직 실행하지 않았지만, 다른 CPU가 새 요청을 보낼 때 이 진입 시도와 동기화할 수 있게 합니다.
kvm_vcpu_srcu_read_unlock(vcpu);게스트를 실행하기 전에 호스트의 SRCU 읽기 구간을 끝냅니다. 게스트 실행 시간이 길어도 KVM 공유 자료의 SRCU 유예 기간을 계속 붙잡고 있지 않도록 하는 전환입니다.
smp_mb__after_srcu_read_unlock();SRCU 해제 뒤 필요한 메모리 순서를 보장합니다. 앞서 IN_GUEST_MODE를 공개한 상태와 뒤의 실행 요청 재검사가 다른 CPU의 요청 전송과 맞물려, 진입 직전에 들어온 작업을 놓치지 않도록 합니다.
kvm_riscv_vcpu_flush_interrupts(vcpu);다른 경로가 남긴 가상 인터럽트 변경 비트를 원자적으로 가져와 vCPU에 저장된 HVIP 상태에 반영합니다. AIA의 상위 인터럽트 상태도 처리하며, 실제 HVIP CSR 쓰기는 뒤의 별도 단계에서 수행합니다.
05 · WORKED EXAMPLE
숫자로 검산하기
guest page fault와 stage-2 fault 구분
guest VA 0x4000 접근이 guest page table에서는 GPA 0x120000으로 변환되지만 해당 GPA memslot이 제거됐다고 가정한다.
- stage 1guest page walker는 GVA 0x4000을 GPA 0x120000으로 정상 변환한다.
- stage 2EL2/EPT/HGATP walker가 GPA에 대응하는 host mapping을 찾지 못해 virtualization exit를 만든다.
- decodearm64 HPFAR, x86 EPT qualification, RISC-V HTVAL이 guest physical fault 정보를 제공한다.
- resolutionKVM MMU가 새 memslot mapping을 만들 수 없으면 userspace에 memory fault/MMIO exit를 반환한다.
결론guest의 일반 page fault와 host가 관리하는 stage-2 fault는 faulting address 공간과 처리 주체가 다르다.
06 · DEEP DIVE
주요 동작 상세 분석
공통 커널 코드와 아키텍처별 함수의 역할 구분
userspace VMM이 KVM_RUN을 호출하면 KVM core가 vCPU를 load하고 pending request/interrupt를 처리한 뒤 architecture enter_guest를 실행한다. hardware exit는 reason을 decode해 kernel에서 처리하거나 exit structure로 userspace에 반환한다.
host userspace, KVM ioctl context, guest execution, hypervisor exception과 host interrupt context를 나눈다. preemption과 migration은 CPU-local virtualization register owner를 바꾸므로 vcpu_load/put 경계를 지켜야 한다.
arm64: EL2 VHE/nVHE와 stage-2 VTTBR_EL2
KVM은 guest EL1 register, timer, GIC virtualization state를 vCPU context에 보관한다. VTTBR_EL2가 VMID와 stage-2 root를 선택하고 HCR_EL2가 trap policy를 정한다. nVHE는 별도 hyp address space와 stack을 사용한다.
host/guest sysreg save, stage-2 TLBI, GIC LR injection과 barrier가 guest entry 전 완료되어야 한다. 디버깅할 때는 HCR_EL2, VTTBR_EL2/VMID, ESR_EL2, FAR/HPFAR, guest PC/PSTATE, GIC LR와 kvm_request bitmap을 본다.
x86-64: VMX VMCS 또는 SVM VMCB와 EPT/NPT
VMCS/VMCB에 guest/host control register, segment, RIP/RSP와 intercept policy를 둔다. EPTP/NCR3가 second-level translation root를 고르고 posted interrupt/APICv가 일부 interrupt를 exit 없이 주입한다.
VM-entry control validation, host FPU/debug/speculation state 교체와 INVEPT/INVVPID ordering을 지켜야 한다. 디버깅할 때는 exit_reason/qualification, guest linear/physical address, EPTP/VPID, interrupt-window flag, VMCS instruction error와 requests를 본다.
RISC-V: H-extension VS/VU state와 HGATP two-stage translation
HS-mode KVM이 VS-level CSR, guest GPR/FP/vector와 AIA interrupt state를 vCPU에 저장한다. HGATP가 VMID와 guest-stage root를 선택하며 HFENCE.GVMA/VVMA가 guest translation cache를 무효화한다.
VS CSR save/restore, HGATP write, HFENCE와 virtual interrupt pending publication이 guest entry 전에 정합해야 한다. 디버깅할 때는 HSTATUS, HGATP/VMID, HTVAL/HTINST, SCAUSE/SEPC, VS CSR, virtual interrupt bits와 request bitmap을 확인한다.
객체를 사용하고 해제하는 주체와 시점을 확인한다
VM, vCPU, memory slot와 stage-2 mapping은 in-flight guest access와 SRCU reader가 끝날 때까지 유지된다. memslot 변경은 MMU invalidation sequence와 remote vCPU kick을 동반한다.
주소나 레지스터 값만 확인하면 갱신되지 않은 상태를 놓칠 수 있다. 상태를 만드는 코드, 다른 코드에 전달하는 시점, 사용하는 코드, 해제하는 시점을 함께 기록한다.
최대 지연 시간은 명령 하나의 실행 시간만으로 결정되지 않는다
entry/exit fixed cost, stage-2 miss, virtual interrupt injection, emulation, host scheduling과 userspace round trip이 더해진다. worst case는 preempted vCPU가 lock 또는 virtual IPI completion을 쥔 상황이다.
평균값뿐 아니라 인터럽트를 비활성화한 시간, 다른 CPU의 응답 시간, 펌웨어 호출 시간, 재시도 횟수를 따로 확인해야 최대 지연의 원인을 찾을 수 있다.
07 · FAILURE
실패를 어떤 증거로 나눌 것인가
| 분류 | 관찰되는 결과 | 첫 확인값 |
|---|---|---|
| arm64 | VMID wrap flush 누락, hyp mapping lifetime 오류 또는 virtual timer offset 불일치는 guest memory leak, hyp fault와 시간 점프를 만든다. | HCR_EL2, VTTBR_EL2/VMID, ESR_EL2, FAR/HPFAR, guest PC/PSTATE, GIC LR와 kvm_request bitmap을 본다. |
| x86-64 | invalid VMCS field는 VM-entry failure를 만들고 stale EPT/VPID는 guest가 이전 mapping을 계속 사용하게 한다. | exit_reason/qualification, guest linear/physical address, EPTP/VPID, interrupt-window flag, VMCS instruction error와 requests를 본다. |
| RISC-V | VMID generation, guest timer delta 또는 virtual instruction emulation 오류가 stale mapping, guest hang과 잘못된 time을 만든다. | HSTATUS, HGATP/VMID, HTVAL/HTINST, SCAUSE/SEPC, VS CSR, virtual interrupt bits와 request bitmap을 확인한다. |
08 · LAB
재현과 계측 절차
- 같은 guest workload에서 hardware exit reason과 userspace exit reason을 따로 count해 virtualization overhead를 분해한다.
- memslot을 변경하며 remote vCPU kick, stage-2 invalidation과 guest 재진입 순서를 trace한다.
- 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
- 소스만 읽고 끝내지 않고 최종
vmlinux의objdump -dr,readelf -SW결과로 선택된 alternative와 section 배치를 확인한다.
09 · REFERENCES
원문 위치
- arm64arch/arm64/kvm/arm.c:1157-1251
- x86-64arch/x86/kvm/x86.c:11924-12024
- RISC-Varch/riscv/kvm/vcpu.c:856-950
Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.