virtio / vhost

리눅스 커널의 반가상화(Paravirtualization) I/O 프레임워크 virtio와 데이터 경로 가속 vhost를 virtqueue 구조부터 디바이스 유형별 구현, vDPA 오프로드까지 심층 분석합니다.

전제 조건: 가상화 (KVM)디바이스 드라이버 문서를 먼저 읽으세요. virtio는 반가상화 I/O 프레임워크이므로 하이퍼바이저와 디바이스 모델 개념을 이해해야 합니다. 관련 문서: VFIO/mdev, 네트워크 스택 개요.
일상 비유: virtio는 통역사가 필요 없는 국제회의와 비슷합니다. 전가상화(e1000 에뮬레이션)는 모든 대화를 통역사(QEMU)가 중계하지만, 반가상화(virtio)는 참가자(게스트)가 공통 언어(virtqueue 프로토콜)를 사용하여 직접 소통합니다. vhost는 동시통역 부스를 없애고 직통 채널을 여는 것이며, vDPA는 전용 통신 장비로 대체하는 것입니다.

핵심 요약

virtio/vhost를 이해하기 위한 6가지 핵심 개념

1

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종 이상의 디바이스 유형을 정의합니다.

2

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) 효율을 크게 개선합니다.

3

Feature Negotiation

virtio 디바이스 초기화 시 드라이버와 디바이스가 각자 지원하는 기능 비트(feature bits)를 교환하여 교집합으로 동작 모드를 결정합니다. 이 협상 과정 덕분에 신규 기능 추가 시에도 하위 호환성이 보장됩니다. 예: VIRTIO_NET_F_MQ(멀티큐), VIRTIO_F_RING_PACKED(packed ring) 등.

4

vhost-net / vhost-user 데이터 패스

vhost-net은 커널 모듈(/dev/vhost-net)로 네트워크 데이터 경로를 QEMU를 거치지 않고 커널에서 직접 처리하여 ~30% 성능을 향상시킵니다. vhost-user는 Unix 도메인 소켓(Socket)을 통해 DPDK/SPDK 같은 유저스페이스 프로세스(Process)에서 데이터 경로를 처리하여 100Gbps급 처리량(Throughput)을 달성합니다.

5

vDPA 하드웨어 오프로드

vDPA(virtio Data Path Acceleration)는 SmartNIC(NVIDIA ConnectX, Intel FPGA 등)의 하드웨어가 직접 virtqueue를 처리하여 호스트 CPU 부담을 완전히 제거합니다. 소프트웨어 virtio 인터페이스를 유지하면서 네이티브에 가까운 성능을 달성하는 것이 핵심입니다.

6

virtio-fs / vsock 확장

virtio-fs는 FUSE 프로토콜 기반 파일시스템(Filesystem) 공유로 DAX window를 통한 직접 메모리 매핑(Mapping)을 지원합니다. virtio-vsockAF_VSOCK 소켓 패밀리로 VM↔호스트 간 네트워크 설정 없이 CID 기반 직접 통신을 제공합니다. 컨테이너(Container)/마이크로VM(Kata Containers, Firecracker)에서 필수적인 통신 채널입니다.

단계별 이해

virtio/vhost 학습 로드맵

1단계

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
2단계

virtqueue 동작 원리 이해

게스트 드라이버가 virtqueue_add_sgs()로 버퍼(Buffer)를 등록하고, virtqueue_kick()으로 호스트에 알리며, virtqueue_get_buf()로 완료된 버퍼를 회수하는 3단계 흐름을 이해합니다. /sys/kernel/debug/virtio-pci/ 디버그 파일을 통해 실제 ring 상태를 관찰할 수 있습니다.

3단계

Feature Negotiation 과정 추적

dmesg | grep virtio로 부팅 시 feature negotiation 로그를 확인합니다. /sys/bus/virtio/devices/virtio0/features에서 협상된 기능 비트를 읽어 어떤 기능이 활성화되었는지 분석합니다.

4단계

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
5단계

