1000sj
SJ CODE
1000sj
전체 방문자
오늘
어제
  • 분류 전체보기
    • Algorithms
      • Crypto
      • Formal Methods
    • Security
      • Fuzzing
      • Exploit
    • System Programming
      • Kernel
      • Compiler
      • Device Driver
      • Emulator
      • Assembly
      • Memory
      • Network
      • Hypervisor
    • Architecture
      • ARM
      • RISC-V
    • Cloud Computing
      • Infrastructure
      • SDN
    • TroubleShooting
      • Debugging
      • Testing
    • Performance improvements
      • Parrelel Processing
      • HPC
    • ETC
      • iamroot 스터디
      • 문화 생활
      • 커뮤니티

인기 글

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
1000sj

SJ CODE

System Programming/Hypervisor

QEMU 코드 분석 #6 VirtIO 기초 & 데이터 구조

2025. 2. 14. 17:13

전가상화·반가상화 비교, virtio의 기본 원리와 프레임워크(프론트엔드/백엔드/virtqueue), virtio 장치의 타입 계층과 초기화, VirtIO-PCI 초기화와 게스트 커널의 PCI 매거(enumeration) 흐름, virtqueue/vring(Descriptor Table, Available Ring, Used Ring)의 데이터 구조

 

1) virtio 개념과 기본 원리
2) virtio 프레임워크와 장치 초기화 (타입 계층)
3) VirtIO-PCI 초기화와 PCI 매거 흐름
4) virtio 데이터 구조 (virtqueue · vring · Descriptor/Avail/Used Ring)

 

1) virtio 개념과 기본 원리

가상머신 입장에서 보면, CPU랑 메모리를 가상화하는 것 외에도 당연히 외부 장치도 가상화해야 한다. 그중에서 가장 전형적이고 핵심적인 게 바로 저장장치(스토리지)랑 네트워크다. 그러면 이 외부 장치들은 어떻게 가상화해야 할까?

 

전가상화(Full Virtualization)

전가상화 방식은 완전 가상화다. QEMU를 써서 장치를 완전히 시뮬레이션하는 거다.

예를 들어 어떤 하드디스크나 네트워크 카드가 있으면, QEMU로 그 하드디스크나 네트워크 카드를 똑같이 흉내 낸다.

이러면 가상머신 안의 운영체제 입장에서는 이 장치들을 쓰는 거랑 실제 물리 장치를 쓰는 거랑 똑같이 보인다.

전통적인 장치 시뮬레이션 방식에서는, 가상머신 내부 장치 드라이버는 자기가 가상화 환경에 있다는 걸 전혀 모른다.

 

네트워크나 저장장치 같은 I/O 작업은 "가상머신 커널 스택 → QEMU → 호스트 커널 스택"이라는 완전한 경로를 거치게 된다.

이 방식으로 완전하고 진짜인 가상화를 구현하긴 했지만, QEMU가 통역관 역할까지 떠맡아서 모든 명령마다 다 번역을 해야 한다. 이 과정에서 수많은 VM Exit랑 VM Entry가 발생해서 성능이 정말 안 좋다. 진짜 너무 느리다.

 

반가상화(Para-virtualization)

 

또 다른 방식이 반가상화다.

가상머신 안의 운영체제가 일반적인 OS가 아니다.

자기가 가상머신 안에서 돌고 있다는 걸 알고, 하드디스크나 네트워크 같은 장치들이 다 가상화된 거라는 것도 명확히 안다.

 

그래서 특수한 드라이버를 로드해야 동작할 수 있다. 이 특수한 드라이버들은 보통 가상머신 외부랑 협력하는 방식으로 동작해서, 물리 저장장치나 네트워크 장치 같은 걸 쓸 때 속도를 높여준다.

 

여기서 virtio가 바로 이 방식을 쓴다.

virtio 방안은 성능을 끌어올리기 위한 최적화 방안이다. 이 방안에서 가상머신은 자기가 가상화 환경에 있다는 걸 감지할 수 있고, 거기에 맞는 virtio 버스 드라이버랑 virtio 장치 드라이버를 로드한다.

 

virtio 기본 원리

가상화 기술 초기에는, 가상화 기술마다 서로 다른 하드디스크(물리 저장장치)랑 네트워크 같은 장치들에 대해 각기 다른 드라이버를 구현했다.

가상머신 안 운영체제도 가상화 기술이랑 물리 저장장치/네트워크 장치에 따라 서로 다른 드라이버를 선택해서 로드해야 했다.

 

근데 하드디스크 장치랑 네트워크 장치가 워낙 많아서, 드라이버가 너무 복잡하고 어지러웠다.

나중에 천천히 표준이 잡혔는데 그게 바로 virtio다.

이름이 말해주듯이 virtio는 virtual io의 줄임말이고, 가상화 I/O 장치라는 뜻이다.

virtio는 가상머신에 통일된 인터페이스를 제공하는 역할을 한다. 다시 말하면, 가상머신 안 운영체제에 드라이버를 하나 깔아두면, 이후로는 다 virtio만 로드하면 된다.

 

반가상화의 기본 원리는 아래 그림과 같다

위 그림은 주로 두 부분으로 이루어져 있다.

하나는 VMM이 만들어낸 시뮬레이션 장치고, 다른 하나는 운영체제 내부에 설치된 그 시뮬레이션 장치의 드라이버다.

이 드라이버랑 장치는 서로 대응되는 인터페이스를 통해서 통신한다.

 

e1000 네트워크 카드를 예로 들어보자.

전통적인 전가상화 방식에서는, 가상머신 커널의 네트워크 카드 드라이버가 구체적인 하드웨어 장치를 다룰 때랑 똑같이 동작한다. 즉, QEMU가 시뮬레이션하는 게 바로 e1000 네트워크 카드인 거다.

가상머신 운영체제도 여전히 전통적인 방식으로 패킷을 주고받는다.

e1000을 비롯한 다른 시뮬레이션 네트워크 카드 드라이버들은 패킷을 송수신할 때, 네트워크 카드 레지스터나 I/O 포트에 많은 read/write 작업을 하게 된다.

이게 VM Exit를 유발해서 네트워크 카드 성능을 안 좋게 만드는 거다.

 

반면 반가상화 환경에서는, 장치랑 드라이버 둘 다 새로 만든, 가상화 환경에 맞춰진 거다.

가상머신 내부의 장치 드라이버랑 QEMU 안의 가상 네트워크 카드 장치가 자기들끼리 데이터 전송용 프로토콜을 정의해서, 자기들이 약속한 인터페이스를 통해 통신을 잘 할 수 있게 된다.

virtio가 바로 이런 식으로 반가상화 기술을 이용해서 I/O 성능을 제공하는 프레임워크다. virtio 프레임워크는 아래 그림과 같다

 

virtio는 프론트엔드-백엔드 아키텍처다.

 

Front-End Driver랑 Back-End Device, 자체 정의한 전송 프로토콜로 구성된다.

전송 프로토콜이 있어서 virtio는 QEMU/KVM 방안에만 쓸 수 있는 게 아니라 다른 가상화 방안에도 쓸 수 있다.

가상머신이 꼭 QEMU여야 하는 것도 아니고 다른 타입의 가상머신도 가능하다. 백엔드도 꼭 QEMU에서 구현해야 하는 게 아니라 커널 안에서 구현해도 된다 (이게 바로 vhost 방안이다).

 

프론트엔드 드라이버

프론트엔드 드라이버는 가상머신 내부의 virtio 시뮬레이션 장치에 대응되는 드라이버다.

프론트엔드 장치마다 거기에 맞는 드라이버가 있어야 정상적으로 동작한다.

프론트엔드 드라이버의 주요 역할은

1) 유저 모드의 요청을 받는다;

2) 그다음에 전송 프로토콜에 따라서 이 요청들을 캡슐화한다;

3) 다시 I/O 포트에 쓴다;

4) QEMU의 백엔드 장치한테 알림(notification)을 하나 보낸다.

 

백엔드 장치

백엔드 장치는 QEMU 안에 있다.

프론트엔드 드라이버가 보내온 I/O 요청을 받아서, 받은 데이터를 전송 프로토콜 포맷에 따라 파싱한다.

네트워크 카드처럼 실제 물리 장치랑 상호작용해야 하는 요청이라면, 백엔드 장치가 물리 장치를 조작해서 요청을 완료한 다음, 인터럽트 메커니즘을 통해 프론트엔드 드라이버에 알린다.

 

virtio 큐

virtio 프론트엔드와 백엔드 드라이버는 데이터 전송을 virtio 큐(virtio queue, virtqueue)를 통해 한다.

장치 하나에는 여러 개의 virtio 큐를 등록할 수 있고, 각 큐가 서로 다른 데이터 전송을 담당한다.

어떤 큐는 컨트롤 레벨, 어떤 큐는 데이터 레벨이다. virtqueue는 vring으로 구현된다.

vring은 가상머신이랑 QEMU 사이에 공유되는 링(ring) 형태의 버퍼다. 가상머신이 QEMU에 요청을 보내야 할 때 데이터를 준비하고, 데이터 설명(description)을 vring에 넣고, I/O 포트에 한 번 쓴다.

그러면 QEMU가 vring에서 데이터 정보를 읽어서 메모리에서 데이터를 가져갈 수 있다. QEMU가 요청을 완료한 후에는, 데이터 구조를 다시 vring에 넣어두면 프론트엔드 드라이버가 vring에서 데이터를 받아갈 수 있다.

 

