virtio_net 与 virtio-pci 驱动关联浅析

virtio-pci 驱动映射 virtio common_cfg resource 空间

virtio-pci 获取 comon_cfg 物理空间的函数调用如下:

mdev->common = vp_modern_map_capability(mdev, common,
				      sizeof(struct virtio_pci_common_cfg), 4,
				      0, sizeof(struct virtio_pci_common_cfg),
				      NULL, NULL);

p_modern_map_capability 函数通过访问 pci 配置空间来获取 virtio 相关属性信息,然后执行 iomap 映射 virito resource 空间到内核虚拟地址中,核心代码如下:

  pci_read_config_byte(dev, off + offsetof(struct virtio_pci_cap,
						 bar),
			     &bar);
	pci_read_config_dword(dev, off + offsetof(struct virtio_pci_cap, offset),
			     &offset);
	pci_read_config_dword(dev, off + offsetof(struct virtio_pci_cap, length),
			      &length);
  ..............................................
	p = pci_iomap_range(dev, bar, offset, length);
		if (!p)

virtio_net 驱动中调用的 virtio_config_ops 方法

virtio_net 驱动通过调用 virtio_config_ops 中实现的不同函数来读写 virtio 网卡 resource 空间,此结构的一个实例如下:

static const struct virtio_config_ops virtio_pci_config_ops = {
	.get		= vp_get,
	.set		= vp_set,
	.generation	= vp_generation,
	.get_status	= vp_get_status,
	.set_status	= vp_set_status,
	.reset		= vp_reset,
	.find_vqs	= vp_modern_find_vqs,
	.del_vqs	= vp_del_vqs,
	.synchronize_cbs = vp_synchronize_vectors,
	.get_features	= vp_get_features,
	.finalize_features = vp_finalize_features,
	.bus_name	= vp_bus_name,
	.set_vq_affinity = vp_set_vq_affinity,
	.get_vq_affinity = vp_get_vq_affinity,
	.get_shm_region  = vp_get_shm_region,
	.disable_vq_and_reset = vp_modern_disable_vq_and_reset,
	.enable_vq_after_reset = vp_modern_enable_vq_after_reset,
};

以 vp_set_status 为例,它实际是对 vp_modern_set_status 函数的封装,vp_modern_set_status 函数的实现如下:

void vp_modern_set_status(struct virtio_pci_modern_device *mdev,
				 u8 status)
{
	struct virtio_pci_common_cfg __iomem *cfg = mdev->common;

	/*
	 * Per memory-barriers.txt, wmb() is not needed to guarantee
	 * that the cache coherent memory writes have completed
	 * before writing to the MMIO region.
	 */
	vp_iowrite8(status, &cfg->device_status);
}
EXPORT_SYMBOL_GPL(vp_modern_set_status);

直接调用 iowrite 来读写 mdev 中 common 字段执行的虚拟内存来以 MMIO 方式写入网卡配置空间。

virtio_net 驱动奇怪的 pci_id_table

#define VIRTIO_ID_NET			1 /* virtio net */
#define VIRTIO_DEV_ANY_ID	0xffffffff

static struct virtio_device_id id_table[] = {
	{ VIRTIO_ID_NET, VIRTIO_DEV_ANY_ID },
	{ 0 },
};

此处并不是 virtio 的 device_id + vendor_id的形式,而是内部定义的值,这里的 vendor id 被设置为全 F,表明只通过 device id 来 match 驱动。

virtio-pci probe 时 virtio_config_ops 的绑定过程

virtio-pci 驱动的 probe 函数中会调用 legacy 与 modern 两种 virtio pci 的 probe 函数来注册 virtio_config_ops 到 virtio 设备中,相关代码如下:

if (force_legacy) {
		rc = virtio_pci_legacy_probe(vp_dev);
		/* Also try modern mode if we can't map BAR0 (no IO space). */
		if (rc == -ENODEV || rc == -ENOMEM)
			rc = virtio_pci_modern_probe(vp_dev);
		if (rc)
			goto err_probe;
	} else {
		rc = virtio_pci_modern_probe(vp_dev);
		if (rc == -ENODEV)
			rc = virtio_pci_legacy_probe(vp_dev);
		if (rc)
			goto err_probe;
	}

virtio-pci 与 virtio_net 如何协作?

