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 코드 분석 #7 VirtIO 디바이스 초기화 — QEMU 백엔드 realize & device_plugged

2026. 3. 5. 06:45

virtio 디바이스(balloon 예시)가 QEMU에서 realize되어 virtio 버스와 PCI 대리 디바이스에 연결되는 과정

 

1) QEMU virtio realize 호출 체인
2) virtio_device_realize 와 virtio_balloon_device_realize
3) virtio_bus_device_plugged 와 virtio_pci_device_plugged

 

1) QEMU virtio realize 호출 체인 (balloon 예시)

virtio-balloon 장치가 만들어질 때 realize 함수가 상속 체인을 따라 연쇄 호출된다.

virtio_balloon_pci_realize  (VirtioPCIClass 레벨)
   ↓ 안에서 parent의 realize 호출
virtio_pci_realize          (PCIDeviceClass 레벨)
   ↓
virtio_pci_dc_realize / pci_qdev_realize  (DeviceClass 레벨, PCI 등록)

자식 클래스의 realize가 자기 일을 한 뒤 부모(parent_dc_realize)의 realize를 호출하면서,

가장 바깥(balloon-pci)부터 안쪽(pci_qdev_realize)까지 내려가며 장치를 단계적으로 완성하는 흐름이다.

 

(1) virtio_pci_do_realize 분석

TYPE_VIRTIO_BALLOON이 실체화될 때 device_set_realized 함수가 가장 먼저 DeviceClass->realize 즉 virtio_pci_dc_realize 함수를 호출한다. (hw/virtio/virtio-pci.c)

static void virtio_pci_dc_realize(DeviceState *qdev, Error **errp)
{
    VirtioPCIClass *vpciklass = VIRTIO_PCI_GET_CLASS(qdev);
    VirtIOPCIProxy *proxy = VIRTIO_PCI(qdev);
    PCIDevice *pci_dev = &proxy->pci_dev;

    if (!(proxy->flags & VIRTIO_PCI_FLAG_DISABLE_PCIE) &&
        virtio_pci_modern(proxy)) {
        pci_dev->cap_present |= QEMU_PCI_CAP_EXPRESS;
    }

    vpciklass->parent_dc_realize(qdev, errp);
}

 

virtio_pci_dc_realize 함수는 먼저 virtio PCI 대리 디바이스가 VIRTIO_PCI_FLAG_DISABLE_PCIE 특성을 가지는지 판단한다. 이 특성이 있으면 virtio PCI 대리 디바이스가 PCIe의 인터페이스를 보여주게 된다.

그런 다음 vpciklass->parent_dc_realize 함수를 호출하는데, 앞의 분석으로 알 수 있듯이 이 콜백 함수는 pci_qdev_realize다.

 

(2) pci_qdev_realize 분석

(hw/pci/pci.c)

static void pci_qdev_realize(DeviceState *qdev, Error **errp)
{
    PCIDevice *pci_dev = (PCIDevice *)qdev;
    PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(pci_dev);
    ObjectClass *klass = OBJECT_CLASS(pc);
    Error *local_err = NULL;
    bool is_default_rom;
    uint16_t class_id;

    /*
     * capped by systemd (see: udev-builtin-net_id.c)
     * as it's the only known user honor it to avoid users
     * misconfigure QEMU and then wonder why acpi-index doesn't work
     */
    if (pci_dev->acpi_index > ONBOARD_INDEX_MAX) {
        error_setg(errp,
                   "acpi-index should be less or equal to %u",
                   ONBOARD_INDEX_MAX);
        return;
    }

    /*
     * make sure that acpi-index is unique across all present PCI devices
     */
    if (pci_dev->acpi_index) {
        GSequence *used_indexes = pci_acpi_index_list();

        if (g_sequence_lookup(used_indexes,
                              GINT_TO_POINTER(pci_dev->acpi_index),
                              g_cmp_uint32, NULL)) {
            error_setg(errp, "a PCI device with acpi-index = %" PRIu32
                       " already exist", pci_dev->acpi_index);
            return;
        }
        g_sequence_insert_sorted(used_indexes,
                                 GINT_TO_POINTER(pci_dev->acpi_index),
                                 g_cmp_uint32, NULL);
    }

    if (pci_dev->romsize != -1 && !is_power_of_2(pci_dev->romsize)) {
        error_setg(errp, "ROM size %u is not a power of two", pci_dev->romsize);
        return;
    }

    /* initialize cap_present for pci_is_express() and pci_config_size(),
     * Note that hybrid PCIs are not set automatically and need to manage
     * QEMU_PCI_CAP_EXPRESS manually */
    if (object_class_dynamic_cast(klass, INTERFACE_PCIE_DEVICE) &&
       !object_class_dynamic_cast(klass, INTERFACE_CONVENTIONAL_PCI_DEVICE)) {
        pci_dev->cap_present |= QEMU_PCI_CAP_EXPRESS;
    }

    if (object_class_dynamic_cast(klass, INTERFACE_CXL_DEVICE)) {
        pci_dev->cap_present |= QEMU_PCIE_CAP_CXL;
    }

    pci_dev = do_pci_register_device(pci_dev,
                                     object_get_typename(OBJECT(qdev)),
                                     pci_dev->devfn, errp);
    if (pci_dev == NULL)
        return;

    if (pc->realize) {
        pc->realize(pci_dev, &local_err);
        if (local_err) {
            error_propagate(errp, local_err);
            do_pci_unregister_device(pci_dev);
            return;
        }
    }

    /*
     * A PCIe Downstream Port that do not have ARI Forwarding enabled must
     * associate only Device 0 with the device attached to the bus
     * representing the Link from the Port (PCIe base spec rev 4.0 ver 0.3,
     * sec 7.3.1).
     * With ARI, PCI_SLOT() can return non-zero value as the traditional
     * 5-bit Device Number and 3-bit Function Number fields in its associated
     * Routing IDs, Requester IDs and Completer IDs are interpreted as a
     * single 8-bit Function Number. Hence, ignore ARI capable devices.
     */
    if (pci_is_express(pci_dev) &&
        !pcie_find_capability(pci_dev, PCI_EXT_CAP_ID_ARI) &&
        pcie_has_upstream_port(pci_dev) &&
        PCI_SLOT(pci_dev->devfn)) {
        warn_report("PCI: slot %d is not valid for %s,"
                    " parent device only allows plugging into slot 0.",
                    PCI_SLOT(pci_dev->devfn), pci_dev->name);
    }

    if (pci_dev->failover_pair_id) {
        if (!pci_bus_is_express(pci_get_bus(pci_dev))) {
            error_setg(errp, "failover primary device must be on "
                             "PCIExpress bus");
            pci_qdev_unrealize(DEVICE(pci_dev));
            return;
        }
        class_id = pci_get_word(pci_dev->config + PCI_CLASS_DEVICE);
        if (class_id != PCI_CLASS_NETWORK_ETHERNET) {
            error_setg(errp, "failover primary device is not an "
                             "Ethernet device");
            pci_qdev_unrealize(DEVICE(pci_dev));
            return;
        }
        if ((pci_dev->cap_present & QEMU_PCI_CAP_MULTIFUNCTION)
            || (PCI_FUNC(pci_dev->devfn) != 0)) {
            error_setg(errp, "failover: primary device must be in its own "
                             "PCI slot");
            pci_qdev_unrealize(DEVICE(pci_dev));
            return;
        }
        qdev->allow_unplug_during_migration = true;
    }

    /* rom loading */
    is_default_rom = false;
    if (pci_dev->romfile == NULL && pc->romfile != NULL) {
        pci_dev->romfile = g_strdup(pc->romfile);
        is_default_rom = true;
    }

    pci_add_option_rom(pci_dev, is_default_rom, &local_err);
    if (local_err) {
        error_propagate(errp, local_err);
        pci_qdev_unrealize(DEVICE(pci_dev));
        return;
    }

    pci_set_power(pci_dev, true);

    pci_dev->msi_trigger = pci_msi_trigger;
}

pci_qdev_realize 함수는 virtio PCI proxy 디바이스를 PCI 버스에 등록한다.

 

virtio 디바이스의 초기화

virtio 디바이스는 먼저 PCI 디바이스를 하나 생성해야 한다. 이를 virtio PCI proxy 디바이스라고 부르고, 이 proxy 디바이스는 PCI 버스에 연결된다.

이어서, virtio proxy 디바이스가 다시 virtio 버스를 하나 생성한다. 이렇게 하면 virtio 디바이스가 이 virtio 버스에 연결될 수 있게 된다.

먼저 virtio PCI proxy 디바이스 타입의 정의를 살펴보자. (hw/virtio/virtio-pci.c)

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)

 

초기 단계: PCI 버스 위의 virtio proxy 디바이스들

pci_qdev_realize 함수가 끝났을 때까지 디바이스들의 관계

 

 

pci_qdev_realize 함수는 주로 세 가지 작업을 한다.

(1) 먼저 do_pci_register_device 함수를 호출해 등록을 진행한다. do_pci_register_device 함수는 PCI 버스에 디바이스를 등록하는 등의 초기화 작업을 마친다. 구체적으로

  1. 만약 지정된 devfn이 -1이면, 버스에서 슬롯을 알아서 선택해서 슬롯을 얻은 후 PCIDevice의 devfn(즉 pci_dev->devfn)에 저장한다. 만약 디바이스 명령행에서 addr를 지정했다면, addr가 디바이스의 devfn이 된다.
  2. 그다음 PCIDevice 구조체 내의 각 도메인을 설정하는데, pci_init_bus_master 함수를 호출해 PCIDevice의 Address 멤버 bus_master_as와 그에 대응하는 MR을 초기화한다.
  3. 그 이후, pci_config_alloc 함수를 호출해 PCI 디바이스의 설정 공간을 할당한다. cmask는 관련 능력을 검사하는 데 쓰고, wmask는 읽기·쓰기를 제어하는 데 쓰고, w1cmask는 RW1C를 구현하는 데 쓴다. 이로부터 일부 초기화 설정, 예를 들어 vendor_id 등을 완성한다.
  4. 그런 다음 디바이스의 config_read와 config_write 함수를 설정한다. 만약 관련된 하위 클래스가 스스로 설정하지 않았다면 기본값인 pci_default_read/write_config 함수를 사용한다.
  5. 마지막으로, 디바이스를 bus->devices 수에 복제해 넣는다.

(2) 다음으로, pci_qdev_realize 함수는 PCI 디바이스가 속한 class의 realize 함수, 즉 pc->realize 함수를 호출하는데, 다시 말해 virtio_pci_realize 함수다.

(3) 마지막으로, pci_add_option_rom 함수를 호출해 PCI 디바이스의 ROM을 로드한다.

 

(3) virtio_pci_realize 분석

(hw/virtio/virtio-pci.c)