vring의 기본 원리는 아래 그림과 같다

vring은 세 부분으로 구성된다:

  1. 디스크립터 테이블(Descriptor Table)
    • I/O 요청의 전송 데이터 정보를 기술하는 데 쓴다. 주소, 길이 같은 정보가 들어 있다.
  2. 사용 가능한 vring(Available Vring)
    • 프론트엔드 드라이버가 설정한 것으로, 백엔드 장치한테 사용 가능하다고 알려주는 디스크립터 테이블의 인덱스다.
  3. 이미 사용한 vring(Used Vring)
    • 백엔드 장치가 디스크립터를 다 쓰고 나서 설정한 인덱스다. 이걸로 프론트엔드 드라이버가 어떤 디스크립터들이 이미 사용됐는지 알 수 있다.

여기서 또 다른 관점과 시각으로 보면, virtio 아키텍처는 4개의 계층으로 나눌 수 있다.

 

(1) 프론트엔드 드라이버

먼저, 가상머신 안쪽의 virtio 프론트엔드는, 서로 다른 타입의 장치에 대해 서로 다른 드라이버 프로그램을 갖지만 인터페이스는 다 통일돼 있다. 예를 들면 하드디스크는 virtio_blk, 네트워크는 virtio_net 이런 식이다.

요점:

  1. 가상머신 안에서 돌아간다.
  2. 장치 타입에 따라 다른 드라이버 프로그램을 쓰지만, 백엔드 드라이버랑 주고받는 인터페이스는 다 통일돼 있다.
  3. Qemu+KVM 가상화 환경에서, 소스코드는 KVM 소스코드(Guest 커널) 안에 있다.

(2) 백엔드 드라이버

다음으로, 호스트 측 QEMU 안에서, virtio 백엔드의 로직을 구현하는데, 주로 하드웨어 장치를 다루는 일이다. 예를 들면 물리 하드디스크 위의 파일에 쓰는 걸로 가상머신의 하드디스크 쓰기 작업을 완료한다. 또는 커널 프로토콜 스택에 네트워크 패킷을 보내서 가상머신의 네트워크 작업을 완료한다.

요점:

  1. 호스트 안에서 돌아간다.
  2. virtio 백엔드 로직을 구현한다. 주로 하드웨어 장치를 조작하는 일이다. 예를 들면 커널 프로토콜 스택에 네트워크 패킷을 보내서 가상머신의 네트워크 작업을 완료한다.
  3. Qemu+KVM 가상화 환경에서, 소스코드는 Qemu 소스코드 안에 있다.

(3, 4) virtio 계층과 virtio-ring 계층

virtio의 프론트엔드와 백엔드 사이에는 통신 계층이 하나 있는데, 거기에 virtio 계층이랑 virtio-ring 계층이 포함된다. virtio 이 계층이 구현하는 건 가상 큐 인터페이스고, 이게 프론트엔드-백엔드 통신의 다리 역할을 한다. virtio-ring은 이 다리의 구체적인 구현이다.

 

virtio 계층

  1. virtio 계층은 가상 큐 인터페이스를 구현해서, 프론트엔드-백엔드 통신의 다리 역할을 한다.
  2. 장치 타입마다 사용하는 가상 큐 개수가 다르다.
  3. Linux 커널 소스코드에서 소스코드는 drivers/virtio/virtio.c 안에 있다.

virtio-ring 계층

  1. virtio-ring 계층은 가상 큐의 구체적인 구현이다.
  2. 소스코드는 Linux 커널 소스 /driver/virtio/virtio_ring.c 안에 있다.

virtio는 virtqueue를 써서 프론트엔드랑 백엔드 사이의 고속 통신을 한다. 장치 타입마다 큐 개수가 다르다. virtio-net은 큐 두 개를 쓴다. 하나는 수신용, 다른 하나는 송신용이다. 반면에 virtio-blk는 큐 하나만 쓴다.

 

게스트가 호스트한테 데이터를 보내고 싶으면, 데이터의 buffer를 virtqueue에 추가하고, 레지스터에 써서 호스트한테 알린다.

이러면 호스트가 virtqueue에서 buffer 안의 데이터를 받을 수 있다 (앞에서 말한 메커니즘이랑 같은 의미다: 가상머신이 QEMU에 요청을 보내야 할 때 데이터를 준비하고, 데이터 설명을 vring에 넣고, I/O 포트에 한 번 쓴다. 그러면 QEMU가 vring에서 데이터 정보를 읽어서 메모리에서 데이터를 가져갈 수 있다. QEMU가 요청을 완료한 후에는, 데이터 구조를 다시 vring에 넣어두면 프론트엔드 드라이버가 vring에서 데이터를 받아갈 수 있다).

 

2) virtio 프레임워크와 장치 초기화 (타입 계층)

virtio 장치의 초기화

본격적으로 코드를 가지고 virtio의 구현 메커니즘이랑 디테일을 확인해보자.

virtio 장치는 먼저 PCI 장치 하나를 만들어야 한다. 이걸 virtio PCI proxy device라고 부르는데, 이 대리 장치는 PCI 버스에 연결된다.

그다음에, virtio PCI proxy device가 다시 virtio 버스를 하나 만든다. 그러면 virtio 장치는 이 virtio 버스에 연결될 수 있게 된다.

 

먼저 virtio PCI 대리 장치 타입(class)의 정의를 보자.

static const TypeInfo virtio_pci_info = {
    .name          = TYPE_VIRTIO_PCI,
    .parent        = TYPE_PCI_DEVICE,
    .instance_size = sizeof(VirtIOPCIProxy),
    .class_init    = virtio_pci_class_init,
    .class_size    = sizeof(VirtioPCIClass),
    .abstract      = true,
};
......
static const TypeInfo virtio_pci_bus_info = {
    .name          = TYPE_VIRTIO_PCI_BUS,
    .parent        = TYPE_VIRTIO_BUS,
    .instance_size = sizeof(VirtioPCIBusState),
    .class_size    = sizeof(VirtioPCIBusClass),
    .class_init    = virtio_pci_bus_class_init,
};

static void virtio_pci_register_types(void)
{
    /* Base types: */
    type_register_static(&virtio_pci_bus_info);
    type_register_static(&virtio_pci_info);
}

type_init(virtio_pci_register_types)

 

/*
 * virtio-pci: This is the PCIDevice which has a virtio-pci-bus.
 */
#define TYPE_VIRTIO_PCI "virtio-pci"
#define TYPE_PCI_DEVICE "pci-device"
#define TYPE_VIRTIO_PCI_BUS "virtio-pci-bus"
#define TYPE_VIRTIO_BUS "virtio-bus"

virtio PCI proxy device(프록시 디바이스) 의 정의를 보면 알 수 있듯이,

virtio PCI proxy device의 부모 장치는 PCI 장치고(.parent = TYPE_PCI_DEVICE,),

타입(class)은 VirtIOPCIClass다(.class_size = sizeof(VirtioPCIClass),).

그리고 인스턴스는 virtIOPCIProxy다(.instance_size = sizeof(VirtIOPCIProxy),).

 

한 가지 주의할 점은 이게 추상 장치(abstract device)라는 거다(.abstract = true,).

그래서 자기 인스턴스는 만들 수 없고, 자기 하위 타입을 통해서만 만들 수 있다.

 

QEMU 안에는 모든 virtio PCI proxy device 가 정의돼 있다.

예를 들면 virtio balloon PCI 장치, virtio scsi PCI 장치, virtio crypto PCI 장치 등등이다.

각 프록시 장치의 정의는 각각 아래와 같다

 

virtio balloon PCI 장치

static const VirtioPCIDeviceTypeInfo virtio_balloon_pci_info = {
    .base_name             = TYPE_VIRTIO_BALLOON_PCI,
    .generic_name          = "virtio-balloon-pci",
    .transitional_name     = "virtio-balloon-pci-transitional",
    .non_transitional_name = "virtio-balloon-pci-non-transitional",
    .instance_size = sizeof(VirtIOBalloonPCI),
    .instance_init = virtio_balloon_pci_instance_init,
    .class_init    = virtio_balloon_pci_class_init,
};

static void virtio_balloon_pci_register(void)
{
    virtio_pci_types_register(&virtio_balloon_pci_info);
}

type_init(virtio_balloon_pci_register)

 

virtio scsi PCI 장치

static const VirtioPCIDeviceTypeInfo virtio_scsi_pci_info = {
    .base_name             = TYPE_VIRTIO_SCSI_PCI,
    .generic_name          = "virtio-scsi-pci",
    .transitional_name     = "virtio-scsi-pci-transitional",
    .non_transitional_name = "virtio-scsi-pci-non-transitional",
    .instance_size = sizeof(VirtIOSCSIPCI),
    .instance_init = virtio_scsi_pci_instance_init,
    .class_init    = virtio_scsi_pci_class_init,
};

static void virtio_scsi_pci_register(void)
{
    virtio_pci_types_register(&virtio_scsi_pci_info);
}

type_init(virtio_scsi_pci_register)

 

virtio crypto PCI 장치

static const VirtioPCIDeviceTypeInfo virtio_crypto_pci_info = {
    .generic_name  = TYPE_VIRTIO_CRYPTO_PCI,
    .instance_size = sizeof(VirtIOCryptoPCI),
    .instance_init = virtio_crypto_pci_initfn,
    .class_init    = virtio_crypto_pci_class_init,
};

