virtio先容 (三)--spdk作为virtio后端处理惩罚nvme盘io的流程--上

[复制链接]
发表于 2025-6-21 09:15:17 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
目录
一 简介
二 vhost-blk层
三 bdev层
四 lvol层
五 bdev_nvme层
六 硬件驱动层
七 完备取io调用栈流程


一 简介

         上节先容了virito的基本原理,背面根据实际代码先容virtio的流程。virtio后端代码相对于前端代码更简单,我们先以spdk中的virtio后端代码为例先容下接口的流转过程。
        spdk作为virito后端,通常有三种形态的磁盘:blk,scsi,nvme。 取其中一种方式:前端GuestOS显示blk,后端使用nvme磁盘。先容下io的完备流程,再先容下io流程中主要组件的初始化部分。spdk版本为24.05.x
        spdk后端io流程大要可分为两部分:

  • 将io从ring环中取出,提交给磁盘驱动
  • io从驱动返回之后,将io返还给前端
二 vhost-blk层

        由于前端是blk盘,所以后端对应的通讯协议代码要先处理惩罚从vhost-blk开始。vhost-blk层将io数据从virtio-ring中取出,该逻辑的入口是process_vq,先获取idx,然后根据idx获取到对应的io数据。
process_vq中简化代码如下:
  1. static int process_vq(struct spdk_vhost_blk_session *bvsession, struct spdk_vhost_virtqueue *vq)
  2. {
  3.         struct spdk_vhost_session *vsession = &bvsession->vsession;
  4.         uint16_t reqs[SPDK_VHOST_VQ_MAX_SUBMISSIONS];
  5.         uint16_t reqs_cnt, i;
  6.         reqs_cnt = vhost_vq_avail_ring_get(vq, reqs, SPDK_COUNTOF(reqs)); //一次性从ring环中取出可用idx总数,批量处理
  7.         for (i = 0; i < reqs_cnt; i++) {
  8.                 process_blk_task(vq, reqs[i]);// io处理是以task形式提交,根据idx获取剩余的数量:desc地址等
  9.         }
  10.         return reqs_cnt;
  11. }
复制代码
idx获取到之后,根据idx的信息从ring环中取出io数据,封装到task(任务)中,其中desc中的数据放到iovs中,接下来以请求的方式将task提交。
  1. -->process_blk_task
  2.         |-->blk_iovs_split_queue_setup        取出io数据
  3.         |        |-->vhost_vq_get_desc                取出desc ring环地址
  4.         |        |-->vhost_vring_desc_to_iov       
  5.         |                |-->vhost_vring_desc_payload_to_iov                将desc中的数据放到iov中
  6.         |                        |-->rte_vhost_va_from_guest_pa                将Guest中的物理地址转化为spdk中的虚拟地址
  7.         |-->vhost_user_process_blk_request
  8.                 |-->virtio_blk_process_request        提交封装task,注册请求的回调接口vhost_user_blk_request_finish
复制代码
根据iov中的类型判定当前io时什么操作:读、写、flush、ummap等。然后根据io类型决定使用bdev层中哪种分装。
  1. int virtio_blk_process_request(struct spdk_vhost_dev *vdev, struct spdk_io_channel *ch, struct spdk_vhost_blk_task *task, virtio_blk_request_cb cb, void *cb_arg)
  2. {
  3.         task->cb = cb; // 注册回调vhost_user_blk_request_finish,用户task完成时的逻辑处理
  4.         task->cb_arg = cb_arg;
  5.         iov = &task->iovs[0];
  6.         memcpy(&req, iov->iov_base, sizeof(req));
  7.         iov = &task->iovs[task->iovcnt - 1];
  8.         payload_len = task->payload_size;
  9.         task->status = iov->iov_base;
  10.         payload_len -= sizeof(req) + sizeof(*task->status);
  11.         iovcnt = task->iovcnt - 2;
  12.         type = req.type;
  13.         switch (type) {
  14.         case VIRTIO_BLK_T_IN:
  15.         case VIRTIO_BLK_T_OUT:
  16.                 if (type == VIRTIO_BLK_T_IN) {
  17.                         task->used_len = payload_len + sizeof(*task->status);
  18.                         rc = spdk_bdev_readv(bvdev->bdev_desc, ch, &task->iovs[1], iovcnt, req.sector * 512, payload_len, blk_request_complete_cb, task);
  19.                 } else if (!bvdev->readonly) { // 写io需要保证磁盘不能只读
  20.                         task->used_len = sizeof(*task->status);
  21.                         rc = spdk_bdev_writev(bvdev->bdev_desc, ch, &task->iovs[1], iovcnt, req.sector * 512, payload_len, blk_request_complete_cb, task);
  22.                 } else {
  23.                         rc = -1;
  24.                 }
  25.                 break;
  26.         case VIRTIO_BLK_T_DISCARD:
  27.         case VIRTIO_BLK_T_WRITE_ZEROES:
  28.         case VIRTIO_BLK_T_FLUSH:
  29.         case VIRTIO_BLK_T_GET_ID:
  30.         default:
  31.         }
  32.         return 0;
  33. }