성능 측정 (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 등 설정 변경 시 성능 차이를 비교합니다.

6단계

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 (비트맵)
커널 설정: 게스트 커널에서 virtio를 사용하려면 CONFIG_VIRTIO, CONFIG_VIRTIO_PCI, CONFIG_VIRTIO_NET, CONFIG_VIRTIO_BLK 등을 활성화해야 합니다. 대부분의 배포판 커널은 이미 모듈로 포함하고 있으며, initramfs에서 부팅하려면 CONFIG_VIRTIO_BLK=y로 빌트인이 필요할 수 있습니다.
Device ID 참고: virtio 디바이스는 PCI vendor ID 0x1AF4(Red Hat)를 사용합니다. Device ID 범위: 0x1000-0x103F는 Transitional(레거시 호환), 0x1040-0x107F는 Modern-only입니다. 예: virtio-net=0x1041, virtio-blk=0x1042.
Split Ring vs Packed Ring Split Ring (레거시) Descriptor Table addr | len | flags | next (16바이트 x N) 드라이버/디바이스 공유 (읽기 전용) Available Ring flags | idx | ring[] 드라이버 → 디바이스 Used Ring flags | idx | ring[] 디바이스 → 드라이버 3개 분리된 메모리 영역 캐시 미스 빈번 (다른 캐시 라인) 메모리 배치: desc[N] ... avail[N] ... used[N] 분산된 메모리 → L1/L2 캐시 비효율 Packed Ring (v1.1+) 통합 Descriptor Ring addr | len | id | flags (16바이트 x N) flags: AVAIL | USED | WRITE | NEXT | INDIRECT AVAIL=wrap → 새 버퍼, USED=wrap → 완료 Wrap Counter: ring 끝 도달 시 토글 단일 연속 메모리 영역 캐시 친화적 (동일 캐시 라인) 성능: ~15% 레이턴시 감소 In-order completion 최적화 가능 virtio Device Status 상태 머신 RESET (0) ACKNOWLEDGE (1) DRIVER (2) FEATURES_OK (8) DRIVER_OK (4) 동작 중 FAILED (128) OS 인식 드라이버 로드 feature 협상 virtqueue 설정 I/O 시작 실패 시 실패 시

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 드라이버없음 (직접 접근)네이티브
게스트 VM virtio-net 드라이버 virtio-blk 드라이버 virtio 코어 (virtio_bus) feature negotiation, config, virtqueue Transport: virtio-pci / virtio-mmio Virtqueue (공유 메모리) descriptor table + available ring + used ring 호스트 QEMU (유저) TAP / 블록 파일 vhost-net / vhost-user 커널/유저스페이스 데이터 경로 가속 KVM (ioeventfd / irqfd) Virtqueue (공유 메모리) 게스트와 동일한 메모리 영역 공유

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+) 두 가지 형식이 있습니다.

