1) CPU 가상화 개념 (VMX, VMCS)
2) x86 가상화의 장애와 VMX 동작 (shadow PT, EPT, VM Entry/Exit)
3) VCPU 생명 주기
4) KVM 개요와 소스 코드 조직
5) KVM 모듈 초기화 (vmx_init / kvm_init / irqfd)
6) QEMU/KVM API 사용 예제 (light-qemu)
1) CPU 가상화 개념 (VMX · VMCS)
물리 머신에서는 운영체제도 응용 프로그램도 모두 하드웨어 위에서 직접 실행된다.
x86 CPU를 예로 들면, 총 ring0~ring3 네 개의 실행 등급이 있다. 운영체제는 프로그램을 두 종류로 나누는데, 즉 방금 말한 사용자 모드 프로그램(응용 프로그램)과 커널 모드 프로그램(커널 코드, 드라이버 프로그램 등)이다.
커널 모드와 사용자 모드를 구분하기 위해 운영체제와 응용 프로그램을 각각 ring0~ring3 이 네 개의 실행 등급에 대응시켜야 했다.
당시 Linux 커널을 작성할 때는 미래에 가상 머신이 개발돼서 크게 성행할 거라곤 예상하지 못했고, 그래서 그들은 "꽤 사치스럽게" 펼쳐서 사용했다.
커널 모드(운영체제)의 코드는 ring0 즉 0등급에서 실행되게 하고, 사용자 모드(응용 프로그램)의 코드는 ring3 즉 3등급에서 실행되게 했다. 양쪽 끝만 차지하고 중간은 다 쓰지 않는다.

이렇게 해서 만약 사용자 모드 프로그램이 일을 하면, 권한을 ring3등급에 둔다. 일단 더 많은 자원을 사용해야 한다고 신청하면, 권한을 ring0등급으로 돌린다. 더 명확하고 자세히 말하면 이렇다.
응용 프로그램이 어떤 민감한 작업이나 시스템 자원에 접근하는 등의 작업을 수행해야 할 때는, 특수한 명령을 실행해서 운영체제 커널에 들어가야 하고, 커널이 일부 안전 검사를 수행한 다음에 응용 프로그램을 대신해서 이런 자원에 접근한다.
ring3 즉 3등급에서 실행할 수 있는 명령을 비특권 명령이라고 부르고, ring0 즉 0등급에서만 실행할 수 있는 명령을 특권 명령이라고 부른다.
명령을 구분하는 데에는 또 다른 방법이 있는데, 시스템 전체에 영향을 미치는지 여부로 구분하는 방법이다. 만약 명령이 시스템 전체에 영향을 미치면 민감 명령이라고 부르고, 만약 명령이 자기가 있는 프로세스에만 영향을 미치면 비민감 명령이라고 부른다.
x86 시스템에서는 민감 명령과 특권 명령이 완전히 똑같지는 않다. 일부 명령은 민감 명령이지만 특권 명령은 아닌데, 다시 말해 사용자 프로그램이 실행할 수 있는, 일부 시스템 전체 자원을 수정/획득할 수 있는 명령이 존재한다.
이건 물리 머신에서는 아무 문제가 없다. 왜냐하면 시스템 전체의 주인이 현재 이 운영체제 하나뿐이기 때문이다. 하지만 가상화 상황에서는 그렇지 않다. 가상화 플랫폼 위에서는 여러 개의 가상 머신이 동시에 실행되는데, 만약 그 안의 운영체제들이 모두 자유롭게 전체 데이터를 읽거나 수정할 수 있다면 그건 엄청나게 혼란스러울 거다. 그래서 전통적으로는 다음 몇 가지 방법으로 가상화를 구현했다.
- Bohs와 QEMU(KVM 미포함) 같은 순수 소프트웨어 시뮬레이션은 엄밀히 말하면 가상화 소프트웨어라고 부를 수 없고, 시뮬레이터라고 불러야 한다. 왜냐하면 얘네들은 다 명령 하나하나를 분석한 다음에 실행하는 방식이기 때문이다.
- 또 다른 종류는 VMWare 초기 방안이다. 가상 머신 사용자 모드의 프로그램은 CPU 위에서 직접 실행되지만, 일부 특권 명령은 동적인 바이너리 번역을 거쳐서 실행된다.
- 그 뒤를 잇는 게 Xen 방안이다. 이 방안은 가상 머신 운영체제 커널의 코드를 수정해서 가상 머신 커널이 ring1에서 실행되게 했고, 또한 가상 머신 안의 운영체제 커널의 민감 명령에 대해서는 교체를 진행해서 Xen 내핵의 ring0에 들어가게 했다.

각 방안마다 결함이 있다는 게 분명하다. 순수 소프트웨어 시뮬레이션은 성능이 매우 안 좋고, Xen 방안은 소스 코드가 있는 운영체제만 지원할 수 있고, 종합해보면 VMWare의 Workstation이 상대적으로 괜찮다.
요즘은 클라우드 컴퓨팅 기술의 지속적인 발전을 등에 업고, 가상화 기술도 빠르게 발전하고 있다. Intel과 AMD가 잇따라 하드웨어 계층에서 가상화를 지원하면서 CPU, 메모리, 디스크, 네트워크 카드 등 자주 사용하는 외부 장치, 하드웨어 계층의 가상화는 이미 클라우드 컴퓨팅의 표준이 되었다.
VMX 아키텍처 간단 소개
Intel은 기존 x86 CPU를 기반으로 VMX 아키텍처를 추가해서 CPU의 하드웨어 가상화를 구현했다.
VMX (Virtual Machine eXtension) 아키텍처는 Intel사의 가상화 기술로, 주로 x86 아키텍처 프로세서의 가상화 지원 능력을 강화하는 데 사용된다.
VMX는 고급 하드웨어 가상화 계층을 제공해서, 운영체제가 단일 물리 프로세서 위에서 여러 개의 독립적인 VM을 실행할 수 있게 한다. 각 VM은 자체적인 CPU 뷰, 메모리 관리, I/O 컨트롤러 등의 자원을 가진다. VMX는 특정 표시 위치를 설정하고 가상화 확장 명령 집합을 사용해서, 호스트 머신이 가상 머신의 행위를 제어하고 격리할 수 있게 한다.
VMX 아키텍처 아래에는 두 종류의 핵심 역할이 정의되어 있다.
- 가상 머신 모니터(Virtual Machine Monitor, VMM)
- 가상 머신(Virtual Machine, VM).
VMM은 시스템 전체의 CPU와 하드웨어에 대한 완전한 통제권을 가지고, 가상화된 CPU를 각 VM에게 추상화해서 주고, VM의 CPU를 물리 CPU 위로 직접 스케줄링해서 실행할 수 있다. 각 VM은 다 가상 머신 인스턴스 하나로, 운영체제 및 각종 소프트웨어 스택과 응용 프로그램을 지원할 수 있다.
VM 자체는 자기가 가상화 환경에 있다는 걸 깨닫지 못한다. 각 VM은 서로 독립적이고, 자기만의 독립적인 CPU, 메모리, 인터럽트, 장비 등을 가지는데, 이런 자원들은 다 VMM이 제공한 거다.
VMM은 각 VM에 대해 관리를 진행해야 하는데, VM 인스턴스 생성, 설정, 삭제, 각 VM에 자원 분배, 각 VM 사이의 격리와 독립 보장 등이 포함된다. 또한 자원에 대한 접근 처리, 공평성 확보도 해야 하고, 이 모든 게 다 VMM이 운영하는 권한 등급이 VM보다 높아야 한다. 이렇게 해야만 VMM이 시스템 전체 자원과 VM에 대한 관리를 구현할 수 있다.
이미 말했듯이, 역사적인 이유로 운영체제 커널은 이미 최고 등급인 ring0에서 실행되고 있다. 그래서 CPU가 VMM과 VM 두 종류 소프트웨어를 지원할 수 있게 하려고, Intel은 CPU에 새로운 모드 한 가지를 도입했다.
- VMX operation.
VMM이 실행되는 모드를 VMX root operation 모드라고 부르고, VM이 실행되는 모드를 VMX non-root operation 모드라고 부른다. 이 두 모드 사이의 전환을 VMX 전환이라고 부른다. VMX root에서 VMX non-root로 전환되는 과정을 VM Entry라고 부르고, VMX non-root에서 VMX root로 전환되는 과정을 VM Exit이라고 부른다.


CPU가 VMX operation에 진입하게 하려면, 먼저 VMXON 명령을 하나 실행해야 한다.
VMM 자체도 일부 데이터를 기록해야 하기 때문에, VMXON 명령을 실행하기 전에 VMXON용 영역을 하나 미리 할당하고 초기화를 진행해야 한다.
VM Entry를 통해, VMM은 어떤 VM 하나를 실행 상태로 진입시킬 수 있다.
처음으로 VM에 진입할 때는 VMLAUNCH 명령으로 시한다. 각 VM은 다 대응되는 가상 머신 제어 구조 (Virtual Machine Control Structure, VMCS) 영역 하나와 대응되는데, 해당 VM의 관련 정보를 저장하는 데 쓰인다. 그래서 VMLAUNCH를 진행하기 전에 미리 VMCS에 대한 분배와 초기화를 해야 한다.
VM 내부에서 특수한 명령을 실행하거나, 일부 이벤트가 발생할 때 VM Exit가 발생한다.
VM은 자기가 가상화 환경에 있다는 걸 모르기 때문에 능동적으로 VM Exit을 진행하지 않는다.
VM의 CPU가 미리 정해진 명령을 실행하거나 VMCS에 일부 이벤트를 설정했을 때만 VM Exit를 진행하게 된다.
VM은 VMM이 지정한 주소 하나로 빠져나가고, 이때 VMM은 실행을 시작해서 VM의 퇴출에 대한 처리를 할 수 있다.
VMM은 VMXOFF 명령을 실행해서 VMX operation을 퇴출할 수 있다.
각 모드마다 자기만의 ring0과 ring3 구조가 있다.
VMX operation과 CPU 특권 등급은 정교하다. 보통의 QEMU/KVM 아키텍처에서는 QEMU 등 사용자 모드 소프트웨어와 KVM 등 호스트 머신의 내핵이 모두 VMX root 모드에서 실행되고, 가상 머신 안에서도 자기만의 ring0과 ring3이 있다.
당연히, VM에서 명령을 실행하는 행위는 절대로 VMM과 똑같을 수 없다.
안 그러면 VMX 아키텍처를 쓸 필요가 없다. VMX non-root 모드에서 각종 명령은 엄격하게 제한되는데, 일부 특수한 명령(위에서 말한 시스템 전체에 영향을 미치는 명령 같은 거)을 실행하거나, 일부 특수한 이벤트가 발생하면 다 VM Exit를 일으켜서 VM이 VMM으로 빠져나가게 한다.
이렇게 해서 VMM은 모든 VM의 프로세서 행위를 통제할 기회를 얻는다.
VMX operation의 도입은 전통적인 운영체제가 시스템 전체 자원을 통제할 수 있게 해줄 뿐만 아니라, 가상 머신의 운영체제도 수정할 필요 없이 실행될 수 있게 해준다. 이건 가상화 플랫폼 즉 VMM의 개발을 매우 간소화시켰다.
VMCS 소개
이미 말했듯이, 각 VM은 다 대응되는 가상 머신 제어 구조(Virtual Machine Control Structure, VMCS) 하나와 대응된다.
각 가상 머신의 VCPU는 다 VMCS 영역 하나를 가진다. VMCS는 VMX non-root Operation의 전환을 관리하고 VCPU의 행위를 제어하는 데 사용된다.
VMCS를 조작하는 명령에는 VMCLEAR, VMPTRLD, VMREAD, VMWRITE가 포함된다. VMCS 영역의 크기는 4KB이고, VMM은 그것의 64비트 주소를 통해 이 영역에 접근한다.

VMCS와 VCPU의 관계는 프로세스 디스크립터와 프로세스의 관계와 비슷하다.
전통적으로 운영체제의 프로세스들은 물리 CPU 자원을 공유하는데, 운영체제는 여러 프로세스 사이에 CPU를 분배하는 것을 책임진다.
각 프로세스는 다 프로세스 디스크립터로 프로세스의 정보를 저장하고, 프로세스 전환 시에 하드웨어 컨텍스트를 보존해서, 프로세스가 다음번에 스케줄링될 때 정상적으로 실행될 수 있게 한다. 마찬가지로 VCPU들도 물리 CPU를 공유하고, VMM은 여러 VCPU 사이에 물리 CPU를 분배하는 것을 책임지고, 각 VCPU는 다 자기만의 디스크립터를 가진다.
VMM이 VCPU 실행을 전환할 때 이 순간의 VCPU 상태를 저장해야 하고, 그래야 다음 번 VCPU 스케줄링 때 VCPU가 중단됐던 그 지점부터 시작해서 정상적으로 실행될 수 있다.
VMCS 영역의 내용은 아래와 같이 표시된다.
| Byte Offset | Contents |
| 0 | Bits 30:0: VMCS revision identifier Bit 31: shadow-VMCS indicator (see Section 24.10) |
| 4 | VMX-abort indicator |
| 8 | VMCS data (implementation-specific format) |
VMCS 형식에서 앞 8바이트는 고정되어 있다.
- Byte 0~3
VMCS의 첫 4바이트의 0번 비트부터 30번 비트까지는 수정 표시자를 나타내는데, 서로 다른 VMCS 버전을 표시하는 데 쓰인다. VMM은 반드시 이 값을 초기화해야 한다. 첫 4바이트의 31번 비트는 shadow-VMCS indicator인데, VMM은 VMCS가 보통 VMCS인지 아니면 shadow VMCS인지에 따라 shadow-VMCS indicator를 설정해야 한다.
- Byte 4~7
VMCS의 두 번째 4바이트는 VMX-abort indicator인데, VM Exit 시에 에러가 발생하면 VMX-abort가 발생해서 프로세서가 꺼짐 상태로 진입하게 되고, 프로세서는 0이 아닌 값 하나를 VMX-abort indicator에 쓴다.
나머지가 바로 VMCS 데이터 영역인데, 그것은 VMX non-root와 VMX root 사이의 전환을 제어한다. 이 영역의 형식은 구현에 의해 결정되고, VMM은 VMREAD와 VMWRITE 명령을 통해 여기서 읽고 쓴다.
VMCS 데이터 영역
8번째 바이트부터 시작하는 VMCS 데이터 영역에 대해 알아보자.
VMCS 데이터 영역은 총 6개 영역이 있다.