static void virtio_pci_realize(PCIDevice *pci_dev, Error **errp)
{
    VirtIOPCIProxy *proxy = VIRTIO_PCI(pci_dev);
    VirtioPCIClass *k = VIRTIO_PCI_GET_CLASS(pci_dev);
    bool pcie_port = pci_bus_is_express(pci_get_bus(pci_dev)) &&
                     !pci_bus_is_root(pci_get_bus(pci_dev));

    if (kvm_enabled() && !kvm_has_many_ioeventfds()) {
        proxy->flags &= ~VIRTIO_PCI_FLAG_USE_IOEVENTFD;
    }

    /* fd-based ioevents can't be synchronized in record/replay */
    if (replay_mode != REPLAY_MODE_NONE) {
        proxy->flags &= ~VIRTIO_PCI_FLAG_USE_IOEVENTFD;
    }

    /*
     * virtio pci bar layout used by default.
     * subclasses can re-arrange things if needed.
     *
     *   region 0   --  virtio legacy io bar
     *   region 1   --  msi-x bar
     *   region 2   --  virtio modern io bar (off by default)
     *   region 4+5 --  virtio modern memory (64bit) bar
     */
    proxy->legacy_io_bar_idx  = 0;
    proxy->msix_bar_idx       = 1;
    proxy->modern_io_bar_idx  = 2;
    proxy->modern_mem_bar_idx = 4;

    proxy->common.offset = 0x0;
    proxy->common.size = 0x1000;
    proxy->common.type = VIRTIO_PCI_CAP_COMMON_CFG;

    proxy->isr.offset = 0x1000;
    proxy->isr.size = 0x1000;
    proxy->isr.type = VIRTIO_PCI_CAP_ISR_CFG;

    proxy->device.offset = 0x2000;
    proxy->device.size = 0x1000;
    proxy->device.type = VIRTIO_PCI_CAP_DEVICE_CFG;

    proxy->notify.offset = 0x3000;
    proxy->notify.size = virtio_pci_queue_mem_mult(proxy) * VIRTIO_QUEUE_MAX;
    proxy->notify.type = VIRTIO_PCI_CAP_NOTIFY_CFG;

    proxy->notify_pio.offset = 0x0;
    proxy->notify_pio.size = 0x4;
    proxy->notify_pio.type = VIRTIO_PCI_CAP_NOTIFY_CFG;

    /* subclasses can enforce modern, so do this unconditionally */
    memory_region_init(&proxy->modern_bar, OBJECT(proxy), "virtio-pci",
                       /* PCI BAR regions must be powers of 2 */
                       pow2ceil(proxy->notify.offset + proxy->notify.size));

    if (proxy->disable_legacy == ON_OFF_AUTO_AUTO) {
        proxy->disable_legacy = pcie_port ? ON_OFF_AUTO_ON : ON_OFF_AUTO_OFF;
    }

    if (!virtio_pci_modern(proxy) && !virtio_pci_legacy(proxy)) {
        error_setg(errp, "device cannot work as neither modern nor legacy mode"
                   " is enabled");
        error_append_hint(errp, "Set either disable-modern or disable-legacy"
                          " to off\n");
        return;
    }

    if (pcie_port && pci_is_express(pci_dev)) {
        int pos;
        uint16_t last_pcie_cap_offset = PCI_CONFIG_SPACE_SIZE;

        pos = pcie_endpoint_cap_init(pci_dev, 0);
        assert(pos > 0);

        pos = pci_add_capability(pci_dev, PCI_CAP_ID_PM, 0,
                                 PCI_PM_SIZEOF, errp);
        if (pos < 0) {
            return;
        }

        pci_dev->exp.pm_cap = pos;

        /*
         * Indicates that this function complies with revision 1.2 of the
         * PCI Power Management Interface Specification.
         */
        pci_set_word(pci_dev->config + pos + PCI_PM_PMC, 0x3);

        if (proxy->flags & VIRTIO_PCI_FLAG_AER) {
            pcie_aer_init(pci_dev, PCI_ERR_VER, last_pcie_cap_offset,
                          PCI_ERR_SIZEOF, NULL);
            last_pcie_cap_offset += PCI_ERR_SIZEOF;
        }

        if (proxy->flags & VIRTIO_PCI_FLAG_INIT_DEVERR) {
            /* Init error enabling flags */
            pcie_cap_deverr_init(pci_dev);
        }

        if (proxy->flags & VIRTIO_PCI_FLAG_INIT_LNKCTL) {
            /* Init Link Control Register */
            pcie_cap_lnkctl_init(pci_dev);
        }

        if (proxy->flags & VIRTIO_PCI_FLAG_INIT_PM) {
            /* Init Power Management Control Register */
            pci_set_word(pci_dev->wmask + pos + PCI_PM_CTRL,
                         PCI_PM_CTRL_STATE_MASK);
        }

        if (proxy->flags & VIRTIO_PCI_FLAG_ATS) {
            pcie_ats_init(pci_dev, last_pcie_cap_offset,
                          proxy->flags & VIRTIO_PCI_FLAG_ATS_PAGE_ALIGNED);
            last_pcie_cap_offset += PCI_EXT_CAP_ATS_SIZEOF;
        }

        if (proxy->flags & VIRTIO_PCI_FLAG_INIT_FLR) {
            /* Set Function Level Reset capability bit */
            pcie_cap_flr_init(pci_dev);
        }
    } else {
        /*
         * make future invocations of pci_is_express() return false
         * and pci_config_size() return PCI_CONFIG_SPACE_SIZE.
         */
        pci_dev->cap_present &= ~QEMU_PCI_CAP_EXPRESS;
    }

    virtio_pci_bus_new(&proxy->bus, sizeof(proxy->bus), proxy);
    if (k->realize) {
        k->realize(proxy, errp);
    }
}

 

(1) VirtIOPCIProxy 디바이스 객체 얻기

virtio_pci_realize 함수는 먼저 virtioPCIProxy 디바이스, 즉 VirtIOPCIProxy 구조체의 객체를 얻는다.

VirtIOPCIProxy *proxy = VIRTIO_PCI(pci_dev);

(2) VirtIOPCIProxy의 BAR 인덱스 설정

그다음, virtio_pci_realize 함수가 VirtIOPCIProxy 디바이스의 여러 BAR 데이터를 초기화하고, 이 BAR들의 인덱스 번호를 설정한다.

/*
     * virtio pci bar layout used by default.
     * subclasses can re-arrange things if needed.
     *
     *   region 0   --  virtio legacy io bar
     *   region 1   --  msi-x bar
     *   region 2   --  virtio modern io bar (off by default)
     *   region 4+5 --  virtio modern memory (64bit) bar
     */
    proxy->legacy_io_bar_idx  = 0;
    proxy->msix_bar_idx       = 1;
    proxy->modern_io_bar_idx  = 2;
    proxy->modern_mem_bar_idx = 4;

이 중에서

  • legacy I/O 주소는 0이고
  • msi-x 주소는 1이고
  • modern IO 주소는 2이고
  • modern MMIO 주소는 4다.

여기서 말하는 legacy와 modern은 서로 다른 virtio 버전을 가리킨다.

 

BAR 레이아웃 시각화

 

(3) VirtIOPCIRegion 초기화

virtio_pci_realize 함수는 또한 여러 VirtIOPCIRegion(VirtIOPCIRegion은 virtio 디바이스의 설정 공간 정보를 나타내는 데 쓴다)을 초기화한다. VirtIOPCIProxy의 common, isr, device, notify 멤버들이다.

proxy->common.offset = 0x0;
    proxy->common.size = 0x1000;
    proxy->common.type = VIRTIO_PCI_CAP_COMMON_CFG;

    proxy->isr.offset = 0x1000;
    proxy->isr.size = 0x1000;
    proxy->isr.type = VIRTIO_PCI_CAP_ISR_CFG;

    proxy->device.offset = 0x2000;
    proxy->device.size = 0x1000;
    proxy->device.type = VIRTIO_PCI_CAP_DEVICE_CFG;

    proxy->notify.offset = 0x3000;
    proxy->notify.size = virtio_pci_queue_mem_mult(proxy) * VIRTIO_QUEUE_MAX;
    proxy->notify.type = VIRTIO_PCI_CAP_NOTIFY_CFG;

    proxy->notify_pio.offset = 0x0;
    proxy->notify_pio.size = 0x4;
    proxy->notify_pio.type = VIRTIO_PCI_CAP_NOTIFY_CFG;

VirtIOPCIRegion은 VirtIOPCIProxy 디바이스의 modern MMIO에 관한 정보를 저장한다.

 

예를 들어 VirtIOProxy의 modern MMIO 중에서 처음 시작 영역은 common 영역인데, 그 크기는 0x1000이고 범위는 [0x0, 0x1000]이다.

그다음 isr 영역인데, 크기 역시 0x1000이고 범위는 [0x1000, 0x2000]이다.

그다음은 device 영역인데, 크기 역시 0x1000이고 범위는 [0x2000, 0x3000]이다. 마지막은 notify 영역인데, 그 크기는 virtio_pci_queue_mem_mult(proxy) * VIRTIO_QUEUE_MAX이고 범위는 [0x3000, 0x3000 + virtio_pci_queue_mem_mult(proxy) * VIRTIO_QUEUE_MAX]다.

 

modern MMIO 메모리 맵

 

(4) modern_bar MemoryRegion 초기화

VirtIOPCIProxy의 modern MMIO에 대응하는 MemoryRegion은 VirtIOPCIProxy의 modern_bar 멤버에 저장된다. 여기에 또 하나의 MemoryRegion이 modern_cfg 멤버에 저장된다(이 부분은 오래된 버전 코드에 있고, 새로운 버전 코드에서는 이미 없어졌다).

/* subclasses can enforce modern, so do this unconditionally */
    memory_region_init(&proxy->modern_bar, OBJECT(proxy), "virtio-pci",
                       /* PCI BAR regions must be powers of 2 */
                       pow2ceil(proxy->notify.offset + proxy->notify.size));

    if (proxy->disable_legacy == ON_OFF_AUTO_AUTO) {
        proxy->disable_legacy = pcie_port ? ON_OFF_AUTO_ON : ON_OFF_AUTO_OFF;
    }

    if (!virtio_pci_modern(proxy) && !virtio_pci_legacy(proxy)) {
        error_setg(errp, "device cannot work as neither modern nor legacy mode"
                   " is enabled");
        error_append_hint(errp, "Set either disable-modern or disable-legacy"
                          " to off\n");
        return;
    }

5) virtio-bus 생성

virtio_pci_realize 함수는 virtio_pci_bus_new 함수를 호출해 virtio-bus를 생성하고, 현재 virtio PCI proxy 디바이스 아래에 연결한다.

virtio_pci_bus_new(&proxy->bus, sizeof(proxy->bus), proxy);

 

이 시점에서 디바이스들의 관계는 아래와 같이 된다.

 

(6) 하위 클래스 realize 호출

virtio_pci_realize 함수는 마지막에 k->realize 함수 포인터가 가리키는 함수를 호출한다. k는 VirtioPCIClass 타입이므로, 앞의 표에 따라

  realize 함수 parent_dc_realize 함수
DeviceClass virtio_pci_dc_realize  
PCIDeviceClass virtio_pci_realize  
VirtioPCIClass virtio_balloon_pci_realize pci_qdev_realize

k->realize 함수 포인터는 실제로 virtio_balloon_pci_realize 함수를 가리킨다.

 

(4) virtio_balloon_pci_realize 분석

(hw/virtio/virtio-balloon-pci.c)

static void virtio_balloon_pci_realize(VirtIOPCIProxy *vpci_dev, Error **errp)
{
    VirtIOBalloonPCI *dev = VIRTIO_BALLOON_PCI(vpci_dev);
    DeviceState *vdev = DEVICE(&dev->vdev);

    vpci_dev->class_code = PCI_CLASS_OTHERS;

    qdev_realize(vdev, BUS(&vpci_dev->bus), errp);
}

virtio_balloon_pci_realize 함수는 먼저 VIRTIO_BALLOON_PCI 매크로를 통해 VirtIOPCIProxy 타입의 디바이스를 VirtIOBalloonPCI 디바이스로 변환한다.

부모 클래스에서 자식 클래스로 변환하는 것에 해당한다.

VirtIOBalloonPCI *dev = VIRTIO_BALLOON_PCI(vpci_dev);
/*
 * virtio-balloon-pci: This extends VirtioPCIProxy.
 */
#define TYPE_VIRTIO_BALLOON_PCI "virtio-balloon-pci-base"
DECLARE_INSTANCE_CHECKER(VirtIOBalloonPCI, VIRTIO_BALLOON_PCI,
                         TYPE_VIRTIO_BALLOON_PCI)