virtio-pci 驱动与常规的 pci 驱动一样,使用 virtio 网卡的 vendor id + device id 匹配设备,匹配到后执行 virtio_pci_probe 函数。此函数核心流程如下:

  1. 创建一个 virtio_pci_device 结构并初始化。

  2. 调用 virtio_pci_modern_probevirtio_pci_legacy_probe 解析 virtio 设备的 capabilities 并初始化相关数据结构,同时也会绑定一个 virtio_config_opsvirtio_device 上。新的 virtio_device 的 vendor_id 与 device_id 也会被设置为 virtio 总线内部使用的数据。

  3. 调用 register_virtio_device 向 virtio 总线注册一个 virtio 设备,此设备由 virtio_pci_device 中的 virtio_device 结构描述。

    register_virtio_device 函数的关键过程如下:

    1. 为当前设备分配一个唯一的 id,并使用此 id 制作 virtioXX 的设备名并填充。
    2. 初始化必要的字段后执行 virtio_reset_device 函数 reset virtio 设备,reset 完成后继续调用 virtio_add_status 通知 qemu 后端。
    3. 调用 device_add 将新的设备添加到总线中,此过程会触发 virtio 总线 match 驱动。
  4. 如果设备是 virtio 网卡设备,virtio_net 驱动成功 match,此驱动完成类似网卡驱动初始化的过程,其中访问 virtio 网卡 resource 依赖 virtio-pci probe 中绑定的 virtio_config_ops 进行。

virtio 总线

virtio bus 与其它总线一样会在 /sys/bus 目录中生成相关内容,包含驱动、设备等属性。其下注册的驱动结构 sys 目录示例如下:

.
├── virtio_balloon
│   ├── bind
│   ├── module -> ../../../../module/virtio_balloon
│   ├── uevent
│   ├── unbind
│   └── virtio1 -> ../../../../devices/pci0000:00/0000:00:07.0/virtio1
├── virtio_console
│   ├── bind
│   ├── module -> ../../../../module/virtio_console
│   ├── uevent
│   ├── unbind
│   └── virtio0 -> ../../../../devices/pci0000:00/0000:00:06.0/virtio0
├── virtio_net
│   ├── bind
│   ├── module -> ../../../../module/virtio_net
│   ├── uevent
│   ├── unbind
│   └── virtio2 -> ../../../../devices/pci0000:00/0000:00:08.0/virtio2
├── virtio_rng
│   ├── bind
│   ├── uevent
│   └── unbind
└── virtio_rproc_serial
    ├── bind
    ├── module -> ../../../../module/virtio_console
    ├── uevent
    └── unbind

virtio 驱动仍旧可以通过写入 bind、unbind 文件来绑定、解绑设备到总线上注册的驱动,只不过设备的标识并非 pci 号,而是 virtioXXX 这种内部的标识,毕竟 virtio bus 是一种独立的总线。

virtio-net 设备驱动绑定示例如下:

[root@openeuler virtio_net]# echo virtio3 > bind
[root@openeuler virtio_net]# ls
bind  module  uevent  unbind  virtio2  virtio3
[root@openeuler virtio_net]# echo virtio3 > unbind 
[root@openeuler virtio_net]# ls 
bind  module  uevent  unbind  virtio2

virtio 设备 sys 目录结构示例如下:

.
├── virtio0 -> ../../../devices/pci0000:00/0000:00:06.0/virtio0
├── virtio1 -> ../../../devices/pci0000:00/0000:00:07.0/virtio1
├── virtio2 -> ../../../devices/pci0000:00/0000:00:08.0/virtio2
└── virtio3 -> ../../../devices/pci0000:00/0000:00:09.0/virtio3

单个设备的内容示例如下:

[root@openeuler virtio3]# cat modalias
virtio:d00000001v00001AF4
[root@openeuler virtio3]# cat ./device 
0x0001
[root@openeuler virtio3]# cat ./vendor 
0x1af4
[root@openeuler virtio3]# cat ./uevent
MODALIAS=virtio:d00000001v00001AF4
[root@openeuler virtio3]# cat ./modalias 
virtio:d00000001v00001AF4
[root@openeuler virtio3]# cat features 
1110010111111111111101010000110010000000000000000000000000000000
[root@openeuler virtio3]# cat status
0x00000001

virtio 总线匹配 virtio 设备的一些特征