Split Virtqueue (3개 영역) Descriptor Table [0] addr=0x1000 len=4096 flags=NEXT next=1 [1] addr=0x2000 len=512 flags=WRITE next=0 [2] addr=0x3000 len=2048 flags=0 next=0 ... (최대 2^15 = 32768 엔트리) Available Ring (게스트 → 호스트) flags | idx 0 2 ... ring[]: descriptor head indices Used Ring (호스트 → 게스트) flags | idx id=0 len=512 id=2 len=0 ring[]: {id, len} 완료 정보 Packed Virtqueue (v1.1): 3개 영역을 1개로 통합, WRAP 카운터 기반, 캐시 효율 향상
/* 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 테이블 */
필드크기설명
addr8바이트게스트 물리 주소 (GPA). 호스트가 GPA→HVA 변환하여 접근
len4바이트버퍼 길이. 디바이스는 이 범위를 초과하여 접근해서는 안 됨
flags2바이트NEXT(체인), WRITE(디바이스 쓰기용), INDIRECT(간접)
next2바이트NEXT 플래그 설정 시 다음 descriptor의 인덱스
Flag 비트의미사용 사례
VRING_DESC_F_NEXT0x1다음 descriptor로 체인scatter-gather I/O
VRING_DESC_F_WRITE0x2디바이스가 쓸 수 있는 버퍼RX 버퍼, 응답 영역
VRING_DESC_F_INDIRECT0x4별도 테이블을 가리키는 간접 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 활성 시) */
};
Indirect Descriptors (VIRTIO_F_INDIRECT_DESC): 단일 Available Ring 슬롯으로 대량의 scatter-gather 버퍼를 전달할 수 있습니다. 간접 descriptor는 VRING_DESC_F_INDIRECT 플래그가 설정된 descriptor가 가리키는 별도 메모리 영역에 descriptor 체인을 담습니다. virtqueue 크기에 관계없이 최대 수백 개의 세그먼트를 하나의 요청으로 묶을 수 있어, virtio-blk에서 대용량 I/O 요청 처리에 유용합니다.
메모리 배리어(Memory Barrier): Split Ring에서 드라이버와 디바이스는 별도 메모리 영역에 독립적으로 접근합니다. 따라서 descriptor 기록 후 Available Ring idx를 증가시키기 전에 반드시 wmb()(write memory barrier)를 삽입해야 합니다. 이 배리어가 없으면 디바이스가 아직 기록되지 않은 descriptor를 읽을 수 있습니다.
Split Ring I/O 흐름 드라이버 (게스트) 1. Descriptor Table [0] addr=0x1000 len=1500 [1] addr=0x2000 flags=WRITE 기록 2. Available Ring ring[idx] = 0 (head) idx++ → kick 등록 3. 디바이스 처리 avail ring 확인 desc 체인 따라 I/O 수행 알림 4. Used Ring ring[idx] = {id=0, len=64} idx++ → 인터럽트 완료 5. 드라이버 회수 virtqueue_get_buf() desc[0] 재사용 가능 IRQ → 폴링 Split Ring 메모리 레이아웃 Descriptor Table (16B x N) Available Ring (2+2+2N B) Used Ring (2+2+8N B) 각 영역은 페이지 정렬, 서로 다른 캐시 라인에 위치 → 멀티코어 환경에서 false sharing 방지

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를 유지합니다.

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 SuppressionDevice 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 RingPacked 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 바이트
Packed Ring 활성화: QEMU에서 -device virtio-net-pci,packed=on으로 활성화합니다. 게스트와 호스트 모두 VIRTIO_F_RING_PACKED feature bit를 지원해야 합니다. Linux 커널 5.0+에서 packed virtqueue 드라이버를 지원합니다.
성능 팁: Packed Ring은 특히 batch 처리와 함께 사용할 때 효과가 큽니다. 여러 descriptor를 한꺼번에 추가하고 단일 kick으로 알릴 수 있으며, in-order completion과 결합하면 디바이스 측의 descriptor 업데이트 오버헤드도 최소화됩니다.

라이브 마이그레이션