static inline G_GNUC_UNUSED VirtIOBalloonPCI* VIRTIO_BALLOON_PCI(const void *obj)
{
    return OBJECT_CHECK(VirtIOBalloonPCI, obj, "virtio-balloon-pci-base");
}
static inline G_GNUC_UNUSED VirtIOBalloonPCI* VIRTIO_BALLOON_PCI(const void *obj)
{
    return ((VirtIOBalloonPCI*)object_dynamic_cast_assert ((ObjectClass *)(obj), ("virtio-balloon-pci-base")
}

 

이어서, 디바이스 VirtIOBalloonPCI의 VirtIOBalloon 부분을 얻는데, 이게 바로 실제 virtio balloon 디바이스다.

DeviceState *vdev = DEVICE(&dev->vdev);
#define TYPE_DEVICE "device"
OBJECT_DECLARE_TYPE(DeviceState, DeviceClass, DEVICE)
/**
 * OBJECT_DECLARE_TYPE:
 * @InstanceType: instance struct name
 * @ClassType: class struct name
 * @MODULE_OBJ_NAME: the object name in uppercase with underscore separators
 *
 * This macro is typically used in a header file, and will:
 *
 *   - create the typedefs for the object and class structs
 *   - register the type for use with g_autoptr
 *   - provide three standard type cast functions
 *
 * The object struct and class struct need to be declared manually.
 */
#define OBJECT_DECLARE_TYPE(InstanceType, ClassType, MODULE_OBJ_NAME) \
    typedef struct InstanceType InstanceType; \
    typedef struct ClassType ClassType; \
    \
    G_DEFINE_AUTOPTR_CLEANUP_FUNC(InstanceType, object_unref) \
    \
    DECLARE_OBJ_CHECKERS(InstanceType, ClassType, \
                         MODULE_OBJ_NAME, TYPE_##MODULE_OBJ_NAME)

위쪽의 값을 대입하면, 우선 다음과 같이 펼쳐진다.

typedef struct DeviceState DeviceState;
typedef struct DeviceClass DeviceClass;

G_DEFINE_AUTOPTR_CLEANUP_FUNC(DeviceState, object_unref)

DECLARE_OBJ_CHECKERS(DeviceState, DeviceClass, DEVICE, TYPE_DEVICE)

DECLARE_OBJ_CHECKERS 매크로의 정의도 include/qom/object.h에 있는데, 아래와 같다.

/**
 * DECLARE_OBJ_CHECKERS:
 * @InstanceType: instance struct name
 * @ClassType: class struct name
 * @OBJ_NAME: the object name in uppercase with underscore separators
 * @TYPENAME: type name
 *
 * Direct usage of this macro should be avoided, and the complete
 * OBJECT_DECLARE_TYPE macro is recommended instead.
 *
 * This macro will provide the three standard type cast functions for a
 * QOM type.
 */
#define DECLARE_OBJ_CHECKERS(InstanceType, ClassType, OBJ_NAME, TYPENAME) \
    DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
    \
    DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME)

 

typedef struct DeviceState DeviceState;
typedef struct DeviceClass DeviceClass;

G_DEFINE_AUTOPTR_CLEANUP_FUNC(DeviceState, object_unref)

DECLARE_INSTANCE_CHECKER(DeviceState, DEVICE, TYPE_DEVICE)

DECLARE_CLASS_CHECKERS(DeviceClass, DEVICE, TYPE_DEVICE)
/**
 * DECLARE_INSTANCE_CHECKER:
 * @InstanceType: instance struct name
 * @OBJ_NAME: the object name in uppercase with underscore separators
 * @TYPENAME: type name
 *
 * Direct usage of this macro should be avoided, and the complete
 * OBJECT_DECLARE_TYPE macro is recommended instead.
 *
 * This macro will provide the instance type cast functions for a
 * QOM type.
 */
#define DECLARE_INSTANCE_CHECKER(InstanceType, OBJ_NAME, TYPENAME) \
    static inline G_GNUC_UNUSED InstanceType * \
    OBJ_NAME(const void *obj) \
    { return OBJECT_CHECK(InstanceType, obj, TYPENAME); }
/**
 * DECLARE_CLASS_CHECKERS:
 * @ClassType: class struct name
 * @OBJ_NAME: the object name in uppercase with underscore separators
 * @TYPENAME: type name
 *
 * Direct usage of this macro should be avoided, and the complete
 * OBJECT_DECLARE_TYPE macro is recommended instead.
 *
 * This macro will provide the class type cast functions for a
 * QOM type.
 */
#define DECLARE_CLASS_CHECKERS(ClassType, OBJ_NAME, TYPENAME) \
    static inline G_GNUC_UNUSED ClassType * \
    OBJ_NAME##_GET_CLASS(const void *obj) \
    { return OBJECT_GET_CLASS(ClassType, obj, TYPENAME); } \
    \
    static inline G_GNUC_UNUSED ClassType * \
    OBJ_NAME##_CLASS(const void *klass) \
    { return OBJECT_CLASS_CHECK(ClassType, klass, TYPENAME); }

이로써, 위 식은 더 펼쳐서 다음과 같다.

typedef struct DeviceState DeviceState;
typedef struct DeviceClass DeviceClass;

G_DEFINE_AUTOPTR_CLEANUP_FUNC(DeviceState, object_unref)

static inline G_GNUC_UNUSED DeviceState *DEVICE(const void *obj)
{
    return OBJECT_CHECK(DeviceState, obj, TYPE_DEVICE);
}

static inline G_GNUC_UNUSED DeviceClass* DEVICE_GET_CLASS(const void *obj)
{
    return OBJECT_GET_CLASS(DeviceClass, obj, TYPE_DEVICE);
}

static inline G_GNUC_UNUSED DeviceClass* DEVICE_CLASS(const void *klass)
{
    return OBJECT_CLASS_CHECK(DeviceClass, klass, TYPE_DEVICE);
}

다시 TYPE_DEVICE의 실제 값을 대입하면, 최종적으로 다음과 같이 된다.

typedef struct DeviceState DeviceState;
typedef struct DeviceClass DeviceClass;

G_DEFINE_AUTOPTR_CLEANUP_FUNC(DeviceState, object_unref)

static inline G_GNUC_UNUSED DeviceState *DEVICE(const void *obj)
{
    return OBJECT_CHECK(DeviceState, obj, "device");
}

static inline G_GNUC_UNUSED DeviceClass* DEVICE_GET_CLASS(const void *obj)
{
    return OBJECT_GET_CLASS(DeviceClass, obj, "device");
}

static inline G_GNUC_UNUSED DeviceClass* DEVICE_CLASS(const void *klass)
{
    return OBJECT_CLASS_CHECK(DeviceClass, klass, "device");
}

 

 

DEVICE 매크로를 통해 VirtIOBalloonPCI 디바이스의 VirtIOBalloon 부분을 얻는데, 이게 바로 실제 virtio balloon 디바이스다.

DeviceState *vdev = DEVICE(&dev->vdev);

 

VirtIOBalloonPCI 구조체 관계도

 

(5) qdev_realize 분석

이어서, hw/core/qdev.c의 qdev_realize 함수를 호출한다.

qdev_realize(vdev, BUS(&vpci_dev->bus), errp);
bool qdev_realize(DeviceState *dev, BusState *bus, Error **errp)
{
    assert(!dev->realized && !dev->parent_bus);

    if (bus) {
        if (!qdev_set_parent_bus(dev, bus, errp)) {
            return false;
        }
    } else {
        assert(!DEVICE_GET_CLASS(dev)->bus_type);
    }

    return object_property_set_bool(OBJECT(dev), "realized", true, errp);
}

qdev_realize 함수는 주로 두 가지 일을 한다.

  1. virtio balloon 디바이스의 버스를 VirtIOPCIProxy 디바이스의 bus 멤버로 설정한다. 즉, 이 virtio balloon 디바이스를 virtio 버스에 연결한다.
  2. object_property_bool 함수를 호출해, virtio balloon 디바이스를 실체화한다. 이는 virtio_device_realize 함수의 실행을 유발한다.

사실 예전 버전에서는 qdev_realize 함수가 없었고, 그 안의 두 가지 작업은 virtio_balloon_pci_realize 함수 안에서 직접 완료됐었다.

이 시점에서 디바이스들의 관계는 아래와 같이 된다.

 

여기까지는 이 장치를 게스트에게 PCI 장치로 어떻게 노출할까를 담당하는 함수들 — virtio_pci_dc_realize → pci_qdev_realize → virtio_pci_realize → virtio_balloon_pci_realize → qdev_realize — 즉 transport/PCI 층의 realize 체인을 살펴봤다.

 

이후 호출되는 virtio_device_realize부터는 virtio 장치 본연의 기능(virtqueue 설정, feature negotiation, balloon 동작 등)을 어떻게 초기화하는가 — 즉 virtio core 층을 확인할 것이다. 

 

그에 앞서, 지금까지 나온 핵심 개념들(realize 체인이 부모-자식으로 연결되는 방식, device_set_realized 진입점, VirtioPCIClass의 realize 등)을 한 번 더 정리하고 넘어가자.

 

전체 호출 흐름

 

핵심 개념

 

virtio 디바이스 realize 과정 분석

virtio_balloon_pci_instance_init() -> virtio_instance_init_common() -> object_initialize_child_with_props() -> object_initialize_child_with_propsv() -> object_initialize()의 과정을 확인했다. 그리고 이 과정으로부터 알 수 있듯이, virtio 디바이스가 생성 과정에서 별로 많은 일을 하지 않으며, 대부분의 일은 디바이스 realize 과정에서 한다는걸 알 수 있다.

 

virtio balloon 디바이스 realize를 설명하기 전에, 먼저 디바이스 realize에서 호출되는 함수를 다시보자.

QEMU는 main 함수에서 모든 "-device" 파라미터를 realize한다 (실제로는 main 함수의 qemu_init 함수에서 진행된다).

디바이스의 realize 함수는 모두 device_set_realized 함수를 호출한다. 이 함수에서는 디바이스 클래스의 realize 함수를 호출할 것이다.

 

-device 옵션과 서로 대응하는 코드는 softmmu/qdev-monitor.c 안에 있다.

QemuOptsList qemu_device_opts = {
    .name = "device",
    .implied_opt_name = "driver",
    .head = QTAILQ_HEAD_INITIALIZER(qemu_device_opts.head),
    .desc = {
        /*
         * no elements => accept any
         * sanity checking will happen later
         * when setting device properties
         */
        { /* end of list */ }
    },
};

 

static void device_set_realized(Object *obj, bool value, Error **errp)
{
    DeviceState *dev = DEVICE(obj);
    DeviceClass *dc = DEVICE_GET_CLASS(dev);
    HotplugHandler *hotplug_ctrl;
    BusState *bus;
    NamedClockList *ncl;
    Error *local_err = NULL;
    bool unattached_parent = false;
    static int unattached_count;

    if (dev->hotplugged && !dc->hotpluggable) {
        error_setg(errp, QERR_DEVICE_NO_HOTPLUG, object_get_typename(obj));
        return;
    }

    if (value && !dev->realized) {
        if (!check_only_migratable(obj, errp)) {
            goto fail;
        }

        if (!obj->parent) {
            gchar *name = g_strdup_printf("device[%d]", unattached_count++);

            object_property_add_child(container_get(qdev_get_machine(),
                                                   "/unattached"),
                                      name, obj);
            unattached_parent = true;
            g_free(name);
        }

        hotplug_ctrl = qdev_get_hotplug_handler(dev);
        if (hotplug_ctrl) {
            hotplug_handler_pre_plug(hotplug_ctrl, dev, &local_err);
            if (local_err != NULL) {
                goto fail;
            }
        }

        if (dc->realize) {
            dc->realize(dev, &local_err);
            if (local_err != NULL) {
                goto fail;
            }
        }

        DEVICE_LISTENER_CALL(realize, Forward, dev);

        /*
         * always free/re-initialize here since the value cannot be cleaned up
         * in device_unrealize due to its usage later on in the unplug path
         */
        g_free(dev->canonical_path);
        dev->canonical_path = object_get_canonical_path(OBJECT(dev));
        QLIST_FOREACH(ncl, &dev->clocks, node) {
            if (ncl->alias) {
                continue;
            } else {
                clock_setup_canonical_path(ncl->clock);
            }
        }

        if (qdev_get_vmsd(dev)) {
            if (vmstate_register_with_alias_id(VMSTATE_IF(dev),
                                               VMSTATE_INSTANCE_ID_ANY,
                                               qdev_get_vmsd(dev), dev,
                                               dev->instance_id_alias,
                                               dev->alias_required_for_version,
                                               &local_err) < 0) {
                goto post_realize_fail;
            }
        }

        /*
         * Clear the reset state, in case the object was previously unrealized
         * with a dirty state.
         */
        resettable_state_clear(&dev->reset);

        QLIST_FOREACH(bus, &dev->child_bus, sibling) {
            if (!qbus_realize(bus, errp)) {
                goto child_realize_fail;
            }
        }
        if (dev->hotplugged) {
            /*
             * Reset the device, as well as its subtree which, at this point,
             * should be realized too.
             */
            resettable_assert_reset(OBJECT(dev), RESET_TYPE_COLD);
            resettable_change_parent(OBJECT(dev), OBJECT(dev->parent_bus),
                                     NULL);
            resettable_release_reset(OBJECT(dev), RESET_TYPE_COLD);
        }
        dev->pending_deleted_event = false;

        if (hotplug_ctrl) {
            hotplug_handler_plug(hotplug_ctrl, dev, &local_err);
            if (local_err != NULL) {
                goto child_realize_fail;
            }
        }

        qatomic_store_release(&dev->realized, value);

    } else if (!value && dev->realized) {

        /*
         * Change the value so that any concurrent users are aware
         * that the device is going to be unrealized
         *
         * TODO: change .realized property to enum that states
         * each phase of the device realization/unrealization
         */
        qatomic_set(&dev->realized, value);
        /*
         * Ensure that concurrent users see this update prior to
         * any other changes done by unrealize.
         */
        smp_wmb();

        QLIST_FOREACH(bus, &dev->child_bus, sibling) {
            qbus_unrealize(bus);
        }
        if (qdev_get_vmsd(dev)) {
            vmstate_unregister(VMSTATE_IF(dev), qdev_get_vmsd(dev), dev);
        }
        if (dc->unrealize) {
            dc->unrealize(dev);
        }
        dev->pending_deleted_event = true;
        DEVICE_LISTENER_CALL(unrealize, Reverse, dev);
    }

    assert(local_err == NULL);
    return;

child_realize_fail:
    QLIST_FOREACH(bus, &dev->child_bus, sibling) {
        qbus_unrealize(bus);
    }

    if (qdev_get_vmsd(dev)) {
        vmstate_unregister(VMSTATE_IF(dev), qdev_get_vmsd(dev), dev);
    }

post_realize_fail:
    g_free(dev->canonical_path);
    dev->canonical_path = NULL;
    if (dc->unrealize) {
        dc->unrealize(dev);
    }

fail:
    error_propagate(errp, local_err);
    if (unattached_parent) {
        /*
         * Beware, this doesn't just revert
         * object_property_add_child(), it also runs bus_remove()!
         */
        object_unparent(OBJECT(dev));
        unattached_count--;
    }
}

 

if (dc->realize) {
            dc->realize(dev, &local_err);
            if (local_err != NULL) {
                goto fail;
            }
        }

디바이스 클래스의 realize 함수에서 가장 처음 호출되는 것은 DeviceClass의 realize 함수이다.

이 회수 호출의 기본 함수는 xxx_device_realize()이다.

당연히, 만약 Device를 상속한 클래스라면, 이 함수를 다시 쓸 수도 있다. 예를 들면 PCIDeviceClass류는 그 클래스 초기화 함수 pci_device_class_init()에서 DeviceClass->realize를 pci_qdev_realize로 다시 썼다.

static void pci_device_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *k = DEVICE_CLASS(klass);

    k->realize = pci_qdev_realize;
    k->unrealize = pci_qdev_unrealize;
    k->bus_type = TYPE_PCI_BUS;
    device_class_set_props(k, pci_props);
}

PCIDeviceClass 자체로 말하자면, 그 PCIDeviceClass->realize는 pci_default_realize 함수로 할당될 수 있다 (이곳은 단지 문제를 더 잘 설명하기 위해서다. 새 QEMU에는 이 함수가 이미 없다).

뒤에 PCIDeviceClass를 상속한 클래스는 자신의 클래스 초기화 함수에서 자기의 realize 함수를 할당할 수 있다.

 

virtio 디바이스 클래스의 상속 체인 관계는 "DeviceClass -> PCIDeviceClass -> VirtioPCIClass"이다.

 

VirtioPCIClass의 realize 함수 분석

  realize 함수
DeviceClass pci_qdev_realize
PCIDeviceClass pci_default_realize

VirtioPCIClass의 관련 정의는 모든 virtio PCI proxy 디바이스의 부 디바이스 TYPE_VIRTIO_PCI에서 진행한다.

그중 클래스 초기화 함수는 virtio_pci_class_init이다.

static void virtio_pci_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);
    VirtioPCIClass *vpciklass = VIRTIO_PCI_CLASS(klass);
    ResettableClass *rc = RESETTABLE_CLASS(klass);

    device_class_set_props(dc, virtio_pci_properties);
    k->realize = virtio_pci_realize;
    k->exit = virtio_pci_exit;
    k->vendor_id = PCI_VENDOR_ID_REDHAT_QUMRANET;
    k->revision = VIRTIO_PCI_ABI_VERSION;
    k->class_id = PCI_CLASS_OTHERS;
    device_class_set_parent_realize(dc, virtio_pci_dc_realize,
                                    &vpciklass->parent_dc_realize);
    rc->phases.hold = virtio_pci_bus_reset_hold;
}

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,
};