(1) Guest-state 영역
VM Entry를 진행할 때, 가상 머신 프로세서의 상태 정보가 이 영역에서 로드된다.
VM Exit를 진행할 때 가상 머신의 현재 상태 정보가 이 영역에 쓰여진다. 이 영역 안에는 전형적으로 각종 레지스터의 상태 및 일부 프로세서의 상태가 있다.
완전한 Guest-state 영역은 아래 표와 같이 표시된다.
| CR0 | CR3 | CR4 | ||
| DR7 | ||||
| RSP | RIP | RPLAGS | ||
| CS | Selector | Base Address | Segment Limit | Access Right |
| SS | Selector | Base Address | Segment Limit | Access Right |
| DS | Selector | Base Address | Segment Limit | Access Right |
| ES | Selector | Base Address | Segment Limit | Access Right |
| FS | Selector | Base Address | Segment Limit | Access Right |
| GS | Selector | Base Address | Segment Limit | Access Right |
| LDTR | Selector | Base Address | Segment Limit | Access Right |
| TR | Selector | Base Address | Segment Limit | Access Right |
| GDTR | Base Address | Segment Limit | ||
| IDTR | Base Address | Segment Limit | ||
| IA32_DEBUGCTL | IA32_SYSENTER_CS | IA32_SYSENTER_ESP | IA32_SYSENTER_EIP | |
| IA32_PERF_GLOBAL_CTRL | IA32_PAT | IA32_EFER | IA32_BNDCFGS | |
| SMBASE | ||||
| Activity state | Interruptibility | |||
| Pending debug exceptions | ||||
| VMCS link pointer | ||||
| VMX-preemption timer value | ||||
| Page-directory-pointer-table entries | PDTE0 | PDTE1 | PDTE2 | PDTE3 |
| Guest interrupt status | ||||
| PML index |
(2) Host-state 영역
VM Exit가 발생할 때 VMM의 컨텍스트로 전환해서 실행해야 하는데, 이때 프로세서의 상태 정보가 이 영역에서 로드된다.
완전한 Host-state 영역은 아래 표와 같이 표시된다.
| CR0 | CR3 | CR4 |
| RSP | RIP | |
| CS | Selector | |
| SS | Selector | |
| DS | Selector | |
| ES | Selector | |
| FS | Selector | Base Address |
| GS | Selector | Base Address |
| TR | Selector | Base Address |
| GDTR | Base Address | |
| IDTR | Base Address | |
| IA32_SYSENTER_CS | IA32_SYSENTER_ESP | IA32_SYSENTER_EIP |
| IA32_PERF_GLOBAL_CTRL | IA32_PAT | IA32_EFER |
(3) VM-execution 제어 영역
이 영역은 VM Entry 진입 후의 프로세서 행위를 제어하는 데 쓰인다.
이 영역은 매우 방대한데, 어떤 이벤트가 VM Exit를 일으킬지, 한 이상 비트가 어떤 이상이 VM Exit를 발생시킬지를 가리키는지, APIC의 가상화 제어 등 다양한 제어를 포함한다.
완전한 VM-execution control 영역은 아래 표와 같이 표시된다.
| 분류 | 제어 항목 |
| Pin-Based VM-Execution Controls | External-interrupt exiting, NMI exiting, Virtual NMIs, Activate VMX-preemption timer, Process posted interrupts |
| Primary processor-based VM-execution controls | Interrupt-window exiting, Use TSC offsetting, HLT exiting, INVLPG exiting, MWAIT exiting, RDPMC exiting, RDTSC exiting, CR3-load exiting, CR3-store exiting, CR8-load exiting, CR8-store exiting, Use TPR shadow, NMI-window exiting, MOV-DR exiting, Unconditional I/O exiting, Use I/O bitmaps, Monitor trap flag, Use MSR bitmaps, MONITOR exiting, PAUSE exiting, Activate secondary controls |
| Secondary processor-based VM-execution controls | Virtualize APIC accesses, Enable EPT, Descriptor-table exiting, Enable RDTSCP, Virtualize x2APIC mode, Enable VPID, WBINVD exiting, Unrestricted guest, APIC-register virtualization, Virtual-interrupt delivery, PAUSE-loop exiting, RDRAND exiting, Enable INVPCID, Enable VM functions, VMCS shadowing, Enable ENCLS exiting, RDSEED exiting, Enable PML, EPT-violation #VE, Conceal VMX non-root operation from Intel PT, Enable XSAVES/XRSTORS, Mode-based execute control for EPT, Use TSC scaling |
| 추가 필드 | Exception Bitmap, I/O-Bitmap Addresses, TSC-offset, Guest/Host Masks for CR0, Guest/Host Masks for CR4, Read Shadow for CR0, Read Shadow for CR4, CR3-target value 0~3, CR3-target count, APIC-access address, Virtual-APIC address, TPR threshold, EOI-exit bitmap 0~3, Posted-interrupt notification vector, Posted-interrupt descriptor address, Read bitmap for low MSRs, Read bitmap for high MSRs, Write bitmap for low MSRs, Write bitmap for high MSRs, Executive-VMCS Pointer, Extended-Page-Table Pointer, Virtual-Processor Identifier, PLE_Gap, PLE_Window, VM-function controls, ENCLS-exiting bitmap, VMREAD bitmap, VMWRITE bitmap, Virtualization-exception information address, EPT index, PML address, XSS-exiting bitmap |
(4) VM Exit 제어 영역
VM Exit 제어 영역은 가상 머신이 VM Exit를 발생시킬 때의 행위, 예를 들면 일부 레지스터의 저장 같은 것을 지정하는 데 쓰인다.
완전한 VM Exit control 영역은 아래 표와 같이 표시된다:
| 분류 | 항목 |
| VM-Exit Controls | Save debug controls, Host Address space size, Load IA32_PERF_GLOBAL_CTRL, Acknowledge interrupt on exit, Save IA32_PAT, Load IA32_PAT, Save IA32_EFER, Load IA32_EFER, Save VMX preemption timer value, Clear IA32_BNDFGS, Conceal VM exits from Intel PT |
| VM-Exit Controls for MSRs | VM-exit MSR-store count, VM-entry MSR-load address, VM-exit MSR-load count, VM-exit MSR-load address |
(5) VM Entry 제어 영역
VM Entry 영역은 가상 머신이 VM Entry를 발생시킬 때의 행위, 예를 들면 일부 레지스터의 로드, 그리고 일부 가상 머신의 이벤트 주입 같은 것을 지정하는 데 쓰인다.
완전한 VM Entry control 영역은 아래 표와 같이 표시된다.
| 분류 | 항목 |
| VM-Entry Controls | Load debug controls, IA-32e mode guest, Entry to SMM, Deactivate dual-monitor treatment, Load IA32_PERF_GLOBAL_CTRL, Load IA32_PAT, Load IA32_EFER, Load IA32_BNDCFGS, Conceal VMX from PT |
| VM-Exit Controls for MSRs | VM-entry MSR-load count, VM-entry MSR-load address |
| VM-Entry for Event Injection | VM-entry interruption-information field, VM-entry exception error code, VM-entry instruction length |
(6) VM Exit 정보 영역
VM Exit 정보 영역에는 가장 최근에 발생한 VM Exit 정보가 담겨 있고, 전형적인 정보로는 퇴출 원인 및 그에 상응하는 데이터, 예를 들면 명령 실행으로 인한 퇴출은 명령의 길이를 기록하는 것 등이 있다.
완전한 VM Exit 정보 영역은 아래 표와 같이 표시된다.
| 분류 | 항목 |
| Base VM-Exit information | Exit reason, Exit qualification, Guest-linear address, Guest-physical address |
| VM Exits Due to Vectored Events | VM-exit interruption information, VM-exit interruption error code |
| VM Exits That Occur During Event Delivery | IDT-vectoring information, IDT-vectoring error code |
| VM Exits Due to instruction Execution | VM-exiting instruction length, VM-exiting instruction information, I/O RCX, I/O RSI, I/O RDI, I/O RIP |
| VM-instruction error field | (단일 필드) |
2) x86 가상화의 장애와 VMX 동작
x86 아키텍처 CPU 가상화
Gerald J. Popek과 Robert P. Goldberg가 1974년에 발표한 논문 《Formal Requirements for Virtualizable Third Generation Architectures》에서 가상화의 세 가지 조건을 제시했다.

(1) 등가성
즉 VMM은 호스트 머신 위에 본질적으로 물리 머신과 일치하는 환경을 시뮬레이션해 내야 한다.
가상 머신이 이 환경에서 실행되는 것과 물리 머신에서 실행되는 게 별 차이가 없어야 한다.
단, 자원 경쟁이나 VMM의 간섭으로 인해 가상 환경에서 약간의 차이를 보이는 건 제외다.
예를 들면 가상 머신의 IO, 네트워크 등이 호스트 머신의 제한으로 인해 또는 여러 가상 머신이 자원을 공유함으로 인해 속도가 단독으로 물리 머신을 차지하는 것보다 좀 느릴 수 있다.
(2) 효율성
즉 가상 머신 명령 실행의 성능이 물리 머신에서 실행될 때의 성능과 비교해서 명백한 손실이 없어야 한다.
이 기준은 가상 머신 안의 대부분 명령이 VMM의 간섭 없이 직접 물리 CPU 위에서 실행될 것을 요구한다.
예를 들면 x86 아키텍처에서 QEMU를 통해 실행되는 ARM은 가상화가 아니라 시뮬레이션이다.
(3) 자원 제어
즉 VMM은 자원을 완전히 통제할 수 있어야 한다.
VMM이 호스트 머신 자원을 각 가상 머신에 협조 분배하지, 가상 머신이 호스트 머신의 자원을 통제할 수는 없다.
Gerald J. Popek과 Robert P. Goldberg가 제시한 가상화 세 가지 조건을 만족시키기 위해, 전형적인 해결 방안이 트랩과 시뮬레이션(Trap and Emulate) 모델이다.
일반적으로 프로세서는 두 종류 실행 모드로 나뉜다.
시스템 모드와 사용자 모드.
그에 상응해서 CPU 명령도 특권 명령과 비특권 명령으로 나뉜다.
특권 명령은 시스템 모드에서만 실행될 수 있고, 만약 사용자 모드에서 실행되면 프로세서 이상이 발생한다. 운영체제는 커널이 시스템 모드에서 실행되는 걸 허용한다. 커널이 시스템 자원을 관리해야 하기 때문에 특권 명령을 실행해야 한다. 그리고 보통의 사용자 프로그램은 사용자 모드에서 실행된다.
트랩-시뮬레이션 모델 아래서, 가상 머신의 사용자 프로그램은 여전히 사용자 모드에서 실행되지만, 가상 머신의 내핵도 사용자 모드에서 실행될 거다. 이런 방식은 특권 등급 압축(Ring Compression) 이라고 부른다.

특권 등급 압축 방식 아래서, 가상 머신 안의 비특권 명령은 프로세서 위에서 직접 실행되어 가상화 표준의 효율성 요구를 만족시킨다.
즉 대부분의 명령은 VMM의 간섭 없이 직접 프로세서 위에서 실행된다. 하지만 가상 머신이 특권 명령을 실행할 때는 사용자 모드에서 실행되기 때문에 프로세서 이상을 일으키고, 이로 인해 VMM에 빠지게 된다.
VMM이 대신 가상 머신을 대신해 시스템 자원에 대한 접근을 완수하는데, 이게 바로 말하는 시뮬레이션(emulate)이다.
이렇게 (이 메커니즘 아래) 또 가상화 표준의 VMM이 시스템 자원을 통제한다는 요구를 만족시켰다. 가상 머신은 특권 명령을 직접 실행해서 호스트 머신의 자원을 수정할 수 없고, 따라서 호스트 머신의 환경을 파괴할 수 없다.
x86 아키텍처 가상화의 장애
Gerald J. Popek과 Robert P. Goldberg는 시스템 자원을 수정하거나, 또는 다른 모드에서 행위가 다르게 나타나는 명령은 모두 민감 명령에 속한다고 지적했다.
가상화 시나리오에서 VMM은 이런 민감 명령을 모니터링해야 한다. 가상화를 지원하는 체계 구조(아키텍처)의 민감 명령은 다 특권 명령에 속하고, 즉 비특권 등급에서 이런 민감 명령을 실행할 때 CPU가 이상을 던져서 VMM의 이상 처리 함수로 진입하게 되고, 그래서 VM이 민감 자원에 접근하는 걸 통제하는 목적을 실현한다.
그런데 x86 아키텍처는 마침 이 표준을 만족시킬 수 없다. x86 아키텍처에서는 모든 민감 명령이 다 특권 명령은 아니고, 일부 민감 명령은 비특권 모드에서 실행될 때 이상을 던지지 않는다.

이때 VMM은 VM의 행위를 잡아서 처리할 수가 없게 된다.
FLAGS 레지스터의 IF (Interrupt Flag)를 수정하는 걸 예로 들어보자.
- 우선, pushf 명령을 사용해서 FLAGS 레지스터의 내용을 스택에 밀어 넣는다.
- 그러고 나서, 스택 꼭대기의 IF를 0으로 만든다.
- 마지막으로, popf를 사용해서 스택에서 FLAGS 레지스터를 복원한다.
만약 가상 머신 커널이 ring 0에서 실행되지 않으면, x86의 CPU는 이상을 던지지 않고, 그저 묵묵히 popf 명령을 무시할 거다. 그래서 가상 머신이 IF를 끄려는 목적은 실효성을 거두지 못한다.

어떤 사람은 반가상화(paravirtualization)의 해결 방안을 제시했는데, 즉 Guest(클라이언트)의 코드를 수정하는 거였다.
하지만 이건 가상화의 투명성 준칙에 부합하지 않는다.
나중에 사람들은 또 이진 번역의 방안을 제시했는데, 정적 번역과 동적 번역을 포함한다.
그중 정적 번역은 실행 전에 전체 실행 가능 파일을 스캔해서 민감 명령에 대해 번역을 진행하고, 새로운 파일을 형성하는 거다.
하지만 정적 번역은 반드시 미리 처리해야 하고, 일부 명령은 실행 시에만 발생하는 부작용에 대해서는 정적 번역으로 처리할 수가 없다.
그래서 동적 번역이 자연스럽게 생겨났는데, 즉 실행 시에 코드 블록을 단위로 동적으로 이진 코드를 수정하는 거다. 동적 번역은 많은 VMM에서 응용을 얻었고, 최적화 효과도 매우 괜찮다.