static void virtio_crypto_pci_register_types(void)
{
    virtio_pci_types_register(&virtio_crypto_pci_info);
}

type_init(virtio_crypto_pci_register_types)

 

virtio block PCI 장치

static const VirtioPCIDeviceTypeInfo virtio_blk_pci_info = {
    .base_name             = TYPE_VIRTIO_BLK_PCI,
    .generic_name          = "virtio-blk-pci",
    .transitional_name     = "virtio-blk-pci-transitional",
    .non_transitional_name = "virtio-blk-pci-non-transitional",
    .instance_size = sizeof(VirtIOBlkPCI),
    .instance_init = virtio_blk_pci_instance_init,
    .class_init    = virtio_blk_pci_class_init,
};

static void virtio_blk_pci_register(void)
{
    virtio_pci_types_register(&virtio_blk_pci_info);
}

type_init(virtio_blk_pci_register)

 

virtio net PCI 장치

static const VirtioPCIDeviceTypeInfo virtio_net_pci_info = {
    .base_name             = TYPE_VIRTIO_NET_PCI,
    .generic_name          = "virtio-net-pci",
    .transitional_name     = "virtio-net-pci-transitional",
    .non_transitional_name = "virtio-net-pci-non-transitional",
    .instance_size = sizeof(VirtIONetPCI),
    .instance_init = virtio_net_pci_instance_init,
    .class_init    = virtio_net_pci_class_init,
};

static void virtio_net_pci_register(void)
{
    virtio_pci_types_register(&virtio_net_pci_info);
}

type_init(virtio_net_pci_register)

 

virtio serial PCI 장치

static const VirtioPCIDeviceTypeInfo virtio_serial_pci_info = {
    .base_name             = TYPE_VIRTIO_SERIAL_PCI,
    .generic_name          = "virtio-serial-pci",
    .transitional_name     = "virtio-serial-pci-transitional",
    .non_transitional_name = "virtio-serial-pci-non-transitional",
    .instance_size = sizeof(VirtIOSerialPCI),
    .instance_init = virtio_serial_pci_instance_init,
    .class_init    = virtio_serial_pci_class_init,
};

static void virtio_serial_pci_register(void)
{
    virtio_pci_types_register(&virtio_serial_pci_info);
}

type_init(virtio_serial_pci_register)

 

virtio input PCI 장치

static const TypeInfo virtio_input_pci_info = {
    .name          = TYPE_VIRTIO_INPUT_PCI,
    .parent        = TYPE_VIRTIO_PCI,
    .instance_size = sizeof(VirtIOInputPCI),
    .class_init    = virtio_input_class_init,
    .abstract      = true,
};

static const TypeInfo virtio_input_hid_pci_info = {
    .name          = TYPE_VIRTIO_INPUT_HID_PCI,
    .parent        = TYPE_VIRTIO_INPUT_PCI,
    .instance_size = sizeof(VirtIOInputHIDPCI),
    .abstract      = true,
};

static const VirtioPCIDeviceTypeInfo virtio_keyboard_pci_info = {
    .generic_name  = TYPE_VIRTIO_KEYBOARD_PCI,
    .parent        = TYPE_VIRTIO_INPUT_HID_PCI,
    .class_init    = virtio_input_hid_kbd_pci_class_init,
    .instance_size = sizeof(VirtIOInputHIDPCI),
    .instance_init = virtio_keyboard_initfn,
};

static const VirtioPCIDeviceTypeInfo virtio_mouse_pci_info = {
    .generic_name  = TYPE_VIRTIO_MOUSE_PCI,
    .parent        = TYPE_VIRTIO_INPUT_HID_PCI,
    .class_init    = virtio_input_hid_mouse_pci_class_init,
    .instance_size = sizeof(VirtIOInputHIDPCI),
    .instance_init = virtio_mouse_initfn,
};

static const VirtioPCIDeviceTypeInfo virtio_tablet_pci_info = {
    .generic_name  = TYPE_VIRTIO_TABLET_PCI,
    .parent        = TYPE_VIRTIO_INPUT_HID_PCI,
    .instance_size = sizeof(VirtIOInputHIDPCI),
    .instance_init = virtio_tablet_initfn,
};

static const VirtioPCIDeviceTypeInfo virtio_multitouch_pci_info = {
    .generic_name  = TYPE_VIRTIO_MULTITOUCH_PCI,
    .parent        = TYPE_VIRTIO_INPUT_HID_PCI,
    .instance_size = sizeof(VirtIOInputHIDPCI),
    .instance_init = virtio_multitouch_initfn,
};

static void virtio_pci_input_register(void)
{
    /* Base types: */
    type_register_static(&virtio_input_pci_info);
    type_register_static(&virtio_input_hid_pci_info);

    /* Implementations: */
    virtio_pci_types_register(&virtio_keyboard_pci_info);
    virtio_pci_types_register(&virtio_mouse_pci_info);
    virtio_pci_types_register(&virtio_tablet_pci_info);
    virtio_pci_types_register(&virtio_multitouch_pci_info);
}

type_init(virtio_pci_input_register)

 

위의 각 virtio 프록시 디바이스들은 다 VirtioPCIDeviceTypeInfo를 쓰고 있다.

/* Input for virtio_pci_types_register() */
typedef struct VirtioPCIDeviceTypeInfo {
    /*
     * Common base class for the subclasses below.
     *
     * Required only if transitional_name or non_transitional_name is set.
     *
     * We need a separate base type instead of making all types
     * inherit from generic_name for two reasons:
     * 1) generic_name implements INTERFACE_PCIE_DEVICE, but
     *    transitional_name does not.
     * 2) generic_name has the "disable-legacy" and "disable-modern"
     *    properties, transitional_name and non_transitional name don't.
     */
    const char *base_name;
    /*
     * Generic device type.  Optional.
     *
     * Supports both transitional and non-transitional modes,
     * using the disable-legacy and disable-modern properties.
     * If disable-legacy=auto, (non-)transitional mode is selected
     * depending on the bus where the device is plugged.
     *
     * Implements both INTERFACE_PCIE_DEVICE and INTERFACE_CONVENTIONAL_PCI_DEVICE,
     * but PCI Express is supported only in non-transitional mode.
     *
     * The only type implemented by QEMU 3.1 and older.
     */
    const char *generic_name;
    /*
     * The transitional device type.  Optional.
     *
     * Implements both INTERFACE_PCIE_DEVICE and INTERFACE_CONVENTIONAL_PCI_DEVICE.
     */
    const char *transitional_name;
    /*
     * The non-transitional device type.  Optional.
     *
     * Implements INTERFACE_CONVENTIONAL_PCI_DEVICE only.
     */
    const char *non_transitional_name;

    /* Parent type.  If NULL, TYPE_VIRTIO_PCI is used */
    const char *parent;

    /* Same as TypeInfo fields: */
    size_t instance_size;
    size_t class_size;
    void (*instance_init)(Object *obj);
    void (*instance_finalize)(Object *obj);
    void (*class_init)(ObjectClass *klass, void *data);
    InterfaceInfo *interfaces;
} VirtioPCIDeviceTypeInfo;

 

virtio balloon 장치를 예로 들어보자.

typedef struct VirtIOBalloonPCI VirtIOBalloonPCI;
struct VirtIOBalloonPCI {
    VirtIOPCIProxy parent_obj;
    VirtIOBalloon vdev;
};
/*
 * virtio-pci: This is the PCIDevice which has a virtio-pci-bus.
 */
#define TYPE_VIRTIO_PCI "virtio-pci"
OBJECT_DECLARE_TYPE(VirtIOPCIProxy, VirtioPCIClass, VIRTIO_PCI)
struct VirtIOPCIProxy {
    PCIDevice pci_dev;
    MemoryRegion bar;
    union {
        struct {
            VirtIOPCIRegion common;
            VirtIOPCIRegion isr;
            VirtIOPCIRegion device;
            VirtIOPCIRegion notify;
            VirtIOPCIRegion notify_pio;
        };
        VirtIOPCIRegion regs[5];
    };
    MemoryRegion modern_bar;
    MemoryRegion io_bar;
    uint32_t legacy_io_bar_idx;
    uint32_t msix_bar_idx;
    uint32_t modern_io_bar_idx;
    uint32_t modern_mem_bar_idx;
    int config_cap;
    uint32_t flags;
    bool disable_modern;
    bool ignore_backend_features;
    OnOffAuto disable_legacy;
    /* Transitional device id */
    uint16_t trans_devid;
    uint32_t class_code;
    uint32_t nvectors;
    uint32_t dfselect;
    uint32_t gfselect;
    uint32_t guest_features[2];
    VirtIOPCIQueue vqs[VIRTIO_QUEUE_MAX];
    VirtIOIRQFD *vector_irqfd;
    int nvqs_with_notifiers;
    VirtioBusState bus;
};
#define TYPE_VIRTIO_BALLOON "virtio-balloon-device"
OBJECT_DECLARE_SIMPLE_TYPE(VirtIOBalloon, VIRTIO_BALLOON)
struct VirtIOBalloon {
    VirtIODevice parent_obj;
    VirtQueue *ivq, *dvq, *svq, *free_page_vq, *reporting_vq;
    uint32_t free_page_hint_status;
    uint32_t num_pages;
    uint32_t actual;
    uint32_t free_page_hint_cmd_id;
    uint64_t stats[VIRTIO_BALLOON_S_NR];
    VirtQueueElement *stats_vq_elem;
    size_t stats_vq_offset;
    QEMUTimer *stats_timer;
    IOThread *iothread;
    QEMUBH *free_page_bh;
    /*
     * Lock to synchronize threads to access the free page reporting related
     * fields (e.g. free_page_hint_status).
     */
    QemuMutex free_page_lock;
    QemuCond  free_page_cond;
    /*
     * Set to block iothread to continue reading free page hints as the VM is
     * stopped.
     */
    bool block_iothread;
    NotifierWithReturn free_page_hint_notify;
    int64_t stats_last_update;
    int64_t stats_poll_interval;
    uint32_t host_features;
    bool qemu_4_0_config_size;
    uint32_t poison_val;
};

