vhost in qemu

本文深入解析vhost架构,包括vhost的用户态程序接口、memorywritelog机制、vhost_dev初始化与清理过程、vhost_virtqueue初始化细节、guest_notifier与host_notifier的工作原理以及vhost_net的初始化流程。
vhost的用户态程序接口定义在/usr/include/linux/vhost.h
vhost目前只支持tap network backend


vhost.h/vhost.c
------------------------------
vhost的memory write log机制
vhost使用一个bitmap记录对Guest物理内存的改变,每VHOST_LOG_PAGE(4K)字节大小的内存使用
一个bit来记录,0表示没有改变过,1表示改变过。
由于vhost_dev.log是以vhost_log_chunk_t (unsigned long)为单位的,因此一个元素包含的位数为
VHOST_LOG_BITS (8 * sizeof(vhost_log_chunk_t)),其所能记录的内存大小为
VHOST_LOG_CHUNK (VHOST_LOG_PAGE * VHOST_LOG_BITS)

因此,对于一个大小为n的内存区域,其log所需的元素数目是 n / VHOST_LOG_CHUNK + 1 

vhost_get_log_size函数用于计算所需要的log size大小。其实在实现上,可以先计算出最大的地址,
然后直接return (max_addr / VHOST_LOG_CHUNK + 1),这样就不用在中间每次都使用除法了。
------------------------------

public interface:
       vhost_dev_init 初始化vhost设备
       vhost_dev_cleanup    清除vhost设备
       vhost_dev_query       
       vhost_dev_start       启动vhost设备
       vhost_dev_stop         停止vhost设备
   
vhost_virtqueue_init函数(被vhost_dev_start调用):
1. ioctl  VHOST_SET_VRING_NUM 设置vring size
2. ioctl VHOST_SET_VRING_BASE 设置   (VirtQueue.last_avail_idx)
3. 设置vhost_virtqueue中ring相关的成员(desc, avail,  used_size,  used_phys,  used, 
        ring_size,  ring_phys, ring)
4. 调用vhost_virtqueue_set_addr设置相关地址
5. 调用ioctl  VHOST_SET_VRING_KICK 设置kick fd (guest -> vhost) (VirtQueue.host_notifier.fd)
6. 调用ioctl  VHOST_SET_VRING_CALL 设置call fd (vhost -> guest) (VirtQueue.guest_notifier.fd)

guest_notifier的初始化:
1. 在vhost_dev_start起始处,会调用vdev->binding->set_guest_notifiers(vdev->binding_opaque, true)
     对于virtio pci设备来说,该函数就是virtio_pci_set_guest_notifiers(virtio-pci.c)
2.  virtio_pci_set_guest_notifiers对每个可能的vq调用virtio_pci_set_guest_notifier
3.  virtio_pci_set_guest_notifier先通过event_notifier_init初始化一个eventfd,然后调用
     qemu_set_fd_handler将该eventfd添加到qemu的selectable fd列表中,并指定其read poll处理
     函数为virtio_pci_guest_notifier_read
4. 调用msix_set_mask_notifier,其中msix_mask_notifier_func设置为virtio_pci_mask_notifier
5. 对该设备的所有MSI-X入口(0 -  msix_entries_nr),调用msix_set_mask_notifier_for_vector
6. 如果该vector没有被屏蔽,则调用dev->msix_mask_notifier(就是virtio_pci_mask_notifier)
7. 对当前的所有VirtQueue,调用virtio_pci_mask_vq
8. 调用kvm_set_irqfd,设置该VirtQueue的irqfd为guest notifier fd
9. 如果设置成功,则调用qemu_set_fd_handler(event_notifier_get_fd(notifier), NULL, NULL, NULL)
     取消掉对该guest notifier fd的select。

guest_notifier的使用:
1. vhost在处理完请求,将buffer放到used ring上面之后,往call fd里面写入
2. 如果成功设置了irqfd,则kvm会直接中断guest。如果没有成功设置,则走以下的路径:
3. qemu通过select调用侦测到该事件(因为vhost的call fd就是qemu里面对应vq的guest_notifier,它
     已经被加入到selectable fd列表)