가상 머신의 라이브 마이그레이션(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-copydirty page 로깅 시작, 전체 메모리 전송수 초~수 분
Stop-and-copy디바이스 중지, 상태 직렬화, 잔여 dirty 페이지 전송수십 ms
Resume대상에서 상태 복원, feature 재검증, virtqueue 재설정수 ms
QEMU 마이그레이션 명령: virsh migrate --live <domain> qemu+ssh://dest/system 또는 QMP: {"execute": "migrate", "arguments": {"uri": "tcp:dest:4444"}}. QEMU가 내부적으로 virtio 디바이스 상태를 자동으로 직렬화/역직렬화합니다.
마이그레이션 제약: vhost-user 백엔드(DPDK 등)는 별도의 마이그레이션 프로토콜이 필요합니다. VHOST_USER_PROTOCOL_F_LOG_SHMFD feature로 dirty logging을 지원하며, postcopy 마이그레이션은 VHOST_USER_PROTOCOL_F_PAGEFAULT가 필요합니다. vDPA 하드웨어 디바이스는 현재 라이브 마이그레이션 지원이 제한적입니다.
virtio 라이브 마이그레이션 시퀀스 Source Host 1. VM 실행 중 2. dirty logging VHOST_SET_LOG_BASE 3. 메모리 전송 Pre-copy 반복 4. VM 일시정지 Stop-and-copy 5. virtio 상태 직렬화 features, status, vq_idx, desc/avail/used 내용 6. 잔여 dirty 페이지 + 상태 전송 TCP/RDMA로 대상 호스트에 전달 Destination Host 7. 메모리 수신 및 매핑 8. virtio 상태 복원 features 재검증, virtqueue 재설정 9. vhost 재연결 (SET_MEM_TABLE) 10. VM 재개 (DRIVER_OK) 게스트 무중단 I/O 재개 전송

Transport 계층

virtio transport는 가상 디바이스의 발견(discovery), 설정(configuration), 알림(notification)을 담당합니다.

Transport발견 방식사용 환경성능
virtio-pciPCI BAR, MSI-Xx86 서버, 클라우드가장 높음 (MSI-X)
virtio-mmioDevice Tree / ACPIARM 임베디드, 경량 VM중간 (단일 인터럽트(Interrupt))
virtio-ccwChannel subsystemIBM 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 비트맵 */
};
MSI-X 멀티큐: virtio-pci modern 모드에서는 MSI-X를 사용하여 각 virtqueue에 별도의 인터럽트 벡터를 할당할 수 있습니다. 이를 통해 멀티큐 디바이스(virtio-net, virtio-blk)에서 CPU별 독립적인 I/O 처리가 가능합니다.

virtio-net

virtio-net은 가장 많이 사용되는 virtio 디바이스로, 네트워크 가상화를 담당합니다.

게스트 앱 socket send() virtio-net drv TX: virtqueue_add_sgs TX Virtqueue kick → ioeventfd vhost-net 커널 데이터 경로 TAP /dev/tapN NIC eth0
# 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_CSUMTX 체크섬(Checksum) 오프로드
VIRTIO_NET_F_GSOGeneric 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 유저스페이스 대신 커널 또는 별도 프로세스에서 처리하여 성능을 크게 향상시킵니다.

