1. 程式人生 > 其它 >virtio分析(二) —— virtio-balloon qemu裝置建立

virtio分析(二) —— virtio-balloon qemu裝置建立

1.概述

根據前一章資訊,virtio裝置分為前端裝置/通訊層/後端裝置,本章從後端裝置裝置(qemu的balloon裝置為例)的初始化開始分析。

從啟動到balloon裝置開始初始化基本呼叫流程如下:

balloon程式碼執行流程如下:

2. 關鍵結構

2.1 balloon裝置結構

typedef struct VirtIOBalloon {
    VirtIODevice parent_obj;
    VirtQueue *ivq, *dvq, *svq;  // 3個 virt queue
    // pages we want guest to give up 
   	uint32_t num_pages; 
    // pages in balloon
    uint32_t actual;
    uint64_t stats[VIRTIO_BALLOON_S_NR];  // status 
    
    // status virtqueue 會用到
    VirtQueueElement *stats_vq_elem;
    size_t stats_vq_offset;
    
    // 定時器, 定時查詢功能
    QEMUTimer *stats_timer;
    int64_t stats_last_update;
    int64_t stats_poll_interval;
    
    // features
    uint32_t host_features;
    // for adjustmem, reserved guest free memory
    uint64_t res_size;
} VirtIOBalloon;

分析:

  • num_pages欄位是balloon中表示我們希望guest歸還給host的記憶體大小
  • actual欄位表示balloon實際捕獲的pages數目

guest處理configuration change中斷,完成之後呼叫virtio_cwrite函式。因為寫balloon裝置的配置空間,所以陷出,
qemu收到後會找到balloon裝置,修改config修改config時,更新balloon->actual欄位

  • stats_last_update在每次從status virtioqueue中取出資料時更新

2.2 訊息通訊結構VirtQueue

struct VirtQueue
{
    VRing vring;

    /* Next head to pop */
    uint16_t last_avail_idx;

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

    uint16_t used_idx;