VMX - Shadow 페이지 테이블과 EPT
비록 많은 사람이 소프트웨어 측면에서 다양한 방안을 채택해서 x86 아키텍처가 가상화 시 부딪히는 문제를 해결하려고 시도했지만, 이런 해결 방안들은 추가 오버헤드를 도입하는 것 외에도 VMM 구현에 큰 복잡성을 가져왔다.
그래서 Intel은 하드웨어 측면에서 이 문제를 해결하려고 시도했다. Intel은 그 비특권 민감 명령들을 특권 명령으로 수정하지는 않았다. 모든 특권 명령이 다 차단 처리가 필요한 건 아니기 때문이다.
전형적인 사례를 하나 들어보자. 운영체제 커널이 프로세스를 전환할 때마다 cr3 레지스터를 전환해서, 현재 실행 중인 프로세스의 페이지 테이블을 가리키게 한다.
그런데 shadow 페이지 테이블을 사용해서 GVA(Guest Virtual Address)에서 HPA(Host Physical Address)로의 매핑을 진행할 때, VMM 모듈은 Guest가 매번 cr3 레지스터를 설정하는 동작(조작)을 잡아서, 그게 shadow 페이지 테이블을 가리키게 해야 한다.

그리고 하드웨어 측면의 EPT 지원이 활성화되면, cr3 레지스터는 더 이상 shadow 페이지 테이블을 가리킬 필요가 없고, 여전히 Guest의 프로세스의 페이지 테이블을 가리킨다.
그래서 VMM이 더 이상 Guest의 cr3 레지스터 설정 동작을 잡을 필요가 없게 되었다.
즉, cr3 레지스터에 쓰는 게 특권 동작이긴 하지만, 이 동작은 VMM에 빠질 필요가 없다.

Intel은 가상화를 지원하기 위해 VT 기술을 개발해서, CPU에 Virtual-Machine eXtensions, 줄여서 VMX를 추가했다.
CPU의 VMX 지원이 활성화되면, CPU는 두 종류 실행 모드를 제공한다.
- VMX Root Mode (VMX 루트 모드)
- VMX non-Root Mode (VMX 비루트 모드)
각 모드는 다 ring 0 ~ ring 3을 지원한다. VMM은 VMX Root Mode에서 실행되는데, VMX를 지원하는 것 외에 VMX Root Mode와 보통의 모드 사이에는 본질적인 차이가 없다.
VM은 VMX non-Root Mode에서 실행되고, Guest는 더 이상 특권 등급 압축 방식을 채택할 필요가 없다.
Guest kernel은 VMX non-Root Mode의 ring 0에서 직접 실행될 수 있다. VMX 실행 모드는 아래 그림과 같다.

VMX - VM Entry/Exit 흐름
CPU는 두 종류 실행 모드를 제공한다.
- VMX Root Mode (VMX 루트 모드)
- VMX non-Root Mode (VMX 비루트 모드).
각 모드는 다 ring 0 ~ ring 3을 지원한다.
VMM은 VMX Root Mode에서 실행되고, VMX를 지원하는 것 외에 VMX Root Mode와 보통의 모드 사이에는 본질적인 차이가 없다.
VM은 VMX non-Root Mode에서 실행되고, Guest는 더 이상 특권 등급 압축 방식을 채택할 필요가 없다.
Guest kernel은 VMX non-Root Mode의 ring 0에서 직접 실행될 수 있다.
- VMX Root Mode에 있는 VMM은 CPU가 제공하는 가상화 명령 VMLaunch를 실행해서 VMX non-Root Mode로 전환할 수 있다. 이 과정이 Guest에 진입하는 것에 상당하기 때문에, 이걸 보통 VM entry라고 부른다.
- Guest 내부에서 민감 명령(예를 들면 일부 I/O 동작 같은 거)을 실행하면, CPU가 트랩에 빠지는 동작을 일으키고, VMX non-Root Mode에서 VMX Root Mode로 다시 전환된다. 이 과정은 VM을 퇴출하는 것에 상당하기 때문에, VM exit라고도 부른다.
- 그러고 나서 VMM은 Guest의 동작에 대한 시뮬레이션을 진행한다.

Guest의 커널도 사용자 모드(ring 1 ~ ring 3)에서 실행되는 방식과 비교했을 때, VMX를 지원하는 CPU는 다음 세 가지 차이점이 있다:
- Guest 모드에서 실행될 때, Guest 사용자 공간의 시스템 호출은 직접 Guest 모드의 커널 공간에 빠지지, Host 모드의 커널 공간에 빠지지 않는다.
- 외부 인터럽트에 대해서는, VMM의 자원에 의해 협조되어야 하기 때문에, Guest 모드의 CPU가 외부 인터럽트를 받으면 CPU가 Guest 모드에서 Host 모드로 퇴출하는 걸 유발한다. Host 커널이 외부 인터럽트를 처리한다. 처리 완료 후 다시 Guest 모드로 전환한다. I/O 효율을 높이기 위해 Intel은 인터럽트 passthrough 모드도 지원한다. 이 모드 아래서는 Guest가 반드시 VM exit를 발생시키지는 않는다.
- 모든 특권 명령이 다 Guest 모드의 CPU에서 VM exit를 일으키는 건 아니다. 민감 명령을 실행할 때만 CPU가 Guest 모드에서 Host 모드로 빠지는 걸 일으킨다. 일부 특권 명령은 VMM의 개입 처리가 필요 없기 때문이다.

VMX - VMCS 데이터 구조와 KVM 코드
CPU 하나는 시간을 나눠서 여러 임무를 실행할 수 있다. 각 임무는 자기만의 컨텍스트가 있고, 스케줄러가 스케줄링할 때 컨텍스트를 전환해서 CPU 하나가 여러 임무를 동시에 실행하는 걸 구현한다.
이와 유사하게, 가상화 시나리오에서 같은 물리 CPU "한 사람이 여러 역할을 분장하는" 식으로 시간을 나눠서 Host와 Guest를 실행하는데, 서로 다른 모드 사이에서 필요에 따라 전환되기 때문에, 다른 모드도 자기 컨텍스트를 저장해야 한다. 이를 위해 VMX는 컨텍스트를 저장하는 데이터 구조 하나를 설계했다.(VMCS)
VM Entry를 통해, VMM은 어떤 VM 하나를 실행 상태로 진입시킬 수 있다. 처음으로 VM에 진입할 때는 VMLAUNCH 명령으로 시작한다.
각 VM은 다 대응되는 가상 머신 제어 구조(Virtual Machine Control Structure, VMCS) 영역 하나와 대응되는데, 해당 VM의 관련 정보를 저장하는 데 쓰인다. 그래서 VMLAUNCH를 진행하기 전에 미리 VMCS에 대한 분배와 초기화를 해야 한다.
VMCS가 VCPU에 대해 가지는 작용은 프로세스 디스크립터가 프로세스에 대해 가지는 작용과 비슷하다. 전통적으로 운영체제의 프로세스들은 물리 CPU 자원을 공유한다.
운영체제는 여러 프로세스 사이에 CPU를 분배하는 것을 책임지고, 각 프로세스는 다 프로세스 디스크립터로 프로세스의 정보를 저장하고, 프로세스 전환 시에 하드웨어 컨텍스트를 보존해서, 프로세스가 다음번에 스케줄링될 때 정상적으로 실행될 수 있게 한다.
마찬가지로 VCPU들도 물리 CPU를 공유하고, VMM은 여러 VCPU 사이에 물리 CPU를 분배하는 것을 책임지고, 각 VCPU는 다 자기만의 디스크립터를 가진다. VMM이 VCPU 실행을 전환할 때 이 순간의 VCPU 상태를 저장해야 하고, 그래야 다음번 VCPU 스케줄링 때 VCPU가 중단됐던 그 지점부터 시작해서 정상적으로 실행될 수 있다.
각 Guest는 다 VMCS 인스턴스 하나를 가진다. 물리 CPU가 다른 VMCS를 로드하면 다른 Guest를 실행한다.

VMCS에는 주로 두 종류 데이터가 저장되어 있다.
하나는 상태인데, Host의 상태와 Guest의 상태를 포함한다.
다른 하나는 Guest 실행 시의 행위를 제어한다.
(1) Guest-state area (가상 머신 상태 저장 영역)
Guest의 CPU 상태를 보관한다. VM Exit 때 Guest 상태가 이 영역에 저장되고, VM Entry 때 이 상태들이 CPU로 로딩된다. 모두 하드웨어가 자동으로 하는 동작이라 VMM이 코드로 간섭할 필요가 없다.
| 레지스터 | 저장되는 내용 |
| CR0, CR3, CR4, DR7 | 값 |
| RSP, RIP, RFLAGS | 값 |
| CS, SS, DS, ES, FS, GS | Selector / Base Address / Segment Limit / Access Right |
(2) Host-state area (호스트 머신 상태 저장 영역)
Host(VMM)의 CPU 상태를 보관한다. VM Exit가 발생할 때 VMM의 컨텍스트로 전환해서 실행해야 하는데, 이때 프로세서의 상태 정보가 이 영역에서 로드된다.
VM Entry 때 CPU가 자동으로 Host 상태를 이 영역에 저장하고, VM Exit 때 CPU가 자동으로 이 영역의 Host 상태를 물리 CPU로 복원한다.
| 레지스터 | 저장되는 내용 |
| CR0, CR3, CR4 | 값 |
| RSP, RIP | 값 |
| CS, SS, DS, ES | Selector |
| FS, GS, TR | Selector / Base Address |
| GDTR, IDTR | Base Address |
(3) VM-exit information fields (VM Exit 정보 영역)
VM Exit가 일어나면 VMM은 그 원인을 알아야 "원인에 맞는 처리"를 해서 알맞은 시뮬레이션 동작을 할 수 있다. 그래서 CPU가 Exit 원인과 관련 데이터(예: 명령 실행으로 인한 Exit이면 그 명령의 길이 등)를 이 영역에 자동으로 기록해 VMM에게 제공한다. 가장 최근 VM Exit 정보가 담긴다.
| 분류 | 항목 |
| Base VM-Exit information | Exit reason, Exit qualification, Guest-linear address, Guest-physical address |
| Vectored Event로 인한 Exit | VM-exit interruption information, VM-exit interruption error code |
| 이벤트 전달 중 발생한 Exit | IDT-vectoring information, IDT-vectoring error code |
| 명령 실행으로 인한 Exit | VM-exiting instruction length, VM-exiting instruction information, I/O RCX, I/O RSI, I/O RDI, I/O RIP |
| (단일 필드) | VM-instruction error field |
(4) VM-execution control fields (VM 실행 제어 영역)
가상 머신 실행 시의 프로세서 동작을 제어하는 필드들이다. 매우 방대하며, 예를 들면:
- Guest가 CR3 레지스터에 접근할 때 VM Exit를 유발할지 여부
- 어떤 이벤트/예외가 VM Exit를 일으킬지
- APIC 가상화 제어
등 다양한 제어를 포함한다. 이와 짝이 되는 영역으로 VM Entry 동작을 제어하는 VM-entry control fields, VM Exit 동작을 제어하는 VM-exit control fields도 있다.
VMCS와 VCPU의 관계
VCPU를 생성할 때 KVM 모듈은 각 VCPU마다 VMCS를 하나씩 할당한다.
CPU가 Guest 모드로 전환할 준비를 할 때마다 VMCS 포인터를 설정해, 곧 진입할 Guest에 대응하는 VMCS 인스턴스를 가리키게 한다. 관련 코드는 Linux 커널 소스의 arch/x86/kvm/vmx/vmx.c에 있다(커널 6.7 기준).
void vmx_vcpu_load_vmcs(struct kvm_vcpu *vcpu, int cpu,
struct loaded_vmcs *buddy)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
bool already_loaded = vmx->loaded_vmcs->cpu == cpu;
struct vmcs *prev;
if (!already_loaded) {
loaded_vmcs_clear(vmx->loaded_vmcs);
local_irq_disable();
/*
* Ensure loaded_vmcs->cpu is read before adding loaded_vmcs to
* this cpu's percpu list, otherwise it may not yet be deleted
* from its previous cpu's percpu list. Pairs with the
* smb_wmb() in __loaded_vmcs_clear().
*/
smp_rmb();
list_add(&vmx->loaded_vmcs->loaded_vmcss_on_cpu_link,
&per_cpu(loaded_vmcss_on_cpu, cpu));
local_irq_enable();
}
prev = per_cpu(current_vmcs, cpu);
if (prev != vmx->loaded_vmcs->vmcs) {
per_cpu(current_vmcs, cpu) = vmx->loaded_vmcs->vmcs;
vmcs_load(vmx->loaded_vmcs->vmcs);
/*
* No indirect branch prediction barrier needed when switching
* the active VMCS within a vCPU, unless IBRS is advertised to
* the vCPU. To minimize the number of IBPBs executed, KVM
* performs IBPB on nested VM-Exit (a single nested transition
* may switch the active VMCS multiple times).
*/
if (!buddy || WARN_ON_ONCE(buddy->vmcs != prev))
indirect_branch_prediction_barrier();
}
if (!already_loaded) {
void *gdt = get_current_gdt_ro();
/*
* Flush all EPTP/VPID contexts, the new pCPU may have stale
* TLB entries from its previous association with the vCPU.
*/
kvm_make_request(KVM_REQ_TLB_FLUSH, vcpu);
/*
* Linux uses per-cpu TSS and GDT, so set these when switching
* processors. See 22.2.4.
*/
vmcs_writel(HOST_TR_BASE,
(unsigned long)&get_cpu_entry_area(cpu)->tss.x86_tss);
vmcs_writel(HOST_GDTR_BASE, (unsigned long)gdt); /* 22.2.4 */
if (IS_ENABLED(CONFIG_IA32_EMULATION) || IS_ENABLED(CONFIG_X86_32)) {
/* 22.2.3 */
vmcs_writel(HOST_IA32_SYSENTER_ESP,
(unsigned long)(cpu_entry_stack(cpu) + 1));
}
vmx->loaded_vmcs->cpu = cpu;
}
}
/*
* Switches to specified vcpu, until a matching vcpu_put(), but assumes
* vcpu mutex is already taken.
*/
static void vmx_vcpu_load(struct kvm_vcpu *vcpu, int cpu)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
vmx_vcpu_load_vmcs(vcpu, cpu, NULL);
vmx_vcpu_pi_load(vcpu, cpu);
vmx->host_debugctlmsr = get_debugctlmsr();
}
모든 상태가 다 CPU에 의해 자동으로 저장되고 복원되는 건 아니다. 효율도 고려해야 한다.
cr2 레지스터를 예로 들면, 대부분의 경우 Guest가 Host로 퇴출했다가 다시 Guest로 들어가는 기간에 Host는 cr2 레지스터의 값을 바꾸지 않을 거다. 그런데 cr2에 쓰는 오버헤드는 매우 크다. 만약 매번 VM entry 시 한 번씩 cr2를 갱신한다면, CPU의 연산력을 낭비하는 것 외에 아무 의미도 없다. 그래서 이런 상태들은 VMM에게 넘겨서 소프트웨어로 알아서 제어하는 게 더 합리적이다.
3) VCPU 생명 주기
각 가상 프로세서(VCPU)에 대해, VMM은 하나의 스레드를 사용해서 VCPU라는 이 엔티티를 대표한다.
Guest가 운행되는 과정에서, 각 VCPU는 기본적으로 아래 그림에 나타난 상태에서 끊임없이 전환된다.

