马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
目录
一 简介
二 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中简化代码如下:
- static int process_vq(struct spdk_vhost_blk_session *bvsession, struct spdk_vhost_virtqueue *vq)
- {
- struct spdk_vhost_session *vsession = &bvsession->vsession;
- uint16_t reqs[SPDK_VHOST_VQ_MAX_SUBMISSIONS];
- uint16_t reqs_cnt, i;
- reqs_cnt = vhost_vq_avail_ring_get(vq, reqs, SPDK_COUNTOF(reqs)); //一次性从ring环中取出可用idx总数,批量处理
- for (i = 0; i < reqs_cnt; i++) {
- process_blk_task(vq, reqs[i]);// io处理是以task形式提交,根据idx获取剩余的数量:desc地址等
- }
- return reqs_cnt;
- }
复制代码 idx获取到之后,根据idx的信息从ring环中取出io数据,封装到task(任务)中,其中desc中的数据放到iovs中,接下来以请求的方式将task提交。
- -->process_blk_task
- |-->blk_iovs_split_queue_setup 取出io数据
- | |-->vhost_vq_get_desc 取出desc ring环地址
- | |-->vhost_vring_desc_to_iov
- | |-->vhost_vring_desc_payload_to_iov 将desc中的数据放到iov中
- | |-->rte_vhost_va_from_guest_pa 将Guest中的物理地址转化为spdk中的虚拟地址
- |-->vhost_user_process_blk_request
- |-->virtio_blk_process_request 提交封装task,注册请求的回调接口vhost_user_blk_request_finish
复制代码 根据iov中的类型判定当前io时什么操作:读、写、flush、ummap等。然后根据io类型决定使用bdev层中哪种分装。
- 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)
- {
- task->cb = cb; // 注册回调vhost_user_blk_request_finish,用户task完成时的逻辑处理
- task->cb_arg = cb_arg;
- iov = &task->iovs[0];
- memcpy(&req, iov->iov_base, sizeof(req));
- iov = &task->iovs[task->iovcnt - 1];
- payload_len = task->payload_size;
- task->status = iov->iov_base;
- payload_len -= sizeof(req) + sizeof(*task->status);
- iovcnt = task->iovcnt - 2;
- type = req.type;
- switch (type) {
- case VIRTIO_BLK_T_IN:
- case VIRTIO_BLK_T_OUT:
- if (type == VIRTIO_BLK_T_IN) {
- task->used_len = payload_len + sizeof(*task->status);
- rc = spdk_bdev_readv(bvdev->bdev_desc, ch, &task->iovs[1], iovcnt, req.sector * 512, payload_len, blk_request_complete_cb, task);
- } else if (!bvdev->readonly) { // 写io需要保证磁盘不能只读
- task->used_len = sizeof(*task->status);
- rc = spdk_bdev_writev(bvdev->bdev_desc, ch, &task->iovs[1], iovcnt, req.sector * 512, payload_len, blk_request_complete_cb, task);
- } else {
- rc = -1;
- }
- break;
- case VIRTIO_BLK_T_DISCARD:
- case VIRTIO_BLK_T_WRITE_ZEROES:
- case VIRTIO_BLK_T_FLUSH:
- case VIRTIO_BLK_T_GET_ID:
- default:
- }
- return 0;
- }
复制代码 三 bdev层
Bdev是"Block Device"的缩写,即块设备层,位于底层存储介质(如NVMe设备)和上层应用(如存储服务)之间的抽象层。
bdev层无论哪种封装,最后都走到bdev_io_submit接口,bdev层最终的io提交在bdev_io_do_submit中进行。接口简化如下:
- static inline void bdev_io_increment_outstanding(...)
- {
- bdev_ch->io_outstanding++; //通道提交数量+1
- shared_resource->io_outstanding++;
- }
- static inline void bdev_submit_request(...)
- {
- bdev->fn_table->submit_request(ioch, bdev_io); //根据注册到bdev通道的驱动来提交
- }
-
- static inline void bdev_io_do_submit(...)
- {
- bdev_io_increment_outstanding(bdev_ch, shared_resource);
- bdev_io->internal.in_submit_request = true; // 该通道正在进行提交
- bdev_submit_request(bdev, ch, bdev_io);
- bdev_io->internal.in_submit_request = false; // 该通过已提交完成
- }
复制代码 四 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也类似)
- -->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_writev_blocks_with_md
- |-->bdev_io_init
- |-->_bdev_io_submit_ext -->bdev_io_submit --> _bdev_io_submit -->bdev_io_do_submit
- -->bdev->fn_table->submit_request --> bdev_nvme_submit_request
复制代码 五 bdev_nvme层
接下来到核心的地方了,spdk实现用户态驱动实际上指的就是这一层,bdev_nvme层将linux内核中关于nvme的驱动又实现了一次。
(这里涉及到存储和pcie相关知识,没办法一下讲清楚,背面单独出一篇先容这个地方,这里只列举下调用栈)。
- -->bdev_nvme_submit_request --> _bdev_nvme_submit_request ---bdev_nvme层------↓
- -->bdev_nvme_writev
- -->spdk_nvme_ns_cmd_writev_with_md
- |-->_nvme_ns_cmd_rw 建立一个请求
- | -->nvme_allocate_request 从qpair的free链表中找到一个可用的
- | -->_nvme_ns_cmd_setup_request 1.从上层下发的参数放到req中 2.拼装nvme的cmd命令,操作码 spdk_nvme_nvm_opcode
- |-->nvme_qpair_submit_request
- -->_nvme_qpair_submit_request
- -->nvme_transport_qpair_submit_request
- -->transport->ops.qpair_submit_request --> nvme_pcie_qpair_submit_request
- |-->pqpair->free_tr 中取出一个,放入到pqpair->outstanding_tr链表中
- |-->注册 io完成时的回调 bdev_nvme_writev_done
- -->nvme_pcie_qpair_submit_request ------------io提交完成------bdev_pcie层------↓
- -->req 封装到tr中
- -->将tr添加到 pqpair->outstanding_tr 链表中
- -->nvme_pcie_qpair_ring_sq_doorbell 门铃机制,通知硬件有数据要处理了
- -->spdk_wmb() 内存屏障,避免系统进行内存优化
- -->pqpair->stat->sq_mmio_doorbell_updates++;
- -->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调用栈流程
- -->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层------↓
- -->bdev_nvme_writev
- -->spdk_nvme_ns_cmd_writev_with_md
- |-->_nvme_ns_cmd_rw 建立一个请求
- | -->nvme_allocate_request 从qpair的free链表中找到一个可用的
- | -->_nvme_ns_cmd_setup_request 1.从上层下发的参数放到req中 2.拼装nvme的cmd命令,操作码 spdk_nvme_nvm_opcode
- |-->nvme_qpair_submit_request
- -->_nvme_qpair_submit_request
- -->nvme_transport_qpair_submit_request
- -->transport->ops.qpair_submit_request --> nvme_pcie_qpair_submit_request
- |-->pqpair->free_tr 中取出一个,放入到pqpair->outstanding_tr链表中
- |-->注册 io完成时的回调 bdev_nvme_writev_done
- -->nvme_pcie_qpair_submit_request ------------io提交完成------bdev_pcie层------↓
- -->req 封装到tr中
- -->将tr添加到 pqpair->outstanding_tr 链表中
- -->nvme_pcie_qpair_ring_sq_doorbell 门铃机制,通知硬件有数据要处理了
- -->spdk_wmb() 内存屏障,避免系统进行内存优化
- -->pqpair->stat->sq_mmio_doorbell_updates++;
- -->spdk_mmio_write_4 写入寄存器4个数据,通过mmio通知硬件,要处理数据了
复制代码
|