在内核 modules.alias 中查询到 virtio 与e1000e 设备的匹配模式如下:

alias pci:v00008086d000010BCsv*sd*bc*sc*i* e1000e
alias pci:v00008086d000010A4sv*sd*bc*sc*i* e1000e
alias pci:v00008086d0000105Fsv*sd*bc*sc*i* e1000e
alias pci:v00008086d0000105Esv*sd*bc*sc*i* e1000e
................................................
alias virtio:d00000005v* virtio_balloon
alias virtio:d00000012v* virtio_input
alias virtio:d00000003v* virtio_console
alias virtio:d00000010v* virtio_gpu
alias virtio:d00000002v* virtio_blk
alias virtio:d00000008v* virtio_scsi
alias virtio:d00000001v* virtio_net
alias virtio:d00000013v* vmw_vsock_virtio_transport

能够看到 virtio 的 alias 中的 v 并没有指定具体的值而是使用 *,表明总线匹配的时候只使用 device_id,而 e1000e 设备的 alias 中严格按照 vendor id + device id 匹配驱动。

对于 virtio 总线而言,virtio device 的 device id 非常重要,它是匹配 virtio 驱动的源数据,此值在 virtio-pci 这一层进行初始化。有如下几条规则:

  1. legacy virtio 设备的 device id 为相应 pci 设备的 subsystem_device 值。
  2. modern virtio 设备的 device id 在对应 pci 设备的 device_id 小于 0x1040 则设置为 pci 设备的 subsystem_device 值,否则为 pci 设备的 device id 减去 0x1040 的值。

virtio bus 匹配设备驱动的代码如下:

static inline int virtio_id_match(const struct virtio_device *dev,
				  const struct virtio_device_id *id)
{
	if (id->device != dev->id.device && id->device != VIRTIO_DEV_ANY_ID)
		return 0;

	return id->vendor == VIRTIO_DEV_ANY_ID || id->vendor == dev->id.vendor;
}

如上文所述,virtio-net 当 device 相等时,virtio 上层驱动生命vendor 是否相等都会返回 true,符合上面的判断。

总结

使用 virtio 虚拟网卡时,virtio pci 设备会绑定到 virtio-pci 驱动上,virtio-pci 驱动负责构建新的 virtio 设备并挂入到总线中并 match 驱动,此时如果加载了 virtio_et 驱动并支持新创建的 virtio 设备,就会执行 virtio-net 驱动的 probe 完成网络设备的初始化。

virtio-pci 驱动可以看做是一个 virtio bus 底层的驱动,它对接 pci 总线,并创建新的 virtio 设备注入到 virtio 总线中,根据设备的类型 match 不同的 virtio 上层驱动以提供某一类服务。