virtio balloon 장치의 인스턴스(instance) 객체는 VirtIOBalloon이다.

구체적인 virtio 장치, virtio PCI 대리 장치, virtio 공통 장치 사이의 관계는 아래 그림과 같다

VirtIOBalloonPCI는 virtio balloon PCI 대리 장치의 인스턴스 객체다.

안에 두 부분이 들어 있다.

하나는 VirtIOPCIProxy인데, 이건 virtio PCI 프록시장치의 공통 구조다. 안에 구체적인 virtio PCI 프록시장치 관련 멤버들이 들어 있다.

다른 하나는 VirtIOBalloon인데, 이 구조에는 virtio balloon 장치 관련 데이터가 들어 있다.

첫 번째 멤버가 VirtIODevice인데, 그게 바로 virtio 공통 장치의 인스턴스 객체다.

VirtIOBalloon의 나머지 멤버들은 virtio balloon 장치 관련 데이터다.

 

3) VirtIO-PCI 초기화와 PCI enumeration 흐름

VirtIO-PCI 초기화

virtio 디바이스는 먼저 PCI 디바이스를 하나 만들어야 한다. virtio PCI 프록시 디바이스라고 부르는데, 이 프록시 디바이스는 PCI 버스에 연결된다.

이어서, virtio 대리 디바이스는 다시 virtio 버스 하나를 만든다. 이렇게 하면 virtio 디바이스는 이 virtio 버스에 연결될 수 있게 된다.

 

VirtIO-PCI 버스 트리 구조

pci.0
  └─ virtio-blk-pci         ← virtio-blk의 PCI 프록시
       └─ virtio-bus
            └─ virtio_blk_device   ← 여기가 virtio-blk 본체

virtio-blk은 virtio-pci에 기반하고, virtio-pci는 pci에 기반하니까, 그래서 virtio-blk 초기화는 pci 디바이스 초기화에서부터 봐야한다.

 

1. PCI 초기화

PCI 드라이버 프레임워크 초기화는 커널 안에서 두 개의 진입점을 가진다. 각각 다음과 같다.

  • arch_initcall(pci_arch_init)
  • subsys_initcall(pci_subsys_init)

 

(1) 열거(enumeration)

열거의 전제는 해당 PCI 디바이스에 접근할 수 있어야 한다는 것이다.

PCI 규범의 규정에 따르면, 디바이스가 아직 설정 주소를 할당받기 전에는,

CPU가 두 개의 IO 포트(CONFIG_ADDRESS: 0xCF8, CONFIG_DATA: 0xCFC)에 각각 주소와 데이터를 써넣는 방식으로 PCI 디바이스 읽기/쓰기를 실현한다.

 

아래 그림처럼, 이 두 IO 포트에 대응되는 것은 Host 브릿지 위의 레지스터다. 이들은 io 명령어를 통해 직접 접근할 수 있다.

CPU와 Host Bridge 구조

 

이 두 레지스터는 호스트 브릿지(Host Bridge) 위에 위치한다. Host Bridge (Intel 5000X MCH 3.5장) 매뉴얼을 펼쳐 보면, 레지스터 각 필드의 구체적인 의미를 찾을 수 있다. 아래 표와 같다.

 

I/O Address: CF8h

Bit Attr Default Description
31 RW 0h CFGE: Configuration Enable — Unless this bit is set, accesses to the CFGDAT register will not produce a configuration access, but will be treated as other I/O accesses. This bit is strictly an enable for the CFC/CF8 access mechanism and is not forwarded to ESI or PCI Express.
30:24 RV 00h Reserved.
23:16 RW 00h Bus Number — If 0, the MCH examines device to determine where to route. If non-zero, route as per PBUSN and SBUSN registers.
15:11 RW 0h Device Number — This field is used to select one of the 32 possible devices per bus.
10:8 RW 0h Function Number — This field is used to select the function of a locally addressed register.
7:2 RW 00h Register Offset — If this register specifies an access to MCH registers, this field specifies a group of four bytes to be addressed. The bytes accessed are defined by the Byte enables of the CFGDAT register access
1:0 RW 0h Writes to these bits have no effect, reads return 0

I/O Address: CFCh

Bit Attr Default Description
31:0 RW 0 Configuration Data Window — The data written or read to the configuration register (if any) specified by CFGADR

cpu가 어떤 pci 디바이스에 접근하려고 할 때, 먼저 0xCF8에 4바이트의 "bus/slot/function" 주소를 써넣고, 그 다음 0xCFC의 IO 공간을 통해 데이터를 읽어오거나 써넣는다.

주소 공간 0xCF8의 초기화는 pci_arch_init() 안에서 발생한다.

/* arch_initcall has too random ordering, so call the initializers
   in the right sequence from here. */
static __init int pci_arch_init(void)
{
    int type, pcbios = 1;

    type = pci_direct_probe();

    if (!(pci_probe & PCI_PROBE_NOEARLY))
        pci_mmcfg_early_init();

    if (x86_init.pci.arch_init)
        pcbios = x86_init.pci.arch_init();

    /*
     * Must happen after x86_init.pci.arch_init(). Xen sets up the
     * x86_init.irqs.create_pci_msi_domain there.
     */
    x86_create_pci_msi_domain();

    if (!pcbios)
        return 0;

    pci_pcibios_init();

    /*
     * don't check for raw_pci_ops here because we want pcbios as last
     * fallback, yet it's needed to run first to set pcibios_last_bus
     * in case legacy PCI probing is used. otherwise detecting peer busses
     * fails.
     */
    pci_direct_init(type);

    if (!raw_pci_ops && !raw_pci_ext_ops)
        printk(KERN_ERR
        "PCI: Fatal: No config space access function found\n");

    dmi_check_pciprobe();

    dmi_check_skip_isa_align();

    return 0;
}
arch_initcall(pci_arch_init);

체계 구조의 초기화, IO 주소 0xCF8 초기화를 포함한다.

 

(2) subsys_initcall(pci_subsys_init)

static int __init pci_subsys_init(void)
{
    /*
     * The init function returns an non zero value when
     * pci_legacy_init should be invoked.
     */
    if (x86_init.pci.init()) {
        if (pci_legacy_init()) {
            pr_info("PCI: System does not support PCI\n");
            return -ENODEV;
        }
    }

    pcibios_fixup_peer_bridges();
    x86_init.pci.init_irq();
    pcibios_init();

    return 0;
}
subsys_initcall(pci_subsys_init);

PCI 서브시스템 초기화, 이 과정은 PCI 버스 트리 위 디바이스의 열거를 완성한다. Host bridge가 PCI 디바이스에 주소 공간을 할당하고 BAR 레지스터에 써넣는다.

 

열거(enumeration) 흐름 심층 분석

1. PCI 초기화

(1) 열거 — 전체 흐름

pci_subsys_init() 안에서 발생하는 열거 과정을 상세히 분석해보자.

 

(1) pci_subsys_init()이 x86_init.pci.init을 호출

pci_subsys_init
    x86_init.pci.init    =>   x86_default_pci_init
/*
 * The platform setup functions are preset with the default functions
 * for standard PC hardware.
 */
struct x86_init_ops x86_init __initdata = {

    .resources = {
        .probe_roms        = probe_roms,
        .reserve_resources = reserve_standard_io_resources,
        .memory_setup      = e820__memory_setup_default,
    },

    .mpparse = {
        .setup_ioapic_ids = x86_init_noop,
        .find_smp_config  = default_find_smp_config,
        .get_smp_config   = default_get_smp_config,
    },

    .irqs = {
        .pre_vector_init    = init_ISA_irqs,
        .intr_init          = native_init_IRQ,
        .intr_mode_select   = apic_intr_mode_select,
        .intr_mode_init     = apic_intr_mode_init,
        .create_pci_msi_domain = native_create_pci_msi_domain,
    },

    .oem = {
        .arch_setup    = x86_init_noop,
        .banner        = default_banner,
    },

    .paging = {
        .pagetable_init    = native_pagetable_init,
    },

    .timers = {
        .setup_percpu_clockev = setup_boot_APIC_clock,
        .timer_init           = hpet_time_init,
        .wallclock_init       = x86_wallclock_init,
    },

    .iommu = {
        .iommu_init    = iommu_init_noop,
    },

    .pci = {
        .init        = x86_default_pci_init,
        .init_irq    = x86_default_pci_init_irq,
        .fixup_irqs  = x86_default_pci_fixup_irqs,
    },

    .hyper = {
        .init_platform     = x86_init_noop,
        .guest_late_init   = x86_init_noop,
        .x2apic_available  = bool_x86_init_noop,
        .msi_ext_dest_id   = bool_x86_init_noop,
        .init_mem_mapping  = x86_init_noop,
        .init_after_bootmem = x86_init_noop,
    },