(1) 유저 공간에서 준비가 끝난 후, VCPU가 위치한 스레드가 (Linux) 커널의 KVM 모듈을 향해 ioctl 요청 KVM_RUN을 보낸다. 커널의 KVM 모듈에게 알려주는 거다, 유저 공간의 조작은 이미 완성됐고, Guest 모드로 전환해서 Guest를 실행할 수 있다고.
(2) 커널 모드로 진입한 후, KVM 모듈은 CPU가 제공하는 가상화 명령을 호출해서 Guest 모드로 전환한다. 만약 처음 Guest를 실행하는 거라면, VMLaunch 명령을 사용하고, 그렇지 않으면 VMResume 명령을 사용한다.

이 전환 과정에서
- 먼저 현재 CPU 상태(= Host 상태)를 VMCS의 Host-state 영역에 저장한다. CPU가 자동 저장하지 않는 것은 KVM이 저장한다.
- 그다음 VMCS의 Guest-state 영역에 저장돼 있던 Guest 상태를 물리 CPU로 복원(로딩)한다. CPU가 자동 복원하지 않는 것은 KVM이 복원한다.
(3) Guest 실행 → 민감 명령에서 빠져나옴 (VM Exit)
물리 CPU가 Guest 모드로 Guest 명령을 실행하다가 민감한 명령을 만나면, Guest 모드에서 Host 모드의 ring0(커널)으로 되돌아가 KVM 모듈로 진입한다.

이 전환 과정에서
- 먼저 현재 CPU 상태(= Guest 상태)를 VMCS의 Guest-state 영역에 저장한다.
- 그다음 VMCS의 Host-state 영역에 있던 Host 상태를 물리 CPU로 복원한다. (CPU가 자동 처리 안 하는 부분은 KVM이 담당)
(4) 커널에서 종료 원인 처리 (경량 Exit) KVM 모듈이 VMCS에서 VM Exit 원인을 읽고 커널 안에서 처리를 시도한다.
커널에서 처리 가능하면 유저 모드로 나갈 필요 없이, 처리 후 곧바로 Guest로 다시 전환한다. 이런 종료를 경량(lightweight) VM Exit이라 부른다.
(5) 커널이 처리 못 하면 유저 공간으로 KVM이 처리할 수 없는 종료면, VCPU가 한 번 더 컨텍스트 전환을 해서 Host 커널 모드 → Host 유저 모드로 나가고, VMM의 유저 공간 부분(QEMU)이 처리한다.
(6) 다시 Guest로 유저 공간 처리가 끝나면 다시 Guest 모드로 전환하는 명령을 실행한다.
전체 실행 과정에서 1~6단계가 순환 반복된다.
while (1)
{
ret = ioctl(vcpufd, KVM_RUN, NULL);
if (ret == -1)
{
printf("exit unknown\n");
return -1;
}
switch (run->exit_reason)
{
case KVM_EXIT_HLT:
puts("KVM_EXIT_HLT");
return 0;
case KVM_EXIT_IO:
putchar( ((char *)run) + run->io.data_offset));
break;
case KVM_EXIT_FAIL_ENTRY:
puts("entry error");
return -1;
default:
puts("other error");
printf("exit_reason: %d\n", run->exit_reason);
return -1;
}
}
4) KVM 개요와 소스 코드 조직
KVM 소스 코드 조직 구조
KVM의 전체 명칭은 Kernel-Based Virtual Machine이다.
KVM은 이스라엘 초기 창업 회사 Qumranet이 CPU 하드웨어 가상화 이후 개발한 커널 기반의 가상 머신 모니터다.
KVM의 아키텍처는 간단명료하고, 커널의 여러 기능을 충분히 재사용해서, 빠르게 커널로 진입할 수 있었다. Red Hat이 Qumranet을 인수한 뒤로, KVM은 더 큰 지지와 발전을 얻었다.
KVM이 Linux 커널 트리 안의 코드 조직(KVM 소스 코드 디렉토리 구조)은 아래 그림과 같다

KVM은 가상화의 통칭 방안이라서, x86 외에 arm 등 다른 아키텍처도 자기들 방안이 있다.
그래서 KVM의 주체 코드는 Linux 커널 트리 virt/kvm 디렉토리 아래에 위치한다.
이건 모든 CPU 아키텍처의 공용 코드를 나타내고, 이것이 바로 커널 kvm.ko에 대응하는 소스 코드이기도 하다.
CPU 체계 구조 관련 코드는 arch 디렉토리 아래에 위치한다. 예를 들면 x86 아키텍처 관련 코드는 arch/x86/kvm/ 아래에 있다.
당연히, 같은 아키텍처라도 여러 다른 구현이 있을 수 있다. 예를 들면, KVM은 Intel과 AMD 두 회사의 구현이 있다(당연히, 실제로는 이 두 회사뿐만이 아니다).
그래서 x86 디렉토리 아래에는 여러 구현 코드가 있다. 예를 들면 Intel의 vmx 디렉토리(Intel VM-X 방안에 대응)와 AMD의 svm 디렉토리(AMD-V 방안에 대응) 같은 거다. ioapic.c와 lapic.c는 인터럽트 컨트롤러의 코드이고, 이건 또한 intel-kvm.ko와 amd-kvm.ko의 출처이기도 하다.
KVM의 모든 가상화 구현(Intel과 AMD)은 모두 KVM 모듈을 향해 하나의 kvm_x86_ops 구조를 등록한다. 이렇게 하면, KVM 안의 일부 함수는 그냥 외관일 뿐이고, 그것이 가장 먼저 kvm_arch_xxx 함수를 호출한다. 이건 CPU 아키텍처 관련 함수를 호출하는 걸 나타낸다.
그리고 만약 kvm_arch_xxx 함수가 구현 관련 코드를 호출해야 한다면, 그러면 kvm_x86_ops 구조 안의 각 관련 콜백 함수를 호출한다. 아래에서는 x86 아키텍처 중 Intel 구현을 예로 들어서 분석한다.
KVM을 사용하려면 kvm.ko와 kvm-intel.ko 두 커널 모듈을 로딩해야 한다.
- kvm.ko
kvm.ko는 KVM의 통용 코드로 생성된 거다. kvm.ko 초기화 코드는 아무 일도 하지 않는다. 그냥 코드를 메모리에 로딩하는 것과 같을 뿐이다.

- kvm-intel.ko
kvm-intel.ko 모듈은 Intel CPU 아키텍처 관련 코드로 생성된 거다. KVM의 개시와 종료는 모두 kvm-intel.ko로 완성된다.
KVM 초기화 완료 후, 유저 공간에게 KVM의 인터페이스를 제시한다. 이 인터페이스들은 모두 kvm.ko가 내보내는 거다.
유저 공간 프로그램이 이 인터페이스들을 호출할 때, kvm.ko 안의 통용 코드는 거꾸로 kvm-intel.ko 아키텍처 안의 관련 코드를 호출한다. 호출 관계는 아래 그림과 같다.