앞 회차에서 말한 바와 같이, virtio_pci_class_init 함수는 가장 먼저 PCIDeviceClass->realize 함수 포인터를 자기의 virtio_pci_realize 함수로 교체한다.

k->realize = virtio_pci_realize;
device_class_set_parent_realize(dc, virtio_pci_dc_realize,
                                &vpciklass->parent_dc_realize);
void device_class_set_parent_realize(DeviceClass *dc,
                                     DeviceRealize dev_realize,
                                     DeviceRealize *parent_realize)
{
    *parent_realize = dc->realize;
    dc->realize = dev_realize;
}

device_class_set_parent 함수는 위아래 글에서 먼저 vpciklass->parent_dc_realize를 dc->realize로 할당한다.

이 값이 바로 pci_qdev_realize이다.

 

그러고 나서 dc->realize를 virtio_pci_dc_realize (중간 변수 dev_realize를 통해서)로 할당한다.

통상적으로 말하면, 부모 클래스의 realize 함수는 자식 클래스의 realize 함수를 호출한다.

예를 들면 DeviceClass->realize (pci_qdev_realize)가 PCIDeviceClass->realize 함수를 호출할 것이다.

그리고 PCIDeviceClass->realize 호출은 자식 클래스의 realize 함수를 호출할 수 있다.

그러나 이 두 줄 코드는 이 순서를 바꿨다.
여기에서, dc->realize는 virtio_pci_dc_realize로 할당된다.

DeviceClass->realize 즉 dc->realize가 가리키는 함수가 먼저 실행될 것이다.

이런 이유로 virtio_pci_dc_realize 함수가 가장 먼저 실행된다.

그러고 나서 원래의 dc->realize 즉 pci_qdev_realize 함수를 VirtioPCIClass->parent_dc_realize 함수에 보존한다.

통상 디바이스 realize 과정에서, 자식 클래스형의 realize 함수가 먼저 어떤 일을 해야 할 때 이런 방법을 사용한다.

virtio balloon PCI proxy 디바이스 유형의 초기화 함수 virtio_balloon_pci_class_init()로 돌아가 본다.

static void virtio_balloon_pci_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    VirtioPCIClass *k = VIRTIO_PCI_CLASS(klass);
    PCIDeviceClass *pcidev_k = PCI_DEVICE_CLASS(klass);

    k->realize = virtio_balloon_pci_realize;
    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
    pcidev_k->vendor_id = PCI_VENDOR_ID_REDHAT_QUMRANET;
    pcidev_k->device_id = PCI_DEVICE_ID_VIRTIO_BALLOON;
    pcidev_k->revision = VIRTIO_PCI_ABI_VERSION;
    pcidev_k->class_id = PCI_CLASS_OTHERS;
}

볼 수 있듯이, 그중 VirtioPCIClass->realize 함수 포인터이 virtio_balloon_pci_realize()를 가리키도록 할당되었다.

종합해서 말하면, virtio balloon 관련 류가 미치는 realize 함수는 아래 표와 같이 보인다.

  realize 함수 parent_dc_realize 함수
DeviceClass virtio_pci_dc_realize  
PCIDeviceClass virtio_pci_realize  
VirtioPCIClass virtio_balloon_pci_realize pci_qdev_realize

 

device_set_realized 함수의 호출 흐름 분석

virtio PCI proxy 디바이스의 realize 속성을 할당할 때, device_set_realized 함수에서 가장 먼저 호출되는 것은 DeviceClass->realize 함수 포인터이 가리키는 함수이다. 즉 virtio_pci_dc_realize 함수이다.

if (dc->realize) {
            dc->realize(dev, &local_err);
            if (local_err != NULL) {
                goto fail;
            }
        }

virtio_pci_dc_realize 함수에서는 VirtioPCIClass->parent_dc_realize를 호출한다. 즉 pci_qdev_realize 함수이다

vpciklass->parent_dc_realize(qdev, errp);
static void pci_qdev_realize(DeviceState *qdev, Error **errp)
{
    PCIDevice *pci_dev = (PCIDevice *)qdev;
    PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(pci_dev);
    ObjectClass *klass = OBJECT_CLASS(pc);
    Error *local_err = NULL;
    bool is_default_rom;
    uint16_t class_id;

    /*
     * capped by systemd (see: udev-builtin-net_id.c)
     * as it's the only known user honor it to avoid users
     * misconfigure QEMU and then wonder why acpi-index doesn't work
     */
    if (pci_dev->acpi_index > ONBOARD_INDEX_MAX) {
        error_setg(errp, "acpi-index should be less or equal to %u",
                    ONBOARD_INDEX_MAX);
        return;
    }

    /*
     * make sure that acpi-index is unique across all present PCI devices
     */
    if (pci_dev->acpi_index) {
        GSequence *used_indexes = pci_acpi_index_list();

        if (g_sequence_lookup(used_indexes,
                              GINT_TO_POINTER(pci_dev->acpi_index),
                              g_cmp_uint32, NULL)) {
            error_setg(errp, "a PCI device with acpi-index = %" PRIu32
                       " already exist", pci_dev->acpi_index);
            return;
        }

        g_sequence_insert_sorted(used_indexes,
                                 GINT_TO_POINTER(pci_dev->acpi_index),
                                 g_cmp_uint32, NULL);
    }

    if (pci_dev->romsize != -1 && !is_power_of_2(pci_dev->romsize)) {
        error_setg(errp, "ROM size %u is not a power of two", pci_dev->romsize);
        return;
    }

    /* initialize cap_present for pci_is_express() and pci_config_size(),
     * Note that hybrid PCIs are not set automatically and need to manage
     * QEMU_PCI_CAP_EXPRESS */
    if (object_class_dynamic_cast(klass, INTERFACE_PCIE_DEVICE) &&
       !object_class_dynamic_cast(klass, INTERFACE_CONVENTIONAL_PCI_DEVICE)) {
        pci_dev->cap_present |= QEMU_PCI_CAP_EXPRESS;
    }

    if (object_class_dynamic_cast(klass, INTERFACE_CXL_DEVICE)) {
        pci_dev->cap_present |= QEMU_PCIE_CAP_CXL;
    }

    pci_dev = do_pci_register_device(pci_dev,
                                     object_get_typename(OBJECT(qdev)),
                                     pci_dev->devfn, errp);
    if (pci_dev == NULL)
        return;

    if (pc->realize) {
        pc->realize(pci_dev, &local_err);
        if (local_err) {
            error_propagate(errp, local_err);
            do_pci_unregister_device(pci_dev);
            return;
        }
    }

    /*
     * A PCIe Downstream Port that do not have ARI Forwarding enabled must
     * associate only Device 0 with the device attached to the bus
     * representing the Link from the Port (PCIe base spec rev 4.0 ver 0.3,
     * sec 7.3.1).
     * With ARI, PCI_SLOT() can return non-zero value as the traditional
     * 5-bit Device Number and 3-bit Function Number fields in its associated
     * Routing IDs, Requester IDs and Completer IDs are interpreted as a
     * single 8-bit Function Number. Hence, ignore ARI capable devices.
     */
    if (pci_is_express(pci_dev) &&
        !pcie_find_capability(pci_dev, PCI_EXT_CAP_ID_ARI) &&
        pcie_has_upstream_port(pci_dev) &&
        PCI_SLOT(pci_dev->devfn)) {
        warn_report("PCI: slot %d is not valid for %s,"
                    " parent device only allows plugging into slot 0.",
                    PCI_SLOT(pci_dev->devfn), pci_dev->name);
    }

    if (pci_dev->failover_pair_id) {
        if (!pci_bus_is_express(pci_get_bus(pci_dev))) {
            error_setg(errp, "failover primary device must be on "
                             "PCIExpress bus");
            pci_qdev_unrealize(DEVICE(pci_dev));
            return;
        }
        class_id = pci_get_word(pci_dev->config + PCI_CLASS_DEVICE);
        if (class_id != PCI_CLASS_NETWORK_ETHERNET) {
            error_setg(errp, "failover primary device is not an "
                             "Ethernet device");
            pci_qdev_unrealize(DEVICE(pci_dev));
            return;
        }
        if ((pci_dev->cap_present & QEMU_PCI_CAP_MULTIFUNCTION)
            || (PCI_FUNC(pci_dev->devfn) != 0)) {
            error_setg(errp, "failover: primary device must be in its own "
                              "PCI slot");
            pci_qdev_unrealize(DEVICE(pci_dev));
            return;
        }
        qdev->allow_unplug_during_migration = true;
    }

    /* rom loading */
    is_default_rom = false;
    if (pci_dev->romfile == NULL && pc->romfile != NULL) {
        pci_dev->romfile = g_strdup(pc->romfile);
        is_default_rom = true;
    }

    pci_add_option_rom(pci_dev, is_default_rom, &local_err);
    if (local_err) {
        error_propagate(errp, local_err);
        pci_qdev_unrealize(DEVICE(pci_dev));
        return;
    }

    pci_set_power(pci_dev, true);

    pci_dev->msi_trigger = pci_msi_trigger;
}

pci_qdev_realize 함수에서는 PCIDeviceClass의 realize 함수 포인터이 가리키는 함수를 호출한다. 즉 virtio_pci_realize 함수이다.

PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(pci_dev);
    ……
    if (pc->realize) {
        pc->realize(pci_dev, &local_err);
        if (local_err) {
            error_propagate(errp, local_err);
            do_pci_unregister_device(pci_dev);
            return;
        }
    }
static void virtio_pci_realize(PCIDevice *pci_dev, Error **errp)
{
    VirtIOPCIProxy *proxy = VIRTIO_PCI(pci_dev);
    VirtioPCIClass *k = VIRTIO_PCI_GET_CLASS(pci_dev);
    bool pcie_port = pci_bus_is_express(pci_get_bus(pci_dev)) &&
                     !pci_bus_is_root(pci_get_bus(pci_dev));

    if (kvm_enabled() && !kvm_has_many_ioeventfds()) {
        proxy->flags &= ~VIRTIO_PCI_FLAG_USE_IOEVENTFD;
    }

    /* fd-based ioevents can't be synchronized in record/replay */
    if (replay_mode != REPLAY_MODE_NONE) {
        proxy->flags &= ~VIRTIO_PCI_FLAG_USE_IOEVENTFD;
    }

    /*
     * virtio pci bar layout used by default.
     * subclasses can re-arrange things if needed.
     *
     *   region 0   --  virtio legacy io bar
     *   region 1   --  msi-x bar
     *   region 2   --  virtio modern io bar (off by default)
     *   region 4+5 --  virtio modern memory (64bit) bar
     *
     */
    proxy->legacy_io_bar_idx  = 0;
    proxy->msix_bar_idx       = 1;
    proxy->modern_io_bar_idx  = 2;
    proxy->modern_mem_bar_idx = 4;

    proxy->common.offset = 0x0;
    proxy->common.size = 0x1000;
    proxy->common.type = VIRTIO_PCI_CAP_COMMON_CFG;

    proxy->isr.offset = 0x1000;
    proxy->isr.size = 0x1000;
    proxy->isr.type = VIRTIO_PCI_CAP_ISR_CFG;

    proxy->device.offset = 0x2000;
    proxy->device.size = 0x1000;
    proxy->device.type = VIRTIO_PCI_CAP_DEVICE_CFG;

    proxy->notify.offset = 0x3000;
    proxy->notify.size = virtio_pci_queue_mem_mult(proxy) * VIRTIO_QUEUE_MAX;
    proxy->notify.type = VIRTIO_PCI_CAP_NOTIFY_CFG;

    proxy->notify_pio.offset = 0x0;
    proxy->notify_pio.size = 0x4;
    proxy->notify_pio.type = VIRTIO_PCI_CAP_NOTIFY_CFG;

    /* subclasses can enforce modern, so do this unconditionally */
    memory_region_init(&proxy->modern_bar, OBJECT(proxy), "virtio-pci",
                       /* PCI BAR regions must be powers of 2 */
                       pow2ceil(proxy->notify.offset + proxy->notify.size));

    if (proxy->disable_legacy == ON_OFF_AUTO_AUTO) {
        proxy->disable_legacy = pcie_port ? ON_OFF_AUTO_ON : ON_OFF_AUTO_OFF;
    }

    if (!virtio_pci_modern(proxy) && !virtio_pci_legacy(proxy)) {
        error_setg(errp, "device cannot work as neither modern nor legacy mode"
                   " is enabled");
        error_append_hint(errp, "Set either disable-modern or disable-legacy"
                          " to off\n");
        return;
    }

    if (pcie_port && pci_is_express(pci_dev)) {
        int pos;
        uint16_t last_pcie_cap_offset = PCI_CONFIG_SPACE_SIZE;

        pos = pcie_endpoint_cap_init(pci_dev, 0);
        assert(pos > 0);

        pos = pci_add_capability(pci_dev, PCI_CAP_ID_PM, 0,
                                 PCI_PM_SIZEOF, errp);
        if (pos < 0) {
            return;
        }

        pci_dev->exp.pm_cap = pos;

        /*
         * Indicates that this function complies with revision 1.2 of the
         * PCI Power Management Interface Specification.
         */
        pci_set_word(pci_dev->config + pos + PCI_PM_PMC, 0x3);

        if (proxy->flags & VIRTIO_PCI_FLAG_AER) {
            pcie_aer_init(pci_dev, PCI_ERR_VER, last_pcie_cap_offset,
                          PCI_ERR_SIZEOF, NULL);
            last_pcie_cap_offset += PCI_ERR_SIZEOF;
        }

        if (proxy->flags & VIRTIO_PCI_FLAG_INIT_DEVERR) {
            /* Init error enabling flags */
            pcie_cap_deverr_init(pci_dev);
        }

        if (proxy->flags & VIRTIO_PCI_FLAG_INIT_LNKCTL) {
            /* Init Link Control Register */
            pcie_cap_lnkctl_init(pci_dev);
        }

        if (proxy->flags & VIRTIO_PCI_FLAG_INIT_PM) {
            /* Init Power Management Control Register */
            pci_set_word(pci_dev->config + pos + PCI_PM_CTRL,
                         PCI_PM_CTRL_STATE_MASK);
        }

        if (proxy->flags & VIRTIO_PCI_FLAG_ATS) {
            pcie_ats_init(pci_dev, last_pcie_cap_offset,
                          proxy->flags & VIRTIO_PCI_FLAG_ATS_PAGE_ALIGNED);
            last_pcie_cap_offset += PCI_EXT_CAP_ATS_SIZEOF;
        }

        if (proxy->flags & VIRTIO_PCI_FLAG_INIT_FLR) {
            /* Set Function Level Reset capability bit */
            pcie_cap_flr_init(pci_dev);
        }
    } else {
        /*
         * make future invocations of pci_is_express() return false
         * and pci_config_size() return PCI_CONFIG_SPACE_SIZE.
         */
        pci_dev->cap_present &= ~QEMU_PCI_CAP_EXPRESS;
    }

    virtio_pci_bus_new(&proxy->bus, sizeof(proxy->bus), proxy);
    if (k->realize) {
        k->realize(proxy, errp);
    }
}