复制代码
三 bdev层

        Bdev是"Block Device"的缩写,即块设备层,位于底层存储介质(如NVMe设备)和上层应用(如存储服务)之间的抽象层。
        bdev层无论哪种封装,最后都走到bdev_io_submit接口,bdev层最终的io提交在bdev_io_do_submit中进行。接口简化如下:
  1. static inline void bdev_io_increment_outstanding(...)
  2. {
  3.         bdev_ch->io_outstanding++;                        //通道提交数量+1
  4.         shared_resource->io_outstanding++;       
  5. }
  6. static inline void bdev_submit_request(...)
  7. {
  8.         bdev->fn_table->submit_request(ioch, bdev_io);        //根据注册到bdev通道的驱动来提交
  9. }
  10.        
  11. static inline void bdev_io_do_submit(...)
  12. {
  13.         bdev_io_increment_outstanding(bdev_ch, shared_resource);
  14.         bdev_io->internal.in_submit_request = true;         // 该通道正在进行提交
  15.         bdev_submit_request(bdev, ch, bdev_io);
  16.         bdev_io->internal.in_submit_request = false; // 该通过已提交完成
  17. }
复制代码
四 lvol层

        lvol 层(Logical Volume Layer)类似传统 LVM 的功能:在物理块设备 (bdev) 或 Blobstore 上创建灵活的、可动态调解大小的逻辑卷,并支持快照和克隆。说人话就是:客户用不了这么大的磁盘,把一块完备的大nvme磁盘变成很多小blk盘。
        一个lvol 逻辑卷本质上就是一个 Blob,所以检察lvol层代码时你会发现,里面lvol开头的接口很少,并且lvol接口实际上都有blob层对应的接口。比如:


  •         lvol_write                 -->        spdk_blob_io_writev_ext
  •         lvol_read                 -->        spdk_blob_io_readv_ext 
  •         lvol_write_zeroes    -->        spdk_blob_io_write_zeroes
  •         lvol_unmap             -->         spdk_blob_io_unmap
注:如果lvol层没有对应的blob接口,那说明spdk不支持这种调用,比如flush。
         回到正题上,通过lvol层之后,最终还会走到bdev_io_do_submit接口,但是这次注册的接口是bdev_nvme_submit_request。 所以这里的逻辑是:nvme磁盘的驱动先通过bdev层注册到lvol层,lvol层再通过bdev层注册到blk层。        
这里列举下写io的在lvol层的调用栈,其他的(读,unmap也类似)
  1. -->vbdev_lvol_submit_request -------------------------------bdev_lvol层--------↓
  2.         -->lvol_write         把bdev_io数据拆开
  3.                 -->spdk_blob_io_writev_ext                ---------------------lib blob层--------↓
  4.                         -->blob_request_submit_rw_iov --> bs_sequence_writev_dev
  5.                                 -->channel->dev->writev --> bdev_blob_writev
  6.                                         -->spdk_bdev_writev_blocks                        ----------bdev层-------↓
  7.                                                 -->bdev_writev_blocks_with_md
  8.                                                         |-->bdev_io_init
  9.                                                         |-->_bdev_io_submit_ext -->bdev_io_submit --> _bdev_io_submit -->bdev_io_do_submit
  10.                                                                 -->bdev->fn_table->submit_request  --> bdev_nvme_submit_request
复制代码
五 bdev_nvme层

        接下来到核心的地方了,spdk实现用户态驱动实际上指的就是这一层,bdev_nvme层将linux内核中关于nvme的驱动又实现了一次。
(这里涉及到存储和pcie相关知识,没办法一下讲清楚,背面单独出一篇先容这个地方,这里只列举下调用栈)。
  1. -->bdev_nvme_submit_request --> _bdev_nvme_submit_request  ---bdev_nvme层------↓
  2.         -->bdev_nvme_writev
  3.                 -->spdk_nvme_ns_cmd_writev_with_md
  4.                         |-->_nvme_ns_cmd_rw 建立一个请求
  5.                         |        -->nvme_allocate_request 从qpair的free链表中找到一个可用的
  6.                         |        -->_nvme_ns_cmd_setup_request 1.从上层下发的参数放到req中 2.拼装nvme的cmd命令,操作码 spdk_nvme_nvm_opcode
  7.                         |-->nvme_qpair_submit_request
  8.                                 -->_nvme_qpair_submit_request
  9.                                         -->nvme_transport_qpair_submit_request
  10.                                                 -->transport->ops.qpair_submit_request --> nvme_pcie_qpair_submit_request
  11.                                                         |-->pqpair->free_tr 中取出一个,放入到pqpair->outstanding_tr链表中
  12.                                                         |-->注册 io完成时的回调 bdev_nvme_writev_done
  13. -->nvme_pcie_qpair_submit_request ------------io提交完成------bdev_pcie层------↓
  14.         -->req 封装到tr中
  15.         -->将tr添加到 pqpair->outstanding_tr 链表中
  16.         -->nvme_pcie_qpair_ring_sq_doorbell                门铃机制,通知硬件有数据要处理了
  17.                 -->spdk_wmb() 内存屏障,避免系统进行内存优化
  18.                 -->pqpair->stat->sq_mmio_doorbell_updates++;
  19.                 -->spdk_mmio_write_4        写入寄存器4个数据,通过mmio通知硬件,要处理数据了
