QEMU/KVM 게스트의 기본 에뮬레이트 NIC(e1000, rtl8139 등)은 물리 NIC 레지스터를 소프트웨어로 그대로 흉내 내, 패킷 하나마다 MMIO/PIO 접근이 VM Exit로 호스트에 트랩된다. 이 트랩 비용 때문에 처리량이 물리 NIC 대비 크게 떨어지고 CPU 사용량도 늘어난다. virtio-net은 실제 하드웨어를 흉내 내는 대신 가상화에 최적화된 인터페이스(virtqueue)를 게스트 드라이버와 공유해 트랩 횟수 자체를 줄인다. 이 글에서는 QEMU에서 e1000과 virtio-net 두 NIC 모델을 실제로 붙여 게스트 드라이버 바인딩 차이를 확인하고, virtio-net 전환 방법과 vhost-net 오프로드까지 정리한다.
virtio-net과 e1000의 구조적 차이
e1000은 Intel 82540EM 칩의 레지스터 맵을 그대로 재현하는 완전 에뮬레이션 디바이스라 모든 접근이 QEMU 디바이스 모델을 거친다. virtio-net은 반가상화(paravirtualization) 방식으로, PCI 벤더 ID 0x1af4(virtio 전용)로 등록되는 별도 디바이스 클래스이며 링 버퍼 구조인 virtqueue를 공유 메모리로 주고받고 알림도 이벤트 기반(ioeventfd/irqfd)으로 처리한다. vhost-net은 여기서 한 단계 더 나아가, QEMU 유저스페이스가 담당하던 데이터 평면 처리를 호스트 커널 모듈로 옮겨 유저-커널 전환 비용까지 줄인다.
| 구성 | 동작 방식 | 패킷 경로 |
|---|---|---|
| e1000 (에뮬레이션) | 물리 NIC 레지스터를 그대로 흉내 | 게스트 → VM Exit → QEMU 디바이스 모델 → 호스트 네트워크 스택 |
| virtio-net (QEMU 유저스페이스) | virtqueue 공유 메모리 + 이벤트 기반 알림 | 게스트 → virtqueue → QEMU 유저스페이스 → 호스트 네트워크 스택 |
| virtio-net + vhost-net | 데이터 평면을 커널 모듈로 오프로드 | 게스트 → virtqueue → 호스트 커널 vhost_net 모듈 → 호스트 네트워크 스택 |
실제 부팅 검증: e1000 vs virtio-net 드라이버 바인딩
iperf3 처리량 벤치마크는 게스트 OS 디스크 이미지가 필요해 검증 범위에서 제외했다. 대신 Alpine Linux virt 커널(6.6.134-0-virt)과 최소 initramfs로, QEMU가 두 NIC 모델을 각각 어떻게 노출하고 게스트 커널이 어떤 드라이버로 바인딩하는지 실제로 부팅해 확인했다.
# e1000 NIC로 부팅
qemu-system-x86_64 -M pc -m 512 \
-kernel vmlinuz-virt -initrd custom-initramfs.gz \
-append "console=ttyS0,115200" \
-netdev user,id=n0 -device e1000,netdev=n0 \
-nographic -no-reboot
# virtio-net NIC로 부팅 (netdev id/커맨드 구조는 동일, device만 교체)
qemu-system-x86_64 -M pc -m 512 \
-kernel vmlinuz-virt -initrd custom-initramfs.gz \
-append "console=ttyS0,115200" \
-netdev user,id=n0 -device virtio-net-pci,netdev=n0 \
-nographic -no-reboot게스트 부팅 후 init 스크립트에서 e1000.ko, virtio_net.ko를 로드하고 /sys/class/net/eth0/device/driver로 실제 바인딩된 드라이버를 확인했다.
--- e1000 실행 ---
[ 2.017306] e1000 0000:00:03.0 eth0: (PCI:33MHz:32-bit) 52:54:00:12:34:56
[ 2.017770] e1000 0000:00:03.0 eth0: Intel(R) PRO/1000 Network Connection
/sys/bus/pci/devices/0000:00:03.0 vendor=0x8086 device=0x100e
IFACE=eth0 DRIVER=e1000
--- virtio-net-pci 실행 ---
/sys/bus/pci/devices/0000:00:03.0 vendor=0x1af4 device=0x1000
IFACE=eth0 DRIVER=virtio_net
vendor 0x8086(Intel)과 0x1af4(virtio)로 서로 다른 PCI 디바이스가 노출되고, 게스트 커널이 각각 e1000/virtio_net 드라이버로 정확히 바인딩됐다.
vhost-net으로 오프로드
vhost-net은 브리지/tap 인터페이스가 필요해 이번 WSL2 환경(루트 브리지 구성 없음)에서는 직접 실행하지 못했다. 참고용으로 활성화 커맨드만 남긴다.
qemu-system-x86_64 -enable-kvm -m 2048 \
-netdev tap,id=n0,ifname=tap0,script=no,downscript=no,vhost=on \
-device virtio-net-pci,netdev=n0 \
-drive file=guest.qcow2,format=qcow2vhost=on 한 줄로 데이터 평면이 QEMU 유저스페이스에서 호스트 커널의 vhost_net 모듈로 넘어간다. 호스트 커널에 CONFIG_VHOST_NET=y가 켜져 있어야 하고 tap 인터페이스를 브리지에 미리 연결해둬야 한다.
공개된 벤치마크 수치(linux-kvm.org, 이 글에서 직접 재현한 값은 아님)는 다음과 같다.
| 테스트 | e1000 | virtio-net |
|---|---|---|
| 단일 스트림 | 296 Mbps | 834 Mbps |
| 양방향 (out/in) | 259 / 62 Mbps | 519 / 531 Mbps |
| 병렬 스트림 4개 합산 | 302 Mbps | 906 Mbps |
| vhost-net 적용 시 | – | 약 7~8 Gbps (일반 virtio 대비 약 8배, 지연시간은 e1000 대비 약 10% 감소) |
주의사항
- 이번 검증은 디바이스 인식과 드라이버 바인딩까지만 확인한 것이다. WSL2 사용자가 kvm 그룹에 속해 있지 않아(
/dev/kvm은 root:kvm)-enable-kvm없이 TCG로 부팅했다. - 게스트 OS에 virtio_net 드라이버가 없으면 NIC 자체가 안 보인다. 최신 Linux는 커널에 기본 포함돼 있지만, Windows 게스트는 virtio-win ISO로 드라이버를 별도 설치해야 한다.
- RHEL5 이전처럼 virtio를 지원하지 않는 게스트는 e1000/rtl8139 같은 에뮬레이트 NIC을 쓸 수밖에 없다.
- TSO, checksum offload 같은 오프로드 기능이 게스트/호스트 조합에 따라 오히려 오류를 유발하는 경우가 있다. 네트워크가 불안정하면 오프로드를 개별적으로 꺼보고 원인을 좁힐 것.
- 운영 중인 VM의 NIC 모델을 바꾸면(e1000 → virtio-net) MAC 주소가 바뀐 것으로 게스트 OS가 인식해 네트워크 설정(udev 규칙, netplan 등)이 꼬일 수 있다.
마무리
e1000은 호환성이 필요할 때만 쓰고, 게스트 OS가 지원한다면 virtio-net을 기본으로 쓰는 게 맞다. PCI vendor ID부터 실제로 다른 디바이스 클래스임이 드러나고, 이번 검증에서도 QEMU가 지정한 모델대로 게스트 커널이 정확히 다른 드라이버에 바인딩하는 걸 확인했다. 처리량을 한 단계 더 끌어올리려면 vhost-net으로 데이터 평면을 커널로 내리는 것까지 고려할 만하다.