이 함수의 가장 마지막에 VirtioPCIClass의 realize 함수 포인터이 가리키는 함수를 호출할 것이다. 즉 virtio_balloon_pci_realize 함수이다.

static void virtio_pci_realize(PCIDevice *pci_dev, Error **errp)
{
    VirtIOPCIProxy *proxy = VIRTIO_PCI(pci_dev);
    VirtioPCIClass *k = VIRTIO_PCI_GET_CLASS(pci_dev);
    ……
    if (k->realize) {
        k->realize(proxy, errp);
    }
}

 

전체 realize 호출 흐름 다이어그램

 

핵심 호출 관계

  • instance_init 단계는 별일 안 한다. 그저 object_initialize까지 내려가서 메모리 초기화하고 자식 속성으로 부모에 붙이는 정도
  • realize 단계가 진짜 일을 한다. device_set_realized에서 시작해서 dc->realize -> parent_dc_realize -> pc->realize -> k->realize 순서로 위에서 아래로 차례로 호출된다.
  • device_class_set_parent_realize의 트릭이 핵심이다.
    • 보통은 부모 클래스 → 자식 클래스 순으로 realize가 흘러가지만, virtio 같이 자식 클래스의 realize가 먼저 일을 좀 해야 하는 경우에는 이 함수로 순서를 뒤집어서 virtio_pci_dc_realize가 먼저 실행되고 그 안에서 원래의 pci_qdev_realize를 호출하도록 만든다.

 

2) virtio_device_realize 와 virtio_balloon_device_realize

virtio_device_realize 함수 분석

이전 분석에서 virtio_balloon_pci_realize 함수의 마지막 단계에서 object_property_set 함수를 호출했고, 그로 인해 virtio_device_realize 함수가 실행됐다는 걸 다뤘다. 이번에는 virtio_device_realize 함수에 대해 분석한다.

 

virtio_device_realize 함수도 사실 범용 함수다. TYPE_VIRTIO_DEVICE 추상 디바이스의 realize 함수다. 모든 virtio 디바이스가 초기화될 때 이 함수를 호출한다.

 

virtio_device_realize의 5단계 구조

 

 

(1) virtio_device_realize 함수는 먼저 virtio 디바이스가 속한 클래스를 얻는다

VirtIODevice *vdev = VIRTIO_DEVICE(dev);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_GET_CLASS(dev);

 

(2) 그러고 나서 구체적인 클래스의 realize 함수를 호출한다

virtio balloon 디바이스 입장에서는 virtio_balloon_device_realize 함수다.

if (vdc->realize != NULL) {
    vdc->realize(dev, &err);
    if (err != NULL) {
        error_propagate(errp, err);
        return;
    }
}

 

(3) 다음으로 virtio_bus_device_plugged 함수를 호출해서 virtio 디바이스를 virtio 버스에 연결한다

virtio_bus_device_plugged(vdev, &err);
if (err != NULL) {
    error_propagate(errp, err);
    vdc->unrealize(dev);
    return;
}

 

(4) 그 후, memory_listener_register 함수를 호출해서 메모리 리스너를 등록한다

memory_listener_register(&vdev->listener, vdev->dma_as);
/**
 * memory_listener_register: register callbacks to be called when memory
 *                           sections are mapped or unmapped into an address
 *                           space
 *
 * @listener: an object containing the callbacks to be called
 * @filter: if non-%NULL, only regions in this address space will be observed
 */
void memory_listener_register(MemoryListener *listener, AddressSpace *filter);

이 함수는 lister(여기서는 vdev->listener)를 filter 주소 공간(여기서는 vdev->dma_as)에 등록한다. filter 주소 공간의 토폴로지 구조에 변화가 생기면, 그 연결 리스트 위의 모든 listener를 호출해서 관련 콜백 함수를 호출한다.

 

메모리 리스너 등록의 관계도

 

(5) 마지막으로 QTAILQ_INSERT_TAIL()을 호출해서 vdev를 virtio_list 끝에 삽입한다

QTAILQ_INSERT_TAIL(&virtio_list, vdev, next);

QTAILQ_INSERT_TAIL은 매크로 하나인데, 원소를 큐 끝에 삽입하는 데 쓴다. QTAIL_INSERT_TAIL 정의는 아래와 같다.

#define QTAILQ_INSERT_TAIL(head, elm, field) do {                       \
        (elm)->field.tqe_next = NULL;                                   \
        (elm)->field.tqe_circ.tql_prev = (head)->tqh_circ.tql_prev;     \
        (head)->tqh_circ.tql_prev->tql_next = (elm);                    \
        (head)->tqh_circ.tql_prev = &(elm)->field.tqe_circ;             \
} while (/*CONSTCOND*/0)

 

do {                                                            \
    (vdev)->next.tqe_next = NULL;                               \
    (vdev)->next.tqe_circ.tql_prev = (&virtio_list)->tqh_circ.tql_prev;     \
    (&virtio_list)->tqh_circ.tql_prev->tql_next = (vdev);       \
    (&virtio_list)->tqh_circ.tql_prev = &(vdev)->next.tqe_circ; \
} while (/*CONSTCOND*/0)

 

virtio_list에 vdev가 삽입되는 모습

 

virtio_balloon_device_realize 함수 분석

virtio_device_realize 함수가 구체적인 클래스의 realize 함수를 호출한다고 했는데, virtio balloon 디바이스 입장에서는 그게 virtio_balloon_device_realize 함수다

구체적인 클래스의 realize 함수를 호출하는 부분은 다음과 같다.

if (vdc->realize != NULL) {
    vdc->realize(dev, &err);
    if (err != NULL) {
        error_propagate(errp, err);
        return;
    }
}
static void virtio_balloon_device_realize(DeviceState *dev, Error **errp)
{
    VirtIODevice *vdev = VIRTIO_DEVICE(dev);
    VirtIOBalloon *s = VIRTIO_BALLOON(dev);
    int ret;

    virtio_init(vdev, VIRTIO_ID_BALLOON, virtio_balloon_config_size(s));

    ret = qemu_add_balloon_handler(virtio_balloon_to_target,
                                    virtio_balloon_stat, s);

    if (ret < 0) {
        error_setg(errp, "Only one balloon device is supported");
        virtio_cleanup(vdev);
        return;
    }

    if (virtio_has_feature(s->host_features, VIRTIO_BALLOON_F_FREE_PAGE_HINT) &&
        !s->iothread) {
        error_setg(errp, "'free-page-hint' requires 'iothread' to be set");
        virtio_cleanup(vdev);
        return;
    }

    s->ivq = virtio_add_queue(vdev, 128, virtio_balloon_handle_output);
    s->dvq = virtio_add_queue(vdev, 128, virtio_balloon_handle_output);
    s->svq = virtio_add_queue(vdev, 128, virtio_balloon_receive_stats);

    if (virtio_has_feature(s->host_features, VIRTIO_BALLOON_F_FREE_PAGE_HINT)) {
        s->free_page_vq = virtio_add_queue(vdev, VIRTQUEUE_MAX_SIZE,
                                            virtio_balloon_handle_free_page_vq);
        precopy_add_notifier(&s->free_page_hint_notify);

        object_ref(OBJECT(s->iothread));
        s->free_page_bh = aio_bh_new_guarded(iothread_get_aio_context(s->iothread),
                                              virtio_balloon_get_free_page_hints, s,
                                              &dev->mem_reentrancy_guard);
    }

    if (virtio_has_feature(s->host_features, VIRTIO_BALLOON_F_REPORTING)) {
        s->reporting_vq = virtio_add_queue(vdev, 32,
                                            virtio_balloon_handle_report);
    }

    reset_stats(s);
}

같은 파일(hw/virtio/virtio-balloon.c)의 virtio_balloon_class_init 함수 안에서, virtio_balloon_device_realize 함수(주소)를 vdc->realize에 할당해놨다.

static void virtio_balloon_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);

    device_class_set_props(dc, virtio_balloon_properties);
    dc->vmsd = &vmstate_virtio_balloon;
    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
    vdc->realize = virtio_balloon_device_realize;
    vdc->unrealize = virtio_balloon_device_unrealize;
    vdc->reset = virtio_balloon_device_reset;
    vdc->get_config = virtio_balloon_get_config;
    vdc->set_config = virtio_balloon_set_config;
    vdc->get_features = virtio_balloon_get_features;
    vdc->set_status = virtio_balloon_set_status;
    vdc->vmsd = &vmstate_virtio_device;
}

 

virtio_balloon_class_init에서 콜백이 할당되는 관계

virtio_balloon_device_realize()는 virtio balloon 디바이스의 realize 함수다. TYPE_VIRTIO_BALLOON_DEVICE의 realize를 구현하는 데 쓴다.

 

(1) virtio_balloon_device_realize 함수는 먼저 virtio_init 함수를 호출해서 virtio 디바이스의 공통 부분을 초기화한다

virtio_init(vdev, VIRTIO_ID_BALLOON, virtio_balloon_config_size(s));
void virtio_init(VirtIODevice *vdev, uint16_t device_id, size_t config_size)
{
    BusState *qbus = qdev_get_parent_bus(DEVICE(vdev));
    VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
    int i;
    int nvectors = k->query_nvectors ? k->query_nvectors(qbus->parent) : 0;

    if (nvectors) {
        vdev->vector_queues =
            g_malloc0(sizeof(*vdev->vector_queues) * nvectors);
    }

    vdev->start_on_kick = false;
    vdev->started = false;
    vdev->vhost_started = false;
    vdev->device_id = device_id;
    vdev->status = 0;
    qatomic_set(&vdev->isr, 0);
    vdev->queue_sel = 0;
    vdev->config_vector = VIRTIO_NO_VECTOR;
    vdev->vq = g_new0(VirtQueue, VIRTIO_QUEUE_MAX);
    vdev->vm_running = runstate_is_running();
    vdev->broken = false;
    for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {
        vdev->vq[i].vector = VIRTIO_NO_VECTOR;
        vdev->vq[i].vdev = vdev;
        vdev->vq[i].queue_index = i;
        vdev->vq[i].host_notifier_enabled = false;
    }

    vdev->name = virtio_id_to_name(device_id);
    vdev->config_len = config_size;
    if (vdev->config_len) {
        vdev->config = g_malloc0(config_size);
    } else {
        vdev->config = NULL;
    }
    vdev->vmstate = qdev_add_vm_change_state_handler(DEVICE(vdev),
                                                      virtio_vmstate_change, vdev);
    vdev->device_endian = virtio_default_endian();
    vdev->use_guest_notifier_mask = true;
}

virtio_init 함수가 하는 일은 모든 virtio 디바이스의 기반 클래스 TYPE_VIRTIO_DEVICE의 인스턴스 VirtIODevice 구조체를 초기화하는 거다. VirtIODevice의 멤버들에 대해 초기화한다.

 

VirtIODevice의 vector_queues 멤버와 config_vector 멤버는 MSI 인터럽트와 관련이 있다.

device_id, status, name 멤버는 각각 디바이스의 id, 상태, 이름을 나타낸다.

isr 멤버는 인터럽트 요청을 표시하는 데 쓴다.

queue_sel 멤버는 디바이스 큐 열의 선택을 진행하는 데 쓴다.

