QEMU源码全解析 —— virtio(27)

123 篇文章 36 订阅 ¥49.90 ¥99.00
本文继续探讨QEMU virtio设备的实现,重点解析`setup_vq`函数剩余步骤,涉及virtqueue的创建过程。通过vring_create_virtqueue函数,区分了支持VIRTIO_F_RING_PACKED特性的packed ring和不支持时的split ring。packed ring优化了内存访问,减少cache冲突,而split ring将descriptor、available和used ring分开。文章还介绍了两种ring的工作原理及其在virtqueue创建中的应用。
摘要由CSDN通过智能技术生成

展开

接前一篇文章:

上一回解析了setup_vq函数的前3步,本回继续解析余下的步骤。为了便于理解和加深印象,再次贴出setup_vq函数的源码,在 Linux 内核源码/drivers/ virtio /virtio_pci_modern.c中,代码如下:

  1. static struct virtqueue *setup_vq(struct virtio_pci_device *vp_dev,
  2. struct virtio_pci_vq_info *info,
  3. unsigned int index,
  4. void (*callback)(struct virtqueue *vq),
  5. const char *name,
  6. bool ctx,
  7. u16 msix_vec)
  8. {
  9. struct virtio_pci_modern_device *mdev = &vp_dev->mdev;
  10. bool (*notify)(struct virtqueue *vq);
  11. struct virtqueue *vq;
  12. u16 num;
  13. int err;
  14. if (__virtio_test_bit(&vp_dev->vdev, VIRTIO_F_NOTIFICATION_DATA))
  15. notify = vp_notify_with_data;
  16. else
  17. notify = vp_notify;
  18. if (index >= vp_modern_get_num_queues(mdev))
  19. return ERR_PTR(-EINVAL);
  20. /* Check if queue is either not available or already active. */
  21. num = vp_modern_get_queue_size(mdev, index);
  22. if (!num || vp_modern_get_queue_enable(mdev, index))
  23. return ERR_PTR(-ENOENT);
  24. info->msix_vector = msix_vec;
  25. /* create the vring */
  26. vq = vring_create_virtqueue(index, num,
  27. SMP_CACHE_BYTES, &vp_dev->vdev,
  28. true, true, ctx,
  29. notify, callback, name);
  30. if (!vq)
  31. return ERR_PTR(-ENOMEM);
  32. vq->num_max = num;
  33. err = vp_active_vq(vq, msix_vec);
  34. if (err)
  35. goto err;
  36. vq->priv = (void __force *)vp_modern_map_vq_notify(mdev, index, NULL);
  37. if (!vq->priv) {
  38. err = -ENOMEM;
  39. goto err;
  40. }
  41. return vq;
  42. err:
  43. vring_del_virtqueue(vq);
  44. return ERR_PTR(err);
  45. }

再来回顾一下前3步。

(1) setup_vq函数 首先检测virtio PCI设备是否具有VIRTIO_F_NOTIFICATION_DATA特性 。代码片段如下:

  1. if (__virtio_test_bit(&vp_dev->vdev, VIRTIO_F_NOTIFICATION_DATA))
  2. notify = vp_notify_with_data;
  3. else
  4. notify = vp_notify;

(2) 接下来, 调用vp_modern_get_num_queues函数获取virtqueues的长度(个数) 。代码片段如下:

  1. if (index >= vp_modern_get_num_queues(mdev))
  2. return ERR_PTR(-EINVAL);

(3) 接下来, 调用vp_modern_get_queue_size函数获得一个virtqueue的大小 。代码片段如下:

  1. /* Check if queue is either not available or already active. */
  2. num = vp_modern_get_queue_size(mdev, index);
  3. if (!num || vp_modern_get_queue_enable(mdev, index))
  4. return ERR_PTR(-ENOENT);

(4)接下来, 调用vring_create_virtqueue函数实际生成virtqueue 。vring_create_virtqueue函数在 Linux内核源码 /drivers/virtio/virtio_ring.c中,代码如下:

  1. struct virtqueue *vring_create_virtqueue(
  2. unsigned int index,
  3. unsigned int num,
  4. unsigned int vring_align,
  5. struct virtio_device *vdev,
  6. bool weak_barriers,
  7. bool may_reduce_num,
  8. bool context,
  9. bool (*notify)(struct virtqueue *),
  10. void (*callback)(struct virtqueue *),
  11. const char *name)
  12. {
  13. if (virtio_has_feature(vdev, VIRTIO_F_RING_PACKED))
  14. return vring_create_virtqueue_packed(index, num, vring_align,
  15. vdev, weak_barriers, may_reduce_num,
  16. context, notify, callback, name, vdev->dev.parent);
  17. return vring_create_virtqueue_split(index, num, vring_align,
  18. vdev, weak_barriers, may_reduce_num,
  19. context, notify, callback, name, vdev->dev.parent);
  20. }
  21. EXPORT_SYMBOL_GPL(vring_create_virtqueue);

