目录
[一 简介](#一 简介)
[二 vhost-blk层](#二 vhost-blk层)
[三 bdev层](#三 bdev层)
[四 lvol层](#四 lvol层)
[五 bdev_nvme层](#五 bdev_nvme层)
[六 硬件驱动层](#六 硬件驱动层)
[七 完整取io调用栈流程](#七 完整取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中简化代码如下:
cpp
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提交。
cpp
-->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层中哪种分装。
cpp
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中进行。接口简化如下:
cpp
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也类似)
cpp
-->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相关知识,没办法一下讲清楚,后面单独出一篇介绍这个地方,这里只列举下调用栈)。
cpp
-->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调用栈流程
cpp
-->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通知硬件,要处理数据了