vq 멤버는 이 디바이스의 virtio queue를 표시하는데, 여기서는 VIRTIO_QUEUE_MAX개의 queue를 할당했고, 또한 초기화를 진행했다.

config_len과 config 멤버는 각각 이 virtio 디바이스 설정 공간의 길이와 데이터 저장 영역을 표시한다.

use_guest_notifier_mask 멤버는 irqfd와 관련이 있다.

 

VirtIODevice 구조체 멤버

 

(2) virtio_init 함수가 VirtIODevice를 초기화한 후에, virtio_add_queue 함수를 호출해서 3개의 virtqueue를 만든다

s->ivq = virtio_add_queue(vdev, 128, virtio_balloon_handle_output);
s->dvq = virtio_add_queue(vdev, 128, virtio_balloon_handle_output);
s->svq = virtio_add_queue(vdev, 128, virtio_balloon_receive_stats);

 

virtio balloon의 3개 virtqueue

virtqueue는 virtio 디바이스의 중요한 구성 부분이고, 가상 머신 안의 운영 체제와 데이터 전송을 하는 데 쓴다.

VirtQueue *virtio_add_queue(VirtIODevice *vdev, int queue_size,
                             VirtIOHandleOutput handle_output)
{
    int i;

    for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {
        if (vdev->vq[i].vring.num == 0)
            break;
    }

    if (i == VIRTIO_QUEUE_MAX || queue_size > VIRTQUEUE_MAX_SIZE)
        abort();

    vdev->vq[i].vring.num = queue_size;
    vdev->vq[i].vring.num_default = queue_size;
    vdev->vq[i].vring.align = VIRTIO_PCI_VRING_ALIGN;
    vdev->vq[i].handle_output = handle_output;
    vdev->vq[i].used_elems = g_new0(VirtQueueElement, queue_size);

    return &vdev->vq[i];
}

virtio_add_queue 함수는 virtio 프레임워크에서 virtqueue를 추가하는 데 쓰는 인터페이스다.

3개의 파라미터는 각각

  • 추가할 디바이스(VirtIODevice *vdev)
  • virtqueue의 크기(int queue_size)
  • 처리 함수(VirtIOHandleOutput handle_output)

를 나타낸다.

 

virtio_add_queue 함수는 VirtIODevice의 vq 배열 멤버에서 아직 사용되지 않은 queue 하나를 찾는다.

하나의 virtqueue는 VirtQueue 구조체로 표시되는데, 이 VirtQueue의 멤버들에 대해 초기화한다.

이 queue의 크기와 정렬 정보 설정도 포함된다. 마지막은 VirtQueue의 handle_output 멤버를 설정하는 건데, 그건 함수 포인터 하나로, 가상 머신이 보낸 IO 요청을 받았을 때 handle_output에 저장된 콜백 함수가 호출된다.

struct VirtQueue
{
    VRing vring;
    VirtQueueElement *used_elems;

    /* Next head to pop */
    uint16_t last_avail_idx;
    bool last_avail_wrap_counter;

    /* Last avail_idx read from VQ. */
    uint16_t shadow_avail_idx;
    bool shadow_avail_wrap_counter;

    uint16_t used_idx;
    bool used_wrap_counter;

    /* Last used index value we have signalled on */
    uint16_t signalled_used;

    /* Last used index value we have signalled on */
    bool signalled_used_valid;

    /* Notification enabled? */
    bool notification;

    uint16_t queue_index;

    unsigned int inuse;

    uint16_t vector;
    VirtIOHandleOutput handle_output;
    VirtIODevice *vdev;
    EventNotifier guest_notifier;
    EventNotifier host_notifier;
    bool host_notifier_enabled;
    QLIST_ENTRY(VirtQueue) node;
};

 

3) virtio_bus_device_plugged 와 virtio_pci_device_plugged

virtio_bus_device_plugged 함수 분석

virtio PCI proxy 디바이스 realize부터 virtio 디바이스 realize까지의 과정을 확인해보았다. 다만 앞서 분석한 내용 중 한 부분이 빠져 있는데, 바로 virtio 디바이스가 virtio 버스에 연결되는 동작이다.

이 과정은 virtio_device_realize 함수 안에서 virtio_bus_device_plugged 함수를 호출해서 완성된다.

 

virtio_bus_device_plugged 함수의 역할은 바로 virtio 디바이스를 virtio 버스에 삽입하는 것이다.

/* A VirtIODevice is being plugged */
void virtio_bus_device_plugged(VirtIODevice *vdev, Error **errp)
{
    DeviceState *qdev = DEVICE(vdev);
    BusState *qbus = BUS(qdev_get_parent_bus(qdev));
    VirtioBusState *bus = VIRTIO_BUS(qbus);
    VirtioBusClass *klass = VIRTIO_BUS_GET_CLASS(bus);
    VirtioDeviceClass *vdc = VIRTIO_DEVICE_GET_CLASS(vdev);
    bool has_iommu = virtio_host_has_feature(vdev, VIRTIO_F_IOMMU_PLATFORM);
    bool vdev_has_iommu;
    Error *local_err = NULL;

    DPRINTF("%s: plug device.\n", qbus->name);

    if (klass->pre_plugged != NULL) {
        klass->pre_plugged(qbus->parent, &local_err);
        if (local_err) {
            error_propagate(errp, local_err);
            return;
        }
    }

    /* Get the features of the plugged device. */
    assert(vdc->get_features != NULL);
    vdev->host_features = vdc->get_features(vdev, vdev->host_features,
                                              &local_err);
    if (local_err) {
        error_propagate(errp, local_err);
        return;
    }

    if (klass->device_plugged != NULL) {
        klass->device_plugged(qbus->parent, &local_err);
    }
    if (local_err) {
        error_propagate(errp, local_err);
        return;
    }

    vdev->dma_as = &address_space_memory;
    if (has_iommu) {
        vdev_has_iommu = virtio_host_has_feature(vdev, VIRTIO_F_IOMMU_PLATFORM);
        /*
         * Present IOMMU_PLATFORM to the driver iff iommu_plattform=on and
         * device operational. If the driver does not accept IOMMU_PLATFORM
         * we fail the device.
         */
        virtio_add_feature(&vdev->host_features, VIRTIO_F_IOMMU_PLATFORM);
        if (klass->get_dma_as) {
            vdev->dma_as = klass->get_dma_as(qbus->parent);
            if (!vdev_has_iommu && vdev->dma_as != &address_space_memory) {
                error_setg(errp,
                            "iommu_platform=true is not supported by the device");
                return;
            }
        }
    }
}

 

virtio_bus_device_plugged 내부 흐름

 

virtio_bus_device_plugged 함수는 주로 VirtioBusClass 클래스 타입의 device_plugged 콜백 함수를 호출했다.

VirtioBusClass *klass = VIRTIO_BUS_GET_CLASS(bus);
……
if (klass->device_plugged != NULL) {
    klass->device_plugged(qbus->parent, &local_err);
}
if (local_err) {
    error_propagate(errp, local_err);
    return;
}

device_plugged 콜백 함수, 즉 klass->device_plugged 함수 포인터가 가리키는 함수는 virtio_pci_bus_class_init 함수 안에서 virtio_pci_device_plugged 함수로 초기화됐다.

static void virtio_pci_bus_class_init(ObjectClass *klass, void *data)
{
    BusClass *bus_class = BUS_CLASS(klass);
    VirtioBusClass *k = VIRTIO_BUS_CLASS(klass);
    bus_class->max_dev = 1;
    k->notify = virtio_pci_notify;
    k->save_config = virtio_pci_save_config;
    k->load_config = virtio_pci_load_config;
    k->save_queue = virtio_pci_save_queue;
    k->load_queue = virtio_pci_load_queue;
    k->save_extra_state = virtio_pci_save_extra_state;
    k->load_extra_state = virtio_pci_load_extra_state;
    k->has_extra_state = virtio_pci_has_extra_state;
    k->query_guest_notifiers = virtio_pci_query_guest_notifiers;
    k->set_guest_notifiers = virtio_pci_set_guest_notifiers;
    k->set_host_notifier_mr = virtio_pci_set_host_notifier_mr;
    k->vmstate_change = virtio_pci_vmstate_change;
    k->pre_plugged = virtio_pci_pre_plugged;
    k->device_plugged = virtio_pci_device_plugged;
    k->device_unplugged = virtio_pci_device_unplugged;
    k->query_nvectors = virtio_pci_query_nvectors;
    k->ioeventfd_enabled = virtio_pci_ioeventfd_enabled;
    k->ioeventfd_assign = virtio_pci_ioeventfd_assign;
    k->get_dma_as = virtio_pci_get_dma_as;
    k->iommu_enabled = virtio_pci_iommu_enabled;
    k->queue_enabled = virtio_pci_queue_enabled;
}

 

VirtioBusClass의 device_plugged 콜백 연결

 

/* This is called by virtio-bus just after the device is plugged. */
static void virtio_pci_device_plugged(DeviceState *d, Error **errp)
{
    VirtIOPCIProxy *proxy = VIRTIO_PCI(d);
    VirtioBusState *bus = &proxy->bus;
    bool legacy = virtio_pci_legacy(proxy);
    bool modern;
    bool modern_pio = proxy->flags & VIRTIO_PCI_FLAG_MODERN_PIO_NOTIFY;
    uint8_t *config;
    uint32_t size;
    VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);

    /*
     * Virtio capabilities present without
     * VIRTIO_F_VERSION_1 confuses guests
     */
    if (!proxy->ignore_backend_features &&
        !virtio_has_feature(vdev->host_features, VIRTIO_F_VERSION_1)) {
        virtio_pci_disable_modern(proxy);

        if (!legacy) {
            error_setg(errp, "Device doesn't support modern mode, and legacy"
                              " mode is disabled");
            error_append_hint(errp, "Set disable-legacy to off\n");

            return;
        }
    }

    modern = virtio_pci_modern(proxy);

    config = proxy->pci_dev.config;
    if (proxy->class_code) {
        pci_config_set_class(config, proxy->class_code);
    }

    if (legacy) {
        if (!virtio_legacy_allowed(vdev)) {
            /*
             * To avoid migration issues, we allow legacy mode when legacy
             * check is disabled in the old machine types (< 5.1).
             */
            if (virtio_legacy_check_disabled(vdev)) {
                warn_report("device is modern-only, but for backward "
                              "compatibility legacy is allowed");
            } else {
                error_setg(errp,
                            "device is modern-only, use disable-legacy=on");
                return;
            }
        }
        /*
         * Legacy and transitional devices use specific subsystem IDs.
         * Note that the subsystem vendor ID (config + PCI_SUBSYSTEM_VENDOR_ID)
         * is set to PCI_SUBVENDOR_ID_REDHAT_QUMRANET by default.
         */
        pci_set_word(config + PCI_SUBSYSTEM_ID, virtio_bus_get_vdev_id(bus));
        if (proxy->trans_devid) {
            pci_config_set_device_id(config, proxy->trans_devid);
        }
    } else {
        /* pure virtio-1.0 */
        pci_set_word(config + PCI_VENDOR_ID,
                      PCI_VENDOR_ID_REDHAT_QUMRANET);
        pci_set_word(config + PCI_DEVICE_ID,
                      PCI_DEVICE_ID_VIRTIO_10_BASE + virtio_bus_get_vdev_id(bus));
        pci_config_set_revision(config, 1);
    }
    config[PCI_INTERRUPT_PIN] = 1;

    if (modern) {
        struct virtio_pci_cap cap = {
            .cap_len = sizeof cap,
        };
        struct virtio_pci_notify_cap notify = {
            .cap.cap_len = sizeof notify,
            .notify_off_multiplier =
                cpu_to_le32(virtio_pci_queue_mem_mult(proxy)),
        };
        struct virtio_pci_cfg_cap cfg = {
            .cap.cap_len = sizeof cfg,
            .cap.cfg_type = VIRTIO_PCI_CAP_PCI_CFG,
        };
        struct virtio_pci_notify_cap notify_pio = {
            .cap.cap_len = sizeof notify,
            .notify_off_multiplier = cpu_to_le32(0x0),
        };

        struct virtio_pci_cfg_cap *cfg_mask;

        virtio_pci_modern_regions_init(proxy, vdev->name);

        virtio_pci_modern_mem_region_map(proxy, &proxy->common, &cap);
        virtio_pci_modern_mem_region_map(proxy, &proxy->isr, &cap);
        virtio_pci_modern_mem_region_map(proxy, &proxy->device, &cap);
        virtio_pci_modern_mem_region_map(proxy, &proxy->notify, &notify.cap);

        if (modern_pio) {
            memory_region_init(&proxy->io_bar, OBJECT(proxy),
                                "virtio-pci-io", 0x4);

            pci_register_bar(&proxy->pci_dev, proxy->modern_io_bar_idx,
                              PCI_BASE_ADDRESS_SPACE_IO, &proxy->io_bar);

            virtio_pci_modern_io_region_map(proxy, &proxy->notify_pio,
                                              &notify_pio.cap);
        }

        pci_register_bar(&proxy->pci_dev, proxy->modern_mem_bar_idx,
                          PCI_BASE_ADDRESS_SPACE_MEMORY |
                          PCI_BASE_ADDRESS_MEM_PREFETCH |
                          PCI_BASE_ADDRESS_MEM_TYPE_64,
                          &proxy->modern_bar);

        proxy->config_cap = virtio_pci_add_mem_cap(proxy, &cfg.cap);
        cfg_mask = (void *)(proxy->pci_dev.wmask + proxy->config_cap);
        pci_set_byte(&cfg_mask->cap.bar, ~0x0);
        pci_set_long((uint8_t *)&cfg_mask->cap.offset, ~0x0);
        pci_set_long((uint8_t *)&cfg_mask->cap.length, ~0x0);
        pci_set_long(cfg_mask->pci_cfg_data, ~0x0);
    }

    if (proxy->nvectors) {
        int err = msix_init_exclusive_bar(&proxy->pci_dev, proxy->nvectors,
                                            proxy->msix_bar_idx, NULL);
        if (err) {
            /* Notice when a system that supports MSIx can't initialize it */
            if (err != -ENOTSUP) {
                warn_report("unable to init msix vectors to %" PRIu32,
                              proxy->nvectors);
            }
            proxy->nvectors = 0;
        }
    }

    proxy->pci_dev.config_write = virtio_write_config;
    proxy->pci_dev.config_read = virtio_read_config;

    if (legacy) {
        size = VIRTIO_PCI_REGION_SIZE(&proxy->pci_dev)
            + virtio_bus_get_vdev_config_len(bus);
        size = pow2ceil(size);

        memory_region_init_io(&proxy->bar, OBJECT(proxy),
                                &virtio_pci_config_ops,
                                proxy, "virtio-pci", size);

        pci_register_bar(&proxy->pci_dev, proxy->legacy_io_bar_idx,
                          PCI_BASE_ADDRESS_SPACE_IO, &proxy->bar);
    }
}

 