QEMU 경로 (느림) vhost 경로 (빠름) 게스트 TX VM Exit → KVM QEMU 유저 처리 syscall write TAP TAP → NIC 게스트 TX ioeventfd → vhost 커널 vhost-net 커널 직접 처리 TAP → NIC QEMU: 4회 컨텍스트 스위치 vhost: 1회 ioeventfd → 커널 직접 vhost 데이터 경로: QEMU 프로세스 우회 → VM Exit/syscall 최소화 → ~30% 처리량 향상

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
공유 메모리: vhost-user는 Unix 도메인 소켓으로 제어 메시지를 교환하고, 게스트 메모리에 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_FEATURESFrontend → BackendBackend 지원 feature bits 조회
SET_FEATURESFrontend → Backend협상된 feature bits 설정
SET_MEM_TABLEFrontend → Backend게스트 메모리 영역 매핑 (fd 전달)
SET_VRING_NUMFrontend → Backendvirtqueue 크기 설정
SET_VRING_ADDRFrontend → Backendvirtqueue 주소 (desc, avail, used)
SET_VRING_KICKFrontend → Backendkick eventfd 전달
SET_VRING_CALLFrontend → Backendcall eventfd 전달 (인터럽트 주입)
SET_VRING_ENABLEFrontend → Backendvirtqueue 활성화/비활성화
GET_CONFIGFrontend → Backend디바이스 설정 공간 읽기
SET_LOG_BASEFrontend → 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);
vhost-user reconnect: QEMU는 reconnect=1 옵션으로 vhost-user 백엔드의 재시작(Reboot)을 지원합니다. 백엔드 프로세스가 재시작되면 QEMU가 자동으로 재연결하여 메모리 테이블과 virtqueue 설정을 다시 전달합니다. 이를 통해 DPDK 애플리케이션의 무중단 업데이트가 가능합니다.
vhost-user multiqueue: VHOST_USER_PROTOCOL_F_MQ 프로토콜 feature로 멀티큐를 지원합니다. 각 virtqueue에 대해 별도의 SET_VRING_* 메시지를 교환하며, 각 큐는 독립적인 eventfd로 kick/call을 처리합니다. RSS를 통해 트래픽을 여러 큐에 분산할 수 있습니다.
vhost-user 메시지 교환 시퀀스 QEMU (Frontend) Unix Domain Socket 제어 채널 DPDK/SPDK (Backend) GET_FEATURES features reply (0x...) SET_FEATURES (협상 결과) SET_MEM_TABLE + fd[] (메모리 매핑) SET_VRING_NUM / SET_VRING_ADDR SET_VRING_KICK + eventfd (알림 채널) SET_VRING_CALL + eventfd (인터럽트 채널) SET_VRING_ENABLE → 데이터 경로 시작 데이터 경로: 공유 메모리 직접 접근 (소켓 우회)

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캐시 없음캐시 없음다중 게스트 동시 접근
성능 비교: virtio-fs + DAX는 9pfs(virtio-9p) 대비 순차 읽기 ~3-5배, 랜덤 읽기 ~10배 빠릅니다. 특히 mmap() 기반 접근에서는 호스트 네이티브 파일시스템과 거의 동일한 성능을 보입니다. Kata Containers, Cloud Hypervisor 등 컨테이너 런타임에서 컨테이너 이미지 마운트(Mount)에 활용됩니다.
shared memory 요구사항: vhost-user-fs는 게스트 메모리를 공유 메모리(share=on)로 할당해야 합니다. QEMU에서 memory-backend-memfd 또는 memory-backend-file + hugepage를 사용하세요. 공유 메모리 없이는 virtiofsd가 게스트 메모리에 접근할 수 없습니다.
virtio-fs DAX 매핑 흐름 게스트 VM 앱: mmap(file) 직접 메모리 접근 FUSE 클라이언트 FUSE_SETUPMAPPING DAX Window (BAR) Virtqueue (제어) 호스트 virtiofsd FUSE 서버 Unix Socket 호스트 Page Cache 파일 데이터가 메모리에 캐시됨 호스트 파일시스템 ext4 / XFS / btrfs DAX 직접 매핑 (복사 없음) 제어 메시지

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 접근 방지
hugepageNUMA 노드별 hugepage 할당TLB 미스 감소 + 로컬 접근
NUMA 진단: 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 부담을 제거합니다.

게스트 virtio-net 드라이버 Virtqueue 공유 메모리 vDPA bus vdpa_device SmartNIC HW mlx5_vdpa / ifcvf vdpa 관리 명령어 vdpa dev add name vdpa0 mgmtdev pci/0000:3b:00.0 vdpa dev show / vdpa dev config show
# 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_net.ko virtio_blk.ko virtio_scsi.ko virtiofs.ko virtio.ko (virtio_bus) virtio_pci.ko virtio_mmio.ko virtio_ring.ko vhost 모듈 vhost.ko vhost_net.ko vhost_vsock.ko vhost_vdpa.ko

성능 최적화

virtio 성능을 극대화하기 위한 주요 기법입니다.

기법설명효과
vhost커널 데이터 경로~30% 처리량 향상
멀티큐CPU별 독립 virtqueue멀티코어 확장성
Packed ring캐시 친화적 ring 구조~15% 레이턴시 감소
Interrupt coalescing알림 억제 (VRING_AVAIL_F_NO_INTERRUPT)VM Exit 감소
Zerocopy TXsendmsg(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) 영역입니다.