vring_create_virtqueue函数根据virtio device是否具有VIRTIO_F_RING_PACKED这一feature,进行了分支处理。

virtio_has_feature函数前文书已经讲解过了,详情参见 QEMU源码全解析 —— virtio(23)

VIRTIO_F_RING_PACKED宏的定义在Linux内核源码/include/uapi/linux/virtio_config.h中,如下:

  1. /* This feature indicates support for the packed virtqueue layout. */
  2. #define VIRTIO_F_RING_PACKED 34

vring_create_virtqueue函数根据后端设备是否支持VIRTIO_F_RING_PACKED这个feature,进行了分支处理。支持此特性,则调用vring_create_virtqueue_packed函数;否则调用vring_create_virtqueue_split函数。一个一个来看。

  • vring_create_virtqueue_packed函数

vring_create_virtqueue_packed函数也在Linux内核源码/drivers/virtio/virtio_ring.c中,代码如下:

  1. static struct virtqueue *vring_create_virtqueue_packed(
  2. unsigned int index,
  3. unsigned int num,
  4. unsigned int vring_align,
  5. struct virtio_device *vdev,
  6. bool weak_barriers,
  7. bool may_reduce_num,
  8. bool context,
  9. bool (*notify)(struct virtqueue *),
  10. void (*callback)(struct virtqueue *),
  11. const char *name,
  12. struct device *dma_dev)
  13. {
  14. struct vring_virtqueue_packed vring_packed = {};
  15. struct vring_virtqueue *vq;
  16. int err;
  17. if (vring_alloc_queue_packed(&vring_packed, vdev, num, dma_dev))
  18. goto err_ring;
  19. vq = kmalloc(sizeof(*vq), GFP_KERNEL);
  20. if (!vq)
  21. goto err_vq;
  22. vq->vq.callback = callback;
  23. vq->vq.vdev = vdev;
  24. vq->vq.name = name;
  25. vq->vq.index = index;
  26. vq->vq.reset = false;
  27. vq->we_own_ring = true;
  28. vq->notify = notify;
  29. vq->weak_barriers = weak_barriers;
  30. #ifdef CONFIG_VIRTIO_HARDEN_NOTIFICATION
  31. vq->broken = true;
  32. #else
  33. vq->broken = false;
  34. #endif
  35. vq->packed_ring = true;
  36. vq->dma_dev = dma_dev;
  37. vq->use_dma_api = vring_use_dma_api(vdev);
  38. vq->premapped = false;
  39. vq->do_unmap = vq->use_dma_api;
  40. vq->indirect = virtio_has_feature(vdev, VIRTIO_RING_F_INDIRECT_DESC) &&
  41. !context;
  42. vq->event = virtio_has_feature(vdev, VIRTIO_RING_F_EVENT_IDX);
  43. if (virtio_has_feature(vdev, VIRTIO_F_ORDER_PLATFORM))
  44. vq->weak_barriers = false;
  45. err = vring_alloc_state_extra_packed(&vring_packed);
  46. if (err)
  47. goto err_state_extra;
  48. virtqueue_vring_init_packed(&vring_packed, !!callback);
  49. virtqueue_init(vq, num);
  50. virtqueue_vring_attach_packed(vq, &vring_packed);
  51. spin_lock(&vdev->vqs_list_lock);
  52. list_add_tail(&vq->vq.list, &vdev->vqs);
  53. spin_unlock(&vdev->vqs_list_lock);
  54. return &vq->vq;
  55. err_state_extra:
  56. kfree(vq);
  57. err_vq:
  58. vring_free_packed(&vring_packed, vdev, dma_dev);
  59. err_ring:
  60. return NULL;
  61. }

vring_create_virtqueue_packed函数用于在Linux内核中创建一个基于packed ring的virtqueue,并返回一个指向该virtqueue的指针。

部分参数说明如下:

  • index:表示virtqueue的索引号。
  • num:表示virtqueue的数量。
  • vdev:指向virtio_device结构体的指针,表示与virtqueue相关联的设备。
  • notify:一个函数指针(回调),用于在数据传输完成后通知宿主机。
  • callback:一个函数指针(回调),用于在接收到宿主机通知时执行相应的操作。
  • name:指定virtqueue的名称。

返回值:

函数执行成功,返回一个指向vring_virtqueue结构体的指针,该结构体包含了管理packed ring的相关信息,如描述符表、可用环和已用环等。

  • vring_create_virtqueue_split函数

vring_create_virtqueue_split函数也在Linux内核源码/drivers/virtio/virtio_ring.c中,代码如下:

  1. static struct virtqueue *vring_create_virtqueue_split(
  2. unsigned int index,
  3. unsigned int num,
  4. unsigned int vring_align,
  5. struct virtio_device *vdev,
  6. bool weak_barriers,
  7. bool may_reduce_num,
  8. bool context,
  9. bool (*notify)(struct virtqueue *),
  10. void (*callback)(struct virtqueue *),
  11. const char *name,
  12. struct device *dma_dev)
  13. {
  14. struct vring_virtqueue_split vring_split = {};
  15. struct virtqueue *vq;
  16. int err;
  17. err = vring_alloc_queue_split(&vring_split, vdev, num, vring_align,
  18. may_reduce_num, dma_dev);
  19. if (err)
  20. return NULL;
  21. vq = __vring_new_virtqueue(index, &vring_split, vdev, weak_barriers,
  22. context, notify, callback, name, dma_dev);
  23. if (!vq) {
  24. vring_free_split(&vring_split, vdev, dma_dev);
  25. return NULL;
  26. }
  27. to_vvq(vq)->we_own_ring = true;
  28. return vq;
  29. }

vring_create_virtqueue_split函数用于在Linux内核中创建一个基于 split ring的virtqueue,并返回一个指向该virtqueue的指针。

该函数的主要参数包括:

  • index:表示virtqueue的索引号。
  • num:表示virtqueue的数量。
  • vdev:指向virtio_device结构体的指针,表示与virtqueue相关联的设备。
  • notify:一个函数指针(回调),用于在数据传输完成后通知宿主机。
  • callback:一个函数指针(回调),用于在接收到宿主机通知时执行相应的操作。
  • name:指定virtqueue的名称。

返回值:

函数执行成功,返回一个指向virtqueue结构体的指针。

这里要进行一下知识补强:split ring和packed ring。

参考以下文章: 异步模式下的Vhost Packed Ring设计介绍 - 知乎

  • split ring

split ring把 descriptor环形缓冲区(descriptor ring) 可用环形缓冲区(available ring) 已用环形缓冲区(used ring) 分别 使用3个数据结构来表示 ,这也是它被叫做split ring的原因。

前端作为生产者将新生产可给后端使用的descriptor在available ring中更新,然后由后端从available ring中读取到自己可用的descriptor进行使用,使用完以后将用完的descriptor写到used ring中,供前端处理,循环使用这些内存。 这里所谓的使用,可以从别的地方把数据拷贝到这些descriptor所描述的内存区域中,也可以是从这些descriptor所描述的内存区域中把数据拷贝到别的地方

  • packed ring

packed ring是Virtio Spec 1.1提出了一种新的ring结构,前后端通信的逻辑与split ring基本相似,不过ring结构发生了些改变, 将split ring中原本分离的ring结构整合在一起全部用一个ring表示 。虽然在操作上确实复杂了一些,但是在前后端收发包操作时packed ring模式下只需访问一个ring结构, 相比于split ring每次需要操作3个ring而言,packed ring减少了所需访问的内存空间,对cache来说更加友好

下图是packed ring的ring结构(这个ring是前后端都可见的):

  • 通过descriptor中的标记位判断这个descriptor的归属权在前端还是在后端。
  • 后端使用两个指针来追踪生产和消费的进度,分别为last_avail_idx和last_used_idx。
  • 前端作为生产者初始化这些descriptor,后端作为消费者不断的取用这些descriptor,并在使用结束后将使用完的descriptor写回这个ring中,前端观察到这个descriptor被写回则更新这个descriptor,使得后端在下次可以继续使用。
  • 值得一提的是,前端按照ring的顺序操作这些descriptor,而后端则是按照完成顺序操作这些descriptor,且在in-order模式下descriptor的可用性一定是连续的,既若X号descriptor不可用,则默认X+1号descriptor同样不可用。这样一套前后端的协作机制构成了packed ring的基本信息传递方式。

当前位于virtqueue创建流程的以下阶段(红色矩形框中)(图片引自 https://note.youdao.com/ynoteshare/index.html?id=f247acce8c21eb4ca4a7e37403f065f9&type=note&_time=1633000040495 ):

更多详情请看下回。

举报

选择你想要举报的内容(必选)
  • 内容涉黄
  • 政治相关
  • 内容抄袭
  • 涉嫌广告
  • 内容侵权
  • 侮辱谩骂
  • 样式问题
  • 其他