    /* 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;
    VirtIOHandleAIOOutput handle_aio_output;
    VirtIODevice *vdev;
    EventNotifier guest_notifier;
    EventNotifier host_notifier;
    QLIST_ENTRY(VirtQueue) node;
};

3. 初始化流程

3.1 裝置型別註冊

type_init(virtio_register_types)
	type_register_static(&virtio_balloon_info);
		->instance_init = virtio_balloon_instance_init,
		->class_init = virtio_balloon_class_init,

3.2 類及例項初始化​

qemu_opts_foreach(qemu_find_opts("device"), device_init_func, NULL, NULL)	//vl.c
  qdev_device_add								//qdev-monitor.c
    object_new()				
       ->class_init
       ->instance_init
    object_property_set_bool(realized)  --> virtio_balloon_device_realize	//virtio-balloon.c
       ->virtio_init
       ->virtio_add_queue

3.3 balloon裝置例項化

virtio_balloon_device_realize例項化函式主要執行兩個函式完成例項化操作,首先呼叫virtio_init初始化virtio裝置的公共部分。 virtio_init的 主要工作是初始化所有virtio裝置的基類TYPE_VIRTIO_DEVICE("virtio-device")的例項VirtIODevice結構體。 例項化程式碼簡化實現如下:
static void virtio_balloon_device_realize(DeviceState *dev, Error **errp)
{
    virtio_init(vdev, "virtio-balloon", VIRTIO_ID_BALLOON,
                sizeof(struct virtio_balloon_config));

    ret = qemu_add_balloon_handler(virtio_balloon_to_target,
                                   virtio_balloon_stat,
                                   virtio_balloon_adjustmem,
                                   virtio_balloon_get_stats, s);

...

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

    reset_stats(s);
}

virio_init的程式碼流程和基本成員註釋如下:

void virtio_init(VirtIODevice *vdev, const char *name,
                 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) {
        //vector_queues與 MSI中斷相關
        vdev->vector_queues =
            g_malloc0(sizeof(*vdev->vector_queues) * nvectors);
    }

    vdev->device_id = device_id;
    vdev->status = 0;
    atomic_set(&vdev->isr, 0);  //中斷請求
    vdev->queue_sel = 0;    //配置佇列的時候選擇佇列
    //config_vector與MSI中斷相關
    vdev->config_vector = VIRTIO_NO_VECTOR;
    //vq分配了1024個virtQueue並進行初始化
    vdev->vq = g_malloc0(sizeof(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->name = name;
    //config_len表示配置空間的長度
    vdev->config_len = config_size;
    if (vdev->config_len) {
        //config表示配置資料的存放區域
        vdev->config = g_malloc0(config_size);
    } else {
        vdev->config = NULL;
    }
    vdev->vmstate = qemu_add_vm_change_state_handler(virtio_vmstate_change,
                                                     vdev);
    vdev->device_endian = virtio_default_endian();
    //use_guest_notifier_mask與irqfd有關
    vdev->use_guest_notifier_mask = true;
}

virtio_init主要操作為:

1. 設定中斷 2. 申請virtqueue空間 3. 申請配置資料空間 初始化操作完成後,realize函式繼續呼叫virtio_add_queue建立了3個virtqueue(ivq、dvq、svq)並將回撥函式virtio_balloon_handle_output掛接到virtqueue的handle_output,用於處理virtqueue中的資料,handle_output函式處理在訊息通訊一節再分析。 virtio_add_queue實現如下
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].handle_aio_output = NULL;

    return &vdev->vq[i];
}

4. balloon處理

4.1 回撥函式處理流程 上一章分析到realize函式註冊了3個virtqueue的回撥函式,先分析inflate和deflate(ivq和dvq)涉及的函式,查詢狀態資訊的函式稍後分析。ivq和dvq註冊的handle_output為virtio_balloon_handle_output,當gust側通過virtqueue進行通知的時候會呼叫handle_out對資料進行處理。
static void virtio_balloon_handle_output(VirtIODevice *vdev, VirtQueue *vq)
{
    VirtIOBalloon *s = VIRTIO_BALLOON(vdev);
    VirtQueueElement *elem;
    MemoryRegionSection section;

    for (;;) {
        size_t offset = 0;
        uint32_t pfn;
        //獲取virtqueue中的資料到qemu側virt-ring通用的資料結構
        //handle_out函式通用操作
        elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
        if (!elem) {
            if (hax_enabled() && vq == s->dvq) {
                hax_issue_invept();
            }
            return;
        }

        while (iov_to_buf(elem->out_sg, elem->out_num, offset, &pfn, 4) == 4) {
            ram_addr_t pa;
            ram_addr_t addr;
            int p = virtio_ldl_p(vdev, &pfn);
            //將頁框轉換成GPA
            pa = (ram_addr_t) p << VIRTIO_BALLOON_PFN_SHIFT;
            offset += 4;

            //根據pa找到對應的MemoryRegionSection
            section = memory_region_find(get_system_memory(), pa, 1);
            if (!int128_nz(section.size) ||
                !memory_region_is_ram(section.mr) ||
                memory_region_is_rom(section.mr) ||
                memory_region_is_romd(section.mr)) {
                trace_virtio_balloon_bad_addr(pa);
                memory_region_unref(section.mr);
                continue;
            }

            trace_virtio_balloon_handle_output(memory_region_name(section.mr),
                                               pa);
            /* Using memory_region_get_ram_ptr is bending the rules a bit, but
               should be OK because we only want a single page.  */
            addr = section.offset_within_region;
            //根據section獲取對應的HVA,然後呼叫balloon函式處理對應頁面
            balloon_page(memory_region_get_ram_ptr(section.mr) + addr, pa,
                         !!(vq == s->dvq));
            memory_region_unref(section.mr);
        }

        //處理完後通知gust,此處為handle_out通用操作
        virtqueue_push(vq, elem, offset);
        virtio_notify(vdev, vq);
        g_free(elem);
    }
}