입력 검증: 호스트(QEMU/vhost)는 게스트가 제공하는 모든 descriptor 주소, 길이, 플래그를 검증해야 합니다. 잘못된 descriptor 체인은 호스트 메모리 접근이나 무한 루프를 유발할 수 있습니다. CVE-2019-14835 (vhost-net 버퍼 오버플로(Buffer Overflow))가 대표적 사례입니다.
위협대응
악의적 descriptorIOMMU + 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 큐
CipherAES-CBC, AES-CTR, AES-XTSdataq (대칭 암호)
HashSHA-256, SHA-512dataq
MACHMAC-SHA256, CMAC-AESdataq
AEADAES-GCM, ChaCha20-Poly1305dataq
AkcipherRSA, 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 연동: virtio-crypto가 Linux Crypto API에 등록되면, AF_ALG 소켓을 통해 유저스페이스 애플리케이션(OpenSSL, GnuTLS)에서 투명하게 사용할 수 있습니다. OpenSSL의 afalg 엔진이나 devcrypto 엔진으로 활성화합니다.
vhost-user-crypto: DPDK의 vhost-user 프레임워크는 crypto 디바이스도 지원합니다. cryptodev-vhost-user backend로 DPDK의 cryptodev를 게스트에 노출하면, Intel QAT/NVIDIA BlueField의 하드웨어 가속을 vhost-user 데이터 경로로 활용할 수 있습니다.
virtio-crypto / virtio-mem 아키텍처 게스트 Crypto API / AF_ALG Memory Hotplug virtio_crypto.ko virtio_mem.ko Virtqueue 호스트 QEMU crypto backend QEMU memory backend QAT HW / SW memfd / hugepage 요청/응답 virtio-mem Subblock 관리 plugged plugged unplugged plugged unplugged unplugged plugged ...

virtio-mem / virtio-pmem (동적 메모리)

virtio-mem은 VM의 메모리를 런타임에 동적으로 추가/제거하는 virtio 디바이스입니다. 기존 virtio-balloon이 메모리 "힌트"만 제공하는 것과 달리, virtio-mem은 실제 메모리 리소스를 plug/unplug합니다.

virtio-mem vs virtio-balloon