KVM 모듈 초기화 (이론 편)
KVM 모듈의 초기화는 주로 초기화 CPU와 아키텍처와 무관한 데이터, 그리고 아키텍처 관련의 가상화 지지를 설정하는 걸 포함한다.
Intel CPU를 예로 들면, Intel Software Developer Manuals (Intel SDM) 31.5에서는 VMM을 개시하고 종료하는 단계를 제시한다. VMM은 CPU가 보호 모드에 있고 페이징을 개시한 상황에서만, 비로소 VMX 모드로 진입할 수 있다.
아래에서 VMX 모드를 개시하는 데 해야 할 일을 간단히 정리해본다.
- CPUID를 사용해서 CPU가 VMX를 지원하는지 검사한다. 만약 CPUID.1:ECX.VMX[bit 5] = 1이면, CPU가 VMX를 지원한다는 걸 나타낸다.
- CPU가 지원하는 VMX의 성능을 검사한다. 이건 VMX 성능과 관련된 MSR 레지스터를 통해서 완성된다. 전형적인 레지스터는 기본 VMX 성능 정보를 나타내는 IA32_VMX_BASIC, 그리고 VMCS 영역 중 VM-execution 관련 영역이 설정할 수 있는 값을 나타내는 IA32_VMX_PINBASED_CTLS와 IA32_VMX_PROCBASD_CTLS를 포함한다.
- 4KB 정렬의 메모리 한 블록을 VMXON 영역으로 분배한다. 그 크기는 IA32_VMX_BASICMSR 레지스터를 읽어서 얻을 수 있다.
- VMXON 영역의 버전 식별자를 초기화한다. 이 식별자도 MSR 레지스터를 통해 제공된다.
- 현재 CPU 동작 모드의 CR0 레지스터가 VMX 진입 조건에 부합하는지 확인한다. 예를 들면 CR0.PE=1, CR0.PG=1 같은 것이다. CR0가 만족해야 하는 설정은 IA32_VMX_CR0_FIXED0과 IA32_VMX_CR0_FIXED1 레지스터로 제공된다.
- CR4.VMXE를 1로 설정해 VMX 모드를 활성화한다. 그 외 CR4가 만족해야 하는 설정은 IA32_VMX_CR4_FIXED0과 IA32_VMX_CR4_FIXED1 레지스터로 제공된다.
- IA32_FEATURE_CONTROL 레지스터가 올바르게 설정됐는지 확인한다. 그 **잠금 비트(bit 0)**가 1이어야 한다. 이 MSR은 보통 BIOS가 설정한다.
- VMXON 영역의 물리 주소를 피연산자로 하여 VMXON 명령을 실행한다. 실행이 끝난 뒤 RFLAGS.CF=0이면 VMXON 명령이 성공했다는 뜻이다.
VMX 모드로 진입한 후, VMX root의 CPL=0일 때 VMXOFF를 실행한다. RFLAGS.CF와 RFLAGS.ZF가 모두 0이면 CPU가 VMX 모드를 닫았다는 걸 나타낸다.
KVM 모듈 초기화 (코드 편)
KVM 모듈의 초기화는 아키텍처와 무관한 부분을 완성해야 하고, 또 지난번에 언급한 아키텍처 관련 데이터도 설정해야 한다.
intel-kvm.ko의 모듈 등록 함수는 vmx_init이다. 이 함수는 Linux 커널 소스 코드 arch/x86/kvm/vmx/vmx.c 안에 있고, 코드는 아래와 같다.
static int __init vmx_init(void)
{
int r, cpu;
if (!kvm_is_vmx_supported())
return -EOPNOTSUPP;
/*
* Note, hv_init_evmcs() touches only VMX knobs, i.e. there's nothing
* to unwind if a later step fails.
*/
hv_init_evmcs();
r = kvm_x86_vendor_init(&vt_init_ops);
if (r)
return r;
/*
* Must be called after common x86 init so enable_ept is properly set
* up. Hand the parameter mitigation value in which was stored in
* the pre module init parser. If no parameter was given, it will
* contain 'auto' which will be turned into the default 'cond'
* mitigation mode.
*/
r = vmx_setup_l1d_flush(vmentry_l1d_flush_param);
if (r) {
goto err_l1d_flush;
for_each_possible_cpu(cpu) {
INIT_LIST_HEAD(&per_cpu(loaded_vmcss_on_cpu, cpu));
pi_init_cpu(cpu);
}
cpu_emergency_register_virt_callback(vmx_emergency_disable);
vmx_check_vmcs12_offsets();
/*
* Shadow paging doesn't have a (further) performance penalty
* from GUEST_MAXPHYADDR < HOST_MAXPHYADDR so enable it
* by default
*/
if (!enable_ept)
allow_smaller_maxphyaddr = true;
/*
* Common KVM initialization _must_ come last, after this, /dev/kvm is
* exposed to userspace!
*/
r = kvm_init(sizeof(struct vcpu_vmx), __alignof__(struct vcpu_vmx),
THIS_MODULE);
if (r)
goto err_kvm_init;
return 0;
err_kvm_init:
__vmx_exit();
err_l1d_flush:
kvm_x86_vendor_exit();
return r;
}
module_init(vmx_init);
그것과 대응해서, intel-kvm.ko의 모듈 등록 취소 함수는 vmx_exit다. 이 함수는 당연히 Linux 커널 소스 코드 arch/x86/kvm/vmx/vmx.c 안에 있고, 코드는 아래와 같다.
static void vmx_exit(void)
{
kvm_exit();
kvm_x86_vendor_exit();
__vmx_exit();
}
module_exit(vmx_exit);
vmx_init 함수로 돌아가보자. vmx_init 함수 중에서는 kvm_init 함수를 호출한다. 아래와 같다.
/*
* Common KVM initialization _must_ come last, after this, /dev/kvm is
* exposed to userspace!
*/
r = kvm_init(sizeof(struct vcpu_vmx), __alignof__(struct vcpu_vmx),
THIS_MODULE);
if (r)
goto err_kvm_init;
위 KVM(즉 Linux 커널)의 버전은 6.10.1이다. 옛 버전의 커널 KVM 코드에서, kvm_init 함수의 첫 번째 파라미터는 &vmx_x86_ops였는데, 이 신규 버전 KVM 코드에서는 이미 제거됐다.
@@ -6,8 +6,7 @@
#include "vmcs12.h"
#include "vmx.h"
#include "x86.h"
+#include "x86_ops.h"
#include "smm.h"
#include "vmx_onhyperv.h"
@@ -528,8 +529,6 @@ static inline void vmx_segment_cache_clear(struct vcpu_vmx *vmx)
static unsigned long host_idt_base;
#if IS_ENABLED(CONFIG_HYPERV)
-static struct kvm_x86_ops vmx_x86_ops __initdata;
-
static bool __read_mostly enlightened_vmcs = true;
module_param(enlightened_vmcs, bool, 0444);
-static struct kvm_x86_ops vmx_x86_ops __initdata = {
- .name = KBUILD_MODNAME,
-
- .check_processor_compatibility = vmx_check_processor_compat,
-
- .hardware_unsetup = vmx_hardware_unsetup,
-
- .hardware_enable = vmx_hardware_enable,
- .hardware_disable = vmx_hardware_disable,
- .has_emulated_msr = vmx_has_emulated_msr,
-
- .vm_size = sizeof(struct kvm_vmx),
- .vm_init = vmx_vm_init,
- .vm_destroy = vmx_vm_destroy,
-
- .vcpu_precreate = vmx_vcpu_precreate,
- .vcpu_create = vmx_vcpu_create,
- .vcpu_free = vmx_vcpu_free,
- .vcpu_reset = vmx_vcpu_reset,
-
- .prepare_switch_to_guest = vmx_prepare_switch_to_guest,
- .vcpu_load = vmx_vcpu_load,
- .vcpu_put = vmx_vcpu_put,
-
- .update_exception_bitmap = vmx_update_exception_bitmap,
- .get_msr_feature = vmx_get_msr_feature,
- .get_msr = vmx_get_msr,
- .set_msr = vmx_set_msr,
- .get_segment_base = vmx_get_segment_base,
- .get_segment = vmx_get_segment,
- r = kvm_x86_vendor_init(&vmx_init_ops);
+ r = kvm_x86_vendor_init(&vt_init_ops);
if (r)
return r;
Title: KVM: VMX: Move out vmx_x86_ops to 'main.c' to wrap VMX and TDX
Body:
KVM accesses Virtual Machine Control Structure (VMCS) with VMX instructions to operate on VM. TDX defines its data structure and TDX SEAMCALL APIs for VMM to operate on Trust Domain (TD) instead. Patchwork
This means we must have a TDX version of kvm_x86_ops. The existing global struct kvm_x86_ops already defines an interface which fits with TDX. But kvm_x86_ops is system-wide, not per-VM structure. To allow VMX to coexist with TDs, the kvm_x86_ops callbacks will have wrappers "if (tdx) tdx_op() else vmx_op()" to switch VMX or TDX at run time. Linux KernelLinux Kernel
To split the runtime switch, the VMX implementation, and the TDX implementation, add main.c, and move out the vmx_x86_ops hooks in preparation for adding TDX, which can coexist with VMX, i.e. KVM can run both VMs and TDs. Use 'vt' for the naming scheme as a nod to VT-x and as a concatenation of VmxTdx.
- VMX는 Intel VT-x(우리가 지금까지 본 그 가상화 기능)이고, TDX(Trust Domain Extensions)는 그 위에 얹은 기밀 컴퓨팅 기술이다. TDX에서는 게스트 메모리/상태를 하이퍼바이저(VMM)조차 들여다보거나 직접 건드리지 못하게 막는다. 그래서 VMM이 VMCS를 직접 만지는 기존 VMX 방식이 안 통하고, SEAMCALL이라는 특수 호출로 보안 모듈(SEAM)을 거쳐 간접적으로만 조작할 수 있다.
- kvm_x86_ops는 KVM이 CPU별 동작(예: VM 진입/종료 처리, 레지스터 접근 등)을 함수 포인터 묶음(콜백 테이블) 으로 추상화해 둔 구조체다. VMX와 TDX는 같은 동작이라도 구현이 다르므로, 호출 시점에 둘 중 맞는 걸 고르도록 if (tdx) ... else ... 래퍼를 끼워 넣겠다는 거다.
- 이 분기 코드와 양쪽 구현을 한 파일에 뒤섞으면 지저분하니, main.c 라는 디스패처 파일을 새로 만들어 "VMX냐 TDX냐 고르는 부분"을 그쪽에 모으고, 함수 포인터 묶음(vmx_x86_ops)도 옮긴다는 정리 작업이다. 접두어 vt (예: vt_x86_ops)는 VT-x를 기리면서 "VMX와 TDX를 합친다"는 뜻을 담은 작명이다.

KVM의 모든 가상화 구현(Intel과 AMD)은 모두 KVM 모듈에 kvm_x86_ops 구조체(인스턴스) 하나를 등록한다.
그리고 vmx_x86_ops (struct kvm_x86_ops vmx_x86_ops)가 나타내는 게 바로 Intel VT-x 구체 구현의 각종 콜백 함수들이다. 이건 아주 거대한 구조이고, 구체 하드웨어 검측, 가상 머신 생성, VCPU의 구현, 일부 레지스터의 설정, 가상 머신 종료의 처리 함수 등을 포함한다.
여기서는 이해를 편리하게 하기 위해, 옛 버전 커널 코드(즉 vmx_x86_ops를 제거하지 않은 이전 코드)를 사용한다. 먼저 첫 번째 단계를 파악하고, 신규 버전에서 언급된 두 번째 단계 TDX를 다시 얘기한다.
처음에는 6.7 버전을 찾았는데, 이미 변경되어 있었다. 어쩔 수 없이,좀 더 이른 Linux 6.1.10 커널 버전을 찾을 수밖에 없었다. 아래와 같이 나타난다.
static int __init vmx_init(void)
{
int r, cpu;
#if IS_ENABLED(CONFIG_HYPERV)
/*
* Enlightened VMCS usage should be recommended and the host needs
* to support eVMCS v1 or above. We can also disable eVMCS support
* with module parameter.
*/
if (enlightened_vmcs &&
ms_hyperv.hints & HV_X64_ENLIGHTENED_VMCS_RECOMMENDED &&
(ms_hyperv.nested_features & HV_X64_ENLIGHTENED_VMCS_VERSION) >=
KVM_EVMCS_VERSION) {
/* Check that we have assist pages on all online CPUs */
for_each_online_cpu(cpu) {
if (!hv_get_vp_assist_page(cpu)) {
enlightened_vmcs = false;
break;
}
}
if (enlightened_vmcs) {
pr_info("KVM: vmx: using Hyper-V Enlightened VMCS\n");
static_branch_enable(&enable_evmcs);
}
if (ms_hyperv.nested_features & HV_X64_NESTED_DIRECT_FLUSH)
vmx_x86_ops.enable_direct_tlbflush
= hv_enable_direct_tlbflush;
} else {
enlightened_vmcs = false;
}
#endif
r = kvm_init(&vmx_init_ops, sizeof(struct vcpu_vmx),
__alignof__(struct vcpu_vmx), THIS_MODULE);
if (r)
return r;
/*
* Must be called after kvm_init() so enable_ept is properly set
* up. Hand the parameter mitigation value in which was stored in
* the pre module init parser. If no parameter was given, it will
* contain 'auto' which will be turned into the default 'cond'
* mitigation mode.
*/
r = vmx_setup_l1d_flush(vmentry_l1d_flush_param);
if (r) {
vmx_exit();
return r;
}
vmx_setup_fb_clear_ctrl();
for_each_possible_cpu(cpu) {
INIT_LIST_HEAD(&per_cpu(loaded_vmcss_on_cpu, cpu));
pi_init_cpu(cpu);
}
#ifdef CONFIG_KEXEC_CORE
rcu_assign_pointer(crash_vmclear_loaded_vmcss,
crash_vmclear_local_loaded_vmcss);
#endif
vmx_check_vmcs12_offsets();
/*
* Shadow paging doesn't have a (further) performance penalty
* from GUEST_MAXPHYADDR < HOST_MAXPHYADDR so enable it
* by default
*/
if (!enable_ept)
allow_smaller_maxphyaddr = true;
return 0;
}
module_init(vmx_init);
전체 흐름 정리


5) KVM 모듈 초기화 (vmx_init · kvm_init · irqfd)
vmx_init 함수와 관련 구조체
KVM 모듈의 초기화 부분을 이해하기 위해선 arch/x86/kvm/vmx/vmx.c 안의 vmx_init 함수를 먼저 봐야한다.
static struct kvm_x86_init_ops vmx_init_ops __initdata = {
.cpu_has_kvm_support = cpu_has_kvm_support,
.disabled_by_bios = vmx_disabled_by_bios,
.check_processor_compatibility = vmx_check_processor_compat,
.hardware_setup = hardware_setup,
.handle_intel_pt_intr = NULL,
.runtime_ops = &vmx_x86_ops,
.pmu_ops = &intel_pmu_ops,
};
앞에서 반복해서 말했듯이, KVM의 모든 가상화 구현 (Intel과 AMD)은 모두 KVM 모듈에 kvm_x86_ops 구조체 (인스턴스)를 등록한다.
이렇게 해서 KVM 안의 일부 함수는 하나의 껍데기가 되는데, 먼저 kvm_arch_xxx 함수를 호출하고, 이게 CPU 아키텍처 관련 콜백 함수를 호출한다는 뜻이다. 만약 kvm_arch_xxx 함수가 구현 관련 코드를 호출해야 한다면, kvm_x86_ops 구조체 안의 관련 콜백 함수를 호출한다.
vmx_init과 kvm_init의 관계

VMX_init()에서 kvm_init 함수를 호출하는 코드
r = kvm_init(&vmx_init_ops, sizeof(struct vcpu_vmx),
__alignof__(struct vcpu_vmx), THIS_MODULE);
if (r)
return r;
사실 이 버전의 Linux 커널 코드도 비교적 새로워서, 가장 초창기처럼 첫 번째 인자로 &vmx_x86_ops를 직접 전달하는 방식이 아니라, 한 겹을 더 감싸서 vmx_x86_ops를 struct kvm_x86_init_ops vmx_init_ops 안에 넣었다 (위 코드 참조).
vmx_x86_ops의 정의와 초기화도 같은 파일 (arch/x86/kvm/vmx/vmx.c) 안에 있다. 아래와 같다.
static struct kvm_x86_ops vmx_x86_ops __initdata = {
.name = "kvm_intel",
.hardware_unsetup = vmx_hardware_unsetup,
.hardware_enable = vmx_hardware_enable,
.hardware_disable = vmx_hardware_disable,
.has_emulated_msr = vmx_has_emulated_msr,
.vm_size = sizeof(struct kvm_vmx),
.vm_init = vmx_vm_init,
.vm_destroy = vmx_vm_destroy,
.vcpu_precreate = vmx_vcpu_precreate,
.vcpu_create = vmx_vcpu_create,
.vcpu_free = vmx_vcpu_free,
.vcpu_reset = vmx_vcpu_reset,
.prepare_switch_to_guest = vmx_prepare_switch_to_guest,
.vcpu_load = vmx_vcpu_load,
.vcpu_put = vmx_vcpu_put,
.update_exception_bitmap = vmx_update_exception_bitmap,
.get_msr_feature = vmx_get_msr_feature,
.get_msr = vmx_get_msr,
.set_msr = vmx_set_msr,
.get_segment_base = vmx_get_segment_base,
.get_segment = vmx_get_segment,
.set_segment = vmx_set_segment,
.get_cpl = vmx_get_cpl,
.get_cs_db_l_bits = vmx_get_cs_db_l_bits,
.set_cr0 = vmx_set_cr0,
.is_valid_cr4 = vmx_is_valid_cr4,
.set_cr4 = vmx_set_cr4,
.set_efer = vmx_set_efer,
.get_idt = vmx_get_idt,
.set_idt = vmx_set_idt,
.get_gdt = vmx_get_gdt,
.set_gdt = vmx_set_gdt,
.set_dr7 = vmx_set_dr7,
.sync_dirty_debug_regs = vmx_sync_dirty_debug_regs,
.cache_reg = vmx_cache_reg,
.get_rflags = vmx_get_rflags,
.set_rflags = vmx_set_rflags,
.get_if_flag = vmx_get_if_flag,
.flush_tlb_all = vmx_flush_tlb_all,
.flush_tlb_current = vmx_flush_tlb_current,
.flush_tlb_gva = vmx_flush_tlb_gva,
.flush_tlb_guest = vmx_flush_tlb_guest,
.vcpu_pre_run = vmx_vcpu_pre_run,
.vcpu_run = vmx_vcpu_run,
.handle_exit = vmx_handle_exit,
.skip_emulated_instruction = vmx_skip_emulated_instruction,
.update_emulated_instruction = vmx_update_emulated_instruction,
.set_interrupt_shadow = vmx_set_interrupt_shadow,
.get_interrupt_shadow = vmx_get_interrupt_shadow,
.patch_hypercall = vmx_patch_hypercall,
.inject_irq = vmx_inject_irq,
.inject_nmi = vmx_inject_nmi,
.inject_exception = vmx_inject_exception,
.cancel_injection = vmx_cancel_injection,
.interrupt_allowed = vmx_interrupt_allowed,
.nmi_allowed = vmx_nmi_allowed,
.get_nmi_mask = vmx_get_nmi_mask,
.set_nmi_mask = vmx_set_nmi_mask,
.enable_nmi_window = vmx_enable_nmi_window,
.enable_irq_window = vmx_enable_irq_window,
.update_cr8_intercept = vmx_update_cr8_intercept,
.set_virtual_apic_mode = vmx_set_virtual_apic_mode,
.set_apic_access_page_addr = vmx_set_apic_access_page_addr,
.refresh_apicv_exec_ctrl = vmx_refresh_apicv_exec_ctrl,
.load_eoi_exitmap = vmx_load_eoi_exitmap,
.apicv_post_state_restore = vmx_apicv_post_state_restore,
.check_apicv_inhibit_reasons = vmx_check_apicv_inhibit_reasons,
.hwapic_irr_update = vmx_hwapic_irr_update,
.hwapic_isr_update = vmx_hwapic_isr_update,
.guest_apic_has_interrupt = vmx_guest_apic_has_interrupt,
.sync_pir_to_irr = vmx_sync_pir_to_irr,
.deliver_interrupt = vmx_deliver_interrupt,
.dy_apicv_has_pending_interrupt = pi_has_pending_interrupt,
.set_tss_addr = vmx_set_tss_addr,
.set_identity_map_addr = vmx_set_identity_map_addr,
.get_mt_mask = vmx_get_mt_mask,
.get_exit_info = vmx_get_exit_info,
.vcpu_after_set_cpuid = vmx_vcpu_after_set_cpuid,
.has_wbinvd_exit = cpu_has_vmx_wbinvd_exit,
.get_l2_tsc_offset = vmx_get_l2_tsc_offset,
.get_l2_tsc_multiplier = vmx_get_l2_tsc_multiplier,
.write_tsc_offset = vmx_write_tsc_offset,
.write_tsc_multiplier = vmx_write_tsc_multiplier,
.load_mmu_pgd = vmx_load_mmu_pgd,
.check_intercept = vmx_check_intercept,
.handle_exit_irqoff = vmx_handle_exit_irqoff,
.request_immediate_exit = vmx_request_immediate_exit,
.sched_in = vmx_sched_in,
.cpu_dirty_log_size = PML_ENTITY_NUM,
.update_cpu_dirty_logging = vmx_update_cpu_dirty_logging,
.nested_ops = &vmx_nested_ops,
.pi_update_irte = vmx_pi_update_irte,
.pi_start_assignment = vmx_pi_start_assignment,
#ifdef CONFIG_X86_64
.set_hv_timer = vmx_set_hv_timer,
.cancel_hv_timer = vmx_cancel_hv_timer,
#endif
.setup_mce = vmx_setup_mce,
.smi_allowed = vmx_smi_allowed,
.enter_smm = vmx_enter_smm,
.leave_smm = vmx_leave_smm,
.enable_smi_window = vmx_enable_smi_window,
.can_emulate_instruction = vmx_can_emulate_instruction,
.apic_init_signal_blocked = vmx_apic_init_signal_blocked,
.migrate_timers = vmx_migrate_timers,
.msr_filter_changed = vmx_msr_filter_changed,
.complete_emulated_msr = kvm_complete_insn_gp,
.vcpu_deliver_sipi_vector = kvm_vcpu_deliver_sipi_vector,
};
struct kvm_x86_ops vmx_x86_ops는 굉장히 큰 구조체인데, 구체적인 하드웨어 검사, 가상머신 생성 VCPU 구현, 일부 레지스터 설정, 가상머신 종료 처리 함수 등을 포함한다.
kvm_init 함수의 두 번째 인자는 sizeof(struct vcpu_vmx)인데, VMX 구현의 VCPU 구조체 크기를 나타낸다. struct vcpu_vmx의 정의는 Linux 커널 소스 코드 arch/x86/kvm/vmx/vmx.h 안에 있는데, 아래와 같다.
struct vcpu_vmx {
struct kvm_vcpu vcpu;
u8 fail;
u8 x2apic_msr_bitmap_mode;
/*
* If true, host state has been stored in vmx->loaded_vmcs for
* the CPU registers that only need to be switched when transitioning
* to/from the kernel, and the registers have been loaded with guest
* values. If false, host state is loaded in the CPU registers
* and vmx->loaded_vmcs->host_state is invalid.
*/
bool guest_state_loaded;
unsigned long exit_qualification;
u32 exit_intr_info;
u32 idt_vectoring_info;
ulong rflags;
/*
* User return MSRs are always emulated when enabled in the guest, but
* only loaded into hardware when necessary, e.g. SYSCALL #UDs outside
* of 64-bit mode or if EFER.SCE=1, thus the SYSCALL MSRs don't need to
* be loaded into hardware if those conditions aren't met.
*/
struct vmx_uret_msr guest_uret_msrs[MAX_NR_USER_RETURN_MSRS];
bool guest_uret_msrs_loaded;
#ifdef CONFIG_X86_64
u64 msr_host_kernel_gs_base;
u64 msr_guest_kernel_gs_base;
#endif
u64 spec_ctrl;
u32 msr_ia32_umwait_control;
/*
* loaded_vmcs points to the VMCS currently used in this vcpu. For a
* non-nested (L1) guest, it always points to vmcs01. For a nested
* guest (L2), it points to a different VMCS.
*/
struct loaded_vmcs vmcs01;
struct loaded_vmcs *loaded_vmcs;
struct msr_autoload {
struct vmx_msrs guest;
struct vmx_msrs host;
} msr_autoload;
struct msr_autostore {
struct vmx_msrs guest;
} msr_autostore;
struct {
int vm86_active;
ulong save_rflags;
struct kvm_segment segs[8];
} rmode;
struct {
u32 bitmask; /* 4 bits per segment (1 bit per field) */
struct kvm_save_segment {
u16 selector;
unsigned long base;
u32 limit;
u32 ar;
} seg[8];
} segment_cache;
int vpid;
bool emulation_required;
union vmx_exit_reason exit_reason;
/* Posted interrupt descriptor */
struct pi_desc pi_desc;
/* Used if this vCPU is waiting for PI notification wakeup. */
struct list_head pi_wakeup_list;
/* Support for a guest hypervisor (nested VMX) */
struct nested_vmx nested;
/* Dynamic PLE window. */
unsigned int ple_window;
bool ple_window_dirty;
bool req_immediate_exit;
/* Support for PML */
#define PML_ENTITY_NUM 512
struct page *pml_pg;
/* apic deadline value in host tsc */
u64 hv_deadline_tsc;
unsigned long host_debugctlmsr;
/*
* Only bits masked by msr_ia32_feature_control_valid_bits can be set in
* msr_ia32_feature_control. FEAT_CTL_LOCKED is always included
* in msr_ia32_feature_control_valid_bits.
*/
u64 msr_ia32_feature_control;
u64 msr_ia32_feature_control_valid_bits;
/* SGX Launch Control public key hash */
u64 msr_ia32_sgxlepubkeyhash[4];
u64 msr_ia32_mcu_opt_ctrl;
bool disable_fb_clear;
struct pt_desc pt_desc;
struct lbr_desc lbr_desc;
/* Save desired MSR intercept (read: pass-through) state */
#define MAX_POSSIBLE_PASSTHROUGH_MSRS 15
struct {
DECLARE_BITMAP(read, MAX_POSSIBLE_PASSTHROUGH_MSRS);
DECLARE_BITMAP(write, MAX_POSSIBLE_PASSTHROUGH_MSRS);
} shadow_msr_intercept;
};
kvm_init 함수 전체 흐름
int kvm_init(void *opaque, unsigned vcpu_size, unsigned vcpu_align,
struct module *module)
{
struct kvm_cpu_compat_check c;
int r;
int cpu;
r = kvm_arch_init(opaque);
if (r)
goto out_fail;
/*
* kvm_arch_init makes sure there's at most one caller
* for architectures that support multiple implementations,
* like intel and amd on x86.
* kvm_arch_init must be called before kvm_irqfd_init to avoid creating
* conflicts in case kvm is already setup for another implementation.
*/
r = kvm_irqfd_init();
if (r)
goto out_irqfd;
if (!zalloc_cpumask_var(&cpus_hardware_enabled, GFP_KERNEL)) {
r = -ENOMEM;
goto out_free_0;
}
r = kvm_arch_hardware_setup(opaque);
if (r < 0)
goto out_free_1;
c.ret = &r;
c.opaque = opaque;
for_each_online_cpu(cpu) {
smp_call_function_single(cpu, check_processor_compat, &c, 1);
if (r < 0)
goto out_free_2;
}
r = cpuhp_setup_state_nocalls(CPUHP_AP_KVM_STARTING, "kvm/cpu:starting",
kvm_starting_cpu, kvm_dying_cpu);
if (r)
goto out_free_2;
register_reboot_notifier(&kvm_reboot_notifier);
/* A kmem cache lets us meet the alignment requirements of fx_save. */
if (!vcpu_align)
vcpu_align = __alignof__(struct kvm_vcpu);
kvm_vcpu_cache =
kmem_cache_create_usercopy("kvm_vcpu", vcpu_size, vcpu_align,
SLAB_ACCOUNT,
offsetof(struct kvm_vcpu, arch),
offsetofend(struct kvm_vcpu, stats_id)
- offsetof(struct kvm_vcpu, arch),
NULL);
if (!kvm_vcpu_cache) {
r = -ENOMEM;
goto out_free_3;
}
for_each_possible_cpu(cpu) {
if (!alloc_cpumask_var_node(&per_cpu(cpu_kick_mask, cpu),
GFP_KERNEL, cpu_to_node(cpu))) {
r = -ENOMEM;
goto out_free_4;
}
}
r = kvm_async_pf_init();
if (r)
goto out_free_4;
kvm_chardev_ops.owner = module;
r = misc_register(&kvm_dev);
if (r) {
pr_err("kvm: misc device register failed\n");
goto out_unreg;
}
register_syscore_ops(&kvm_syscore_ops);
kvm_preempt_ops.sched_in = kvm_sched_in;
kvm_preempt_ops.sched_out = kvm_sched_out;
kvm_init_debug();
r = kvm_vfio_ops_init();
WARN_ON(r);
return 0;
out_unreg:
kvm_async_pf_deinit();
out_free_4:
for_each_possible_cpu(cpu)
free_cpumask_var(per_cpu(cpu_kick_mask, cpu));
kmem_cache_destroy(kvm_vcpu_cache);
out_free_3:
unregister_reboot_notifier(&kvm_reboot_notifier);
cpuhp_remove_state_nocalls(CPUHP_AP_KVM_STARTING);
out_free_2:
kvm_arch_hardware_unsetup();
out_free_1:
free_cpumask_var(cpus_hardware_enabled);
out_free_0:
kvm_irqfd_exit();
out_irqfd:
kvm_arch_exit();
out_fail:
return r;
}
EXPORT_SYMBOL_GPL(kvm_init);
이해를 돕기 위해서, vmx_init() 안에서 kvm_init 함수를 호출하는 코드 조각을 다시 한 번 붙여 놓는다. 아래와 같다.
r = kvm_init(&vmx_init_ops, sizeof(struct vcpu_vmx),
__alignof__(struct vcpu_vmx), THIS_MODULE);
if (r)
return r;
kvm_init 함수 호출 흐름도

함수를 하나씩 하나씩 보자.
(1) kvm_arch_init 함수
r = kvm_arch_init(opaque);
if (r)
goto out_fail;
kvm_arch_init 함수는 아키텍처 관련 코드를 초기화하는 데 쓴다. 이건 아키텍처 관련 함수인데, 다른 아키텍처 마다 모두 구현이 있으며, 다른 체계 구조에 따라 달라진다.
각 아키텍처별 kvm_arch_init 구현 위치

여기서도 x86을 예로 든다. x86 체계 구조 아래의 kvm_arch_init 함수는 Linux 커널 소스 코드 루트 디렉토리 arch/x86/kvm/x86.c 안에 있는데, 코드는 아래와 같다.
int kvm_arch_init(void *opaque)
{
struct kvm_x86_init_ops *ops = opaque;
u64 host_pat;
int r;
if (kvm_x86_ops.hardware_enable) {
pr_err("kvm: already loaded vendor module '%s'\n", kvm_x86_ops.name);
return -EEXIST;
}
if (!ops->cpu_has_kvm_support()) {
pr_err_ratelimited("kvm: no hardware support for '%s'\n",
ops->runtime_ops->name);
return -EOPNOTSUPP;
}
if (ops->disabled_by_bios()) {
pr_err_ratelimited("kvm: support for '%s' disabled by bios\n",
ops->runtime_ops->name);
return -EOPNOTSUPP;
}
/*
* KVM explicitly assumes that the guest has an FPU and
* FXSAVE/FXRSTOR. For example, the KVM_GET_FPU explicitly casts the
* vCPU's FPU state as a fxregs_state struct.
*/
if (!boot_cpu_has(X86_FEATURE_FPU) || !boot_cpu_has(X86_FEATURE_FXSR)) {
printk(KERN_ERR "kvm: inadequate fpu\n");
return -EOPNOTSUPP;
}
if (IS_ENABLED(CONFIG_PREEMPT_RT) && !boot_cpu_has(X86_FEATURE_CONSTANT_TSC)) {
pr_err("RT requires X86_FEATURE_CONSTANT_TSC\n");
return -EOPNOTSUPP;
}
/*
* KVM assumes that PAT entry '0' encodes WB memtype and simply zeroes
* the PAT bits in SPTEs. Bail if PAT[0] is programmed to something
* other than WB. Note, EPT doesn't utilize the PAT, but don't bother
* with an exception. PAT[0] is set to WB on RESET and also by the
* kernel, i.e. failure indicates a kernel bug or broken firmware.
*/
if (rdmsrl_safe(MSR_IA32_CR_PAT, &host_pat) ||
(host_pat & GENMASK(2, 0)) != 6) {
pr_err("kvm: host PAT[0] is not WB\n");
return -EIO;
}
x86_emulator_cache = kvm_alloc_emulator_cache();
if (!x86_emulator_cache) {
pr_err("kvm: failed to allocate cache for x86 emulator\n");
return -ENOMEM;
}
user_return_msrs = alloc_percpu(struct kvm_user_return_msrs);
if (!user_return_msrs) {
printk(KERN_ERR "kvm: failed to allocate percpu kvm_user_return_msrs\n");
goto out_free_x86_emulator_cache;
}
kvm_nr_uret_msrs = 0;
/* ... 코드 계속 ... */
r = kvm_mmu_vendor_module_init();
if (r)
goto out_free_percpu;
kvm_timer_init();
if (boot_cpu_has(X86_FEATURE_XSAVE)) {
host_xcr0 = xgetbv(XCR_XFEATURE_ENABLED_MASK);
kvm_caps.supported_xcr0 = host_xcr0 & KVM_SUPPORTED_XCR0;
}
if (pi_inject_timer == -1)
pi_inject_timer = housekeeping_enabled(HK_TYPE_TIMER);
#ifdef CONFIG_X86_64
pvclock_gtod_register_notifier(&pvclock_gtod_notifier);
if (hypervisor_is_type(X86_HYPER_MS_HYPERV))
set_hv_tscchange_cb(kvm_hyperv_tsc_notifier);
#endif
return 0;
out_free_percpu:
free_percpu(user_return_msrs);
out_free_x86_emulator_cache:
kmem_cache_destroy(x86_emulator_cache);
return r;
}
kvm_arch_init 함수는 일부 초기화 작업을 하는데, KVM이 하나만 커널에 로드될 수 있도록 보장한다.
KVM 구현의 구조체는 전역 변수 kvm_x86_ops에 할당된다 (앞에서 말한 것처럼).
여기서 kvm_arch_init에 전달되는 인자 opaque는 바로 vmx_x86_ops (새 버전에서는 한 겹 더 감싸졌는데, 의미는 똑같다)인데, 그 안에는 Intel CPU 아래에서 KVM 구현의 각종 콜백 함수가 들어 있다.
kvm_arch_init 함수는 구현 관련 콜백 함수 cpu_has_kvm_support()와 disable_by_bios()를 호출한다.
if (!ops->cpu_has_kvm_support()) {
pr_err_ratelimited("kvm: no hardware support for '%s'\n",
ops->runtime_ops->name);
return -EOPNOTSUPP;
}
if (ops->disabled_by_bios) {
pr_err_ratelimited("kvm: support for '%s' disabled by bios\n",
ops->runtime_ops->name);
return -EOPNOTSUPP;
}
cpu_has_kvm_support()는 CPU가 VMX 모드를 지원하는지 검사하는 함수로(앞서 말한 활성화 조건 1에 해당), CPUID 명령의 반환값으로 판단한다.
disabled_by_bios()는 BIOS에서 (VMX가) 꺼져 있는지 검사하는 함수로(활성화 조건 7에 해당), MSR 레지스터를 읽어서 판단한다.
kvm_arch_init 안의 콜백 함수 호출 관계

kvm_arch_init 함수의 다른 초기화 작업으로는 아래와 같은 게 있다:
- percpu 변수 user_return_msrs 할당
user_return_msrs = alloc_percpu(struct kvm_user_return_msrs);
if (!user_return_msrs) {
printk(KERN_ERR "kvm: failed to allocate percpu kvm_user_return_msrs\n");
r = -ENOMEM;
goto out_free_x86_emulator_cache;
}
kvm_nr_uret_msrs = 0;
- 메모리 가상화 초기화 작업 완료
r = kvm_mmu_vendor_module_init();
if (r)
goto out_free_percpu;
- timer 초기화 완료
kvm_timer_init();
더 이른 커널 버전에서는 kvm_arch_init 함수 안에 아래 함수들이 더 있었다.
- kvm_set_mmio_spte_mask()
MMIO 메모리의 식별자를 설정한다 (그 식별자는 shadow_mmio_mask로 표시한다).
- kvm_lapic_init()
lapic의 초기화를 완료한다.
하지만 6.1.10 커널 버전 안에서는 이 함수들이 이미 없다.
(2) kvm_irqfd_init 함수
/*
* kvm_arch_init makes sure there's at most one caller
* for architectures that support multiple implementations,
* like intel and amd on x86.
* kvm_arch_init must be called before kvm_irqfd_init to avoid creating
* conflicts in case kvm is already setup for another implementation.
*/
r = kvm_irqfd_init();
if (r)
goto out_irqfd;
이 함수의 관련 선언은 Linux 커널 소스 코드 /include/linux/kvm_host.h 안에 있다.
#ifdef CONFIG_HAVE_KVM_IRQFD
int kvm_irqfd_init(void);
void kvm_irqfd_exit(void);
#else
static inline int kvm_irqfd_init(void)
{
return 0;
}
static inline void kvm_irqfd_exit(void)
{
}
#endif
볼 수 있듯이, 이건 설정 가능한 함수다. CONFIG_HAVE_KVM_IRQFD가 설정됐을 때만, 이 함수에 실제 내용이 있다. 그렇지 않으면 빈 함수일 뿐이다.
설정이 유효할 때의 kvm_irqfd_init 함수는 Linux 커널 소스 코드 virt/kvm/eventfd.c 안에 있다. 코드는 아래와 같다.
/*
* create a host-wide workqueue for issuing deferred shutdown requests
* aggregated from all vm* instances. We need our own isolated
* queue to ease flushing work items when a VM exits.
*/
int kvm_irqfd_init(void)
{
irqfd_cleanup_wq = alloc_workqueue("kvm-irqfd-cleanup", 0, 0);
if (!irqfd_cleanup_wq)
return -ENOMEM;
return 0;
}
void kvm_irqfd_exit(void)
{
destroy_workqueue(irqfd_cleanup_wq);
}
여기서 QEMU/KVM의 irqfd 메커니즘을 해설할 필요가 있다.
irqfd 메커니즘은 ioeventfd 메커니즘과 비슷한데, 기본 원리는 모두 eventfd 기반이다.
ioeventfd 메커니즘은 Guest가 QEMU/KVM으로 알림을 보내는 빠른 통로를 제공한다 (Guest -> QEMU/KVM).
대응되게, irqfd 메커니즘은 QEMU/KVM이 Guest로 알림을 보내는 빠른 통로를 제공한다 (QEMU/KVM -> Guest).
irqfd 메커니즘은 하나의 eventfd와 하나의 전역 인터럽트 번호를 연결시켜서, 이 eventfd로 신호를 보낼 때, 대응되는 인터럽트가 가상머신에 주입되도록 한다.

QEMU의 irqfd 등록
ioeventfd와 비슷하게, irqfd는 사용 전에 반드시 먼저 하나의 EventNotifier 객체를 초기화해야 한다 (event_notifier_init 함수로 초기화). EventNotifier 객체 초기화가 완료된 후에 하나의 eventfd를 얻는다.
kvm으로 인터럽트 irqfd 요청 등록 보내기
하나의 eventfd를 얻은 후에, QEMU는 kvm_irqchip_add_irqfd_notifier_gsi => kvm_irqchip_assign_irqfd를 통해 kvm_irqchip 구조체를 구축하고, kvm으로 ioctl(KVM_IRQFD)를 보낸다.
static int kvm_irqchip_assign_irqfd(KVMState *s, int fd, int rfd, int virq,
bool assign)
{
struct kvm_irqfd irqfd = {
.fd = fd,
.gsi = virq,
.flags = assign ? 0 : KVM_IRQFD_FLAG_DEASSIGN,
};
if (rfd != -1) {
irqfd.flags |= KVM_IRQFD_FLAG_RESAMPLE;
irqfd.resamplefd = rfd;
}
if (!kvm_irqfds_enabled()) {
return -ENOSYS;
}
return kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);
}
kvm_irqchip_assign_irqfd 함수에서는, 먼저 kvm_irqfd 구조의 변수 irqfd를 구축했는데, 그 중 fd는 이전에 초기화한 eventfd이고, gsi는 전역 시스템 인터럽트이며, flags에는 kvm으로 irqfd를 등록할지 (flags=0) 아니면 irqfd 등록을 해제할지 (KVM_IRQFD_FLAG_DEASSIGN, 즉 flags=1)가 정의돼 있다.
flags의 bit1 (KVM_IRQFD_FLAG_RESAMPLE)은 그 인터럽트가 레벨 트리거인지 여부를 나타낸다.
- KVM_IRQFD_FLAG_RESAMPLE 관련 정보
irqfd 메커니즘