    .acpi = {
        .set_root_pointer    = x86_default_set_root_pointer,
        .get_root_pointer    = x86_default_get_root_pointer,
        .reduced_hw_early_init = acpi_generic_reduced_hw_init,
    },
};

x86_init.pci와 관련된 단락은

.pci = {
        .init        = x86_default_pci_init,
        .init_irq    = x86_default_pci_init_irq,
        .fixup_irqs  = x86_default_pci_fixup_irqs,
    },

그러니까, pci_subsys_init 함수 안에서 호출되는 x86_init.pci.init()은 사실 x86_default_pci_init()이다.

이 x86_default_pci_init은 실제로는 매크로 정의다.

#ifdef CONFIG_PCI
# ifdef CONFIG_ACPI
#  define x86_default_pci_init      pci_acpi_init
# else
#  define x86_default_pci_init      pci_legacy_init
# endif
# define x86_default_pci_init_irq    pcibios_irq_init
# define x86_default_pci_fixup_irqs pcibios_fixup_irqs
#else
# define x86_default_pci_init       NULL
# define x86_default_pci_init_irq   NULL
# define x86_default_pci_fixup_irqs NULL
#endif

 

x86_default_pci_init 분기 정리

만약 CONFIG_PCI가 정의되지 않았다면, 즉 PCI가 활성화되지 않았다면, x86_default_pci_init은 NULL이 된다.

 

만약 CONFIG_PCI는 정의되었는데, CONFIG_ACPI가 정의되지 않았다면,

즉 PCI는 활성화되었지만 ACPI가 활성화되지 않은 경우라면, x86_default_pci_init은 pci_legacy_init이 된다.

 

만약 CONFIG_PCI와 CONFIG_ACPI가 둘 다 정의되었다면, 즉 PCI도 활성화되고 ACPI도 활성화된 경우라면, x86_default_pci_init은 pci_acpi_init이 된다.

 

int __init pci_legacy_init(void)
{
    if (!raw_pci_ops)
        return 1;

    pr_info("PCI: Probing PCI hardware\n");
    pcibios_scan_root(0);
    return 0;
}

 

int __init pci_acpi_init(void)
{
    struct pci_dev *dev = NULL;

    if (acpi_noirq)
        return -ENODEV;

    printk(KERN_INFO "PCI: Using ACPI for IRQ routing\n");
    acpi_irq_penalty_init();
    pcibios_enable_irq = acpi_pci_irq_enable;
    pcibios_disable_irq = acpi_pci_irq_disable;
    x86_init.pci.init_irq = x86_init_noop;

    if (pci_routeirq) {
        /*
         * PCI IRQ routing is set up by pci_enable_device(), but we
         * also do it here in case there are still broken drivers that
         * don't use pci_enable_device().
         */
        printk(KERN_INFO "PCI: Routing PCI interrupts for all devices because \"pci=routeirq\" specified\n");
        for_each_pci_dev(dev)
            acpi_pci_irq_enable(dev);
    }

    return 0;
}

 

pci_legacy_init 함수 안의 raw_pci_ops

int __init pci_legacy_init(void)
{
    if (!raw_pci_ops)
        return 1;

    pr_info("PCI: Probing PCI hardware\n");
    pcibios_scan_root(0);
    return 0;
}

pci_legacy_init 함수는 매우 간단하다. 주로 pcibios_scan_root 함수를 호출한다.

다만 호출하기 전에, pci_legacy_init 함수는 처음에 먼저 raw_pci_ops가 비어 있는지(NULL) 아닌지 판단한다.

 

커널 코드에서 raw_pci_ops를 검색해 보면, 몇 군데에서 값이 할당되는 걸 볼 수 있다. 여기서 하나씩 나열해 보자.

 

  • arch/x86/pci/ce4100.c의 ce4100_pci_init 함수 안
static const struct pci_raw_ops ce4100_pci_conf = {
    .read   = ce4100_conf_read,
    .write  = ce4100_conf_write,
};

int __init ce4100_pci_init(void)
{
    init_sim_regs();
    raw_pci_ops = &ce4100_pci_conf;
    /* Indicate caller that it should invoke pci_legacy_init() */
    return 1;
}
  • arch/x86/pci/direct.c의 pci_direct_init 함수 안
void __init pci_direct_init(int type)
{
    if (type == 0)
        return;
    printk(KERN_INFO "PCI: Using configuration type %d for base access\n",
        type);
    if (type == 1) {
        raw_pci_ops = &pci_direct_conf1;
        if (raw_pci_ext_ops)
            return;
        if (!(pci_probe & PCI_HAS_IO_ECS))
            return;
        printk(KERN_INFO "PCI: Using configuration type 1 "
            "for extended access\n");
        raw_pci_ext_ops = &pci_direct_conf1;
        return;
    }
    raw_pci_ops = &pci_direct_conf2;
}
  • arch/x86/pci/direct.c의 pci_direct_probe 함수 안
int __init pci_direct_probe(void)
{
    if ((pci_probe & PCI_PROBE_CONF1) == 0)
        goto type2;
    if (!request_region(0xCF8, 8, "PCI conf1"))
        goto type2;

    if (pci_check_type1()) {
        raw_pci_ops = &pci_direct_conf1;
        port_cf9_safe = true;
        return 1;
    }
    release_region(0xCF8, 8);

 type2:
    if ((pci_probe & PCI_PROBE_CONF2) == 0)
        return 0;
    if (!request_region(0xCF8, 4, "PCI conf2"))
        return 0;
    if (!request_region(0xC000, 0x1000, "PCI conf2"))
        goto fail2;

    if (pci_check_type2()) {
        raw_pci_ops = &pci_direct_conf2;
        port_cf9_safe = true;
        return 2;
    }

    release_region(0xC000, 0x1000);
 fail2:
    release_region(0xCF8, 4);
    return 0;
}
  • arch/x86/pci/numachip.c의 pci_numachip_init 함수 안
static const struct pci_raw_ops pci_mmcfg_numachip = {
    .read = pci_mmcfg_read_numachip,
    .write = pci_mmcfg_write_numachip,
};

int __init pci_numachip_init(void)
{
    int ret = 0;
    u32 val;

    /* For remote I/O, restrict bus 0 access to the actual number of AMD
       Northbridges, which starts at device number 0x18 */
    ret = raw_pci_read(0, 0, PCI_DEVFN(0x18, 0), 0x60, sizeof(val), &val);
    if (ret)
        goto out;

    /* HyperTransport fabric size in bits 6:4 */
    limit = PCI_DEVFN(0x18 + ((val >> 4) & 7) + 1, 0);

    /* Use NumaChip PCI accessors for non-extended and extended access */
    raw_pci_ops = raw_pci_ext_ops = &pci_mmcfg_numachip;
out:
    return ret;
}
  • arch/x86/pci/olpc.c의 pci_olpc_init 함수 안
static const struct pci_raw_ops pci_olpc_conf = {
    .read = pci_olpc_read,
    .write = pci_olpc_write,
};

int __init pci_olpc_init(void)
{
    printk(KERN_INFO "PCI: Using configuration type OLPC XO-1\n");
    raw_pci_ops = &pci_olpc_conf;
    is_lx = is_geode_lx();
    return 0;
}
  • arch/x86/pci/pcbios.c의 pci_pcbios_init 함수 안
void __init pci_pcbios_init(void)
{
    if ((pci_probe & PCI_PROBE_BIOS)
        && ((raw_pci_ops = pci_find_bios()))) {
        pci_bios_present = 1;
    }
}

 

4) virtio 데이터 구조

먼저 《Virtual I/O Device (VIRTIO) Version 1.3》 규범의 "2.6 Virtqueues" 절 안의 내용을 봐자.

OASIS OPEN

Virtual I/O Device (VIRTIO) Version 1.3

Committee Specification Draft 01

06 October 2023

This stage:

  • https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/tex/ (Authoritative)
  • https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/virtio-v1.3-csd01.pdf
  • https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/virtio-v1.3-csd01.html

Previous stage: N/A

Latest stage:

  • https://docs.oasis-open.org/virtio/virtio/v1.3/virtio-v1.3.pdf
  • https://docs.oasis-open.org/virtio/virtio/v1.3/virtio-v1.3.html

Technical Committee: OASIS Virtual I/O Device (VIRTIO) TC

Chairs:

  • Michael S. Tsirkin (mst@redhat.com), Red Hat
  • Cornelia Huck (cohuck@redhat.com), Red Hat

Editors:

  • Michael S. Tsirkin (mst@redhat.com), Red Hat
  • Cornelia Huck (cohuck@redhat.com), Red Hat

Additional artifacts: This prose specification is one component of a Work Product that also includes:

  • Example Driver Listing

2.6 Virtqueues

The mechanism for bulk data transport on virtio devices is pretentiously called a virtqueue. Each device can have zero or more virtqueues¹.

A virtio device can have maximum of 65536 virtqueues. Each virtqueue is identified by a virtqueue index. A virtqueue index has a value in the range of 0 to 65535.

Driver makes requests available to device by adding an available buffer to the queue, i.e., adding a buffer describing the request to a virtqueue, and optionally triggering a driver event, i.e., sending an available buffer notification to the device.

Device executes the requests and - when complete - adds a used buffer to the queue, i.e., lets the driver know by marking the buffer as used. Device can then trigger a device event, i.e., send a used buffer notification to the driver.

