virtio / vhost
리눅스 커널의 반가상화(Paravirtualization) I/O 프레임워크 virtio와 데이터 경로 가속 vhost를 virtqueue 구조부터 디바이스 유형별 구현, vDPA 오프로드까지 심층 분석합니다.
핵심 요약
virtio/vhost를 이해하기 위한 6가지 핵심 개념
OASIS 표준 가상화 I/O
virtio는 OASIS(Organization for the Advancement of Structured Information Standards)에서 관리하는 표준 가상화 I/O 프레임워크입니다. 게스트 OS와 하이퍼바이저 간 통일된 인터페이스를 제공하여, 어떤 하이퍼바이저(KVM, Xen, Hyper-V)에서든 동일한 드라이버로 동작합니다. 현재 virtio 1.3 스펙까지 발표되었으며, 네트워크·블록·SCSI·GPU·암호화(Encryption) 등 20종 이상의 디바이스 유형을 정의합니다.
Virtqueue (Split/Packed Ring)
virtqueue는 게스트와 호스트 간 데이터를 교환하는 공유 메모리 기반 링 버퍼(Ring Buffer)입니다. 레거시 Split Ring은 Descriptor Table, Available Ring, Used Ring 3개 테이블로 구성되고, 최신 Packed Ring(v1.1+)은 단일 테이블에 AVAIL/USED 플래그와 wrap counter를 사용하여 캐시(Cache) 효율을 크게 개선합니다.
Feature Negotiation
virtio 디바이스 초기화 시 드라이버와 디바이스가 각자 지원하는 기능 비트(feature bits)를 교환하여 교집합으로 동작 모드를 결정합니다. 이 협상 과정 덕분에 신규 기능 추가 시에도 하위 호환성이 보장됩니다. 예: VIRTIO_NET_F_MQ(멀티큐), VIRTIO_F_RING_PACKED(packed ring) 등.
vhost-net / vhost-user 데이터 패스
vhost-net은 커널 모듈(/dev/vhost-net)로 네트워크 데이터 경로를 QEMU를 거치지 않고 커널에서 직접 처리하여 ~30% 성능을 향상시킵니다. vhost-user는 Unix 도메인 소켓(Socket)을 통해 DPDK/SPDK 같은 유저스페이스 프로세스(Process)에서 데이터 경로를 처리하여 100Gbps급 처리량(Throughput)을 달성합니다.
vDPA 하드웨어 오프로드
vDPA(virtio Data Path Acceleration)는 SmartNIC(NVIDIA ConnectX, Intel FPGA 등)의 하드웨어가 직접 virtqueue를 처리하여 호스트 CPU 부담을 완전히 제거합니다. 소프트웨어 virtio 인터페이스를 유지하면서 네이티브에 가까운 성능을 달성하는 것이 핵심입니다.
virtio-fs / vsock 확장
virtio-fs는 FUSE 프로토콜 기반 파일시스템(Filesystem) 공유로 DAX window를 통한 직접 메모리 매핑(Mapping)을 지원합니다. virtio-vsock은 AF_VSOCK 소켓 패밀리로 VM↔호스트 간 네트워크 설정 없이 CID 기반 직접 통신을 제공합니다. 컨테이너(Container)/마이크로VM(Kata Containers, Firecracker)에서 필수적인 통신 채널입니다.
단계별 이해
virtio/vhost 학습 로드맵
QEMU + virtio-net 첫 설정
QEMU에서 virtio-net 디바이스를 추가하고 게스트 부팅 후 lspci, lsmod로 virtio 드라이버가 로드되었는지 확인합니다. TAP 네트워크를 설정하여 호스트-게스트 간 ping이 동작하는지 검증합니다.
# 최소 virtio-net QEMU 실행
qemu-system-x86_64 -m 2G -smp 2 \
-netdev tap,id=net0,ifname=tap0,script=no \
-device virtio-net-pci,netdev=net0
virtqueue 동작 원리 이해
게스트 드라이버가 virtqueue_add_sgs()로 버퍼(Buffer)를 등록하고, virtqueue_kick()으로 호스트에 알리며, virtqueue_get_buf()로 완료된 버퍼를 회수하는 3단계 흐름을 이해합니다. /sys/kernel/debug/virtio-pci/ 디버그 파일을 통해 실제 ring 상태를 관찰할 수 있습니다.
Feature Negotiation 과정 추적
dmesg | grep virtio로 부팅 시 feature negotiation 로그를 확인합니다. /sys/bus/virtio/devices/virtio0/features에서 협상된 기능 비트를 읽어 어떤 기능이 활성화되었는지 분석합니다.
vhost-net 활성화
QEMU 옵션에 vhost=on을 추가하여 커널 데이터 경로를 활성화합니다. perf stat으로 VM Exit 횟수 감소를 확인하고, iperf3로 실제 처리량 향상을 측정합니다.
# vhost-net 활성화된 QEMU 설정
qemu-system-x86_64 -m 2G -smp 4 \
-netdev tap,id=net0,vhost=on,queues=4 \
-device virtio-net-pci,netdev=net0,mq=on,vectors=10
성능 측정 (iperf3/fio)
네트워크: iperf3 -c <host> -P 4로 멀티스트림 대역폭(Bandwidth), netperf로 레이턴시를 측정합니다. 스토리지: fio --ioengine=io_uring --bs=4k --numjobs=4 --iodepth=64로 IOPS를 벤치마크합니다. vhost on/off, split/packed ring 등 설정 변경 시 성능 차이를 비교합니다.
vDPA 하드웨어 오프로드
SmartNIC(예: ConnectX-6 Dx)이 있다면 vdpa dev add 명령으로 vDPA 디바이스를 생성하고, vhost-vdpa 백엔드로 QEMU에 연결합니다. 호스트 CPU 사용률이 거의 0에 가까운 것을 확인합니다.
virtio 입문 가이드
전가상화 vs 반가상화: virtio를 왜 쓰는가
전가상화(full virtualization)에서는 게스트 OS가 실제 하드웨어 드라이버(e1000, IDE 등)를 사용하고, QEMU가 해당 하드웨어를 소프트웨어로 완전히 에뮬레이션합니다. 모든 I/O 접근이 VM Exit를 유발하므로 성능이 크게 저하됩니다. 반가상화(paravirtualization)인 virtio는 게스트가 가상화 환경을 인지하고, 공유 메모리 기반의 최적화된 프로토콜로 통신하여 VM Exit를 최소화합니다.
| 비교 항목 | 전가상화 (e1000) | 반가상화 (virtio-net) |
|---|---|---|
| VM Exit 횟수/패킷(Packet) | ~10회 | ~1회 (batch 시 0) |
| 네트워크 대역폭 | ~2 Gbps | ~40 Gbps (vhost) |
| CPU 오버헤드(Overhead) | 높음 | 낮음 |
| 게스트 드라이버 | 기존 HW 드라이버 | virtio 전용 드라이버 필요 |
QEMU에서 virtio 디바이스 추가
# virtio-net + virtio-blk 기본 설정
qemu-system-x86_64 -m 4G -smp 4 -enable-kvm \
# virtio 네트워크
-netdev user,id=net0 \
-device virtio-net-pci,netdev=net0 \
# virtio 블록 디바이스
-drive file=disk.qcow2,if=none,id=drv0 \
-device virtio-blk-pci,drive=drv0 \
# virtio 콘솔
-device virtio-serial-pci \
-device virtconsole,chardev=con0 \
-chardev stdio,id=con0
게스트 커널에서 virtio 드라이버 확인
# PCI 디바이스 확인
lspci | grep -i virtio
# 00:03.0 Ethernet controller: Red Hat, Inc. Virtio network device
# 00:04.0 SCSI storage controller: Red Hat, Inc. Virtio block device
# 로드된 모듈 확인
lsmod | grep virtio
# virtio_net 65536 0
# virtio_blk 24576 2
# virtio_pci 28672 0
# virtio_ring 36864 3 virtio_net,virtio_blk,virtio_pci
# virtio 20480 3 virtio_net,virtio_blk,virtio_pci
# sysfs에서 virtio 디바이스 정보
ls /sys/bus/virtio/devices/
# virtio0 virtio1 virtio2
cat /sys/bus/virtio/devices/virtio0/device
# 0x0001 (network device)
cat /sys/bus/virtio/devices/virtio0/features
# 협상된 feature bits (비트맵)
CONFIG_VIRTIO, CONFIG_VIRTIO_PCI, CONFIG_VIRTIO_NET, CONFIG_VIRTIO_BLK 등을 활성화해야 합니다. 대부분의 배포판 커널은 이미 모듈로 포함하고 있으며, initramfs에서 부팅하려면 CONFIG_VIRTIO_BLK=y로 빌트인이 필요할 수 있습니다.
0x1AF4(Red Hat)를 사용합니다. Device ID 범위: 0x1000-0x103F는 Transitional(레거시 호환), 0x1040-0x107F는 Modern-only입니다. 예: virtio-net=0x1041, virtio-blk=0x1042.
virtio 개요
virtio는 OASIS 표준으로 정의된 반가상화(paravirtualized) I/O 프레임워크입니다. 게스트 OS가 하이퍼바이저의 가상 디바이스와 효율적으로 통신하기 위한 표준 인터페이스를 제공하며, 전가상화(full virtualization) 대비 최소 2-5배 높은 I/O 성능을 달성합니다.
| 방식 | 게스트 드라이버 | 디바이스 에뮬레이션 | 성능 |
|---|---|---|---|
| 전가상화 | 실제 HW 드라이버 (e1000, IDE) | QEMU에서 전체 HW 에뮬레이션 | 낮음 (VM Exit 빈번) |
| 반가상화 (virtio) | virtio 전용 드라이버 | 공유 메모리 + 이벤트 알림 | 높음 (최소 VM Exit) |
| 패스스루 (VFIO) | 실제 HW 드라이버 | 없음 (직접 접근) | 네이티브 |
virtio 아키텍처
virtio는 3개 계층으로 구성됩니다: 드라이버(frontend), virtio 코어, transport.
/* include/linux/virtio.h — 핵심 구조체 */
struct virtio_device {
int index;
bool failed;
bool config_enabled;
struct device dev;
struct virtio_device_id id; /* device/vendor ID */
const struct virtio_config_ops *config;
const struct vringh_config_ops *vringh_config;
struct list_head vqs; /* virtqueue 리스트 */
u64 features; /* 협상된 기능 비트 */
void *priv; /* 드라이버 전용 데이터 */
};
struct virtio_driver {
struct device_driver driver;
const struct virtio_device_id *id_table;
const unsigned int *feature_table;
unsigned int feature_table_size;
int (*probe)(struct virtio_device *dev);
void (*remove)(struct virtio_device *dev);
void (*config_changed)(struct virtio_device *dev);
};
기능 협상(feature negotiation)은 virtio의 핵심 메커니즘입니다. 드라이버와 디바이스가 각자 지원하는 기능 비트의 교집합으로 동작 모드를 결정합니다.
Virtqueue
Virtqueue는 게스트와 호스트 간 데이터 교환의 핵심 구조입니다. Split Virtqueue(레거시)와 Packed Virtqueue(v1.1+) 두 가지 형식이 있습니다.
/* include/uapi/linux/virtio_ring.h */
struct vring_desc {
__le64 addr; /* 게스트 물리 주소 */
__le32 len; /* 버퍼 길이 */
__le16 flags; /* VRING_DESC_F_NEXT | WRITE | INDIRECT */
__le16 next; /* 체인의 다음 descriptor 인덱스 */
};
/* virtqueue API (게스트 드라이버 사용) */
int virtqueue_add_sgs(struct virtqueue *vq,
struct scatterlist *sgs[], unsigned int out_sgs,
unsigned int in_sgs, void *data, gfp_t gfp);
bool virtqueue_kick(struct virtqueue *vq);
void *virtqueue_get_buf(struct virtqueue *vq, unsigned int *len);
Split Ring 상세 구조
Split Virtqueue는 virtio 1.0 이전부터 사용된 레거시 형식으로, 3개의 독립된 메모리 영역으로 구성됩니다. 각 영역의 역할과 구조를 상세히 분석합니다.
Descriptor Table
Descriptor Table은 게스트 메모리에 위치한 버퍼의 주소와 속성을 기술하는 배열입니다. 각 엔트리는 16바이트이며, 최대 215(32768)개의 descriptor를 포함할 수 있습니다.
/* include/uapi/linux/virtio_ring.h — Descriptor 구조 */
struct vring_desc {
__le64 addr; /* 게스트 물리 주소 (GPA) */
__le32 len; /* 버퍼 길이 (바이트) */
__le16 flags; /* NEXT | WRITE | INDIRECT */
__le16 next; /* 체인의 다음 descriptor 인덱스 */
};
/* Descriptor flags */
#define VRING_DESC_F_NEXT 1 /* 다음 desc로 체인 계속 */
#define VRING_DESC_F_WRITE 2 /* 디바이스 쓰기용 (수신 버퍼) */
#define VRING_DESC_F_INDIRECT 4 /* 간접 descriptor 테이블 */
| 필드 | 크기 | 설명 |
|---|---|---|
addr | 8바이트 | 게스트 물리 주소 (GPA). 호스트가 GPA→HVA 변환하여 접근 |
len | 4바이트 | 버퍼 길이. 디바이스는 이 범위를 초과하여 접근해서는 안 됨 |
flags | 2바이트 | NEXT(체인), WRITE(디바이스 쓰기용), INDIRECT(간접) |
next | 2바이트 | NEXT 플래그 설정 시 다음 descriptor의 인덱스 |
| Flag 비트 | 값 | 의미 | 사용 사례 |
|---|---|---|---|
VRING_DESC_F_NEXT | 0x1 | 다음 descriptor로 체인 | scatter-gather I/O |
VRING_DESC_F_WRITE | 0x2 | 디바이스가 쓸 수 있는 버퍼 | RX 버퍼, 응답 영역 |
VRING_DESC_F_INDIRECT | 0x4 | 별도 테이블을 가리키는 간접 descriptor | 대용량 scatter-gather |
Available Ring
Available Ring은 드라이버(게스트)가 디바이스(호스트)에 제출하는 descriptor head 인덱스 목록입니다.
struct vring_avail {
__le16 flags; /* VRING_AVAIL_F_NO_INTERRUPT */
__le16 idx; /* 다음 기록할 ring[] 위치 */
__le16 ring[]; /* descriptor head 인덱스 배열 */
/* __le16 used_event; (VIRTIO_F_EVENT_IDX 활성 시) */
};
/* 드라이버: 버퍼 제출 흐름 */
/* 1. desc[i] 기록 (addr, len, flags, next) */
/* 2. avail->ring[avail->idx % queue_size] = i; */
/* 3. wmb(); (메모리 배리어) */
/* 4. avail->idx++; */
/* 5. 필요 시 kick (MMIO write / PCI notify) */
Used Ring
Used Ring은 디바이스(호스트)가 처리 완료한 descriptor를 드라이버(게스트)에 반환하는 목록입니다.
struct vring_used_elem {
__le32 id; /* 완료된 descriptor head 인덱스 */
__le32 len; /* 디바이스가 실제 기록한 바이트 수 */
};
struct vring_used {
__le16 flags; /* VRING_USED_F_NO_NOTIFY */
__le16 idx; /* 다음 기록할 ring[] 위치 */
struct vring_used_elem ring[]; /* {id, len} 쌍의 배열 */
/* __le16 avail_event; (VIRTIO_F_EVENT_IDX 활성 시) */
};
VRING_DESC_F_INDIRECT 플래그가 설정된 descriptor가 가리키는 별도 메모리 영역에 descriptor 체인을 담습니다. virtqueue 크기에 관계없이 최대 수백 개의 세그먼트를 하나의 요청으로 묶을 수 있어, virtio-blk에서 대용량 I/O 요청 처리에 유용합니다.
wmb()(write memory barrier)를 삽입해야 합니다. 이 배리어가 없으면 디바이스가 아직 기록되지 않은 descriptor를 읽을 수 있습니다.
Packed Ring 상세 구조
Packed Virtqueue(virtio 1.1)는 Split Ring의 3개 영역을 단일 descriptor ring으로 통합하여 캐시 효율을 크게 개선합니다. 각 descriptor에 AVAIL/USED 플래그를 포함하고, Wrap Counter 메커니즘으로 ring의 소유권을 관리합니다.
/* Packed Virtqueue Descriptor */
struct vring_packed_desc {
__le64 addr; /* 버퍼 주소 (GPA) */
__le32 len; /* 버퍼 길이 */
__le16 id; /* 버퍼 식별자 (콜백 데이터 매핑) */
__le16 flags; /* AVAIL | USED | WRITE | NEXT | INDIRECT */
};
/* Packed descriptor flags */
#define VRING_PACKED_DESC_F_AVAIL (1 << 7) /* 드라이버 소유 */
#define VRING_PACKED_DESC_F_USED (1 << 15) /* 디바이스 소유 */
Wrap Counter 메커니즘
Packed Ring은 순환 버퍼의 경계를 Wrap Counter로 관리합니다. 드라이버와 디바이스 각각 독립적인 wrap counter를 유지합니다.
- 초기 상태: 드라이버 wrap counter = 1, 디바이스 wrap counter = 1
- 드라이버가 descriptor를 추가할 때:
AVAIL = wrap_counter,USED = !wrap_counter - ring 끝에 도달하면 wrap counter를 토글(0↔1)하고 ring 처음으로 돌아감
- 디바이스가 descriptor를 소비할 때:
AVAIL = wrap_counter,USED = wrap_counter(두 비트 모두 같은 값)
In-order Completion (VIRTIO_F_IN_ORDER)
디바이스가 descriptor를 제출 순서대로 처리 완료하는 것이 보장될 때 사용합니다. 디바이스는 마지막 descriptor의 USED 플래그만 설정하면 되므로 오버헤드가 크게 줄어듭니다. virtio-blk의 순차 I/O, virtio-net의 순차 전송 등에서 활용됩니다.
Event Suppression
불필요한 notification(kick/interrupt)을 억제하여 VM Exit를 줄입니다. Packed Ring은 Driver Event Suppression과 Device Event Suppression 구조를 별도로 제공합니다.
/* Event Suppression 구조 */
struct vring_packed_desc_event {
__le16 off_wrap; /* 이벤트 발생 위치 (off[15:1] | wrap[0]) */
__le16 flags; /* ENABLE(0) | DISABLE(1) | DESC(2) */
};
/* flags=DISABLE: 알림 완전 억제 (폴링 모드) */
/* flags=DESC: 특정 descriptor 위치에서만 알림 */
| 비교 항목 | Split Ring | Packed Ring |
|---|---|---|
| 메모리 영역 | 3개 (desc + avail + used) | 1개 (통합 desc ring) |
| 캐시 라인(Cache Line) 접근 | 최소 3개 캐시 라인 | 1개 캐시 라인으로 가능 |
| Wrap 관리 | idx 모듈러 연산 | Wrap Counter 토글 |
| In-order 최적화 | 불가 | VIRTIO_F_IN_ORDER |
| 레이턴시 | 기준 | ~15% 감소 |
| 메모리 사용 | 16N + 6+2N + 6+8N 바이트 | 16N 바이트 |
-device virtio-net-pci,packed=on으로 활성화합니다. 게스트와 호스트 모두 VIRTIO_F_RING_PACKED feature bit를 지원해야 합니다. Linux 커널 5.0+에서 packed virtqueue 드라이버를 지원합니다.
라이브 마이그레이션
가상 머신의 라이브 마이그레이션(live migration) 시 virtio 디바이스의 상태를 정확히 저장하고 복원해야 합니다. 이 과정에서 virtqueue의 ring 상태, feature 협상 결과, 디바이스 전용 설정을 모두 직렬화(Serialization)해야 합니다.
Device State Save/Restore 프로토콜
마이그레이션은 크게 3단계로 진행됩니다: (1) 디바이스 일시 중지, (2) 상태 직렬화 및 전송, (3) 대상 호스트에서 상태 복원 및 재개.
/* virtio 디바이스 마이그레이션 상태 (QEMU 내부) */
struct VirtIODeviceMigrationState {
u8 status; /* device status register */
u8 isr; /* ISR 상태 */
u16 queue_sel; /* 선택된 virtqueue */
u64 guest_features; /* 협상된 feature bits */
u32 config_gen; /* config generation counter */
/* 각 virtqueue별: */
u16 vq_last_avail_idx; /* 마지막 처리한 avail 인덱스 */
u16 vq_used_idx; /* 현재 used 인덱스 */
u16 vq_signalled_used; /* 인터럽트 전달된 used 위치 */
/* descriptor table, avail/used ring 내용 전체 */
};
VIRTIO_F_VERSION_1 호환성
마이그레이션 시 소스와 대상 호스트의 QEMU 버전이 다를 수 있으므로, VIRTIO_F_VERSION_1 feature bit로 modern(little-endian) 형식을 보장합니다. 레거시(guest-endian) 디바이스와 modern 디바이스 간 마이그레이션은 지원되지 않습니다.
Dirty Page Tracking
라이브 마이그레이션 중 게스트가 계속 실행되므로, 변경된 메모리 페이지를 추적해야 합니다. vhost는 VHOST_SET_LOG_BASE ioctl로 dirty page 비트맵(Bitmap)을 설정합니다.
/* vhost dirty page logging */
struct vhost_memory_log {
__u64 log_base; /* dirty 비트맵 시작 주소 */
__u64 log_size; /* 비트맵 크기 */
};
/* vhost가 게스트 메모리에 쓸 때마다 해당 페이지의 */
/* dirty bit를 log_base 비트맵에 설정 */
ioctl(vhost_fd, VHOST_SET_LOG_BASE, &log);
/* 마이그레이션 완료 후 */
ioctl(vhost_fd, VHOST_SET_LOG_FD, -1); /* 로깅 중지 */
| 마이그레이션 단계 | virtio 관련 작업 | 소요 시간 |
|---|---|---|
| Pre-copy | dirty page 로깅 시작, 전체 메모리 전송 | 수 초~수 분 |
| Stop-and-copy | 디바이스 중지, 상태 직렬화, 잔여 dirty 페이지 전송 | 수십 ms |
| Resume | 대상에서 상태 복원, feature 재검증, virtqueue 재설정 | 수 ms |
virsh migrate --live <domain> qemu+ssh://dest/system 또는 QMP: {"execute": "migrate", "arguments": {"uri": "tcp:dest:4444"}}. QEMU가 내부적으로 virtio 디바이스 상태를 자동으로 직렬화/역직렬화합니다.
VHOST_USER_PROTOCOL_F_LOG_SHMFD feature로 dirty logging을 지원하며, postcopy 마이그레이션은 VHOST_USER_PROTOCOL_F_PAGEFAULT가 필요합니다. vDPA 하드웨어 디바이스는 현재 라이브 마이그레이션 지원이 제한적입니다.
Transport 계층
virtio transport는 가상 디바이스의 발견(discovery), 설정(configuration), 알림(notification)을 담당합니다.
| Transport | 발견 방식 | 사용 환경 | 성능 |
|---|---|---|---|
| virtio-pci | PCI BAR, MSI-X | x86 서버, 클라우드 | 가장 높음 (MSI-X) |
| virtio-mmio | Device Tree / ACPI | ARM 임베디드, 경량 VM | 중간 (단일 인터럽트(Interrupt)) |
| virtio-ccw | Channel subsystem | IBM s390x | 높음 |
virtio-pci
virtio-pci는 가장 널리 사용되는 transport입니다. PCI Capability 구조를 통해 디바이스 설정 영역에 접근합니다.
/* drivers/virtio/virtio_pci_modern.c */
struct virtio_pci_modern_device {
struct pci_dev *pci_dev;
/* PCI Capability 매핑 영역 */
struct virtio_pci_common_cfg __iomem *common; /* 공통 설정 */
void __iomem *device; /* 디바이스 전용 설정 */
void __iomem *notify; /* 알림 영역 */
u8 __iomem *isr; /* ISR 상태 */
u32 notify_offset_multiplier;
int modern_bars; /* 사용 중인 BAR 비트맵 */
};
virtio-net
virtio-net은 가장 많이 사용되는 virtio 디바이스로, 네트워크 가상화를 담당합니다.
# QEMU virtio-net 설정
qemu-system-x86_64 \
-netdev tap,id=net0,vhost=on \
-device virtio-net-pci,netdev=net0,mq=on,vectors=6
# 게스트에서 멀티큐 활성화
ethtool -L eth0 combined 4
# 오프로드 확인
ethtool -k eth0 | grep -E "tx-checksum|tso|gro"
| 기능 비트 | 설명 |
|---|---|
VIRTIO_NET_F_MQ | 멀티큐 (RSS) |
VIRTIO_NET_F_CSUM | TX 체크섬(Checksum) 오프로드 |
VIRTIO_NET_F_GSO | Generic Segmentation Offload |
VIRTIO_NET_F_GUEST_TSO4/6 | 게스트 측 TSO 수신 |
VIRTIO_NET_F_CTRL_VQ | 제어 virtqueue (MAC 필터, VLAN) |
virtio-blk
virtio-blk은 블록 디바이스 가상화를 제공합니다. 멀티큐를 지원하여 고성능 NVMe SSD도 효율적으로 가상화합니다.
# QEMU virtio-blk 설정
qemu-system-x86_64 \
-drive file=disk.qcow2,if=none,id=drive0 \
-device virtio-blk-pci,drive=drive0,num-queues=4
# 게스트에서 확인
lsblk -d -o NAME,MODEL,ROTA,RQ-SIZE
# vda virtio_blk 0 256
# fio 벤치마크
fio --name=test --ioengine=io_uring --bs=4k --numjobs=4 \
--iodepth=64 --rw=randread --filename=/dev/vda
virtio-scsi
virtio-scsi는 SCSI 명령어를 전달하는 가상 HBA입니다. virtio-blk 대비 장점: 디스크 핫플러그(Hotplug), SCSI passthrough, 다수의 LUN 지원.
# virtio-scsi 설정 (QEMU)
qemu-system-x86_64 \
-device virtio-scsi-pci,id=scsi0,num_queues=4 \
-drive file=disk.qcow2,if=none,id=d0 \
-device scsi-hd,drive=d0,bus=scsi0.0,channel=0,scsi-id=0,lun=0
virtio-fs
virtio-fs는 호스트-게스트 간 파일시스템 공유를 위한 디바이스입니다. FUSE 프로토콜 기반이지만 DAX window로 데이터를 직접 매핑하여 높은 성능을 제공합니다.
# 호스트: virtiofsd 데몬 시작
virtiofsd --socket-path=/tmp/vhostqemu --shared-dir=/path/to/share
# QEMU 설정
qemu-system-x86_64 \
-chardev socket,id=char0,path=/tmp/vhostqemu \
-device vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=myfs
# 게스트에서 마운트
mount -t virtiofs myfs /mnt/shared
virtio-gpu
virtio-gpu는 GPU 가상화를 제공합니다. 2D 렌더링은 기본이며, virgl(OpenGL) 또는 Venus(Vulkan) 프로토콜로 3D 가속을 지원합니다.
# 3D 가속 virtio-gpu (virgl)
qemu-system-x86_64 \
-device virtio-gpu-gl-pci \
-display gtk,gl=on
virtio-balloon
virtio-balloon은 VM의 메모리를 동적으로 조절합니다. 벌루닝(inflate)으로 게스트 메모리를 호스트에 반환하고, 디플레이트(deflate)로 다시 할당받습니다.
# 벌루닝: 호스트에서 QMP로 메모리 조정
{ "execute": "balloon", "arguments": { "value": 2147483648 } }
# → 게스트 메모리를 2GB로 제한
# Free Page Reporting (v5.6+)
# 게스트가 free 페이지를 호스트에 알려 KSM/ballooning 효율 향상
qemu-system-x86_64 -device virtio-balloon-pci,free-page-reporting=on
vhost 아키텍처
vhost는 virtio의 데이터 경로(data plane)를 QEMU 유저스페이스 대신 커널 또는 별도 프로세스에서 처리하여 성능을 크게 향상시킵니다.
vhost-net
vhost-net은 커널 모듈(/dev/vhost-net)로 virtio-net의 데이터 경로를 커널에서 직접 처리합니다.
/* drivers/vhost/net.c — 핵심 구조 */
struct vhost_net {
struct vhost_dev dev;
struct vhost_net_virtqueue vqs[VHOST_NET_VQ_MAX];
struct vhost_poll poll[VHOST_NET_VQ_MAX];
struct socket *tx_sock; /* TAP 소켓 */
struct socket *rx_sock;
};
/* QEMU에서 vhost-net 활성화 */
/* ioctl(vhost_fd, VHOST_SET_OWNER) */
/* ioctl(vhost_fd, VHOST_SET_MEM_TABLE, ...) */
/* ioctl(vhost_fd, VHOST_NET_SET_BACKEND, ...) */
vhost-user
vhost-user는 데이터 경로를 유저스페이스 프로세스에서 처리합니다. DPDK/SPDK와의 통합에 핵심입니다.
# DPDK testpmd를 vhost-user 백엔드로 사용
dpdk-testpmd --vdev 'net_vhost0,iface=/tmp/vhost-user0,queues=2' \
-- -i --nb-cores=2
# QEMU에서 vhost-user 프론트엔드 연결
qemu-system-x86_64 \
-chardev socket,id=chr0,path=/tmp/vhost-user0 \
-netdev vhost-user,id=net0,chardev=chr0,vhostforce \
-device virtio-net-pci,netdev=net0
mmap으로 직접 접근합니다. 별도의 데이터 복사 없이 zero-copy에 가깝게 동작합니다.
vhost-vsock
vhost-vsock은 AF_VSOCK 소켓을 통해 VM-호스트 간 직접 통신을 제공합니다. 네트워크 설정 없이 CID(Context ID) 기반으로 연결합니다.
# QEMU에서 vsock 활성화
qemu-system-x86_64 -device vhost-vsock-pci,guest-cid=3
# 호스트에서 서버 실행 (CID 2 = 호스트)
socat VSOCK-LISTEN:1234,fork -
# 게스트에서 연결 (CID 2 = 호스트)
socat - VSOCK-CONNECT:2:1234
vhost-user 프로토콜
vhost-user는 QEMU와 유저스페이스 데이터 패스 프로세스(DPDK, SPDK 등) 간 Unix 도메인 소켓을 통해 제어 메시지를 교환하고, 게스트 메모리에 mmap으로 직접 접근하는 프로토콜입니다.
메시지 형식
모든 vhost-user 메시지는 고정 크기 헤더와 가변 페이로드(Payload)로 구성됩니다. 파일 디스크립터(메모리 매핑, eventfd 등)는 Unix 소켓의 ancillary data로 전달합니다.
/* vhost-user 메시지 헤더 */
struct vhost_user_msg_header {
u32 request; /* 메시지 유형 (enum VhostUserRequest) */
u32 flags; /* VERSION(0x1) | REPLY(0x4) | NEED_REPLY(0x8) */
u32 size; /* 페이로드 크기 */
};
/* + payload (최대 ~4KB) */
/* + ancillary data: fd[] (SCM_RIGHTS) */
/* 메모리 테이블 설정 예 */
struct vhost_user_memory_region {
u64 guest_phys_addr; /* 게스트 물리 주소 */
u64 memory_size; /* 영역 크기 */
u64 userspace_addr; /* QEMU 가상 주소 */
u64 mmap_offset; /* mmap 오프셋 */
};
/* fd로 memfd/hugepage 파일 디스크립터 전달 */
| 메시지 유형 | 방향 | 설명 |
|---|---|---|
GET_FEATURES | Frontend → Backend | Backend 지원 feature bits 조회 |
SET_FEATURES | Frontend → Backend | 협상된 feature bits 설정 |
SET_MEM_TABLE | Frontend → Backend | 게스트 메모리 영역 매핑 (fd 전달) |
SET_VRING_NUM | Frontend → Backend | virtqueue 크기 설정 |
SET_VRING_ADDR | Frontend → Backend | virtqueue 주소 (desc, avail, used) |
SET_VRING_KICK | Frontend → Backend | kick eventfd 전달 |
SET_VRING_CALL | Frontend → Backend | call eventfd 전달 (인터럽트 주입) |
SET_VRING_ENABLE | Frontend → Backend | virtqueue 활성화/비활성화 |
GET_CONFIG | Frontend → Backend | 디바이스 설정 공간 읽기 |
SET_LOG_BASE | Frontend → Backend | 마이그레이션 dirty logging 시작 |
Shared Memory 매핑
vhost-user backend는 SET_MEM_TABLE 메시지와 함께 전달받은 파일 디스크립터를 mmap()하여 게스트 메모리에 직접 접근합니다. hugepage 기반 메모리(-mem-path /dev/hugepages)를 사용하면 TLB 미스를 줄여 성능이 향상됩니다.
DPDK vhost-user Backend 예제
/* DPDK vhost-user backend 초기화 (rte_vhost API) */
static const struct rte_vhost_device_ops vhost_ops = {
.new_device = vhost_new_device, /* DRIVER_OK 시 호출 */
.destroy_device = vhost_destroy_device,
.vring_state_changed = vhost_vring_state,
};
/* Unix 소켓 등록 */
rte_vhost_driver_register("/tmp/vhost-user0", 0);
rte_vhost_driver_set_features("/tmp/vhost-user0", features);
rte_vhost_driver_callback_register("/tmp/vhost-user0", &vhost_ops);
rte_vhost_driver_start("/tmp/vhost-user0");
/* 데이터 경로: zero-copy enqueue/dequeue */
uint16_t nb = rte_vhost_dequeue_burst(vid, queue_id,
mbuf_pool, pkts, MAX_BURST);
rte_vhost_enqueue_burst(vid, queue_id, pkts, nb);
reconnect=1 옵션으로 vhost-user 백엔드의 재시작(Reboot)을 지원합니다. 백엔드 프로세스가 재시작되면 QEMU가 자동으로 재연결하여 메모리 테이블과 virtqueue 설정을 다시 전달합니다. 이를 통해 DPDK 애플리케이션의 무중단 업데이트가 가능합니다.
VHOST_USER_PROTOCOL_F_MQ 프로토콜 feature로 멀티큐를 지원합니다. 각 virtqueue에 대해 별도의 SET_VRING_* 메시지를 교환하며, 각 큐는 독립적인 eventfd로 kick/call을 처리합니다. RSS를 통해 트래픽을 여러 큐에 분산할 수 있습니다.
virtio-fs DAX/fscache 최적화
virtio-fs는 FUSE 프로토콜 기반의 호스트-게스트 파일시스템 공유 솔루션입니다. 일반적인 네트워크 파일시스템(9p, NFS)과 달리, DAX(Direct Access) window를 통해 호스트 페이지 캐시(Page Cache)를 게스트에서 직접 메모리 매핑하여 데이터 복사를 완전히 제거할 수 있습니다.
virtio-fs 아키텍처
virtio-fs는 게스트의 FUSE 클라이언트가 virtqueue를 통해 호스트의 virtiofsd 데몬과 통신합니다. virtiofsd는 FUSE 요청을 받아 호스트 파일시스템에 대한 실제 I/O를 수행합니다.
# 호스트: virtiofsd 데몬 시작 (Rust 버전, 권장)
virtiofsd \
--socket-path=/tmp/virtiofs.sock \
--shared-dir=/export/shared \
--cache=auto \
--thread-pool-size=4
# QEMU 설정 (DAX window 활성화)
qemu-system-x86_64 -m 4G -smp 4 -enable-kvm \
-chardev socket,id=fs0,path=/tmp/virtiofs.sock \
-device vhost-user-fs-pci,queue-size=1024,\
chardev=fs0,tag=myfs,cache-size=2G \
-object memory-backend-memfd,id=mem,size=4G,share=on \
-numa node,memdev=mem
# 게스트에서 마운트
mount -t virtiofs myfs /mnt/shared
# DAX 모드 확인
mount | grep virtiofs
# myfs on /mnt/shared type virtiofs (rw,relatime,dax=inode)
DAX Window 매핑
DAX window는 게스트의 PCI BAR 영역에 매핑된 메모리 창입니다. 게스트가 파일을 mmap()하면, virtiofsd가 해당 파일 영역을 DAX window에 매핑하고, 게스트는 이후 데이터를 복사 없이 직접 접근합니다.
| cache 모드 | 메타데이터 캐시 | 데이터 캐시 | 용도 |
|---|---|---|---|
cache=auto | 타임아웃 기반 | DAX 가능 | 일반 사용 (권장) |
cache=always | 영구 캐시 | DAX 가능 | 단일 게스트 접근 |
cache=never | 캐시 없음 | 캐시 없음 | 다중 게스트 동시 접근 |
mmap() 기반 접근에서는 호스트 네이티브 파일시스템과 거의 동일한 성능을 보입니다. Kata Containers, Cloud Hypervisor 등 컨테이너 런타임에서 컨테이너 이미지 마운트(Mount)에 활용됩니다.
share=on)로 할당해야 합니다. QEMU에서 memory-backend-memfd 또는 memory-backend-file + hugepage를 사용하세요. 공유 메모리 없이는 virtiofsd가 게스트 메모리에 접근할 수 없습니다.
NUMA 인지 Virtqueue 할당
대규모 VM(수십 개 vCPU, 수백 GB 메모리)에서는 virtqueue 버퍼와 vhost 워커 스레드(Thread)의 NUMA(Non-Uniform Memory Access) 배치가 성능에 큰 영향을 미칩니다. 잘못된 NUMA 배치는 원격 메모리 접근으로 인해 레이턴시가 2-3배 증가할 수 있습니다.
Virtqueue 버퍼 NUMA-aware 할당
Linux 커널의 virtio 드라이버는 find_vqs() 호출 시 IRQ affinity hint를 전달하여, 각 virtqueue의 인터럽트가 해당 NUMA 노드의 CPU에 배정되도록 합니다. virtqueue 버퍼도 해당 NUMA 노드의 메모리에서 할당하는 것이 이상적입니다.
/* virtio-pci: IRQ affinity와 NUMA 매핑 */
static int vp_find_vqs_msix(struct virtio_device *vdev,
unsigned int nvqs, struct virtqueue *vqs[],
struct irq_affinity *desc)
{
/* MSI-X 벡터를 NUMA 인지하여 할당 */
/* irq_set_affinity_hint()로 CPU affinity 설정 */
/* 각 큐의 인터럽트가 동일 NUMA 노드 CPU에 배정 */
}
/* vring 버퍼 할당 시 NUMA 노드 지정 */
void *vring_alloc_queue(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag, int node)
{
/* NUMA 노드를 지정하여 메모리 할당 */
return dma_alloc_coherent_node(dev, size, dma_handle,
flag, node);
}
vhost Worker Thread CPU Affinity
vhost-net 커널 스레드(vhost-<pid>)를 적절한 CPU에 바인딩하면 캐시 효율이 개선됩니다.
# vhost 워커 스레드 확인
ps aux | grep vhost
# root 1234 vhost-1234-0 (TX)
# root 1235 vhost-1234-1 (RX)
# NUMA 노드 0의 CPU(0-7)에 바인딩
taskset -p -c 0-7 1234
taskset -p -c 0-7 1235
# libvirt XML로 설정
# <cputune>
# <emulatorpin cpuset="0-7"/>
# </cputune>
Large VM NUMA 토폴로지(Topology) 매핑
| 설정 항목 | 권장 사항 | 효과 |
|---|---|---|
| vCPU pinning | 동일 NUMA 노드 물리 CPU에 고정 | L3 캐시 공유, 원격 접근 제거 |
| 메모리 바인딩 | numactl --membind 또는 libvirt memnode | 로컬 메모리 접근 보장 |
| Virtqueue IRQ | 해당 vCPU의 물리 CPU와 동일 NUMA | 인터럽트 처리 최적화 |
| vhost 스레드 | emulatorpin으로 NUMA 내 고정 | cross-NUMA 접근 방지 |
| hugepage | NUMA 노드별 hugepage 할당 | TLB 미스 감소 + 로컬 접근 |
numastat -p <qemu-pid>로 QEMU 프로세스의 NUMA 노드별 메모리 사용량을 확인하세요. numa_miss 카운터가 높으면 cross-NUMA 접근이 빈번한 것이므로 CPU/메모리 바인딩을 점검해야 합니다. perf stat -e node-loads,node-load-misses로 NUMA 접근 패턴도 분석할 수 있습니다.
virtio-iommu
virtio-iommu는 가상 IOMMU를 제공하여 게스트 내에서 디바이스 DMA 격리(Isolation)를 구현합니다. 중첩 가상화나 VFIO 디바이스의 게스트 내 격리에 사용됩니다.
# QEMU virtio-iommu 설정
qemu-system-x86_64 \
-device virtio-iommu-pci \
-device virtio-net-pci,iommu_platform=on
vDPA (virtio Data Path Acceleration)
vDPA는 virtio 데이터 경로를 하드웨어에 오프로드합니다. SmartNIC의 virtio 호환 하드웨어가 직접 virtqueue를 처리하여 호스트 CPU 부담을 제거합니다.
# vDPA 디바이스 생성 (Mellanox ConnectX-6 예)
vdpa dev add name vdpa0 mgmtdev pci/0000:3b:00.0
# vhost-vdpa 백엔드로 QEMU 연결
qemu-system-x86_64 \
-netdev vhost-vdpa,vhostdev=/dev/vhost-vdpa-0,id=net0 \
-device virtio-net-pci,netdev=net0
# 커널 모듈: vhost-vdpa, vdpa, mlx5_vdpa / ifcvf
커널 내부 구조
virtio 서브시스템은 virtio_bus를 중심으로 드라이버와 transport를 연결합니다.
/* include/linux/virtio_config.h — config 콜백 */
struct virtio_config_ops {
void (*get)(struct virtio_device *vdev, unsigned offset,
void *buf, unsigned len);
void (*set)(struct virtio_device *vdev, unsigned offset,
const void *buf, unsigned len);
u32 (*generation)(struct virtio_device *vdev);
u8 (*get_status)(struct virtio_device *vdev);
void (*set_status)(struct virtio_device *vdev, u8 status);
void (*reset)(struct virtio_device *vdev);
int (*find_vqs)(struct virtio_device *, unsigned nvqs,
struct virtqueue *vqs[],
vq_callback_t *callbacks[],
const char * const names[],
struct irq_affinity *desc);
void (*del_vqs)(struct virtio_device *);
u64 (*get_features)(struct virtio_device *vdev);
int (*finalize_features)(struct virtio_device *vdev);
};
성능 최적화
virtio 성능을 극대화하기 위한 주요 기법입니다.
| 기법 | 설명 | 효과 |
|---|---|---|
| vhost | 커널 데이터 경로 | ~30% 처리량 향상 |
| 멀티큐 | CPU별 독립 virtqueue | 멀티코어 확장성 |
| Packed ring | 캐시 친화적 ring 구조 | ~15% 레이턴시 감소 |
| Interrupt coalescing | 알림 억제 (VRING_AVAIL_F_NO_INTERRUPT) | VM Exit 감소 |
| Zerocopy TX | sendmsg(MSG_ZEROCOPY) | CPU 사용률 감소 |
| NAPI 연동 | virtio-net RX: NAPI 폴링(Polling) 모드 | 높은 PPS 처리 |
| vDPA | 하드웨어 오프로드 | 네이티브 성능 |
# 최적화된 QEMU 설정
qemu-system-x86_64 \
-netdev tap,id=net0,vhost=on,queues=4 \
-device virtio-net-pci,netdev=net0,mq=on,vectors=10,\
packed=on,iommu_platform=off \
-drive file=disk.qcow2,if=none,id=drv0,cache=none,aio=io_uring \
-device virtio-blk-pci,drive=drv0,num-queues=4
# 게스트에서 멀티큐 활성화
ethtool -L eth0 combined 4
# iperf3 벤치마크
iperf3 -c 192.168.1.1 -P 4 -t 30
보안 고려사항
virtio의 보안 모델에서 게스트는 비신뢰(untrusted) 영역입니다.
| 위협 | 대응 |
|---|---|
| 악의적 descriptor | IOMMU + bounce buffer, 주소 범위 검증 |
| DMA 공격 | virtio-iommu, VFIO 격리 |
| 기밀 컴퓨팅(Confidential Computing) | AMD SEV + virtio-mem (encrypted memory) |
| DoS (무한 체인) | descriptor 체인 길이 제한 (indirect_desc_max) |
실전 활용
QEMU/KVM + libvirt 환경에서 virtio 설정 예시입니다.
<!-- libvirt XML: virtio 디바이스 설정 -->
<devices>
<!-- virtio-net with vhost -->
<interface type='network'>
<source network='default'/>
<model type='virtio'/>
<driver name='vhost' queues='4'/>
</interface>
<!-- virtio-blk -->
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2' cache='none' io='io_uring'/>
<source file='/var/lib/libvirt/images/disk.qcow2'/>
<target dev='vda' bus='virtio'/>
</disk>
<!-- virtio-fs -->
<filesystem type='mount' accessmode='passthrough'>
<driver type='virtiofs'/>
<source dir='/path/to/share'/>
<target dir='myfs'/>
</filesystem>
<!-- vsock -->
<vsock model='virtio'>
<cid auto='yes'/>
</vsock>
</devices>
virtio-crypto (암호 오프로드)
virtio-crypto는 암호화 연산(cipher, hash, MAC, AEAD)을 가상 디바이스로 오프로드하는 virtio 디바이스입니다. 게스트의 Linux Crypto API와 통합되어, 호스트의 하드웨어 암호 가속기(Intel QAT, ARM CE 등)를 투명하게 활용할 수 있습니다.
지원 암호 서비스
| 서비스 | 알고리즘 예시 | virtio 큐 |
|---|---|---|
| Cipher | AES-CBC, AES-CTR, AES-XTS | dataq (대칭 암호) |
| Hash | SHA-256, SHA-512 | dataq |
| MAC | HMAC-SHA256, CMAC-AES | dataq |
| AEAD | AES-GCM, ChaCha20-Poly1305 | dataq |
| Akcipher | RSA, ECDSA (v1.2+) | dataq (비대칭 암호) |
/* drivers/crypto/virtio/virtio_crypto_core.c */
static int virtcrypto_probe(struct virtio_device *vdev)
{
struct virtio_crypto *vcrypto;
/* Feature 협상 */
if (virtio_has_feature(vdev, VIRTIO_CRYPTO_F_CIPHER_STATELESS))
vcrypto->cipher_algo_l = vp_ioread32(...);
/* Crypto API 등록 */
virtio_crypto_algs_register(vcrypto);
/* → crypto_register_skcipher(), crypto_register_aead() */
}
QEMU Backend 설정
# QEMU virtio-crypto 설정 (builtin backend)
qemu-system-x86_64 -m 4G -enable-kvm \
-object cryptodev-backend-builtin,id=crypto0 \
-device virtio-crypto-pci,cryptodev=crypto0
# Intel QAT 하드웨어 가속 backend
qemu-system-x86_64 -m 4G -enable-kvm \
-object cryptodev-vhost-user,id=crypto0,\
chardev=chr0,queues=2 \
-chardev socket,id=chr0,path=/tmp/qat-vhost.sock \
-device virtio-crypto-pci,cryptodev=crypto0
성능 벤치마크
# 게스트에서 virtio-crypto 확인
cat /proc/crypto | grep -A5 virtio
# name : cbc(aes)
# driver : virtio-crypto-cbc-aes
# priority : 150
# OpenSSL 벤치마크 (AF_ALG 엔진 사용)
openssl speed -engine afalg -evp aes-128-cbc
# virtio-crypto + QAT: ~5 GB/s (vs SW: ~1.5 GB/s)
AF_ALG 소켓을 통해 유저스페이스 애플리케이션(OpenSSL, GnuTLS)에서 투명하게 사용할 수 있습니다. OpenSSL의 afalg 엔진이나 devcrypto 엔진으로 활성화합니다.
cryptodev-vhost-user backend로 DPDK의 cryptodev를 게스트에 노출하면, Intel QAT/NVIDIA BlueField의 하드웨어 가속을 vhost-user 데이터 경로로 활용할 수 있습니다.
virtio-mem / virtio-pmem (동적 메모리)
virtio-mem은 VM의 메모리를 런타임에 동적으로 추가/제거하는 virtio 디바이스입니다. 기존 virtio-balloon이 메모리 "힌트"만 제공하는 것과 달리, virtio-mem은 실제 메모리 리소스를 plug/unplug합니다.
virtio-mem vs virtio-balloon
| 비교 항목 | virtio-balloon | virtio-mem |
|---|---|---|
| 메커니즘 | 게스트 페이지를 "풍선"에 넣어 사용 불가 표시 | 메모리 블록 단위 plug/unplug |
| 메모리 할당 | QEMU 시작 시 전체 할당 | 필요 시 동적 할당 |
| 해제 보장 | 약함 (게스트 협조 필요) | 강함 (호스트 제어) |
| Granularity | 4KB 페이지 | Subblock (기본 2MB/128MB) |
| Hot-remove | 지원 안 함 | 지원 (ZONE_MOVABLE) |
| 오버커밋 | 가능 | 정확한 메모리 관리(Memory Management) |
virtio-mem 동작 원리
virtio-mem은 메모리 영역을 subblock 단위로 관리합니다. 호스트가 요청한 크기(requested_size)에 맞춰 게스트가 subblock을 plug(메모리 핫플러그)하거나 unplug(메모리 핫리무브)합니다.
/* drivers/virtio/virtio_mem.c — 핵심 구조 */
struct virtio_mem {
struct virtio_device *vdev;
u64 plugged_size; /* 현재 plug된 메모리 */
u64 requested_size; /* 호스트 요청 크기 */
u64 region_size; /* 전체 메모리 영역 크기 */
u64 usable_region_size; /* 사용 가능 영역 */
u32 subblock_size; /* subblock 크기 (기본 2MB) */
/* 내부적으로 memory_block_online/offline 연동 */
};
/* virtio-mem 메시지 유형 */
enum virtio_mem_req_type {
VIRTIO_MEM_REQ_PLUG, /* subblock 플러그 */
VIRTIO_MEM_REQ_UNPLUG, /* subblock 언플러그 */
VIRTIO_MEM_REQ_UNPLUG_ALL, /* 전체 언플러그 */
VIRTIO_MEM_REQ_STATE, /* subblock 상태 조회 */
};
# QEMU virtio-mem 설정
qemu-system-x86_64 -m 2G,maxmem=16G \
-object memory-backend-ram,id=mem1,size=14G \
-device virtio-mem-pci,id=vmem0,memdev=mem1,\
node=0,requested-size=4G,block-size=128M
# 런타임 메모리 조정 (QMP)
# 메모리 늘리기
{ "execute": "qom-set",
"arguments": { "path": "/machine/peripheral/vmem0",
"property": "requested-size", "value": 8589934592 } }
# 메모리 줄이기
{ "execute": "qom-set",
"arguments": { "path": "/machine/peripheral/vmem0",
"property": "requested-size", "value": 2147483648 } }
virtio-pmem: 영구 메모리 Passthrough
virtio-pmem은 호스트의 영구 메모리(Persistent Memory, PMEM) 또는 일반 파일을 게스트에 NVDIMM처럼 노출합니다. 게스트는 /dev/pmemN 디바이스를 통해 DAX 모드로 직접 접근하거나, ext4/XFS의 dax=inode 옵션으로 파일시스템을 마운트합니다.
# QEMU virtio-pmem 설정
qemu-system-x86_64 -m 4G \
-object memory-backend-file,id=pmem0,\
mem-path=/dev/dax0.0,size=8G,share=on,pmem=on \
-device virtio-pmem-pci,memdev=pmem0,id=vpmem0
# 게스트에서 사용
ls /dev/pmem*
# /dev/pmem0
mkfs.ext4 /dev/pmem0
mount -o dax=inode /dev/pmem0 /mnt/pmem
/dev/daxN.M)는 파일시스템 없이 메모리를 직접 매핑하여 RDMA나 대용량 인메모리 데이터베이스에 사용합니다. Filesystem DAX(dax=inode)는 파일 단위로 직접 매핑하여 일반 애플리케이션 호환성을 유지하면서 페이지 캐시를 우회합니다.
memhp_default_state=online_movable 커널 파라미터로 핫플러그 메모리를 MOVABLE 존에 배치하는 것을 권장합니다.
성능 분석
virtio의 성능을 극대화하려면 virtqueue 수준의 최적화 기법을 이해해야 합니다. 배치 처리, zero-copy, 폴링 모드, notification suppression을 조합하면 네이티브에 가까운 성능을 달성할 수 있습니다.
Batch Processing (virtqueue_kick 최적화)
여러 버퍼를 Available Ring에 추가한 후 한 번의 kick으로 호스트에 알리면 VM Exit 횟수를 크게 줄일 수 있습니다. virtio-net의 xmit_more 힌트와 napi_schedule 연동이 대표적입니다.
/* virtio-net TX 배치 처리 */
static netdev_tx_t start_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct send_queue *sq = ...;
/* 버퍼 추가 (kick 없이) */
virtqueue_add_outbuf(sq->vq, sg, num, skb, GFP_ATOMIC);
/* xmit_more가 false일 때만 kick */
if (!netdev_xmit_more())
virtqueue_kick(sq->vq);
/* → 여러 패킷을 모아서 single kick */
/* → VM Exit 1회로 N개 패킷 전송 */
}
Zero-copy Transmission
데이터 복사를 제거하여 CPU 사용률을 감소시킵니다.
# vhost-net zero-copy TX 활성화
# QEMU 옵션: 기본 활성화 (커널 3.4+)
modprobe vhost_net experimental_zcopytx=1
# XDP + AF_XDP 기반 zero-copy (고급)
# 게스트의 AF_XDP 소켓이 virtqueue 버퍼를 직접 참조
# → 커널 네트워크 스택 우회 + 복사 제거
# 확인: vhost zerocopy 통계
cat /sys/module/vhost_net/parameters/experimental_zcopytx
# 1
Polling Mode (Busy Polling)
높은 PPS(Packets Per Second) 환경에서는 인터럽트 대신 폴링으로 레이턴시를 줄일 수 있습니다.
# 게스트: busy polling 활성화 (마이크로초)
sysctl -w net.core.busy_poll=50
sysctl -w net.core.busy_read=50
# ethtool coalescing 조정
ethtool -C eth0 rx-usecs 0 tx-usecs 0
# → 즉시 처리 (폴링 모드와 결합)
# virtio-net NAPI weight 조정
# napi_weight=256 (기본 64보다 높은 배치)
ethtool -C eth0 rx-frames 256
Notification Suppression
VRING_AVAIL_F_NO_INTERRUPT 플래그로 디바이스가 Used Ring에 기록할 때 인터럽트를 억제합니다. 드라이버는 NAPI 폴링으로 완료를 확인합니다.
| 최적화 기법 | 대상 | 효과 | 트레이드오프 |
|---|---|---|---|
| Batch kick | TX 경로 | VM Exit ~80% 감소 | 약간의 레이턴시 증가 |
| Zero-copy TX | vhost-net | CPU ~40% 감소 | completion 대기 필요 |
| Busy polling | RX 경로 | 레이턴시 ~50% 감소 | CPU 사용률 증가 |
| Notification suppression | 양방향 | 인터럽트 ~90% 감소 | 폴링 지연(Latency) |
| Packed ring | 전체 | 캐시 효율 ~15% 향상 | 레거시 호환 불가 |
| 멀티큐 + IRQ affinity | 전체 | CPU 확장성 | 큐 수 = vCPU 수 권장 |
vhost=on), (2) 멀티큐 + IRQ affinity 설정, (3) packed ring 활성화(packed=on), (4) hugepage 메모리(-mem-path /dev/hugepages), (5) vCPU pinning + NUMA 정렬, (6) 게스트에서 busy_poll 활성화. 이 조합으로 가상화 I/O 오버헤드를 5% 이내로 줄일 수 있습니다.
Virtio 사양(Specification)과 디바이스 초기화 시퀀스
Virtio 디바이스는 OASIS 표준 문서에 정의된 엄격한 초기화 시퀀스를 따라야 합니다. 이 시퀀스를 위반하면 디바이스가 정의되지 않은(undefined) 상태에 빠집니다.
디바이스 초기화 8단계
/* virtio 디바이스 초기화 시퀀스 (virtio 1.2 스펙 기준) */
/* 1. 디바이스 리셋 */
vp_reset(vdev); /* status = 0 기록 */
/* 2. ACKNOWLEDGE 비트 설정 — "드라이버가 디바이스를 인식" */
vp_set_status(vdev, VIRTIO_CONFIG_S_ACKNOWLEDGE);
/* 3. DRIVER 비트 설정 — "드라이버가 이 디바이스를 처리할 수 있음" */
vp_set_status(vdev, VIRTIO_CONFIG_S_DRIVER);
/* 4. 기능 비트(Feature Bits) 읽기 — 디바이스 지원 기능 확인 */
u64 device_features = vp_get_features(vdev);
/* 5. 기능 협상 — 드라이버가 사용할 기능 선택 */
u64 driver_features = device_features & supported_features;
vp_set_features(vdev, driver_features);
/* 6. FEATURES_OK 비트 설정 */
vp_set_status(vdev, VIRTIO_CONFIG_S_FEATURES_OK);
/* 7. FEATURES_OK 재확인 — 디바이스가 수락했는지 검증 */
if (!(vp_get_status(vdev) & VIRTIO_CONFIG_S_FEATURES_OK))
return -ENODEV; /* 디바이스가 기능 조합을 거부 */
/* 8. virtqueue 설정, 디바이스 고유 설정, DRIVER_OK 비트 설정 */
setup_virtqueues(vdev);
vp_set_status(vdev, VIRTIO_CONFIG_S_DRIVER_OK);
/* → 디바이스 사용 가능 */
주요 기능 비트(Feature Bits)
| 기능 비트 | 값 | 디바이스 | 설명 |
|---|---|---|---|
VIRTIO_F_RING_PACKED | 34 | 공통 | Packed Ring 사용 (v1.1+) |
VIRTIO_F_VERSION_1 | 32 | 공통 | 비레거시 virtio 1.0+ 디바이스 |
VIRTIO_F_ORDER_PLATFORM | 36 | 공통 | 플랫폼 메모리 순서 사용 |
VIRTIO_F_SR_IOV | 37 | 공통 | SR-IOV VF 지원 |
VIRTIO_NET_F_MQ | 22 | net | 멀티큐 (vCPU당 큐) |
VIRTIO_NET_F_CSUM | 0 | net | TX 체크섬 오프로드 |
VIRTIO_NET_F_HOST_TSO4 | 11 | net | 호스트 TCP 세그멘테이션 오프로드 |
VIRTIO_BLK_F_MQ | 12 | blk | 블록 멀티큐 |
VIRTIO_BLK_F_DISCARD | 13 | blk | TRIM/DISCARD 지원 |
전송 계층(Transport) 상세 비교
Virtio는 여러 전송 방식을 지원합니다. 각 전송 방식은 virtqueue의 위치 알림(notification)과 설정 공간(config space) 접근 방법이 다릅니다.
| 특성 | virtio-pci | virtio-mmio | virtio-ccw |
|---|---|---|---|
| 플랫폼 | x86, ARM (PCI 있는 환경) | ARM, RISC-V (임베디드) | s390x (IBM Z) |
| 디바이스 탐지 | PCI enumeration | Device Tree / ACPI | Channel 서브시스템 |
| config 접근 | PCI BAR (MMIO) | 메모리 매핑 레지스터 | CCW 명령 |
| 알림 (kick) | PCI BAR write → ioeventfd | MMIO write → ioeventfd | SSCH 명령 |
| 인터럽트 | MSI-X (벡터별) | 단일 IRQ / 공유 | Adapter 인터럽트 |
| 멀티큐 | 지원 (MSI-X 벡터별) | 제한적 | 지원 |
| 성능 | 최고 (MSI-X + ioeventfd) | 중간 | s390x 최적화 |
| QEMU 옵션 | -device virtio-*-pci | -device virtio-*-device | 자동 (s390x) |
virtio-pci 레지스터 맵
/* virtio-pci Capability 구조 (PCI config space) */
struct virtio_pci_cap {
__u8 cap_vndr; /* PCI cap ID: 0x09 (vendor-specific) */
__u8 cap_next; /* 다음 capability 오프셋 */
__u8 cap_len; /* capability 길이 */
__u8 cfg_type; /* COMMON=1, NOTIFY=2, ISR=3, DEVICE=4, PCI=5 */
__u8 bar; /* BAR 번호 (0-5) */
__u8 id; /* 다중 capability 구분용 */
__le16 padding;
__le32 offset; /* BAR 내 오프셋 */
__le32 length; /* 영역 길이 */
};
/*
* cfg_type별 용도:
* COMMON (1): device_feature, driver_feature, queue_select, queue_size, ...
* NOTIFY (2): virtqueue kick 레지스터 (ioeventfd와 매핑)
* ISR (3): 인터럽트 상태 (레거시 모드)
* DEVICE (4): 디바이스 고유 config (예: virtio-net의 mac, status)
* PCI (5): PCI 접근 대체 config 공간
*/
Virtio 드라이버 작성 가이드
새로운 virtio 디바이스 유형에 대한 커널 드라이버를 작성하는 방법을 설명합니다. virtio_driver 구조체 등록, probe/remove 콜백, virtqueue 사용법이 핵심입니다.
드라이버 기본 골격
/* 최소 virtio 드라이버 구현 예시 */
#include <linux/virtio.h>
#include <linux/virtio_config.h>
/* 디바이스 ID (OASIS 표준 또는 실험용) */
static const struct virtio_device_id my_id_table[] = {
{ VIRTIO_ID_MY_DEVICE, VIRTIO_DEV_ANY_ID },
{ 0 },
};
/* 사용할 기능 비트 */
static unsigned int my_features[] = {
VIRTIO_MY_F_FEATURE_A,
VIRTIO_MY_F_FEATURE_B,
};
/* 디바이스별 컨텍스트 */
struct my_device {
struct virtio_device *vdev;
struct virtqueue *vq; /* 단일 virtqueue */
};
/* virtqueue 콜백 — 호스트가 Used Ring에 기록할 때 호출 */
static void my_vq_callback(struct virtqueue *vq)
{
struct my_device *mydev = vq->vdev->priv;
void *buf;
unsigned int len;
/* Used Ring에서 완료된 버퍼 회수 */
while ((buf = virtqueue_get_buf(vq, &len)) != NULL) {
/* 응답 처리 */
process_response(buf, len);
kfree(buf);
}
}
/* probe — 디바이스 탐지 시 호출 */
static int my_probe(struct virtio_device *vdev)
{
struct my_device *mydev;
mydev = kzalloc(sizeof(*mydev), GFP_KERNEL);
mydev->vdev = vdev;
vdev->priv = mydev;
/* virtqueue 생성 (이름, 콜백) */
mydev->vq = virtio_find_single_vq(vdev, my_vq_callback,
"my_requests");
if (IS_ERR(mydev->vq))
return PTR_ERR(mydev->vq);
/* 디바이스 사용 시작 */
virtio_device_ready(vdev);
return 0;
}
/* remove — 디바이스 해제 */
static void my_remove(struct virtio_device *vdev)
{
struct my_device *mydev = vdev->priv;
virtio_reset_device(vdev);
vdev->config->del_vqs(vdev);
kfree(mydev);
}
/* 드라이버 등록 */
static struct virtio_driver my_driver = {
.feature_table = my_features,
.feature_table_size = ARRAY_SIZE(my_features),
.driver.name = "my_virtio",
.driver.owner = THIS_MODULE,
.id_table = my_id_table,
.probe = my_probe,
.remove = my_remove,
};
module_virtio_driver(my_driver);
virtqueue 주요 작업
/* 요청 전송 (게스트 → 호스트) */
static int send_request(struct my_device *mydev,
void *data, size_t len)
{
struct scatterlist sg;
/* scatter-gather 리스트 설정 */
sg_init_one(&sg, data, len);
/* Available Ring에 OUT 버퍼 추가 */
int ret = virtqueue_add_outbuf(mydev->vq, &sg, 1,
data, GFP_KERNEL);
if (ret)
return ret;
/* 호스트에 알림 (kick) */
virtqueue_kick(mydev->vq);
return 0;
}
/* 양방향 버퍼 (요청 + 응답) */
static int send_request_response(struct my_device *mydev,
void *req, size_t req_len,
void *resp, size_t resp_len)
{
struct scatterlist sgs[2];
sg_init_one(&sgs[0], req, req_len); /* OUT: 게스트→호스트 */
sg_init_one(&sgs[1], resp, resp_len); /* IN: 호스트→게스트 */
/* OUT 1개 + IN 1개 */
return virtqueue_add_sgs(mydev->vq, sgs_arr,
1, /* out_sgs */
1, /* in_sgs */
req, GFP_KERNEL);
}
virtqueue_add_*이 -ENOSPC를 반환하므로,
드라이버는 이 경우 백프레셔(backpressure)를 구현해야 합니다.
Packed Virtqueue
Packed Virtqueue(v1.1+)는 Split Ring의 3개 테이블(Descriptor, Available, Used)을 단일 Descriptor Ring으로 통합하여 캐시(Cache) 효율을 극대화합니다.
Split vs Packed 구조 비교
Packed Ring 플래그 동작
| AVAIL 비트 | USED 비트 | Wrap Counter | 의미 |
|---|---|---|---|
| 1 | 0 | 초기 | 드라이버가 제출, 디바이스 처리 전 |
| 1 | 1 | 초기 | 디바이스가 처리 완료 |
| 0 | 0 | 초기 | 비어 있음 (아직 사용 안 됨) |
| 0 | 1 | 반전 후 | 드라이버가 제출 (wrap 후) |
| 0 | 0 | 반전 후 | 디바이스가 처리 완료 (wrap 후) |
# QEMU에서 packed ring 활성화
qemu-system-x86_64 \
-device virtio-net-pci,packed=on,mq=on,vectors=5 \
-netdev tap,id=net0,vhost=on \
...
# 게스트에서 packed ring 확인
ethtool -i eth0 | grep driver
# driver: virtio_net
# 기능 비트 확인
cat /sys/bus/virtio/devices/virtio0/features
# ... VIRTIO_F_RING_PACKED(34) ...
Vhost 커널 가속
vhost는 데이터 경로(data path)를 QEMU 사용자 공간에서 커널 또는 별도 프로세스로 이동시켜 컨텍스트 스위칭(Context Switching) 오버헤드를 제거합니다.
데이터 경로 비교
vhost-user 프로토콜 핵심
vhost-user는 QEMU와 사용자 공간 백엔드(DPDK, SPDK 등) 사이에서 Unix 도메인 소켓(Socket)을 통해 제어 메시지를 교환합니다. 데이터 경로는 공유 메모리(mmap)를 통해 직접 접근합니다.
/* vhost-user 메시지 구조 */
struct vhost_user_msg {
__u32 request; /* 메시지 유형 */
__u32 flags; /* REPLY_ACK 등 */
__u32 size; /* payload 크기 */
union {
__u64 u64;
struct vhost_vring_state state;
struct vhost_vring_addr addr;
struct vhost_user_memory memory;
} payload;
};
/* 주요 메시지 유형 */
// VHOST_USER_GET_FEATURES — 기능 비트 조회
// VHOST_USER_SET_MEM_TABLE — 공유 메모리 영역 설정 (fd 전달)
// VHOST_USER_SET_VRING_KICK — kick eventfd 설정
// VHOST_USER_SET_VRING_CALL — interrupt eventfd 설정
// VHOST_USER_SET_VRING_ENABLE — virtqueue 활성화
| vhost 유형 | 데이터 경로 | 제어 경로 | 사용 사례 | 성능 |
|---|---|---|---|---|
vhost-net | 커널 (/dev/vhost-net) | QEMU ioctl | 일반 VM 네트워크 | ~15Gbps |
vhost-scsi | 커널 (/dev/vhost-scsi) | QEMU ioctl | VM 스토리지 | ~6GB/s |
vhost-vsock | 커널 (/dev/vhost-vsock) | QEMU ioctl | VM-Host 통신 | ~5Gbps |
vhost-user net | DPDK 프로세스 | Unix 소켓 | NFV, 100GbE | ~100Gbps |
vhost-user blk | SPDK 프로세스 | Unix 소켓 | NVMe 패스스루 | ~10GB/s |
vDPA | SmartNIC 하드웨어 | 커널 vDPA bus | HW 오프로드 | 네이티브 |
Virtio-IOMMU
Virtio-IOMMU는 게스트 OS에 가상 IOMMU를 제공하는 virtio 디바이스입니다. 게스트 커널이 DMA 리매핑(remapping)을 직접 제어할 수 있어, 네스티드 가상화(nested virtualization)와 보안 격리에 활용됩니다.
Virtio-IOMMU 아키텍처
/* virtio-iommu 요청 유형 */
#define VIRTIO_IOMMU_T_ATTACH 0x01 /* 디바이스를 도메인에 연결 */
#define VIRTIO_IOMMU_T_DETACH 0x02 /* 디바이스를 도메인에서 분리 */
#define VIRTIO_IOMMU_T_MAP 0x03 /* IOVA → GPA 매핑 추가 */
#define VIRTIO_IOMMU_T_UNMAP 0x04 /* 매핑 제거 */
#define VIRTIO_IOMMU_T_PROBE 0x05 /* 디바이스 기능 조회 */
/* 매핑 요청 */
struct virtio_iommu_req_map {
struct virtio_iommu_req_head head; /* type = T_MAP */
__le32 domain; /* IOMMU 도메인 ID */
__le64 virt_start; /* IOVA 시작 */
__le64 virt_end; /* IOVA 끝 */
__le64 phys_start; /* GPA 시작 */
__le32 flags; /* R/W/MMIO */
};
/*
* 게스트 OS (Linux IOMMU 드라이버)
* → virtio-iommu 드라이버 (virtqueue로 요청 전송)
* → 호스트 QEMU (요청 해석, 실제 IOMMU 프로그래밍)
* → 호스트 IOMMU (VT-d/SMMU)
*/
| vIOMMU 유형 | 에뮬레이션 | 게스트 드라이버 | 성능 | 사용 사례 |
|---|---|---|---|---|
| virtio-iommu | Virtio 프로토콜 | drivers/iommu/virtio-iommu.c | 중간 | ARM, RISC-V (표준화) |
| Intel vIOMMU | VT-d 에뮬레이션 | intel-iommu.c | 느림 (전체 에뮬레이션) | x86 레거시 호환 |
| AMD vIOMMU | AMD-Vi 에뮬레이션 | amd_iommu.c | 느림 | AMD 호환 |
| vSMMU | ARM SMMU 에뮬레이션 | arm-smmu-v3.c | 느림 | ARM 레거시 |
# QEMU에서 virtio-iommu 사용
qemu-system-aarch64 \
-machine virt,iommu=virtio \
-device virtio-iommu-pci \
-device virtio-net-pci,iommu_platform=on \
...
# x86에서 Intel vIOMMU 사용
qemu-system-x86_64 \
-device intel-iommu,intremap=on,caching-mode=on \
-device virtio-net-pci,iommu_platform=on \
...
# 게스트에서 IOMMU 확인
dmesg | grep -i iommu
# virtio-iommu: input address: 48 bits
# virtio-iommu: page mask: 0x1fffff
참고 링크
사양 문서
- OASIS virtio Specification v1.3 — virtio 공식 최신 사양서입니다
- OASIS virtio Specification v1.2 — 이전 버전 사양서입니다
커널 공식 문서
- Kernel Doc: virtio — 커널 공식 virtio 드라이버 API 문서입니다
- Kernel Doc: vhost — 커널 공식 vhost 문서입니다
- Kernel Doc: virtio_vsock — VM-호스트 간 소켓 통신(vsock) 문서입니다
LWN 기사
- LWN: Virtio — An I/O virtualization framework — virtio 프레임워크 초기 설계 배경을 설명합니다
- LWN: Virtio 1.0 — virtio 1.0 표준화 과정을 다룹니다
- LWN: vhost-net and virtio networking — vhost-net 커널 백엔드 구조를 분석합니다
- LWN: vhost-user (userspace backend) — 유저스페이스 백엔드 vhost-user를 소개합니다
- LWN: Packed virtqueues — packed ring 형식의 설계와 성능 개선을 설명합니다
- LWN: virtio-mem — 가상 머신 메모리 핫플러그 메커니즘을 다룹니다
- LWN: vDPA — virtio data path acceleration — vDPA를 통한 데이터 경로 가속을 설명합니다
커널 소스 코드
drivers/virtio/virtio.c— virtio 버스 드라이버 코어 구현입니다drivers/virtio/virtio_ring.c— virtqueue(split/packed ring) 구현입니다drivers/virtio/virtio_pci_common.c— virtio-pci 트랜스포트 구현입니다drivers/virtio/virtio_mmio.c— virtio-mmio 트랜스포트 구현입니다drivers/net/virtio_net.c— virtio-net 네트워크 드라이버입니다drivers/block/virtio_blk.c— virtio-blk 블록 드라이버입니다drivers/vhost/vhost.c— vhost 코어(커널 내 백엔드) 구현입니다drivers/vhost/net.c— vhost-net(커널 내 네트워크 백엔드) 구현입니다drivers/vhost/vsock.c— vhost-vsock(VM-호스트 소켓 통신) 구현입니다include/linux/virtio.h— virtio 코어 헤더 파일입니다include/uapi/linux/virtio_config.h— virtio 설정 상수 정의 파일입니다
QEMU 문서
- QEMU: virtio devices — QEMU의 virtio 디바이스 설정 가이드입니다
- QEMU: vhost-user — QEMU vhost-user 프로토콜 사양입니다
기타 자료
- Red Hat: Introduction to virtio networking and vhost-net — virtio 네트워킹과 vhost-net의 기본 개념을 소개합니다
- Red Hat: Deep dive into Virtio networking and vhost-net — virtio 네트워킹 내부 구조를 심층 분석합니다
관련 문서
virtio/vhost와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.