인터럽트가 edge 트리거 모드에 있을 때, irqfd->fd는 kvm의 인터럽트 칩 (irqchip)의 gsi 핀에 연결되며, irqfd->fd가 인터럽트의 toggle, 그리고 유저 공간의 handler 트리거를 책임진다.
인터럽트가 레벨 트리거 모드에 있을 때, 마찬가지로 irqfd->fd가 kvm의 인터럽트 칩의 gsi 핀에 연결되는데, 인터럽트 칩이 EOI (end of interrupt) 재샘플링 신호를 받았을 때, gsi는 전평 반전을 진행하고, 유저 공간에 대한 알림은 irqfd->resample_fd가 완성한다 (resample_fd도 하나의 eventfd다).
kvm_irqchip_assign_irqfd는 최종적으로 kvm_vm_ioctl(s, KVM_IRQFD, &irqfd)를 호출해서, kvm으로 위에서 구축한 kvm_irqfd 정보를 포함한 irqfd 등록 요청을 보낸다.
kvm의 irqfd 등록
ioctl(KVM_IRQFD)를 받은 후에, KVM은 먼저 전달된 데이터 구조 kvm_irqfd의 정보를 가져오고, 그 다음 kvm_irqfd 함수를 호출한다.
case KVM_IRQFD: {
struct kvm_irqfd data;
r = -EFAULT;
if (copy_from_user(&data, argp, sizeof(data)))
goto out;
r = kvm_irqfd(kvm, &data);
break;
}
kvm_irqfd 함수에서, 먼저 전달된 kvm_irqfd 구조 안의 flags의 bit0이 irqfd 등록을 요청하는 건지 아니면 irqfd의 등록 해제인지 구별한다.
irqfd 등록은 kvm_irqfd_assign 함수를 호출하고, 등록 해제는 kvm_irqfd_deassign 함수를 호출한다.
int
kvm_irqfd(struct kvm *kvm, struct kvm_irqfd *args)
{
if (args->flags & ~(KVM_IRQFD_FLAG_DEASSIGN | KVM_IRQFD_FLAG_RESAMPLE))
return -EINVAL;
if (args->flags & KVM_IRQFD_FLAG_DEASSIGN)
return kvm_irqfd_deassign(kvm, args);
return kvm_irqfd_assign(kvm, args);
}
kvm_irqfd_assign 함수는 같은 파일 (Linux 커널 소스 코드 virt/kvm/eventfd.c) 안에 있다.
static int
kvm_irqfd_assign(struct kvm *kvm, struct kvm_irqfd *args)
{
struct kvm_kernel_irqfd *irqfd, *tmp;
struct fd f;
struct eventfd_ctx *eventfd = NULL, *resamplefd = NULL;
int ret;
__poll_t events;
int idx;
if (!kvm_arch_intc_initialized(kvm))
return -EAGAIN;
if (!kvm_arch_irqfd_allowed(kvm, args))
return -EINVAL;
irqfd = kzalloc(sizeof(*irqfd), GFP_KERNEL_ACCOUNT);
if (!irqfd)
return -ENOMEM;
irqfd->kvm = kvm;
irqfd->gsi = args->gsi;
INIT_LIST_HEAD(&irqfd->list);
INIT_WORK(&irqfd->inject, irqfd_inject);
INIT_WORK(&irqfd->shutdown, irqfd_shutdown);
seqcount_spinlock_init(&irqfd->irq_entry_sc, &kvm->irqfds.lock);
f = fdget(args->fd);
if (!f.file) {
ret = -EBADF;
goto out;
}
eventfd = eventfd_ctx_fileget(f.file);
if (IS_ERR(eventfd)) {
ret = PTR_ERR(eventfd);
goto fail;
}
irqfd->eventfd = eventfd;
if (args->flags & KVM_IRQFD_FLAG_RESAMPLE) {
struct kvm_kernel_irqfd_resampler *resampler;
resamplefd = eventfd_ctx_fdget(args->resamplefd);
if (IS_ERR(resamplefd)) {
ret = PTR_ERR(resamplefd);
goto fail;
}
irqfd->resamplefd = resamplefd;
INIT_LIST_HEAD(&irqfd->resampler_link);
mutex_lock(&kvm->irqfds.resampler_lock);
list_for_each_entry(resampler,
&kvm->irqfds.resampler_list, link) {
if (resampler->notifier.gsi == irqfd->gsi) {
irqfd->resampler = resampler;
break;
}
}
if (!irqfd->resampler) {
resampler = kzalloc(sizeof(*resampler),
GFP_KERNEL_ACCOUNT);
if (!resampler) {
ret = -ENOMEM;
mutex_unlock(&kvm->irqfds.resampler_lock);
goto fail;
}
resampler->kvm = kvm;
INIT_LIST_HEAD(&resampler->list);
resampler->notifier.gsi = irqfd->gsi;
resampler->notifier.irq_acked = irqfd_resampler_ack;
INIT_LIST_HEAD(&resampler->link);
list_add(&resampler->link, &kvm->irqfds.resampler_list);
kvm_register_irq_ack_notifier(kvm,
&resampler->notifier);
irqfd->resampler = resampler;
}
list_add_rcu(&irqfd->resampler_link, &irqfd->resampler->list);
synchronize_srcu(&kvm->irq_srcu);
mutex_unlock(&kvm->irqfds.resampler_lock);
}
/*
* Install our own custom wake-up handling so we are notified via
* a callback whenever someone signals the underlying eventfd
*/
init_waitqueue_func_entry(&irqfd->wait, irqfd_wakeup);
init_poll_funcptr(&irqfd->pt, irqfd_ptable_queue_proc);
spin_lock_irq(&kvm->irqfds.lock);
ret = 0;
list_for_each_entry(tmp, &kvm->irqfds.items, list) {
if (irqfd->eventfd != tmp->eventfd)
continue;
/* This fd is used for another irq already. */
ret = -EBUSY;
spin_unlock_irq(&kvm->irqfds.lock);
goto fail;
}
idx = srcu_read_lock(&kvm->irq_srcu);
irqfd_update(kvm, irqfd);
list_add_tail(&irqfd->list, &kvm->irqfds.items);
spin_unlock_irq(&kvm->irqfds.lock);
/*
* Check if there was an event already pending on the eventfd
* before we registered, and trigger it as if we didn't miss it.
*/
events = vfs_poll(f.file, &irqfd->pt);
if (events & EPOLLIN)
schedule_work(&irqfd->inject);
#ifdef CONFIG_HAVE_KVM_IRQ_BYPASS
if (kvm_arch_has_irq_bypass()) {
irqfd->consumer.token = (void *)irqfd->eventfd;
irqfd->consumer.add_producer = kvm_arch_irq_bypass_add_producer;
irqfd->consumer.del_producer = kvm_arch_irq_bypass_del_producer;
irqfd->consumer.stop = kvm_arch_irq_bypass_stop;
irqfd->consumer.start = kvm_arch_irq_bypass_start;
ret = irq_bypass_register_consumer(&irqfd->consumer);
if (ret)
pr_info("irq bypass consumer (token %p) registration fails: %d\n",
irqfd->consumer.token, ret);
}
#endif
srcu_read_unlock(&kvm->irq_srcu, idx);
/*
* do not drop the file until the irqfd is fully initialized, otherwise
* we might race against the EPOLLHUP
*/
fdput(f);
return 0;
fail:
if (irqfd->resampler)
irqfd_resampler_shutdown(irqfd);
if (resamplefd && !IS_ERR(resamplefd))
eventfd_ctx_put(resamplefd);
if (eventfd && !IS_ERR(eventfd))
eventfd_ctx_put(eventfd);
fdput(f);
out:
kfree(irqfd);
return ret;
}
kvm_irqfd_assign 함수에서, 먼저 kvm_kernel_irqfd 구조 타입의 변수 irqfd를 위해 공간을 할당하고, 그 다음 irqfd의 각 하위 필드에 대해 값을 할당한다.
irqfd = kzalloc(sizeof(*irqfd), GFP_KERNEL_ACCOUNT);
if (!irqfd)
return -ENOMEM;
irqfd->kvm = kvm;
irqfd->gsi = args->gsi;
INIT_LIST_HEAD(&irqfd->list);
INIT_WORK(&irqfd->inject, irqfd_inject);
INIT_WORK(&irqfd->shutdown, irqfd_shutdown);
seqcount_spinlock_init(&irqfd->irq_entry_sc, &kvm->irqfds.lock);
kvm_kernel_irqfd 구조 안에는 2개의 work_struct, inject와 shutdown이 있어서 각각 인터럽트 트리거와 인터럽트 닫기를 책임지는데, 이 두 work_struct에 각자 대응되는 조작 함수는 각각 irqfd_inject와 irqfd_shutdown이다.
kvm_irq_assign이 호출한 init_waitqueue_func_entry 함수는 irqfd_wakeup 함수를 irqfd 안 대기열 entry 활성화 시의 처리 함수로 등록한다. 이렇게 해서 그 irqfd에 대응되는 eventfd를 어떻게 쓰는 동작이든 모두 이 함수가 트리거되도록 한다.
그리고, kvm_irq_assign 함수는 init_poll_funcptr를 이용해서 irqfd_ptable_queue_proc 함수를 irqfd 안의 poll table의 처리 함수로 등록한다. irqfd_ptable_queue_proc는 poll table 안의 대응되는 wait queue entry를 wait queue에 추가한다.
/*
* Install our own custom wake-up handling so we are notified via
* a callback whenever someone signals the underlying eventfd
*/
init_waitqueue_func_entry(&irqfd->wait, irqfd_wakeup);
init_poll_funcptr(&irqfd->pt, irqfd_ptable_queue_proc);
kvm_irqfd_assign 함수 동작 흐름