비교 항목virtio-balloonvirtio-mem
메커니즘게스트 페이지를 "풍선"에 넣어 사용 불가 표시메모리 블록 단위 plug/unplug
메모리 할당QEMU 시작 시 전체 할당필요 시 동적 할당
해제 보장약함 (게스트 협조 필요)강함 (호스트 제어)
Granularity4KB 페이지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
Device DAX vs Filesystem DAX: Device DAX(/dev/daxN.M)는 파일시스템 없이 메모리를 직접 매핑하여 RDMA나 대용량 인메모리 데이터베이스에 사용합니다. Filesystem DAX(dax=inode)는 파일 단위로 직접 매핑하여 일반 애플리케이션 호환성을 유지하면서 페이지 캐시를 우회합니다.
Hot-remove 제약: virtio-mem의 메모리 unplug는 해당 subblock의 모든 페이지가 이동 가능(ZONE_MOVABLE)해야 합니다. 커널 내부 할당(slab, 페이지 테이블(Page Table) 등)이 해당 영역에 있으면 unplug가 실패합니다. 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 kickTX 경로VM Exit ~80% 감소약간의 레이턴시 증가
Zero-copy TXvhost-netCPU ~40% 감소completion 대기 필요
Busy pollingRX 경로레이턴시 ~50% 감소CPU 사용률 증가
Notification suppression양방향인터럽트 ~90% 감소폴링 지연(Latency)
Packed ring전체캐시 효율 ~15% 향상레거시 호환 불가
멀티큐 + IRQ affinity전체CPU 확장성큐 수 = vCPU 수 권장
종합 최적화 체크리스트: (1) vhost-net 활성화(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% 이내로 줄일 수 있습니다.
Notification Suppression + Batch 흐름 게스트 (Driver) 1. add_buf: 패킷 A → desc[0] 2. add_buf: 패킷 B → desc[1] (kick 보류) 3. add_buf: 패킷 C → desc[2] (kick 보류) 4. xmit_more=false → single kick! 5. NAPI poll (NO_INTERRUPT 설정) 인터럽트 없이 used ring 폴링 호스트 (Device) ioeventfd 수신 (1회 VM Exit) avail ring에서 3개 desc 일괄 확인 패킷 A, B, C 일괄 처리 (TAP 전송) used ring에 3개 완료 기록 NO_NOTIFY 확인 → 인터럽트 생략 kick 폴링으로 확인

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_PACKED34공통Packed Ring 사용 (v1.1+)
VIRTIO_F_VERSION_132공통비레거시 virtio 1.0+ 디바이스
VIRTIO_F_ORDER_PLATFORM36공통플랫폼 메모리 순서 사용
VIRTIO_F_SR_IOV37공통SR-IOV VF 지원
VIRTIO_NET_F_MQ22net멀티큐 (vCPU당 큐)
VIRTIO_NET_F_CSUM0netTX 체크섬 오프로드
VIRTIO_NET_F_HOST_TSO411net호스트 TCP 세그멘테이션 오프로드
VIRTIO_BLK_F_MQ12blk블록 멀티큐
VIRTIO_BLK_F_DISCARD13blkTRIM/DISCARD 지원
기능 협상의 중요성: 기능 협상(Feature Negotiation)이 없다면, 새 기능 추가 시 기존 드라이버가 깨질 수 있습니다. 예를 들어 호스트가 packed ring을 지원하더라도, 구형 게스트 드라이버가 split ring만 알면 split ring으로 동작합니다. 이것이 virtio가 10년 이상 하위 호환성을 유지할 수 있는 핵심 메커니즘입니다.

전송 계층(Transport) 상세 비교

Virtio는 여러 전송 방식을 지원합니다. 각 전송 방식은 virtqueue의 위치 알림(notification)과 설정 공간(config space) 접근 방법이 다릅니다.

특성virtio-pcivirtio-mmiovirtio-ccw
플랫폼x86, ARM (PCI 있는 환경)ARM, RISC-V (임베디드)s390x (IBM Z)
디바이스 탐지PCI enumerationDevice Tree / ACPIChannel 서브시스템
config 접근PCI BAR (MMIO)메모리 매핑 레지스터CCW 명령
알림 (kick)PCI BAR write → ioeventfdMMIO write → ioeventfdSSCH 명령
인터럽트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 / virtio_device) 공통 API: virtqueue_add_buf() / virtqueue_kick() / virtqueue_get_buf() virtio-pci PCI BAR MMIO MSI-X 인터럽트 (벡터별) ioeventfd kick 최고 성능 x86 / ARM (PCI) virtio_pci_modern.c virtio-mmio 메모리 매핑 레지스터 단일/공유 IRQ MMIO write notify 중간 성능 ARM / RISC-V (임베디드) virtio_mmio.c virtio-ccw Channel Command Word Adapter 인터럽트 SSCH 명령 notify s390x 최적화 IBM Z (s390x) virtio_ccw.c 호스트: QEMU / vhost-net / vhost-user / vDPA HW 공유 메모리 기반 virtqueue 처리 공유 메모리: Virtqueue (Descriptor Table + Available Ring + Used Ring) 게스트와 호스트가 동일한 물리 메모리를 읽고 씀 — 전송 계층은 알림(kick/interrupt)만 처리

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 사이즈 선택: virtqueue의 디스크립터 수는 2의 거듭제곱이어야 합니다. virtio-net은 기본 256개, virtio-blk는 128개를 사용합니다. 디스크립터가 부족하면 virtqueue_add_*-ENOSPC를 반환하므로, 드라이버는 이 경우 백프레셔(backpressure)를 구현해야 합니다.

Packed Virtqueue

Packed Virtqueue(v1.1+)는 Split Ring의 3개 테이블(Descriptor, Available, Used)을 단일 Descriptor Ring으로 통합하여 캐시(Cache) 효율을 극대화합니다.

Split vs Packed 구조 비교