复制代码
六 硬件驱动层

        由于没有硬件代码,简单先容下硬件读/写io的大要流程:
        1. 读取提交队列SQ
            控制器从SQ中取出下令,解析 PRP/SGL 字段,获取数据缓冲区的物理地址
        2.发起DMA操作
            写操作(Host -> Device): 控制器通过 DMA 从主机内存的 PRP/SGL 地址读取数据, 写入SSD
            读操作(Device -> Host):控制器通过 DMA 将SSD数据 写入到主机内存的 PRP/SGL 地址。
        3.写入完成队列 CQ
            操作完成后,控制器将完成状态写入CQ,并通过中断 或者 MSI-X 通知主机(SPDK通常采用轮训模式)
七 完备取io调用栈流程

  1. -->vdev_worker                ---------------------------vhost_blk层----↓        -->process_vq --> process_blk_task                -->vhost_user_process_blk_request                        -->virtio_blk_process_request                                 |-->spdk_bdev_readv                                |-->spdk_bdev_writev                                                |-->spdk_bdev_readv        ------------------------------bdev层------↓|        -->spdk_bdev_readv_blocks        |                -->bdev_readv_blocks_with_md        从通道中取出一个bdev_io,初始化bdev_io, 然后提交|                        |-->bdev_io_init|                        |-->_bdev_io_submit_ext -->bdev_io_submit||-->spdk_bdev_writev|        -->spdk_bdev_writev_blocks|                -->bdev_writev_blocks_with_md|                        |-->bdev_io_init|                        |-->_bdev_io_submit_ext -->bdev_io_submit|                                -->_bdev_io_submit -->bdev_io_do_submit|                                        -->bdev_submit_request|                                                -->bdev->fn_table->submit_request  --> vbdev_lvol_submit_request                                                         -->vbdev_lvol_submit_request -------------------------------bdev_lvol层--------↓        -->lvol_write         把bdev_io数据拆开                -->spdk_blob_io_writev_ext                ---------------------lib blob层--------↓                        -->blob_request_submit_rw_iov --> bs_sequence_writev_dev                                -->channel->dev->writev --> bdev_blob_writev                                        -->spdk_bdev_writev_blocks                        ----------bdev层-------↓                                                -->..... 同上bdev层的调用栈,最终走到 submit_request 回调。                                                        -->bdev->fn_table->submit_request  --> bdev_nvme_submit_request         -->bdev_nvme_submit_request --> _bdev_nvme_submit_request  ---bdev_nvme层------↓
  2.         -->bdev_nvme_writev
  3.                 -->spdk_nvme_ns_cmd_writev_with_md
  4.                         |-->_nvme_ns_cmd_rw 建立一个请求
  5.                         |        -->nvme_allocate_request 从qpair的free链表中找到一个可用的
  6.                         |        -->_nvme_ns_cmd_setup_request 1.从上层下发的参数放到req中 2.拼装nvme的cmd命令,操作码 spdk_nvme_nvm_opcode
  7.                         |-->nvme_qpair_submit_request
  8.                                 -->_nvme_qpair_submit_request
  9.                                         -->nvme_transport_qpair_submit_request
  10.                                                 -->transport->ops.qpair_submit_request --> nvme_pcie_qpair_submit_request
  11.                                                         |-->pqpair->free_tr 中取出一个,放入到pqpair->outstanding_tr链表中
  12.                                                         |-->注册 io完成时的回调 bdev_nvme_writev_done
  13. -->nvme_pcie_qpair_submit_request ------------io提交完成------bdev_pcie层------↓
  14.         -->req 封装到tr中
  15.         -->将tr添加到 pqpair->outstanding_tr 链表中
  16.         -->nvme_pcie_qpair_ring_sq_doorbell                门铃机制,通知硬件有数据要处理了
  17.                 -->spdk_wmb() 内存屏障,避免系统进行内存优化
  18.                 -->pqpair->stat->sq_mmio_doorbell_updates++;
  19.                 -->spdk_mmio_write_4        写入寄存器4个数据,通过mmio通知硬件,要处理数据了
复制代码



回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表