Device reports the number of bytes it has written to memory for each buffer it uses. This is referred to as "used length".

Device is not generally required to use buffers in the same order in which they have been made available by the driver.

Some devices always use descriptors in the same order in which they have been made available. These devices can offer the VIRTIO_F_IN_ORDER feature. If negotiated, this knowledge might allow optimizations or simplify driver and/or device code.

Each virtqueue can consist of up to 3 parts:

  • Descriptor Area - used for describing buffers
  • Driver Area - extra data supplied by driver to the device
  • Device Area - extra data supplied by device to driver

Note: Note that previous versions of this spec used different names for these parts (following 2.7):

  • Descriptor Table - for the Descriptor Area
  • Available Ring - for the Driver Area
  • Used Ring - for the Device Area

Two formats are supported: Split Virtqueues (see 2.7 Split Virtqueues) and Packed Virtqueues (see 2.8 Packed Virtqueues).

Every driver and device supports either the Packed or the Split Virtqueue format, or both.

¹For example, the simplest network device has one virtqueue for transmit and one for receive.

 

2.6 Virtqueue (한국어 번역)

virtio 디바이스 위에서 대량의 데이터 전송을 진행하는 메커니즘을 virtqueue라고 자명한다. 각 디바이스는 0개 또는 여러 개의 virtqueue를 가질 수 있다. 예를 들어, 가장 간단한 네트워크 디바이스는 전송용 virtqueue 하나와 수신용 virtqueue 하나를 가진다.

하나의 virtio 디바이스는 최대 65536개의 virtqueue를 가질 수 있다. 각 virtqueue는 virtqueue 인덱스로 식별된다. virtqueue의 값은 0에서 65535 사이의 범위 안에 있다.

드라이버 프로그램은 큐에 사용 가능한 버퍼를 추가하는 방식(예를 들어, virtqueue에 요청을 기술하는 버퍼를 추가)으로, 그리고 선택적으로 드라이버 이벤트(예를 들어, 디바이스에 사용 가능 버퍼 알림을 보냄)를 트리거하는 방식으로 요청이 디바이스에 대해 사용 가능하도록 만든다.

디바이스는 요청을 실행하고, 완료될 때 사용한 버퍼를 큐에 추가한다 (예를 들어, 버퍼를 사용됨으로 표시해서 드라이버 프로그램에게 알린다). 그런 다음, 디바이스는 디바이스 이벤트를 트리거할 수 있다 (예를 들어, 드라이버 프로그램에 사용된 버퍼 알림을 보낸다).

디바이스는 자신이 사용한 각 버퍼에 대해 메모리에 써넣은 바이트 수를 보고한다. 이는 "사용 길이(used length)"라고 불린다. 디바이스는 보통 드라이버 프로그램이 제공한 버퍼의 순서대로 버퍼를 사용할 필요는 없다.

일부 디바이스는 항상 사용 가능 순서대로 디스크립터를 사용한다. 이런 디바이스는 VIRTIO_F_IN_ORDER 기능을 제공할 수 있다. 만약 협상이 일치되면, 이런 지식은 최적화를 허용하거나 드라이버 프로그램 및/또는 디바이스 코드를 단순화할 수 있다.

 

virtqueue 3개 구성 부분 (신구 명칭 대조)

각 virtqueue는 최대 3개 부분으로 구성될 수 있다.

  • 디스크립터 영역 (Descriptor Area) — 버퍼를 기술하는 데 사용된다.
  • 드라이버 영역 (Driver Area) — 드라이버가 디바이스에 제공하는 추가 데이터.
  • 디바이스 영역 (Device Area) — 디바이스가 드라이버에 제공하는 추가 데이터.

주의해야 할 점은, 본 규범의 이전 버전에서는 이 부품들에 대해 다른 이름을 사용했다는 것이다 (이하 2.7에 따름)

  • 디스크립터 표 (Descriptor Table) — 디스크립터 영역에 사용된다.
  • 사용 가능 환 (Available Ring) — 드라이버 영역에 사용된다.
  • 사용된 환 (Used Ring) — 디바이스 영역에 사용된다.

VirtIO 데이터 구조

 

(1) 디스크립터 표 (Descriptor Table)

Descriptor Table 안에는 Guest Driver (게스트 드라이버, 프론트엔드 드라이버)가 제공하는 buffer의 포인터가 저장된다.

각 항목은 Guest Driver가 할당한 수신/송신 데이터 buffer를 가리킨다.

주의: VRing 안 buffer 공간의 할당은 영원히 Guest Driver가 책임진다. Guest Driver가 데이터를 보낼 때는, 또 buffer에 데이터를 채워 넣어야 한다;

Guest Driver가 데이터를 수신할 때는, buffer 공간을 할당한 다음 Host에게 buffer에 데이터를 채워 넣으라고 통지한다.

 

Descriptor Table 안에 저장되는 건 하나하나의 vring_desc 구조 (인스턴스)다.

모든 vring_desc (인스턴스)들이 모여서 하나의 Descriptor Table을 구성한다. Descriptor Table의 항목 수가 바로 virtqueue의 큐 depth다.

Guest가 한 번에 최대로 저장할 수 있는 데이터 buffer의 개수를 나타낸다. QEMU는 기본적으로 128로 설정한다.

이로부터 데이터 구조 struct vring_desc가 끌려 나온다. struct vring_desc의 정의는 QEMU 소스 코드 안에 여러 군데 있다(qemu-8.1.4 버전은 총 3군데).

/* Virtio ring descriptors: 16 bytes. These can chain together via "next". */
struct vring_desc {
    /* Address (guest-physical) */
    __virtio64 addr;
    /* Length */
    __virtio32 len;
    /* The flags as indicated above */
    __virtio16 flags;
    /* We chain unused descriptors via this, too */
    __virtio16 next;
};

vring_desc 구조는 하나의 buffer 디스크립터다. 하나의 Guest 메모리의 buffer를 대표한다고 생각할 수 있다. 전송될 데이터를 가리킨다.

 

vring_desc 구조 한눈에

  • __virtio64 addr — 데이터의 물리 주소.
  • __virtio32 len — 데이터의 길이.
  • __virtio16 flags — 데이터가 Host에 대해 읽기 가능한지 쓰기 가능한지를 표시한다. 만약 buffer가 데이터 송신에 사용되면, Host에 대해 읽기 전용; 그렇지 않으면, Host에 대해 쓰기 전용.

flags의 값은 다음과 같다

/* This marks a buffer as continuing via the next field. */
#define VRING_DESC_F_NEXT    1
/* This marks a buffer as write-only (otherwise read-only). */
#define VRING_DESC_F_WRITE    2
/* This means the buffer contains a list of buffer descriptors. */
#define VRING_DESC_F_INDIRECT    4

VRING_DESC_F_NEXT: 이 buffer 뒤에도 buffer가 있다는 걸 나타낸다. 모든 buffer는 next를 통해 하나의 descriptor chain으로 연결될 수 있다.

VRING_DESC_F_WRITE: 이 buffer가 쓰기만 가능하다는 걸 나타낸다. buffer가 데이터 수신에 사용될 때, Host에게 buffer를 제공해야 한다. 이때 buffer를 쓰기로 표시한다; 반대로 데이터 송신이라면, 읽기로 표시한다.

VRING_DESC_F_INDIRECT: 이 buff가 buffer 디스크립터의 링크 표 하나를 포함한다는 걸 나타낸다.

  • __virtio16 next — 다음 buffer가 Descriptor Table 안의 어디에 위치하는지를 저장한다.

주의해야 할 점은, next는 데이터의 물리 주소를 저장하는 게 아니라는 것이다. 그 클래스형(type)을 보면 어렵지 않게 판단할 수 있다. next는 다음 buffer가 Descriptor Table 안의 인덱스를 저장하는 것이다.

 

여기까지 해서, 첫 번째 데이터 구조——Descriptor Table 및 그 뒤의 실제 vring_desc 구조의 분석이 끝났다.

 

전체 흐름 정리

 

핵심 개념 요약

 

(2) virtqueue / vring 구조체 상세

지난번에 setup_vq 함수의 4단계, 즉 vring_create_virtqueue 함수를 호출해서 실제로 virtqueue를 만드는 과정까지 분석했었다. 그 안의 두 핵심 함수 vring_create_virtqueue_packed()와 vring_create_virtqueue_split()를 자세히 분석하기 전에,  virtqueue 생성 과정과 virtio 전체에서 쓰이는 구조체들을 먼저 정리하고 가는게 좋을듯 하다.

 

지난번에는 struct virtio_device, struct virtio_device_id, struct virtio_config_ops 이렇게 3개의 구조체를 확인했었다.

 

전체 구조체 관계

 

virtqueue 구조

/**
 * struct virtqueue - a queue to register buffers for sending or receiving.
 * @list: the chain of virtqueues for this device
 * @callback: the function to call when buffers are consumed (can be NULL).
 * @name: the name of this virtqueue (mainly for debugging)
 * @vdev: the virtio device this queue was created for.
 * @priv: a pointer for the virtqueue implementation to use.
 * @index: the zero-based ordinal number for this queue.
 * @num_free: number of elements we expect to be able to fit.
 * @num_max: the maximum number of elements supported by the device.
 * @reset: vq is in reset state or not.
 *
 * A note on @num_free: with indirect buffers, each buffer needs one
 * element in the queue, otherwise a buffer will need one element per
 * sg element.
 */