Split Ring vs Packed Ring Split Ring (레거시) Descriptor Table (16B x N) desc[0]: addr desc[1]: addr desc[2]: addr ... Available Ring (드라이버 → 디바이스) flags idx ring[0] ring[1] ... Used Ring (디바이스 → 드라이버) flags idx elem[0] elem[1] ... 3개 테이블 → 캐시 미스(miss) 다수 드라이버: Desc + Avail / 디바이스: Desc + Used Packed Ring (v1.1+) 통합 Descriptor Ring (16B x N) desc[0] addr | len | id | flags AVAIL=1 USED=0 (사용 가능) desc[1] addr | len | id | flags AVAIL=1 USED=1 (처리 완료) desc[2] addr | len | id | flags AVAIL=0 USED=0 (비어 있음) desc[3] ... Wrap Counter: 링 끝에 도달 시 AVAIL/USED 비트 의미 반전 단일 테이블 → 캐시 친화적 드라이버·디바이스 모두 같은 배열 접근 성능 비교 Split Ring 캐시 라인 접근: 3+ (Desc, Avail, Used) 메모리 배리어: smp_mb() 다수 Indirect: 별도 desc 테이블 할당 false sharing 발생 가능 QEMU: packed=off (기본) Packed Ring 캐시 라인 접근: 1-2 (연속 배열) 메모리 배리어: 감소 (flags 원자적) In-order: desc 자체가 순서 인코딩 false sharing 최소화 QEMU: -device virtio-net-pci,packed=on

Packed Ring 플래그 동작

AVAIL 비트USED 비트Wrap Counter의미
10초기드라이버가 제출, 디바이스 처리 전
11초기디바이스가 처리 완료
00초기비어 있음 (아직 사용 안 됨)
01반전 후드라이버가 제출 (wrap 후)
00반전 후디바이스가 처리 완료 (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) 오버헤드를 제거합니다.

데이터 경로 비교

데이터 경로 비교: QEMU vs vhost-net vs vhost-user 순수 QEMU (느림) Guest virtio-net VM Exit QEMU (유저스페이스) syscall 커널 TAP 물리 NIC 컨텍스트 스위칭 3회 데이터 복사 2회 vhost-net (빠름) Guest virtio-net ioeventfd QEMU (설정만) vhost-net (커널) 물리 NIC QEMU 우회 (커널 직접) ~30% 성능 향상 vhost-user (최고) Guest virtio-net ioeventfd QEMU (설정만) DPDK vhost-user DPDK PMD 물리 NIC (UIO/VFIO) 커널 우회 (유저스페이스) 100Gbps+ 달성 핵심 차이: 데이터 경로가 어디를 통과하느냐 순수 QEMU: Guest → KVM → QEMU → Kernel → NIC | vhost-net: Guest → KVM → Kernel(vhost) → NIC | vhost-user: Guest → DPDK → NIC

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 ioctlVM 스토리지~6GB/s
vhost-vsock커널 (/dev/vhost-vsock)QEMU ioctlVM-Host 통신~5Gbps
vhost-user netDPDK 프로세스Unix 소켓NFV, 100GbE~100Gbps
vhost-user blkSPDK 프로세스Unix 소켓NVMe 패스스루~10GB/s
vDPASmartNIC 하드웨어커널 vDPA busHW 오프로드네이티브

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-iommuVirtio 프로토콜drivers/iommu/virtio-iommu.c중간ARM, RISC-V (표준화)
Intel vIOMMUVT-d 에뮬레이션intel-iommu.c느림 (전체 에뮬레이션)x86 레거시 호환
AMD vIOMMUAMD-Vi 에뮬레이션amd_iommu.c느림AMD 호환
vSMMUARM 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
virtio-iommu의 장점: x86의 Intel vIOMMU 에뮬레이션은 복잡한 VT-d 스펙 전체를 구현해야 하므로 코드가 방대하고 성능 오버헤드가 큽니다. virtio-iommu는 간결한 virtio 프로토콜로 동일한 기능을 제공하며, 특히 ARM/RISC-V 플랫폼에서 표준 vIOMMU로 자리잡고 있습니다.

참고 링크

사양 문서

커널 공식 문서

LWN 기사

커널 소스 코드

QEMU 문서

기타 자료

virtio/vhost와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.