handle_output函式使用virtqueue_pop取出virtqueue中對應的資料到VirtQueueElement結構體中,在經過地址轉換後得到了HVA地址,然後將HVA和佇列資訊(dvq/ivq?)傳入balloon_page進行qemu側的balloon處理。

4.2 qemu處理佇列分類
balloon_page根據deflate引數判斷此次操作時inflate還是deflate,分如下操作: 1. 如果使deflate操作,直接返回。因為deflate操作表示gust會再次使用對應的頁面地址,主要是gust內部取消掉這部分頁面不可用的標誌,QEMU側因為提供給gust的虛擬地址空間一直是保留狀態所以無需特殊處理 2. 如果使inflate操作,表示對應的頁面將不會再提供給gust使用,所以此時先取消對應的ept對映再對QEMU側的HVA地址使用qemu_madvise進行處理。 具體程式碼如下:
static void balloon_page(void *addr, ram_addr_t gpa, int deflate)
{
    if (!qemu_balloon_is_inhibited() && (!kvm_enabled() ||
                                         kvm_has_sync_mmu())) {
#ifdef _WIN32
        if (!hax_enabled() || !hax_ept_set_supported()) {
            return;
        }
        // For deflation, ept entry can be rebuilt via VMX EPT VIOLATION.
        if (deflate || hax_invalid_ept_entries(gpa, BALLOON_PAGE_SIZE)) {
            return;
        }
#endif

        qemu_madvise(addr, BALLOON_PAGE_SIZE,
                deflate ? QEMU_MADV_WILLNEED : QEMU_MADV_DONTNEED);
    }
}
4.3 qemu處理虛擬記憶體 balloon_page對操作型別分類後,呼叫qemu_madvise針對不同作業系統處理虛擬地址空間,windows上流程如下: qemu_madvise-》win32_madvise。 win32_madvise處理兩種情況willneed和dontneed,分別表示deflate和inflate過程,上一步已經說明過deflate過程主要在GUST側取消頁面不可用標記,這裡目前只處理dontneed過程。 因為windows中虛擬地址申請函式VirtualAlloc可以有提交(commit)和保留(reserve)操作,只有commit的頁面才可以在訪問時申請物理空間。 在系統初始化時(參考這裡的pc.ram的初始化流程),qemu_anon_ram_alloc函式使用VirtualAlloc(MEM_COMMIT | MEM_RESERVE)為pc.ram保留並提交了4G空間(可配置,不一定是4G)。所以GUST訪問的空間都是已經提交過並且保留下來不會被其他malloc之類的函式佔用的,因此這4G是連續的。 當gust執行inflate操作後,放入balloon中的頁面也不會再被訪問,在上一步中取消EPT對映後需要在free掉對應的虛擬地址以釋放記憶體,但是為了保證pc.ram的記憶體連續並且隨時可用,所以free後再次virtualAlloc(MEM_COMMIT),保持頁面是提交狀態,避免gust進行deflate後訪問對應介面而發生異常。對應程式碼如下
int win32_madvise(void *addr, size_t len, int advice)
{
    const size_t page_size = qemu_real_host_page_size;
    LPVOID start = (LPVOID)QEMU_ALIGN_PTR_DOWN(addr, page_size);
    len = ROUND_UP(len, page_size);

    switch (advice) {
    case QEMU_MADV_WILLNEED:
        return 0;
    case QEMU_MADV_DONTNEED: {
        /*
         * We have not chosen DiscardVirtualMemory() due to its low performance.
         * Besides, we dont have to call VirtualUnlock here, because free will call unlock internally.
         */
        if (!VirtualFree(start, len, MEM_DECOMMIT)) {
            fprintf(stderr, "%s failed to decommmit memory: %lu\n",
                    __func__, GetLastError());
            break;
        }
        if (!VirtualAlloc(start, len, MEM_COMMIT, PAGE_READWRITE)) {
            fprintf(stderr, "%s failed to re-commit memory: %lu\n",
                    __func__, GetLastError());
            break;
        }

        return 0;
    }
    }

    return -EINVAL;
}