4. 调用virtio_pci_guest_notifier_read通知guest
5. guest从used ring上获取相关的数据

host_notifier的初始化:
1. 在vhost_virtqueue_init中,会调用
     vdev->binding->set_host_notifiers(vdev->binding_opaque, idx, true)
     对于virtio pci设备来说,该函数就是virtio_pci_set_host_notifier(virtio-pci.c)
2.  virtio_pci_set_host_notifier调用virtio_pci_set_host_notifier_internal
3.  virtio_pci_set_host_notifier_internal先通过event_notifier_init初始化一个eventfd,再调用
     kvm_set_ioeventfd_pio_word
4.  kvm_set_ioeventfd_pio_word通过调用kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &kick)来设置kick
     fd,KVM_IOEVENTFD的文档参见  http://permalink.gmane.org/gmane.comp.emulators.kvm.devel/73076


在vhost_dev_init中,调用cpu_register_phys_memory_client注册了物理内存的客户端,三个hook函数
分别设置为vhost_client_set_memory,  vhost_client_sync_dirty_bitmap,  vhost_client_migration_log

CPUPhysMemoryClient结构用于在Guest物理内存变化时得到通知:
typedef struct CPUPhysMemoryClient CPUPhysMemoryClient;
struct CPUPhysMemoryClient {
      void (*set_memory)(struct CPUPhysMemoryClient *client,
                                   target_phys_addr_t start_addr,
                                   ram_addr_t size,
                                   ram_addr_t phys_offset);
      int (*sync_dirty_bitmap)(struct CPUPhysMemoryClient *client,
                                            target_phys_addr_t start_addr,
                                            target_phys_addr_t end_addr);
      int (*migration_log)(struct CPUPhysMemoryClient *client,
                                      int enable);
      QLIST_ENTRY(CPUPhysMemoryClient) list;
};
set_memory在添加Guest物理内存区域(Region)的时候被调用
sync_dirty_bitmap 同步指定内存区间的dirty bitmap(sync back)
migration_log 设置启用/禁用migration log


vhost_client_set_memory函数:
1. 调用qemu_realloc使vhost_memory的区域数目增加1
2. 调用vhost_dev_unassign_memory确保当前内存区域不和已存在的其他区域有交集
3. 如果是RAM,那么调用vhost_dev_assign_memory添加该内存区域
4. 如果vhost还未启动,则直接返回
5. 调用vhost_verify_ring_mappings,检查新添加的内存区域和所有的VirtQueue区域都没有交集
6. 如果禁用了memory write log,那么调用ioctl  VHOST_SET_MEM_TABLE设置内存区域之后返回
7. 如果log size变大,那么先调用vhost_dev_log_resize设置log size,再调用
     ioctl  VHOST_SET_MEM_TABLE设置内存区域
8. 如果log size变小,那么先调用ioctl  VHOST_SET_MEM_TABLE 设置内存区域,再调用
     vhost_dev_log_resize设置log size


vhost_client_sync_dirty_bitmap函数(同步对应于指定内存区间的dirty bitmap):
1. 如果禁用了memory write log或者vhost没有启动,则直接返回
2. 对所有vhost_dev上面的内存区域,调用vhost_dev_sync_region
3. 对所有vhost_dev上面的vhost_virtqueue的used ring所对应的内存区域,调用vhost_dev_sync_region

-----------------------------------------------------------------------------------------------
vhost_net.h/vhost_net.c

每个虚拟网卡都对应一个vhost_net,每个vhost_net都包含一个vhost_dev
代码很简单,基本是封装了vhost_dev的相关操作,并加入一些对tap的控制,对feature的控制等。

vhost_net的启用是在命令行的-netdev tap,... 中指定vhost=on选项,其初始化流程如下:
1. 根据<<qemu network backend的初始化>>一文,在其最后会调用net_client_types[i].init函数
     对于tap,该函数是net_init_tap
