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针对不同操作系统处理虚拟地址空间:
qemu_madvise-》 *_madvise。
*_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后访问对应界面而发生异常。

浙公网安备 33010602011771号