virtio_pci_device_plugged 함수 자세한 분석

(1) virtio_pci_device_plugged 함수는 virtio PCI proxy 디바이스의 설정 공간 안에 vendor id와 device id를 세팅한다

if (legacy) {
    ……
} else {
    /* pure virtio-1.0 */
    pci_set_word(config + PCI_VENDOR_ID,
                  PCI_VENDOR_ID_REDHAT_QUMRANET);
    pci_set_word(config + PCI_DEVICE_ID,
                  PCI_DEVICE_ID_VIRTIO_10_BASE + virtio_bus_get_vdev_id(bus));
    pci_config_set_revision(config, 1);
}

 

vendor id를 PCI_VENDOR_ID_REDHAT_QUMRANE로 세팅한다.

/* Red Hat / Qumranet (for QEMU) -- see pci-ids.txt */
#define PCI_VENDOR_ID_REDHAT_QUMRANET    0x1af4
#define PCI_SUBVENDOR_ID_REDHAT_QUMRANET 0x1af4
#define PCI_SUBDEVICE_ID_QEMU            0x1100

 

device id를 PCI_DEVICE_ID_VIRTIO_10_BASE에 virtio 디바이스 클래스 형태의 id를 더한 걸로 세팅한다.

/*
 * modern virtio-pci devices get their id assigned automatically,
 * there is no need to add #defines here.  It gets calculated as
 *
 * PCI_DEVICE_ID = PCI_DEVICE_ID_VIRTIO_10_BASE +
 *                 virtio_bus_get_vdev_id(bus)
 */
#define PCI_DEVICE_ID_VIRTIO_10_BASE     0x1040

 

virtual balloon 디바이스 입장에서는 VIRTIO_ID_BALLOON이다.

#define VIRTIO_ID_BALLOON        5 /* virtio balloon */

 

vendor id / device id 계산 관계

 

(2) 다음으로 virtio 디바이스의 레지스터 설정 정보를 PCI capability로서 설정 공간에 써넣는다

if (modern) {
    struct virtio_pci_cap cap = {
        .cap_len = sizeof cap,
    };
    struct virtio_pci_notify_cap notify = {
        .cap.cap_len = sizeof notify,
        .notify_off_multiplier =
            cpu_to_le32(virtio_pci_queue_mem_mult(proxy)),
    };
    struct virtio_pci_cfg_cap cfg = {
        .cap.cap_len = sizeof cfg,
        .cap.cfg_type = VIRTIO_PCI_CAP_PCI_CFG,
    };
    struct virtio_pci_notify_cap notify_pio = {
        .cap.cap_len = sizeof notify,
        .notify_off_multiplier = cpu_to_le32(0x0),
    };

    struct virtio_pci_cfg_cap *cfg_mask;

    virtio_pci_modern_regions_init(proxy, vdev->name);

    virtio_pci_modern_mem_region_map(proxy, &proxy->common, &cap);
    virtio_pci_modern_mem_region_map(proxy, &proxy->isr, &cap);
    virtio_pci_modern_mem_region_map(proxy, &proxy->device, &cap);
    virtio_pci_modern_mem_region_map(proxy, &proxy->notify, &notify.cap);

    if (modern_pio) {
        memory_region_init(&proxy->io_bar, OBJECT(proxy),
                            "virtio-pci-io", 0x4);

        pci_register_bar(&proxy->pci_dev, proxy->modern_io_bar_idx,
                          PCI_BASE_ADDRESS_SPACE_IO, &proxy->io_bar);

        virtio_pci_modern_io_region_map(proxy, &proxy->notify_pio,
                                          &notify_pio.cap);
    }

    pci_register_bar(&proxy->pci_dev, proxy->modern_mem_bar_idx,
                      PCI_BASE_ADDRESS_SPACE_MEMORY |
                      PCI_BASE_ADDRESS_MEM_PREFETCH |
                      PCI_BASE_ADDRESS_MEM_TYPE_64,
                      &proxy->modern_bar);

    proxy->config_cap = virtio_pci_add_mem_cap(proxy, &cfg.cap);
    cfg_mask = (void *)(proxy->pci_dev.wmask + proxy->config_cap);
    pci_set_byte(&cfg_mask->cap.bar, ~0x0);
    pci_set_long((uint8_t *)&cfg_mask->cap.offset, ~0x0);
    pci_set_long((uint8_t *)&cfg_mask->cap.length, ~0x0);
    pci_set_long(cfg_mask->pci_cfg_data, ~0x0);
}

이 코드 조각을 이해하려면 먼저 pci capability에 대해 알아둬야 한다.

pci capability는 디바이스의 기능을 명시하는 데 쓴다.

virtio는 여러 개의 MemoryRegion을 VirtIOPCIProxy 디바이스 MMIO에 대응하는 MemoryRegion의 서브 MemoryRegion(비교적 어색하다)으로 삼는데,

이 몇 개의 MemoryRegion의 정보를 capability로 삼아 virtioPCI proxy 디바이스의 설정 공간에 써넣는다.

이 capability들의 헤더 구조는 struct virtio_pci_cap 구조체를 쓴다.

/* This is the PCI capability header: */
struct virtio_pci_cap {
    uint8_t cap_vndr;        /* Generic PCI field: PCI_CAP_ID_VNDR */
    uint8_t cap_next;        /* Generic PCI field: next ptr. */
    uint8_t cap_len;         /* Generic PCI field: capability length */
    uint8_t cfg_type;        /* Identifies the structure. */
    uint8_t bar;             /* Where to find it. */
    uint8_t id;              /* Multiple capabilities of the same type */
    uint8_t padding[2];      /* Pad to full dword. */
    uint32_t offset;         /* Offset within structure, in bytes. */
    uint32_t length;         /* Length of the structure, in bytes. */
};
  • cap_vndr는 capability의 id를 나타내는 데 쓴다. 일부 표준 capability를 제외하고, 만약 디바이스가 직접 정의한 것(여기 virtio 디바이스처럼)이면, PCI_CAP_ID_VNDR로 세팅된다.
  • cap_next는 PCI 설정 공간에서 다음 capability의 오프셋을 가리킨다.
  • bar는 이 capability가 어느 bar를 사용하는지 나타낸다.
  • offset은 이 capability가 대표하는 MemoryRegion이 virtioPCI proxy 디바이스의 bar 안에서 어디부터 시작하는지 나타낸다.
  • length는 이 capability의 길이를 나타낸다.

 

virtio_pci_cap 구조체 필드

 

 

PCI 설정 공간과 cap, MMIO의 관계

 

전체 데이터 구조 관계

 

(심화) virtio_pci_device_plugged 상세 (modern regions / BAR / msix)

virtio_pci_device_plugged 함수 분석 - 앞부분

/* This is called by virtio-bus just after the device is plugged. */
static void virtio_pci_device_plugged(DeviceState *d, Error **errp)
{
    VirtIOPCIProxy *proxy = VIRTIO_PCI(d);
    VirtioBusState *bus = &proxy->bus;
    bool legacy = virtio_pci_legacy(proxy);
    bool modern;
    bool modern_pio = proxy->flags & VIRTIO_PCI_FLAG_MODERN_PIO_NOTIFY;
    uint8_t *config;
    uint32_t size;
    VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);

    /*
     * Virtio capabilities present without
     * VIRTIO_F_VERSION_1 confuses guests
     */
    if (!proxy->ignore_backend_features &&
            !virtio_has_feature(vdev->host_features, VIRTIO_F_VERSION_1)) {
        virtio_pci_disable_modern(proxy);

        if (!legacy) {
            error_setg(errp, "Device doesn't support modern mode, and legacy"
                             " mode is disabled");
            error_append_hint(errp, "Set disable-legacy to off\n");

            return;
        }
    }

    modern = virtio_pci_modern(proxy);

    config = proxy->pci_dev.config;
    if (proxy->class_code) {
        pci_config_set_class(config, proxy->class_code);
    }

    if (legacy) {
        if (!virtio_legacy_allowed(vdev)) {
            /*
             * To avoid migration issues, we allow legacy mode when legacy
             * check is disabled in the old machine types (< 5.1).
             */
            if (virtio_legacy_check_disabled(vdev)) {
                warn_report("device is modern-only, but for backward "
                            "compatibility legacy is allowed");
            } else {
                error_setg(errp,
                           "device is modern-only, use disable-legacy=on");
                return;
            }
        }
        if (virtio_host_has_feature(vdev, VIRTIO_F_IOMMU_PLATFORM)) {
            error_setg(errp, "VIRTIO_F_IOMMU_PLATFORM was supported by"
                       " neither legacy nor transitional device");
            return;
        }
        /*
         * Legacy and transitional devices use specific subsystem IDs.
         * Note that the subsystem vendor ID (config + PCI_SUBSYSTEM_VENDOR_ID)
         * is set to PCI_SUBVENDOR_ID_REDHAT_QUMRANET by default.
         */
        pci_set_word(config + PCI_SUBSYSTEM_ID, virtio_bus_get_vdev_id(bus));
        if (proxy->trans_devid) {
            pci_config_set_device_id(config, proxy->trans_devid);
        } else {
            /* pure virtio-1.0 */
            pci_set_word(config + PCI_VENDOR_ID,
                         PCI_VENDOR_ID_REDHAT_QUMRANET);
            pci_set_word(config + PCI_DEVICE_ID,
                         PCI_DEVICE_ID_VIRTIO_10_BASE + virtio_bus_get_vdev_id(bus));
            pci_config_set_revision(config, 1);
        }
        config[PCI_INTERRUPT_PIN] = 1;

        if (modern) {
            struct virtio_pci_cap cap = {
                .cap_len = sizeof cap,
            };
            struct virtio_pci_notify_cap notify = {
                .cap.cap_len = sizeof notify,
                .notify_off_multiplier =
                    cpu_to_le32(virtio_pci_queue_mem_mult(proxy)),
            };
            struct virtio_pci_cfg_cap cfg = {
                .cap.cap_len = sizeof cfg,
                .cap.cfg_type = VIRTIO_PCI_CAP_PCI_CFG,
            };
            struct virtio_pci_notify_cap notify_pio = {
                .cap.cap_len = sizeof notify,
                .notify_off_multiplier = cpu_to_le32(0x0),
            };

            struct virtio_pci_cfg_cap *cfg_mask;

            virtio_pci_modern_regions_init(proxy, vdev->name);

            virtio_pci_modern_mem_region_map(proxy, &proxy->common, &cap);
            virtio_pci_modern_mem_region_map(proxy, &proxy->isr, &cap);
            virtio_pci_modern_mem_region_map(proxy, &proxy->device, &cap);
            virtio_pci_modern_mem_region_map(proxy, &proxy->notify, &notify.cap);

            if (modern_pio) {
                memory_region_init(&proxy->io_bar,
                    OBJECT(proxy),
                    "virtio-pci-io", 0x4);

                pci_register_bar(&proxy->pci_dev, proxy->modern_io_bar_idx,
                                 PCI_BASE_ADDRESS_SPACE_IO, &proxy->io_bar);

                virtio_pci_modern_io_region_map(proxy, &proxy->notify_pio,
                                                &notify_pio.cap);
            }

            pci_register_bar(&proxy->pci_dev, proxy->modern_mem_bar_idx,
                             PCI_BASE_ADDRESS_SPACE_MEMORY |
                             PCI_BASE_ADDRESS_MEM_PREFETCH |
                             PCI_BASE_ADDRESS_MEM_TYPE_64,
                             &proxy->modern_bar);

            proxy->config_cap = virtio_pci_add_mem_cap(proxy, &cfg.cap);
            cfg_mask = (void *)(proxy->pci_dev.wmask + proxy->config_cap);
            pci_set_byte(&cfg_mask->cap.bar, ~0x0);
            pci_set_long((uint8_t *)&cfg_mask->cap.offset, ~0x0);
            pci_set_long((uint8_t *)&cfg_mask->cap.length, ~0x0);
            pci_set_long(cfg_mask->pci_cfg_data, ~0x0);
        }

        if (proxy->nvectors) {
            int err = msix_init_exclusive_bar(&proxy->pci_dev, proxy->nvectors,
                                              proxy->msix_bar_idx, NULL);

            if (err) {
                /* Notice when a system that supports MSIx can't initialize it */
                if (err != -ENOTSUP) {
                    warn_report("unable to init msix vectors to %" PRIu32,
                                proxy->nvectors);
                }
                proxy->nvectors = 0;
            }
        }

        proxy->pci_dev.config_write = virtio_write_config;
        proxy->pci_dev.config_read = virtio_read_config;

        if (legacy) {
            size = VIRTIO_PCI_REGION_SIZE(&proxy->pci_dev)
                + virtio_bus_get_vdev_config_len(bus);
            size = pow2ceil(size);

            memory_region_init_io(&proxy->bar, OBJECT(proxy),
                                  &virtio_pci_config_ops,
                                  proxy, "virtio-pci", size);

            pci_register_bar(&proxy->pci_dev, proxy->legacy_io_bar_idx,
                             PCI_BASE_ADDRESS_SPACE_IO, &proxy->bar);
        }
}

 