static void vhost_user_rdma_device_realize(DeviceState *dev, Error **errp) { VirtIODevice *vdev = VIRTIO_DEVICE(dev); VHostUserRdma *r = VHOST_USER_RDMA(vdev); Error *err = NULL; int i, ret; if (!r->chardev.chr) { error_setg(errp, "vhost-user-rdma: chardev is mandatory"); return; } r->num_queues = VHOST_USER_RDMA_NUM_QUEUES; if (r->num_queues > VIRTIO_QUEUE_MAX) { error_setg(errp, "vhost-user-rdma: invalid number of IO queues"); return; } if (!vhost_user_init(&r->vhost_user, &r->chardev, errp)) { return; } virtio_init(vdev, VIRTIO_ID_RDMA, sizeof(struct virtio_rdma_config)); r->virtqs = g_new(VirtQueue *, r->num_queues); for (i = 0; i < r->num_queues; i++) { r->virtqs[i] = virtio_add_queue(vdev, VHOST_USER_RDMA_QUEUE_SIZE, vhost_user_rdma_handle_output); } r->vhost_vqs = g_new0(struct vhost_virtqueue, r->num_queues); r->connected = false; qemu_chr_fe_set_handlers(&r->chardev, NULL, NULL, vhost_user_rdma_event, NULL, (void *)dev, NULL, true); reconnect: if (qemu_chr_fe_wait_connected(&r->chardev, &err) < 0) { error_report_err(err); goto virtio_err; } /* check whether vhost_user_rdma_connect() failed or not */ if (!r->connected) { goto reconnect; } ret = vhost_dev_get_config(&r->dev, (uint8_t *)&r->rdmacfg, sizeof(struct virtio_rdma_config), &err); if (ret < 0) { error_report("vhost-user-rdma: get rdma config failed"); goto reconnect; } return; virtio_err: g_free(r->vhost_vqs); r->vhost_vqs = NULL; for (i = 0; i < r->num_queues; i++) { virtio_delete_queue(r->virtqs[i]); } g_free(r->virtqs); virtio_cleanup(vdev); vhost_user_cleanup(&r->vhost_user); }
03-25
<think>好的,我现在要分析用户提供的这个函数`static void vhost_user_rdma_device_realize(DeviceState *dev, Error **errp)`。这个函数看起来是QEMU中实现vhost-user RDMA设备初始化的代码。首先,我需要理解这个函数的结构各个部分的作用,然后逐步解释给用户。 首先,函数参数是`DeviceState *dev``Error **errp`,说明这是一个设备实现的realize函数,用于设备的实例化。接下来,函数内部将`dev`转换为`VirtIODevice``VHostUserRdma`结构体指针,这符合QEMU设备模型的常见做法,即通过类型转换获取特定设备的实例。 然后,检查`r->chardev.chr`是否存在。如果没有设置chardev,就报错返回。这是因为vhost-user设备通常需要通过字符设备(比如Unix socket)后端服务通信,所以chardev是必需的。这一步是参数校验,确保必要的配置已经提供。 接下来设置`r->num_queues`为预定义的`VHOST_USER_RDMA_NUM_QUEUES`,然后检查队列数量是否超过VIRTIO的最大限制。如果超过,报错返回。这里确保队列数量在合法范围内。 接下来调用`vhost_user_init`初始化vhost-user连接,如果失败则返回。这一步可能是建立后端服务的基本连接或参数设置。 然后调用`virtio_init`初始化VirtIO设备,传入设备类型`VIRTIO_ID_RDMA`配置结构体的大小。这里注册设备为VirtIO类型,并分配配置空间。 之后,为虚拟队列分配内存,使用`g_new`分配指针数组。接着循环为每个队列调用`virtio_add_queue`,指定队列大小处理函数`vhost_user_rdma_handle_output`。这说明每个队列都有一个对应的处理函数来处理IO事件。 然后分配`vhost_vqs`结构体数组,初始化为0,并设置`connected`标志为false。接下来设置字符设备的事件处理函数`vhost_user_rdma_event`,用于处理连接事件等。 尝试连接到字符设备,使用`qemu_chr_fe_wait_connected`,如果失败则跳转到错误处理部分。如果连接成功但`r->connected`仍未true(可能在事件回调中设置),则重连。这里可能存在重连机制,确保连接建立。 之后,调用`vhost_dev_get_config`获取设备的配置信息,如果失败则重连。成功则返回,否则进入错误处理。 错误处理部分`virtio_err`会释放之前分配的资源,包括虚拟队列、virtio设备清理以及vhost-user的清理。这说明在初始化过程中任何失败都会回滚已分配的资源,防止内存泄漏。 需要确认几个问题:`vhost_user_rdma_connect`可能在事件回调中被调用,设置`connected`标志。重连的逻辑是否合理?如果后端服务未准备好,可能会一直重试,但代码中使用goto reconnect可能会造成循环,是否有退出条件?比如,用户提供的代码中,如果连接失败,会报告错误并跳转到virtio_err,但在某些情况下可能进入无限重连,需要看`qemu_chr_fe_wait_connected``r->connected`的设置方式。比如,可能事件处理函数在连接成功后设置`connected`为true,否则可能需要超时或其他机制,否则可能死循环。 另外,`vhost_user_init``vhost_user_cleanup`的配对使用是否正确,确保资源释放。函数中在错误处理时调用了`vhost_user_cleanup`,是正确的。 总结,这个函数主要负责初始化vhost-user RDMA设备,包括参数检查、virtio设备初始化、队列创建、连接后端,以及错误处理。需要向用户解释每个步骤的作用,可能的错误点,以及整个流程如何确保设备正确初始化资源释放。</think>以下是对`vhost_user_rdma_device_realize`函数的逐层解析: --- ### **功能概述** 这是QEMU中实现vhost-user RDMA设备初始化的核心函数,负责将虚拟RDMA设备后端服务(如DPDK)通过vhost-user协议建立连接。主要流程包括**参数校验、队列初始化、vhost-user通信建立错误回滚处理**。 --- ### **逐层代码解析** #### **1. 参数校验** ```c if (!r->chardev.chr) { error_setg(errp, "vhost-user-rdma: chardev is mandatory"); return; } ``` - **作用**: 检查字符设备是否存在。vhost-user必须通过字符设备(如Unix socket)后端通信。 - **失败处理**: 若无字符设备,直接报错返回。 --- #### **2. 队列数量检查** ```c r->num_queues = VHOST_USER_RDMA_NUM_QUEUES; if (r->num_queues > VIRTIO_QUEUE_MAX) { error_setg(errp, "vhost-user-rdma: invalid number of IO queues"); return; } ``` - **作用**: 确保队列数不超过VIRTIO协议定义的最大值$VIRTIO\_QUEUE\_MAX$。 - **意义**: 防止因配置错误导致内存越界或性能问题。 --- #### **3. vhost-user初始化** ```c if (!vhost_user_init(&r->vhost_user, &r->chardev, errp)) { return; } ``` - **作用**: 初始化vhost-user通信上下文,建立后端的控制通道。 - **底层行为**: 可能涉及socket连接建立、协议版本协商等。 --- #### **4. VirtIO设备初始化** ```c virtio_init(vdev, VIRTIO_ID_RDMA, sizeof(struct virtio_rdma_config)); ``` - **参数解析**: - `VIRTIO_ID_RDMA`: 设备类型标识符,表示这是一个RDMA设备。 - `sizeof(struct virtio_rdma_config)`: 分配设备配置空间,用于存储后端提供的参数。 --- #### **5. 虚拟队列创建** ```c r->virtqs = g_new(VirtQueue *, r->num_queues); for (i = 0; i < r->num_queues; i++) { r->virtqs[i] = virtio_add_queue(vdev, VHOST_USER_RDMA_QUEUE_SIZE, vhost_user_rdma_handle_output); } ``` - **关键点**: - `g_new`动态分配队列指针数组。 - `virtio_add_queue`为每个队列分配内存,并绑定处理函数`vhost_user_rdma_handle_output`(负责IO事件响应)。 --- #### **6. 连接后端服务** ```c qemu_chr_fe_set_handlers(&r->chardev, NULL, NULL, vhost_user_rdma_event, NULL, (void *)dev, NULL, true); reconnect: if (qemu_chr_fe_wait_connected(&r->chardev, &err) < 0) { error_report_err(err); goto virtio_err; } if (!r->connected) goto reconnect; ``` - **重连机制**: - 若首次连接失败(如后端未启动),通过`goto reconnect`尝试重连。 - `r->connected`需在事件回调函数`vhost_user_rdma_event`中设置为`true`以退出循环。 --- #### **7. 获取设备配置** ```c ret = vhost_dev_get_config(&r->dev, (uint8_t *)&r->rdmacfg, sizeof(struct virtio_rdma_config), &err); if (ret < 0) goto reconnect; ``` - **作用**: 从后端服务获取RDMA设备的具体配置(如最大队列大小、特性位等)。 - **失败处理**: 若获取失败,可能因连接不稳定,尝试重新连接。 --- #### **8. 错误处理** ```c virtio_err: g_free(r->vhost_vqs); for (i = 0; i < r->num_queues; i++) { virtio_delete_queue(r->virtqs[i]); } g_free(r->virtqs); virtio_cleanup(vdev); vhost_user_cleanup(&r->vhost_user); ``` - **资源释放**: - 释放队列内存、VirtIO设备资源、vhost-user上下文。 - 确保在初始化失败时无内存泄漏。 --- ### **潜在问题改进** 1. **无限重连风险** 代码中`goto reconnect`可能导致死循环。需在事件回调中设置超时或重试次数限制。 2. **线程安全性** `vhost_user_rdma_event`需正确处理多线程环境下的状态同步(如`r->connected`的原子修改)。 --- ### **总结** 此函数是vhost-user RDMA设备初始化的核心链路,涵盖从基础参数检查到复杂重连逻辑的实现。其设计体现了QEMU设备模型的典型模式:**分层初始化 + 逆向资源释放**。理解它对调试RDMA设备在虚拟化环境中的连接问题至关重要。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值