kvm_irq_assign은 이어서 그 eventfd가 이미 다른 인터럽트에 의해 쓰이고 있는지 판단한다.
전체 KVM 모듈 초기화 흐름

kvm_arch_init이 콜백을 거치는 구조

irqfd 메커니즘 전체 정리

kvm_irqfd_init 함수와 그것이 만드는 irqfd 메커니즘에 대해 자세히 살펴봤다. kvm_irqfd_init 자체는 단지 워크큐를 하나 할당하는 단순한 함수이지만, irqfd 메커니즘 전체는 QEMU/KVM이 Guest로 인터럽트를 주입하는 핵심 빠른 경로다.
6) QEMU/KVM API 사용 예제 (light-qemu)
이전 글에서, QEMU 파라미터 해석, QEMU Module, QOM을 중점적으로 다뤘다.
이러한 기초를 다진 후에, 본격적으로 각 하드웨어 모듈의 상세 분석을 시작해야 하는데, 그 전에 먼저 하나의 QEMU/KVM API 사용 예시를 알아보자.
QEMU를 말하면 항상 KVM이 따라오고, 반대로 KVM을 말하면 역시 마찬가지다. 사실 본질적으로 보면, QEMU와 KVM은 반드시 서로에게 의존할 필요는 없다. 이 둘을 "묶어서" 함께 놓는 이유는 KVM이 만들어질 초기에 QEMU의 디바이스 에뮬레이션 부분을 재사용했기 때문이다.
여기서는 하나의 간단한 예제로 QEMU와 KVM의 관계를 보여주고, 나중에 각 모듈을 깊이 파고드는 연구를 위한 가장 간단하고 기초적인 프레임워크 흐름의 참고 자료를 제공한다.
예제는 두 부분으로 구성된다: 첫 번째 부분은 초간단 커널로, 그 임무는 오직 I/O 포트에 데이터를 쓰는 것뿐이다. 두 번째 부분은 간소화 QEMU로 볼 수 있는데, 그 임무도 매우 간단하다. 이 초간단 커널이 포트에 쓴 데이터를 출력하는 거다.
커널 코드는 다음과 같다 (x86 어셈블리, test.S)
start:
# 'H' 출력
mov $0x48, %al
outb %al, $0xf1
# 'e' 출력
mov $0x65, %al
outb %al, $0xf1
# 'l' 출력
mov $0x6c, %al
outb %al, $0xf1
# 'l' 출력
mov $0x6c, %al
outb %al, $0xf1
# 'o' 출력
mov $0x6f, %al
outb %al, $0xf1
# 개행 문자 '\a' 출력
mov $0x0a, %al
outb %al, $0xf1
# 정지
hlt
코드는 매우 간단하고, 코드 주석이 이미 아주 명확하다. 기능은 포트 0xf1에 Hello 문자열을 쓰고, 그 다음 hlt 명령을 호출하는 거다.
아래 명령으로 위의 어셈블리 코드를 컴파일한다.
$ as -32 test.S -o test.o
$ objcopy -O binary test.o test.bin
QEMU 코드는 다음과 같다 (qemu.c)
int main(void)
{
struct kvm_sregs sregs;
int ret = 0;
int kvmfd = open("/dev/kvm", O_RDWR);
ioctl(kvmfd, KVM_GET_API_VERSION, NULL);
int vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);
unsigned char *ram = mmap(NULL, 0x1000, PORT_READ | PORT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
int kfd = open("test.bin", O_RDONLY);
read(kfd, ram, 4096);
struct kvm_userspace_memory_region mem = {
.slot = 0,
.guest_phys_addr = 0,
.memory_size = 0x1000,
.userspace_addr = (unsigned long)ram,
};
ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &mem);
int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, 0);
int mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);
struct kvm_run *run = mmap(NULL, mmap_size, PORT_READ | PORT_WRITE, MAP_SHARED, vcpufd, 0);
ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
sregs.cds.base = 0;
sregs.cs.selector = 0;
ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);
struct kvm_regs regs = {
.rip = 0,
};
ret = ioctl(vcpufd, KVM_SET_REGS, ®s);
while (1)
{
ret = ioctl(vcpufd, KVM_RUN, NULL);
if (ret == -1)
{
printf("exit unknown\n");
return -1;
}
switch(run->exit_reason)
{
case KVM_EXIT_HLT:
puts("KVM_EXIT_HLT");
return 0;
case KVM_EXIT_IO:
putchar( ((char *)run) + run->io.data_offset));
break;
case KVM_EXIT_FAIL_ENTRY:
puts("entry error");
return -1;
default:
puts("other error");
printf("exit_reason: %d\n", run->exit_reason);
return -1;
}
}
return 0;
}
qemu.c 파일을 컴파일한다.
$ gcc qemu.c -o light-qemu
컴파일 후 생성된 실행 파일을 실행하면, 명령과 결과는 다음과 같다.
$ ./light-qemu
Hello
KVM_EXIT_HLT
보다시피, light-qemu라는 이름의 QEMU가 커널이 포트에 쓴 데이터를 출력했다. 아래에서 qemu.c 코드를 상세히 설명한다.
KVM ioctl 인터페이스 구조
KVM은 일련의 ioctl 시스템 호출을 통해 유저 공간에 인터페이스를 제공하고, 이 인터페이스를 통해 가상머신의 생성, 가상머신 메모리의 설정, 가상머신 VCPU의 생성과 실행 등을 할 수 있다.
사용하는 파일 디스크립터 (file descriptor, fd)에 따라, KVM의 이 시스템 호출 그룹은 세 가지로 나눌 수 있다.
(1) 시스템 전역 ioctl. 이 종류의 ioctl의 작용 대상은 KVM 모듈 자체다.
- 예를 들어 일부 전역 설정 항목이나, 가상머신을 생성하는 ioctl도 여기에 해당한다.
(2) 가상머신 관련 ioctl. 이 종류의 ioctl의 작용 대상은 하나의 가상머신이다.
- 예를 들어 가상머신의 메모리 레이아웃을 설정하거나, 가상머신 VCPU를 생성하는 것도 여기에 해당한다.
(3) 가상머신 VCPU 관련 ioctl. 이 종류의 ioctl의 작용 대상은 하나의 가상머신의 VCPU다.
- 예를 들어 가상머신 VCPU의 실행을 시작하는 것이다.