struct virtqueue {
    struct list_head list;
    void (*callback)(struct virtqueue *vq);
    const char *name;
    struct virtio_device *vdev;
    unsigned int index;
    unsigned int num_free;
    unsigned int num_max;
    bool reset;
    void *priv;
};

struct virtqueue는 가상화 환경에서 데이터 전송을 하는 데 쓰이는 데이터 구조다. Linux 커널에서 구현된 메커니즘 중 하나로, 호스트와 가상머신 사이에서 효율적인 데이터 전송을 하는 데 쓴다.

 

virtqueue 멤버 구성

  • struct list_head list 이 디바이스의 virtqueue들이 연결된 리스트. virtio_device의 vqs 리스트에 들어간다.
  • void (*callback)(struct virtqueue *vq) 버퍼가 소비될 때 호출되는 함수다 (NULL일 수도 있다). virtqueue가 트리거되어 인터럽트가 실행될 때의 콜백 함수다.
  • const char *name 이 virtqueue의 이름이다 (주로 디버깅용).
  • struct virtio_device *vdev 이 큐를 만든 virtio 디바이스, 즉 이 virtqueue가 속한 virtio_device다.
  • unsigned int index 이 큐의 0부터 시작하는 순번, 즉 virtqueue의 인덱스 번호다.
  • unsigned int num_free 수용할 수 있을 거라고 기대되는 원소의 개수. 즉 virtqueue 안에서 비어있는 descriptor의 개수다.
  • unsigned int num_max 디바이스가 지원하는 최대 원소 개수. 즉 virtqueue 안에서 가질 수 있는 가장 큰 descriptor 개수다.
  • bool reset vq가 리셋 상태인지 아닌지.
  • void *priv virtqueue 구현이 사용하는 포인터다.

 

vring 관련 구조

struct vring {
    unsigned int num;

    vring_desc_t *desc;

    vring_avail_t *avail;

    vring_used_t *used;
};

struct vring은 가상 ring(가상화 환)을 구현하기 위한 데이터 구조로, 가상화 기술에서 디바이스 모델과 디바이스 드라이버 사이의 통신에 자주 쓰인다. 보통 세 부분으로 구성되어 있다.

  • descriptor table, available ring, used ring.

  • unsigned int num virtqueue의 크기, 즉 이 vring의 descriptor(vring_desc)의 개수를 나타낸다.
  • vring_desc_t *desc descriptor table (디스크립터 표).
  • vring_avail_t *avail available ring. 프론트엔드 드라이버(Guest)가 생산하고, 백엔드 디바이스(Host)가 소비한다.
  • vring_used_t *used used ring. 백엔드 디바이스(Host)가 생산하고, 프론트엔드 드라이버(Guest)가 소비한다.

struct vring의 desc, avail, used는 각각 descriptor table, available ring, used ring의 시작 주소를 가리킨다.

 

Guest ↔ Host 데이터 흐름도

 

(3) vring_desc_t 구조 (vring_desc 구조)

typedef struct vring_desc __attribute__((aligned(VRING_DESC_ALIGN_SIZE)))
    vring_desc_t;
/**
 * struct vring_desc - Virtio ring descriptors,
 * 16 bytes long. These can chain together via @next.
 *
 * @addr: buffer address (guest-physical)
 * @len: buffer length
 * @flags: descriptor flags
 * @next: index of the next descriptor in the chain,
 *        if the VRING_DESC_F_NEXT flag is set. We chain unused
 *        descriptors via this, too.
 */
struct vring_desc {
    __virtio64 addr;
    __virtio32 len;
    __virtio16 flags;
    __virtio16 next;
};

struct vring_desc는 virtio ring(가상 환) 디스크립터를 나타내고, 가상 환을 묘사하는 데이터 구조다. 가상화 기술에서 널리 쓰이는데, 가상 환은 가상머신과 호스트 사이에서 데이터를 전달하는 메커니즘이다.

 

descriptor 체인 구조

flags에 VRING_DESC_F_NEXT가 설정되어 있으면 next 필드를 따라서 다음 descriptor로 이어진다. 이렇게 여러 descriptor를 묶어서 하나의 큰 I/O를 표현한다.

  • __virtio64 addr I/O의 물리 주소 (GPA).
  • __virtio32 len I/O의 길이. 보통 고정된 값이다.
  • __virtio16 flags 디스크립터의 플래그 비트. 현재 디스크립터의 상태나 속성을 나타내는 데 쓴다. f
    • lags는 VRING_DESC_F_NEXT일 수 있는데, 이 I/O가 다음에 이어지는 vring_desc를 포함한다는 뜻이다.
    • 또는 VRING_DESC_F_WRITE일 수도 있는데, 이는 이 I/O가 쓰기만 가능하다는 뜻이다 (디바이스 입장에서).
    • 또 VRING_DESC_F_INDIRECT일 수도 있는데, 이건 이 I/O가 연속되지 않은 vring_desc로 구성된다는 뜻이다.

flags 종류 정리

  • __virtio16 next 다음 디스크립터(vring_desc)의 인덱스를 가리킨다. descriptor 체인을 만드는 데 쓴다.

 

(4) vring_avail_t 구조

typedef struct vring_avail __attribute__((aligned(VRING_AVAIL_ALIGN_SIZE)))
    vring_avail_t;
struct vring_avail {
    __virtio16 flags;
    __virtio16 idx;
    __virtio16 ring[];
};

struct vring_avail은 virtio ring의 available ring을 묘사하는 데이터 구조다.

 

vring_avail이 descriptor table을 가리키는 방식

 

(5) vring_used_t 구조 (vring_used 구조)

typedef struct vring_used __attribute__((aligned(VRING_USED_ALIGN_SIZE)))
    vring_used_t;
struct vring_used {
    __virtio16 flags;
    __virtio16 idx;
    vring_used_elem_t ring[];
};

struct vring_used는 virtio ring의 이미 사용된 버퍼 영역을 묘사하는 데이터 구조다.

보통 가상화 기술에서 공유 메모리 데이터 전송을 구현하는 데 쓰인다.

 

vring_avail vs vring_used 비교

struct vring_used의 각 멤버를 자세히 설명하면 아래와 같다.

  • __virtio16 flags 플래그 비트. used ring의 상태를 나타내는 데 쓴다. vring_used에서도 flags는 보통 쓰이지 않는다.
  • __virtio16 idx 다음에 virtio 디바이스가 ring 배열의 어디에 써야 하는지를 알려준다.
  • vring_used_elem_t ring[] 하나의 인덱스 배열이다.
    • 크기는 virtqueue의 크기와 같다.
    • ring[]의 각 항목은 두 부분으로 되어 있는데,
      • 첫 번째 부분은 id로 사용한 descriptor table의 인덱스를 나타내고,
      • 두 번째 부분은 len으로 이번 virtio 디바이스가 총 얼마만큼의 데이터를 썼는지를 나타낸다.

 

(6) vring_used_elem_t 구조 (vring_used_elem 구조)

typedef struct vring_used_elem __attribute__((aligned(VRING_USED_ALIGN_SIZE)))
    vring_used_elem_t;
/* u32 is used here for ids for padding reasons. */
struct vring_used_elem {
    /* Index of start of used descriptor chain. */
    __virtio32 id;
    /* Total length of the descriptor chain which was used (written to) */
    __virtio32 len;
};

struct vring_used_elem은 virtio 디바이스에서 이미 사용된 버퍼 원소를 묘사하는 구조체다.

virtio 디바이스에서 데이터 전송에 쓰이는 버퍼는 두 부분으로 나뉘는데,

available ring과 used ring이다.

available ring은 디바이스에 새로 사용 가능한 버퍼가 있다는 걸 알리는 데 쓰고, used ring은 드라이버에 디바이스가 이미 어떤 버퍼들을 썼는지 알리는 데 쓴다.

 

vring_used_elem 항목 구조

  • __virtio32 id 이미 사용된 descriptor table의 인덱스를 나타낸다.
  • __virtio32 len 이미 사용된 버퍼의 길이를 나타낸다. 즉 이번 virtio 디바이스가 총 얼마만큼의 데이터를 썼는지.

 

한 번의 I/O 전체 시퀀스 (Guest ↔ Host)

지금까지 본 구조체들이 실제로 어떻게 협력해서 한 번의 I/O가 굴러가는지, 시퀀스로 정리하면 다음과 같다.

 

virtio 전체 계층 구조

여기까지 본 모든 데이터 구조가 virtio 스택의 어디에 해당하는지 정리하면 다음과 같다.

 

Available Ring 과 Used Ring 상세

앞에서 《Virtual I/O Device (VIRTIO) Version 1.3》 규범의 "2.6 Virtqueues" 절에서 언급한 관련 데이터 구조를 확인했다.

 

다시 한번 지난 회의 그 3가지 구조를 되짚어 보면

각 virtqueue는 최대 3개 부분으로 구성될 수 있다.

  • Descriptor Area :버퍼를 기술하는 데 사용한다.
  • Driver Area : 드라이버가 디바이스에 제공하는 추가 데이터다.
  • Device Area : 디바이스가 드라이버에 제공하는 추가 데이터다.

