virtio分析(二) —— virtio-balloon qemu裝置建立
阿新 • • 發佈:2022-01-25
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; }