함수 전체 흐름

 

2.1) virtio_pci_modern_regions_init 함수

초기화는 5개의 MemoryRegion을 만든다. 각각 virtio-pci-common, virtio-pci-isr, virtio-pci-device, virtio-pci-notify, virtio-pci-notify-pio다.

virtio_pci_modern_regions_init(proxy, vdev->name);
static void virtio_pci_modern_regions_init(VirtIOPCIProxy *proxy,
                                           const char *vdev_name)
{
    static const MemoryRegionOps common_ops = {
        .read = virtio_pci_common_read,
        .write = virtio_pci_common_write,
        .impl = {
            .min_access_size = 1,
            .max_access_size = 4,
        },
        .endianness = DEVICE_LITTLE_ENDIAN,
    };
    static const MemoryRegionOps isr_ops = {
        .read = virtio_pci_isr_read,
        .write = virtio_pci_isr_write,
        .impl = {
            .min_access_size = 1,
            .max_access_size = 4,
        },
        .endianness = DEVICE_LITTLE_ENDIAN,
    };
    static const MemoryRegionOps device_ops = {
        .read = virtio_pci_device_read,
        .write = virtio_pci_device_write,
        .impl = {
            .min_access_size = 1,
            .max_access_size = 4,
        },
        .endianness = DEVICE_LITTLE_ENDIAN,
    };
    static const MemoryRegionOps notify_ops = {
        .read = virtio_pci_notify_read,
        .write = virtio_pci_notify_write,
        .impl = {
            .min_access_size = 1,
            .max_access_size = 4,
        },
        .endianness = DEVICE_LITTLE_ENDIAN,
    };
    static const MemoryRegionOps notify_pio_ops = {
        .read = virtio_pci_notify_read,
        .write = virtio_pci_notify_write_pio,
        .impl = {
            .min_access_size = 1,
            .max_access_size = 4,
        },
        .endianness = DEVICE_LITTLE_ENDIAN,
    };
    g_autoptr(GString) name = g_string_new(NULL);

    g_string_printf(name, "virtio-pci-common-%s", vdev_name);
    memory_region_init_io(&proxy->common.mr, OBJECT(proxy),
                          &common_ops,
                          proxy,
                          name->str,
                          proxy->common.size);

    g_string_printf(name, "virtio-pci-isr-%s", vdev_name);
    memory_region_init_io(&proxy->isr.mr, OBJECT(proxy),
                          &isr_ops,
                          proxy,
                          name->str,
                          proxy->isr.size);

    g_string_printf(name, "virtio-pci-device-%s", vdev_name);
    memory_region_init_io(&proxy->device.mr, OBJECT(proxy),
                          &device_ops,
                          proxy,
                          name->str,
                          proxy->device.size);

    g_string_printf(name, "virtio-pci-notify-%s", vdev_name);
    memory_region_init_io(&proxy->notify.mr, OBJECT(proxy),
                          &notify_ops,
                          proxy,
                          name->str,
                          proxy->notify.size);

    g_string_printf(name, "virtio-pci-notify-pio-%s", vdev_name);
    memory_region_init_io(&proxy->notify_pio.mr, OBJECT(proxy),
                          &notify_pio_ops,
                          proxy,
                          name->str,
                          proxy->notify_pio.size);
}

 

5개 MemoryRegion 구조

 

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;
};

virtio-pci-common, virtio-pci-isr, virtio-pci-device, virtio-pci-notify, virtio-pci-notify-pio 이 5개의 MemoryRegion 관련 정보는 VirtIOPCIProxy 구조체 안의 몇 개의 VirtIOPCIRegion 멤버에 저장한다.

 

2.2) virtio_pci_device_plugged는 이어서 virtio_pci_modern_mem_region_map 함수를 호출한다

virtio_pci_modern_mem_region_map(proxy, &proxy->common, &cap);
virtio_pci_modern_mem_region_map(proxy, &proxy->isr, &cap);
virtio_pci_modern_mem_region_map(proxy, &proxy->device, &cap);
virtio_pci_modern_mem_region_map(proxy, &proxy->notify, &notify.cap);
static void virtio_pci_modern_mem_region_map(VirtIOPCIProxy *proxy,
                                             VirtIOPCIRegion *region,
                                             struct virtio_pci_cap *cap)
{
    virtio_pci_modern_region_map(proxy, region, cap,
                                 &proxy->modern_bar, proxy->modern_mem_bar_idx);
}

virtio_pci_modern_mem_region_map 함수는 virtio_pci_modern_region_map 함수만 호출한다.

static void virtio_pci_modern_region_map(VirtIOPCIProxy *proxy,
                                         VirtIOPCIRegion *region,
                                         struct virtio_pci_cap *cap,
                                         MemoryRegion *mr,
                                         uint8_t bar)
{
    memory_region_add_subregion(mr, region->offset, &region->mr);

    cap->cfg_type = region->type;
    cap->bar = bar;
    cap->offset = cpu_to_le32(region->offset);
    cap->length = cpu_to_le32(region->size);
    virtio_pci_add_mem_cap(proxy, cap);
}

 

virtio_pci_modern_region_map 동작

virtio_pci_modern_region_map 함수는 두 가지 기능을 완성한다.

  • VirtIOPCIRegion의 mr 멤버 virtio-pci-***를 자식 MemoryRegion으로 만들어서 VirtIOProxy의 modern_bar 멤버에 추가한다. 그래서 가상머신 내부에서 virtio PCI proxy의 MMIO에 쓰기를 할 때 이 몇 개의 virtio 장치의 MemoryRegion의 콜백 함수로 떨어지게 된다.
  • virtio_pci_add_mem_cap 함수를 호출해서 이 레지스터 정보들을 virtio PCI proxy 장치의 pci capability에 추가한다. 
static int virtio_pci_add_mem_cap(VirtIOPCIProxy *proxy,
                                   struct virtio_pci_cap *cap)
{
    PCIDevice *dev = &proxy->pci_dev;
    int offset;

    offset = pci_add_capability(dev, PCI_CAP_ID_VNDR, 0,
                                cap->cap_len, &error_abort);

    assert(cap->cap_len >= sizeof *cap);
    memcpy(dev->config + offset + PCI_CAP_FLAGS, &cap->cap_len,
           cap->cap_len - PCI_CAP_FLAGS);

    return offset;
}

 

2.3) virtio_pci_device_plugged 함수는 이어서 pci_register_bar 함수를 호출한다

VirtIOPCIProxy의 modern_bar라는 MemoryRegion을 시스템에 등록한다.

pci_register_bar(&proxy->pci_dev, proxy->modern_mem_bar_idx,
                 PCI_BASE_ADDRESS_SPACE_MEMORY |
                 PCI_BASE_ADDRESS_MEM_PREFETCH |
                 PCI_BASE_ADDRESS_MEM_TYPE_64,
                 &proxy->modern_bar);
void pci_register_bar(PCIDevice *pci_dev, int region_num,
                      uint8_t type, MemoryRegion *memory)
{
    PCIIORegion *r;
    uint32_t addr; /* offset in pci config space */
    uint64_t wmask;
    pcibus_t size = memory_region_size(memory);
    uint8_t hdr_type;

    assert(!pci_is_vf(pci_dev)); /* VFs must use pcie_sriov_vf_register_bar */
    assert(region_num >= 0);
    assert(region_num < PCI_NUM_REGIONS);
    assert(is_power_of_2(size));

    /* A PCI bridge device (with Type 1 header) may only have at most 2 BARs */
    hdr_type =
        pci_dev->config[PCI_HEADER_TYPE] & ~PCI_HEADER_TYPE_MULTI_FUNCTION;
    assert(hdr_type != PCI_HEADER_TYPE_BRIDGE || region_num < 2);

    r = &pci_dev->io_regions[region_num];
    r->addr = PCI_BAR_UNMAPPED;
    r->size = size;
    r->type = type;
    r->memory = memory;
    r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO
                        ? pci_get_bus(pci_dev)->address_space_io
                        : pci_get_bus(pci_dev)->address_space_mem;

    wmask = ~(size - 1);
    if (region_num == PCI_ROM_SLOT) {
        /* ROM enable bit is writable */
        wmask |= PCI_ROM_ADDRESS_ENABLE;
    }

    addr = pci_bar(pci_dev, region_num);
    pci_set_long(pci_dev->config + addr, type);

    if (!(r->type & PCI_BASE_ADDRESS_SPACE_IO) &&
        r->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {
        pci_set_quad(pci_dev->wmask + addr, wmask);
        pci_set_quad(pci_dev->cmask + addr, ~0ULL);
    } else {
        pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff);
        pci_set_long(pci_dev->cmask + addr, 0xffffffff);
    }
}

 

3) 이어서 msix_init_exclusive_bar 함수를 호출해서 msi 인터럽트에 관련된 데이터를 등록한다

if (proxy->nvectors) {
    int err = msix_init_exclusive_bar(&proxy->pci_dev, proxy->nvectors,
                                      proxy->msix_bar_idx, NULL);

    if (err) {
        /* Notice when a system that supports MSIx can't initialize it */
        if (err != -ENOTSUP) {
            warn_report("unable to init msix vectors to %" PRIu32,
                        proxy->nvectors);
        }
        proxy->nvectors = 0;
    }
}
int msix_init_exclusive_bar(PCIDevice *dev, unsigned short nentries,
                            uint8_t bar_nr, Error **errp)
{
    int ret;
    char *name;
    uint32_t bar_size = 4096;
    uint32_t bar_pba_offset = bar_size / 2;
    uint32_t bar_pba_size = QEMU_ALIGN_UP(nentries, 64) / 8;

    /*
     * Migration compatibility dictates that this remains a 4k
     * BAR with the vector table in the lower half and PBA in
     * the upper half for nentries which is lower or equal to 128.
     * No need to care about using more than 65 entries for legacy
     * machine types who has at most 64 queues.
     */
    if (nentries * PCI_MSIX_ENTRY_SIZE > bar_pba_offset) {
        bar_pba_offset = nentries * PCI_MSIX_ENTRY_SIZE;
    }

    if (bar_pba_offset + bar_pba_size > 4096) {
        bar_size = bar_pba_offset + bar_pba_size;
    }

    bar_size = pow2ceil(bar_size);

    name = g_strdup_printf("%s-msix", dev->name);
    memory_region_init(&dev->msix_exclusive_bar, OBJECT(dev), name, bar_size);

    ret = msix_init(dev, nentries, &dev->msix_exclusive_bar, bar_nr,
                    0, &dev->msix_exclusive_bar,
                    bar_nr, bar_pba_offset,
                    0, errp);
    if (ret) {
        return ret;
    }

    pci_register_bar(dev, bar_nr, PCI_BASE_ADDRESS_SPACE_MEMORY,
                     &dev->msix_exclusive_bar);

    return 0;
}

 

4) virtio_pci_device_plugged 함수는 또한 VirtPCIProxy 장치 PCI 구성 공간의 읽기/쓰기 함수를 각각 virtio_write_config와 virtio_read_config로 설정한다

proxy->pci_dev.config_write = virtio_write_config;
proxy->pci_dev.config_read = virtio_read_config;

virtio_pci_device_plugged 함수는 일련의 함수 호출을 거쳐서, QEMU 측에서 virtio balloon 장치를 준비한다. 다른 장치도 이와 비슷하다.

 

virtio PCI proxy 장치와 virtio 장치의 관련 타입 상속 관계

 

 

virtio 장치 초기화 과정에서 관련되는 함수와 그것이 대응하는 타입

 

 

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

QEMU 코드 분석 #9 블록 디바이스 가상화 — QEMU 백엔드 (virtio-blk 클래스 생성 & 디스크 열기)  (0) 2026.03.09
QEMU 코드 분석 #8 VirtIO 디바이스 초기화— 게스트 커널 드라이버 로딩 & virtqueue 설정  (0) 2026.03.09
QEMU 코드 분석 #6 VirtIO 기초 & 데이터 구조  (0) 2025.02.14
QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트  (0) 2024.11.18
QEMU 코드 분석 #4 Machine & i440fx  (0) 2024.09.19
    'System Programming/Hypervisor' 카테고리의 다른 글
    • QEMU 코드 분석 #9 블록 디바이스 가상화 — QEMU 백엔드 (virtio-blk 클래스 생성 & 디스크 열기)
    • QEMU 코드 분석 #8 VirtIO 디바이스 초기화— 게스트 커널 드라이버 로딩 & virtqueue 설정
    • QEMU 코드 분석 #6 VirtIO 기초 & 데이터 구조
    • QEMU 코드 분석 #5 PCI 디바이스 에뮬레이션 & 인터럽트
    1000sj
    1000sj

    티스토리툴바