2. 在其中会检查选项中是否指定vhost=on,如果指定,则会调用vhost_net_init函数进行初始化,
     并将返回的vhost_net赋值给TAPState.vhost_net成员
3. 在Guest初始化设备并改变设备状态时,会调用到virtio-net.c中的virtio_net_set_status函数,
     如果初始化设备成功(status &  VIRTIO_CONFIG_S_DRIVER_OK != 0),则会调用vhost_net_start
     启动vhost_net。



参考文章:
http://blog.vmsplice.net/2011/09/qemu-internals-vhost-architecture.html
/* * Vhost Vdpa Device PCI Bindings * * Copyright (c) Huawei Technologies Co., Ltd. 2022. All Rights Reserved. * * Authors: * Longpeng <longpeng2@huawei.com> * * Largely based on the "vhost-user-blk-pci.c" and "vhost-user-blk.c" * implemented by: * Changpeng Liu <changpeng.liu@intel.com> * * This work is licensed under the terms of the GNU LGPL, version 2 or later. * See the COPYING.LIB file in the top-level directory. */ #include "qemu/osdep.h" #include <sys/ioctl.h> #include <linux/vhost.h> #include "hw/virtio/virtio.h" #include "hw/virtio/vdpa-dev.h" #include "hw/pci/pci.h" #include "hw/qdev-properties.h" #include "qapi/error.h" #include "qemu/error-report.h" #include "qemu/module.h" #include "hw/virtio/virtio-pci.h" #include "qom/object.h" typedef struct VhostVdpaDevicePCI VhostVdpaDevicePCI; #define TYPE_VHOST_VDPA_DEVICE_PCI "vhost-vdpa-device-pci-base" DECLARE_INSTANCE_CHECKER(VhostVdpaDevicePCI, VHOST_VDPA_DEVICE_PCI, TYPE_VHOST_VDPA_DEVICE_PCI) struct VhostVdpaDevicePCI { VirtIOPCIProxy parent_obj; VhostVdpaDevice vdev; }; static void vhost_vdpa_device_pci_instance_init(Object *obj) { VhostVdpaDevicePCI *dev = VHOST_VDPA_DEVICE_PCI(obj); virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev), TYPE_VHOST_VDPA_DEVICE); object_property_add_alias(obj, "bootindex", OBJECT(&dev->vdev), "bootindex"); } static Property vhost_vdpa_device_pci_properties[] = { DEFINE_PROP_END_OF_LIST(), }; static int vhost_vdpa_device_pci_post_init(VhostVdpaDevice *v, Error **errp) { VhostVdpaDevicePCI *dev = container_of(v, VhostVdpaDevicePCI, vdev); VirtIOPCIProxy *vpci_dev = &dev->parent_obj; vpci_dev->class_code = virtio_pci_get_class_id(v->vdev_id); vpci_dev->trans_devid = virtio_pci_get_trans_devid(v->vdev_id); /* one for config vector */ vpci_dev->nvectors = v->num_queues + 1; return 0; } static void vhost_vdpa_device_pci_realize(VirtIOPCIProxy *vpci_dev, Error **errp) { VhostVdpaDevicePCI *dev = VHOST_VDPA_DEVICE_PCI(vpci_dev); dev->vdev.post_init = vhost_vdpa_device_pci_post_init; qdev_realize(DEVICE(&dev->vdev), BUS(&vpci_dev->bus), errp); } static void vhost_vdpa_device_pci_class_init(ObjectClass *klass, void *data) { DeviceClass *dc = DEVICE_CLASS(klass); VirtioPCIClass *k = VIRTIO_PCI_CLASS(klass); set_bit(DEVICE_CATEGORY_MISC, dc->categories); device_class_set_props(dc, vhost_vdpa_device_pci_properties); k->realize = vhost_vdpa_device_pci_realize; } static const VirtioPCIDeviceTypeInfo vhost_vdpa_device_pci_info = { .base_name = TYPE_VHOST_VDPA_DEVICE_PCI, .generic_name = "vhost-vdpa-device-pci", .transitional_name = "vhost-vdpa-device-pci-transitional", .non_transitional_name = "vhost-vdpa-device-pci-non-transitional", .instance_size = sizeof(VhostVdpaDevicePCI), .instance_init = vhost_vdpa_device_pci_instance_init, .class_init = vhost_vdpa_device_pci_class_init, }; static void vhost_vdpa_device_pci_register(void) { virtio_pci_types_register(&vhost_vdpa_device_pci_info); } type_init(vhost_vdpa_device_pci_register); 解读一下代码
11-21
<think> 这段代码是QEMU中实现vhost-vdpa设备的PCI绑定。vhost-vdpa是一种利用内核vdpa框架加速virtio设备数据路径的技术。该文件将vhost-vdpa设备封装成PCI设备,使得虚拟机可以通过标准的PCI接口使用该设备。 代码结构解读: 1. 包含必要的头文件,包括virtio、pci、qdev等。 2. 定义设备类型和结构体: - 定义了一个结构体VhostVdpaDevicePCI,它包含一个VirtIOPCIProxy(virtio PCI代理)和一个VhostVdpaDevice(实际的vhost-vdpa设备)。 - 使用DECLARE_INSTANCE_CHECKER进行类型检查。 3. 实例初始化函数(vhost_vdpa_device_pci_instance_init): - 调用virtio_instance_init_common初始化virtio设备,并将VhostVdpaDevice作为子对象。 - 将PCI设备的"bootindex"属性别名指向子设备的同名属性。 4. 设备属性(vhost_vdpa_device_pci_properties): - 目前没有定义特定属性,使用DEFINE_PROP_END_OF_LIST()结束。 5. 后初始化函数(vhost_vdpa_device_pci_post_init): - 该函数在vhost_vdpa_device_realize中被调用(在之前的代码分析中,我们看到realize函数会调用post_init)。 - 从VhostVdpaDevicePCI结构中获取父设备(VirtIOPCIProxy)。 - 设置PCI设备的类代码(class_code)和传输设备ID(trans_devid),这些值根据virtio设备ID(v->vdev_id)确定。 - 设置中断向量数(nvectors)为队列数+1(额外的1用于配置变更通知)。 6. PCI设备的realize函数(vhost_vdpa_device_pci_realize): - 设置vdev的post_init回调为上述的vhost_vdpa_device_pci_post_init。 - 调用qdev_realize实现子设备(即VhostVdpaDevice)的初始化。 7. 类初始化函数(vhost_vdpa_device_pci_class_init): - 设置设备类别为MISC(杂项设备)。 - 设置设备属性(目前为空列表)。 - 设置realize回调为vhost_vdpa_device_pci_realize。 8. 定义设备类型信息(vhost_vdpa_device_pci_info): - 包括基础名称、通用名称、过渡设备名称(用于兼容旧系统)、非过渡设备名称。 - 指定实例大小、实例初始化函数和类初始化函数。 9. 注册函数(vhost_vdpa_device_pci_register): - 调用virtio_pci_types_register注册该PCI设备类型。 - 使用type_init宏,使得该注册函数在QEMU启动时被调用。 整个流程: - 当QEMU启动时,调用type_init注册vhost-vdpa-device-pci设备。 - 当用户通过命令行添加该设备时,QEMU会创建该设备的实例,调用instance_init进行初始化(创建子设备并设置属性别名)。 - 然后,在设备realize阶段,会调用vhost_vdpa_device_pci_realize,该函数设置post_init回调并实现子设备(VhostVdpaDevice)的realize。 - 在子设备realize过程中,会调用之前设置的post_init回调(vhost_vdpa_device_pci_post_init)来设置PCI相关的参数(类代码、设备ID、中断向量数)。 注意:这个PCI设备是一个代理,它把主要工作委托给内部的VhostVdpaDevice设备(在vdpa-dev.c中实现)。 相关代码解释: - virtio_pci_get_class_id: 根据virtio设备ID获取PCI类代码。 - virtio_pci_get_trans_devid: 根据virtio设备ID获取传输设备ID(用于过渡设备或非过渡设备)。 - qdev_realize: 实现(realize)一个设备,即调用该设备的realize函数,并将其挂载到指定的总线上。 这样,一个vhost-vdpa设备就以PCI设备的形式暴露给了虚拟机。 </think>以下是 `vhost-vdpa-device-pci.c` 代码的详细解读,我将从代码结构、关键函数和设计模式三个方面进行解析: ### 1. 代码结构与类型定义 ```c typedef struct VhostVdpaDevicePCI VhostVdpaDevicePCI; #define TYPE_VHOST_VDPA_DEVICE_PCI "vhost-vdpa-device-pci-base" DECLARE_INSTANCE_CHECKER(VhostVdpaDevicePCI, VHOST_VDPA_DEVICE_PCI, TYPE_VHOST_VDPA_DEVICE_PCI) struct VhostVdpaDevicePCI { VirtIOPCIProxy parent_obj; // PCI代理层 VhostVdpaDevice vdev; // 核心vDPA设备 }; ``` - **关键点**: - 使用 QOM (QEMU Object Model) 实现设备模型 - `VirtIOPCIProxy`:处理 PCI 配置空间、中断路由等 PCI 层功能 - `VhostVdpaDevice`:实现实际的 vDPA 设备逻辑(在另一个文件中定义) - `DECLARE_INSTANCE_CHECKER`:实现类型安全的类型转换宏 ### 2. 核心函数解析 #### 2.1 实例初始化 (`instance_init`) ```c static void vhost_vdpa_device_pci_instance_init(Object *obj) { VhostVdpaDevicePCI *dev = VHOST_VDPA_DEVICE_PCI(obj); // 初始化核心vDPA设备 virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev), TYPE_VHOST_VDPA_DEVICE); // 创建属性别名 object_property_add_alias(obj, "bootindex", OBJECT(&dev->vdev), "bootindex"); } ``` - **功能**: - 创建并初始化核心设备 `VhostVdpaDevice` - 建立 PCI 层属性与核心设备属性的映射 - **关键调用**: - `virtio_instance_init_common()`:标准 virtio 设备初始化模板 - `object_property_add_alias()`:实现属性代理(PCI层透传配置到核心层) #### 2.2 PCI 后初始化 (`post_init`) ```c static int vhost_vdpa_device_pci_post_init(VhostVdpaDevice *v, Error **errp) { VhostVdpaDevicePCI *dev = container_of(v, VhostVdpaDevicePCI, vdev); VirtIOPCIProxy *vpci_dev = &dev->parent_obj; // 设置PCI类代码(基于virtio设备ID) vpci_dev->class_code = virtio_pci_get_class_id(v->vdev_id); // 设置设备ID(支持传统/现代模式) vpci_dev->trans_devid = virtio_pci_get_trans_devid(v->vdev_id); // 计算中断向量数(队列数+配置向量) vpci_dev->nvectors = v->num_queues + 1; return 0; } ``` - **关键参数**: - `class_code`:PCI 配置空间的类代码(如 0x0100 表示 SCSI 设备) - `trans_devid`:设备 ID(区分传统/现代 virtio 设备) - `nvectors`:MSI-X 中断向量数量(队列数+1) #### 2.3 PCI 设备实现 (`realize`) ```c static void vhost_vdpa_device_pci_realize(VirtIOPCIProxy *vpci_dev, Error **errp) { VhostVdpaDevicePCI *dev = VHOST_VDPA_DEVICE_PCI(vpci_dev); // 设置后初始化回调 dev->vdev.post_init = vhost_vdpa_device_pci_post_init; // 实现核心设备 qdev_realize(DEVICE(&dev->vdev), BUS(&vpci_dev->bus), errp); } ``` - **执行流程**: 1. 注册 `post_init` 回调函数 2. 调用 `qdev_realize()` 实例化核心设备 3. 核心设备初始化过程中会调用 `post_init` #### 2.4 类初始化与注册 ```c static void vhost_vdpa_device_pci_class_init(ObjectClass *klass, void *data) { DeviceClass *dc = DEVICE_CLASS(klass); VirtioPCIClass *k = VIRTIO_PCI_CLASS(klass); set_bit(DEVICE_CATEGORY_MISC, dc->categories); device_class_set_props(dc, vhost_vdpa_device_pci_properties); k->realize = vhost_vdpa_device_pci_realize; } static const VirtioPCIDeviceTypeInfo vhost_vdpa_device_pci_info = { .base_name = TYPE_VHOST_VDPA_DEVICE_PCI, .generic_name = "vhost-vdpa-device-pci", .transitional_name = "vhost-vdpa-device-pci-transitional", .non_transitional_name = "vhost-vdpa-device-pci-non-transitional", .instance_size = sizeof(VhostVdpaDevicePCI), .instance_init = vhost_vdpa_device_pci_instance_init, .class_init = vhost_vdpa_device_pci_class_init, }; static void vhost_vdpa_device_pci_register(void) { virtio_pci_types_register(&vhost_vdpa_device_pci_info); } type_init(vhost_vdpa_device_pci_register); ``` - **关键设计**: - **三种设备类型**: - `generic`:通用设备(自动选择模式) - `transitional`:兼容传统 virtio 0.9.5 设备 - `non-transitional`:纯 virtio 1.0+ 设备 - `type_init`:QEMU 模块初始化入口 ### 3. 设计模式解析 #### 3.1 组合模式 (Composition Pattern) ```c struct VhostVdpaDevicePCI { VirtIOPCIProxy parent_obj; // PCI层 VhostVdpaDevice vdev; // 核心设备层 }; ``` - **目的**:分离 PCI 逻辑与核心设备逻辑 - **优势**: - 核心设备可复用(如支持其他总线类型) - PCI 层可处理通用 PCI 配置 #### 3.2 回调模式 (Callback Pattern) ```c // 设置回调 dev->vdev.post_init = vhost_vdpa_device_pci_post_init; // 在核心设备中调用 if (v->post_init && v->post_init(v, errp) < 0) { goto vhost_cleanup; } ``` - **目的**:允许不同总线类型定制初始化逻辑 - **流程**: 1. PCI 层设置回调函数 2. 核心设备在初始化时调用该回调 3. PCI 层实现设备特定的配置 #### 3.3 代理模式 (Proxy Pattern) ```c object_property_add_alias(obj, "bootindex", OBJECT(&dev->vdev), "bootindex"); ``` - **目的**:PCI 设备作为核心设备的前端代理 - **实现**: - 属性访问转发到核心设备 - 例如 `bootindex` 属性用于控制设备启动顺序 ### 4. 中断向量计算 ```c vpci_dev->nvectors = v->num_queues + 1; ``` - **分配策略**: - 每个 virtqueue 分配一个中断向量 - 额外一个向量用于配置空间变更通知 - 例如:4 队列设备需要 5 个中断向量 ### 5. 设备注册流程 ```mermaid graph LR A[type_init 注册] --> B[vhost_vdpa_device_pci_register] B --> C[virtio_pci_types_register] C --> D[QOM 类型系统] D --> E[用户创建设备] E --> F[instance_init] F --> G[设置核心设备] G --> H[realize 阶段] H --> I[调用 post_init] ``` ### 6. 关键数据结构关系 ```mermaid classDiagram class VirtIOPCIProxy { +uint16_t class_code +uint16_t trans_devid +uint32_t nvectors +realize() } class VhostVdpaDevice { +uint32_t vdev_id +uint16_t num_queues +post_init() } class VhostVdpaDevicePCI { -VirtIOPCIProxy parent_obj -VhostVdpaDevice vdev } VhostVdpaDevicePCI --|> VirtIOPCIProxy VhostVdpaDevicePCI o-- VhostVdpaDevice ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值