qemu.c 상세 단계
아래는 qemu.c의 main 함수 코드의 상세 단계다.
(1) 먼저 /dev/kvm을 열어서 시스템에 있는 KVM 서브시스템의 파일 디스크립터 kvmfd를 얻는다.
int kvmfd = open("/dev/kvm", O_RDWR);
(2) 응용 계층과 커널의 통일성을 유지하기 위해, ioctl을 통해 KVM의 API 버전 번호를 가져올 수 있다.
이렇게 하면 응용 계층에서 관련 인터페이스가 커널에서 지원되는지 알 수 있다.
ioctl(kvmfd, KVM_GET_API_VERSION, NULL);
(3) 이어서 kvmfd에 대해 ioctl을 호출해서 하나의 가상머신을 생성한다.
int vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);
이 ioctl은 가상머신을 대표하는 파일 디스크립터 vmfd를 리턴한다. 이건 하나의 완전한 가상머신 시스템을 대표하고, vmfd를 통해 가상머신의 메모리, VCPU 등을 제어할 수 있다.
(4) 메모리는 컴퓨터에 빠질 수 없는 구성 요소이므로, 가상머신을 만든 후에는 가상머신에 물리 메모리를 할당해야 한다.
unsigned char *ram = mmap(NULL, 0x1000, PORT_READ | PORT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
int kfd = open("test.bin", O_RDONLY);
read(kfd, ram, 4096);
가상머신의 물리 메모리는 QEMU의 프로세스 주소 공간에 대응한다.
여기서는 mmap 시스템 호출을 사용해서 한 페이지 (4096, 4K)의 가상머신 메모리를 할당하고, 커널의 코드를 이 메모리 공간에 읽어 넣었다.
(5) 그 다음 할당된 가상 메모리 주소로 kvm_userspace_memory_region 객체를 초기화하고, ioctl(KVM_SET_USER_MEMORY_REGION)을 호출한다.
이렇게 하면 가상머신에 하나의 메모리 슬롯을 지정하는 거다.
struct kvm_userspace_memory_region mem = {
.slot = 0,
.guest_phys_addr = 0,
.memory_size = 0x1000,
.userspace_addr = (unsigned long)ram,
};
ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &mem);
- slot — 서로 다른 공간을 나타내는 데 쓴다.
- guest_phys_addr — 이 공간이 가상머신 물리 메모리 공간에서의 위치를 나타낸다.
- memory_size — 이 물리 메모리의 크기를 나타낸다.
- userspace_addr — 이 물리 공간이 호스트 머신에서 대응하는 가상머신 주소를 나타낸다.
(6) 가상머신의 메모리를 설정한 후, 이어서 vmfd에 대해 ioctl을 호출해서 가상머신 VCPU를 생성한다.
int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, 0);
(7) 모든 VCPU에는 하나의 struct kvm_run 구조가 있고, QEMU(user)와 KVM(kernel)가 데이터를 공유하는 데 쓴다.
유저 프로그램은 이 메모리 영역을 유저 공간에 매핑해야 하므로, 이를 위해 먼저 ioctl(KVM_GET_VCPU_MMAP_SIZE)을 호출해서 이 구조체의 크기를 얻는다.
int mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);
여기서 kvmfd에 대해 ioctl을 호출한다는 점에 주의하자. 이건 KVM의 모든 VCPU에 대해 동일하기 때문이다.
(8) 이어서 mmap 함수를 호출해서, struct kvm_run의 대상을 유저 태 공간에 매핑한다.
struct kvm_run *run = mmap(NULL, mmap_size, PORT_READ | PORT_WRITE, MAP_SHARED, vcpufd, 0);
(9) 가상머신 VCPU를 실행시키려면, VCPU 관련 레지스터를 설정해야 한다.
그 중, 세그먼트 레지스터와 컨트롤 레지스터 등 특수 레지스터는 kvm_sregs 구조체에 담겨 있고, ioctl(KVM_GET_SREGS)과 ioctl(KVM_SET_SREGS)을 통해 읽고 수정한다.
범용 레지스터는 kvm_regs 구조체에 담겨 있고, ioctl(KVM_GET_REGS)과 ioctl(KVM_SET_REGS)을 통해 읽고 수정한다.
ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
sregs.cds.base = 0;
sregs.cs.selector = 0;
ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);
struct kvm_regs regs = {
.rip = 0,
};
ret = ioctl(vcpufd, KVM_SET_REGS, ®s);
그 중 가장 중요한 건 CS와 IP 레지스터를 설정하는 건데, 이 예제에서는 둘 다 0으로 설정했다. 코드가 가상머신 물리 주소 0에 로드되었기 때문에, 가상머신 VCPU가 실행될 때 주소 0부터 바로 시작하게 된다.
(10) 이제 간단한 가상머신과 가상머신 VCPU, 메모리가 모두 준비되었고, 레지스터도 설정되었으니, 가상머신을 돌릴 차례다.
보통 하나의 루프 안에서 vcpufd에 대해 ioctl(KVM_RUN)을 호출한다.
while (1)
{
ret = ioctl(vcpufd, KVM_RUN, NULL);
if (ret == -1)
{
printf("exit unknown\n");
return -1;
}
......
}
(11) 커널이 이 ioctl을 처리할 때, VCPU를 물리 CPU에 스케줄링해서 실행시킨다.
VCPU가 실행 과정에서 일부 민감한 명령을 만나면 종료한다. 만약 커널 태의 KVM이 처리할 수 없는 거라면, 응용 계층 소프트웨어에 넘겨서 처리하게 한다.
이때 ioctl 시스템 호출이 리턴되고, 일부 정보를 kvm_run 구조체에 저장한다. 이렇게 하면 유저 프로그램이 가상머신이 종료된 이유를 알 수 있고, 그 다음 구체적인 원인에 따라 해당하는 처리를 한다.
switch(run->exit_reason)
{
case KVM_EXIT_HLT:
puts("KVM_EXIT_HLT");
return 0;
case KVM_EXIT_IO:
putchar( ((char *)run) + run->io.data_offset));
break;
case KVM_EXIT_FAIL_ENTRY:
puts("entry error");
return -1;
default:
puts("other error");
printf("exit_reason: %d\n", run->exit_reason);
return -1;
}
이 예제에서, 가상머신 커널이 포트에 데이터를 쓰면 KVM_EXIT_IO로 종료되는데, 이건 가상머신 내부에서 포트를 읽고 썼다는 뜻이다.
데이터를 출력한 후에 가상머신을 계속 실행시키고, 마지막 hlt 명령까지 실행하면 KVM_EXIT_HLT 타입의 종료가 발생하고, 이때 가상머신 실행이 끝난다.
물론, 예제에서의 커널과 QEMU에 비하면, 실제 QEMU와 실제 운영체제 커널의 복잡도는 이것보다 훨씬 높은 수준이지만, 그 기본 원리는 모두 비슷하다.
QEMU/KVM 전체 실행 흐름

'System Programming > Hypervisor' 카테고리의 다른 글
| QEMU 코드 분석 #6 VirtIO 기초 & 데이터 구조 (0) | 2025.02.14 |
|---|---|
| QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트 (0) | 2024.11.18 |
| QEMU 코드 분석 #4 Machine & i440fx (0) | 2024.09.19 |
| QEMU 코드 분석 #3 메모리 가상화 & 메모리 초기화 (0) | 2024.07.08 |
| QEMU 코드 분석 #1 파라미터 해석 · 모듈 초기화 · QOM · 속성 (0) | 2024.03.25 |