참고로, 본 규범의 이전 버전에서는 이 부품들에 대해 다른 명칭을 사용했다 (이하 2.7로 표기)

  • Descriptor Table :디스크립터 영역을 기술하는 데 사용한다.
  • Available Ring :드라이버 영역을 기술하는 데 사용한다.
  • Used Ring :디바이스 영역을 기술하는 데 사용한다.

이해를 돕기 위해 여기서는 구버전 명칭으로 설명한다. 먼저 VirtIO 데이터 구조를 보자.

 

VirtIO 데이터 구조

  • virtio_blk 같은 디바이스는 vqs[i].vq로 vring_virtqueue를 가리키고, parent로 virtio_device를 가리킨다.
  • vring_virtqueue 안에서 Guest front-end와 Host back-end (qemu/vhost) 사이에 깊이가 N인 virtqueue가 양방향으로 자리한다.
  • Guest는 "Add buffer" 흐름으로 Avail Ring에 새로 추가한 buffer 위치를 기록하고, Host는 "Get buffer" 흐름으로 Used Ring을 통해 처리 결과를 돌려준다.
  • Avail Ring과 Used Ring의 ring[]은 모두 Descriptor Table의 인덱스를 저장하며, 그 인덱스가 가리키는 vring_desc들이 next 포인터로 연결돼서 Descriptor Chain을 이루고, 그 끝에 실제 buffer들이 매달려 있다.

(1) Available Ring

Descriptor Table 인덱스를 저장하며, Descriptor Table 안의 한 entry를 가리킨다.

Guest Driver가 Vring에 buffer를 추가할 때 한 번에 하나 또는 여러 개의 buffer를 추가할 수 있고, 모든 buffer는 하나의 Descriptor chain (바로 Descriptor Table이다)을 구성한다.

Guest Driver가 buffer 추가에 성공한 후에는 Descriptor chain 머리 부분의 주소를 Avail Ring에 기록해서, Host 측이 새로 추가된 가용 buffer가 VRing의 어느 위치에서 시작되는지 알 수 있도록 한다.

 

Host가 Descriptor chain의 머리 주소를 찾으려면 두 번의 인덱싱을 거쳐야 한다.

Buffer Address = Descriptor Table[Avail Ring[last_avail_idx]]. last_avail_idx는 Host 측이 기록한 Guest가 지난번에 추가한 buffer가 Avail Ring 안에서의 위치다.

Guest Driver는 buffer를 한 번 추가할 때마다 Avail Ring의 idx에 1을 더해서, 자신이 작업한 Avail Ring 안의 위치가 어디인지 표시한다. Avail Ring은 Guest가 유지 관리하며, Host가 사용하도록 제공한다.

 

Avail Ring 2단계 인덱싱 흐름

Available Ring에 대응하는 구조는 struct vring_avail이다. struct vring_avail의 정의는 QEMU 소스코드 안에 한 군데가 아닌 여러 곳에 있다 (qemu-8.1.4 버전에는 총 3곳).

struct vring_avail {
    __virtio16 flags;
    __virtio16 idx;
    __virtio16 ring[];
};

Guest는 Avail Ring을 통해 Host에 buffer를 제공하며, Guest가 추가한 buffer 위치와 현재 작업 위치를 가리킨다.

 

  • __virtio16 flags

Host에게 자신이 buffer를 처리 완료하고 Descriptor index를 Used Ring에 쓴 후에, 인터럽트 주입을 통해 Guest에게 통지할지 여부를 지시하는 데 사용한다.

만약 flags가 0이면 Host는 buffer를 한 번 처리 완료할 때마다 Guest에게 통지하게 되고, 그로 인해 VMExit이 발생해서 오버헤드가 증가한다.

만약 flags가 1이면 Guest에게 통지하지 않는다. 이건 비교적 거친 방식인데, 통지하지 않을 거면 다 안 하고, 통지할 거면 다 한다. 더 우아한 방식도 있는데, VIRTIO_F_EVENT_IDX 특성이라는 것으로, 앞단과 뒷단의 처리 속도에 따라 통지 여부를 판단한다.

만약 이 특성이 열려 있으면 flags의 의미가 바뀌고, Guest는 flags를 반드시 0으로 설정한 후 used_event 메커니즘을 통해 통지를 구현해야 한다.

 

  • __virtio16 idx

Guest가 다음번에 buffer를 추가할 때 Avail Ring 안에서의 위치를 지시한다. 바꿔 말하면, idx에 저장되는 건 ring 배열 인덱스이며, ring[idx]에 저장되는 게 다음번에 추가할 buffer 머리의 Descriptor Table 안의 위치다.

 

  • __virtio16 ring[]

Descriptor Table 인덱스의 링을 저장하며,

하나의 배열이고 길이는 최대 1개로,

그중 마지막 한 개는 Event 방식 통지 메커니즘으로 사용된다.

VirtIO는 두 단계의 인덱스를 구현했다 (Buffer Address = Descriptor Table[Avail Ring[last_avail_idx]])

1단계 인덱스는 Descriptor Table 안의 원소를 가리키며, Avail Ring과 Used Ring이 대표하는 게 1단계 인덱스이고, 핵심은 바로 여기 ring[] 배열 멤버다

2단계 인덱스는 buffer의 물리 주소를 가리키며, Descriptor Table이 2단계 인덱스다.

 

Avail Ring flags 동작 모드

 

(2) Used Ring

Descriptor Table 인덱스를 저장하며, Descriptor Table 안의 한 entry를 가리킨다.

Host가 Avail Ring에서 제공한 정보를 근거로 VRing에서 buffer를 꺼내고, 처리를 완료한 후 Used Ring을 갱신해서, 이번에 처리한 Descriptor chain 머리 부분의 주소를 Used Ring에 넣는다.

Host가 buffer를 한 번 꺼낼 때마다 Used Ring의 idx에 1을 더해서, 자신이 Used Ring 안의 어느 위치에서 작업하고 있는지 표시한다. Used Ring은 Host가 유지 관리하며, Guest가 사용하도록 제공한다.

 

Used Ring에 대응하는 구조는 struct vring_used이다.

struct vring_used의 정의는 QEMU 소스코드 안에 한 군데가 아닌 여러 곳에 있다 (qemu-8.1.4 버전에는 총 3곳).

struct vring_used_elem {
    /* Index of start of used descriptor chain */
    __virtio32 id;
    /* Total length of the descriptor chain which was used (written to) */
    __virtio32 len;
};

struct vring_used {
    __virtio16 flags;
    __virtio16 idx;
    struct vring_used_elem ring[];
};

Host는 Used Ring을 통해 Host에 정보를 제공하며, Host가 처리한 buffer의 위치를 지시한다.

 

  • __virtio16 flags

Guest에게 자신이 buffer 추가를 완료하고 Descriptor index를 Avail Ring에 쓴 후에, notification을 보내서 Host에게 통지할지 여부를 지시하는 데 사용한다.

만약 flags를 0으로 설정하면 Guest는 buffer를 한 번 추가할 때마다 Host에게 통지하고; 만약 flags가 1이면 Host에게 통지하지 않는다.

Used Ring flags의 의미와 Avail Ring flags의 의미는 비슷한 종류로, 둘 다 앞단과 뒷단의 처리 완료 후 상대방에게 통지할지 여부를 지시한다.

마찬가지로, VIRTIO_F_EVENT_IDX 특성이 열려 있을 때 flags는 반드시 0으로 설정되어야 하며, Guest는 avail_event 방식을 사용해 Host에게 통지한다.

 

  • __virtio16 idx

Host가 다음번에 조작할 buffer의 Used Ring 안에서의 위치를 지시한다.

바꿔 말하면, idx에 저장되는 건 ring 배열 인덱스이며, ring[idx]에 저장되는 게 다음번에 조작할 buffer 머리의 Descriptor Table 안의 위치다.

 

  • struct vring_used_elem ring[]

Descriptor Table 인덱스의 링을 저장한다.

의미는 Avail Ring 안의 ring과 비슷한 종류로, 둘 다 Descriptor Table을 가리키는 인덱스를 저장한다.

단, Used Ring과 다른 점은 그 원소가 len 필드를 추가로 더 가지고 있다는 것이며, 이건 Host가 buffer에서 얼마나 많은 데이터를 처리했는지 표시하는 데 사용된다. 이 필드는 일부 시나리오에서 유용하게 쓰인다.

 

 

Used Ring flags 동작 모드

 

Avail Ring vs Used Ring 비교 정리

 

전체 흐름 시퀀스 다이어그램

'System Programming > Hypervisor' 카테고리의 다른 글

QEMU 코드 분석 #8 VirtIO 디바이스 초기화— 게스트 커널 드라이버 로딩 & virtqueue 설정  (0) 2026.03.09
QEMU 코드 분석 #7 VirtIO 디바이스 초기화 — QEMU 백엔드 realize & device_plugged  (0) 2026.03.05
QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트  (0) 2024.11.18
QEMU 코드 분석 #4 Machine & i440fx  (0) 2024.09.19
QEMU 코드 분석 #3 메모리 가상화 & 메모리 초기화  (0) 2024.07.08
    'System Programming/Hypervisor' 카테고리의 다른 글
    • QEMU 코드 분석 #8 VirtIO 디바이스 초기화— 게스트 커널 드라이버 로딩 & virtqueue 설정
    • QEMU 코드 분석 #7 VirtIO 디바이스 초기화 — QEMU 백엔드 realize & device_plugged
    • QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트
    • QEMU 코드 분석 #4 Machine & i440fx
    1000sj
    1000sj

    티스토리툴바