在去年的一篇 文章 里,我们实现了 vhost-user-blk 关于 resize volume 的支持。同时,也简单介绍了 virtio-scsi 如何感知到 resize 事件的。本文继续那篇文章遗留的问题,给 SPDK 加上 vhost-user-scsi 的 resize 支持。
跟 vhost-user-blk 不同,因为 SCSI 有丰富的命令支持,获取大小可以通过 SCSI 命令来实现。
在 Linux kernel 的 virtio-scsi 实现中,通过 SPDK_SBC_SAI_READ_CAPACITY_16
命令来查询后端设备的大小。
我们先来看一下流程图:
对于 vhost-user-blk,由于不具有这个能力,所以不得不实现新的 vhost-user 协议命令(GET_CONFIG
),且大小信息需要放到 PCI 配置空间里,才能传递到 guest os。
我们从 virtio-scsi 协议说起。
virtio-scsi
virtio 协议 详情见这里,可以找到 virtio-scsi 的章节。
virtio-scsi 分为前后端实现。前端 guest 驱动在 linux kernel 里面,即 drivers/scsi/virtio_scsi.c
。
SCSI Host Device
virtio-scsi 定义了一个 SCSI host device。virtio SCSI host device 把一个或者多个 lun 聚合在一起,允许通过 SCSI transport protocol 来通信。
一个设备实例代表一个 SCSI host
,上面有多个 target 和 lun attach 在上面。
virtio SCSI 设备服务两种 requests:
- 对 lun 的命令请求;
-
Task Management Functions(TMF)
。
Device ID
分配给 virtio-scsi 的 device id 是 8。
通过 lspci 可以查看。
00:0a.0 SCSI storage controller: Red Hat, Inc. Virtio SCSI
Subsystem: Red Hat, Inc. Device 0008
Physical Slot: 10
Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR+ FastB2B- DisINTx+
Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
Latency: 0
Interrupt: pin A routed to IRQ 10
Region 0: I/O ports at d080 [size=64]
Region 1: Memory at fea5a000 (32-bit, non-prefetchable) [size=4K]
Region 4: Memory at fe210000 (64-bit, prefetchable) [size=16K]
Capabilities: [98] MSI-X: Enable+ Count=4 Masked-
Vector table: BAR=1 offset=00000000
PBA: BAR=1 offset=00000800
Capabilities: [84] Vendor Specific Information: VirtIO: <unknown>
BAR=0 offset=00000000 size=00000000
Capabilities: [70] Vendor Specific Information: VirtIO: Notify
BAR=4 offset=00003000 size=00001000 multiplier=00000004
Capabilities: [60] Vendor Specific Information: VirtIO: DeviceCfg
BAR=4 offset=00002000 size=00001000
Capabilities: [50] Vendor Specific Information: VirtIO: ISR
BAR=4 offset=00001000 size=00001000
Capabilities: [40] Vendor Specific Information: VirtIO: CommonCfg
BAR=4 offset=00000000 size=00001000
Kernel driver in use: virtio-pci
Kernel modules: virtio_pci
virtqueues
virtqueue 用来进行消息传递的。
virtio-scsi 定义了 3 种 virtqueue。
0: controlq
1: eventq
2…n: request queues(SCSI multiqueue)
特性位
feature bits 是 virtio 的基本能力,用于功能协商和前后向兼容。
virtio-scsi 支持如下:
VIRTIO_SCSI_F_INOUT
VIRTIO_SCSI_F_HOTPLUG
VIRTIO_SCSI_F_CHANGE
VIRTIO_SCSI_F_T10_PI
设备空间布局
这里是 SCSI 的控制器基本属性。
struct virtio_scsi_config {
le32 num_queues;
le32 seg_max;
le32 max_sectors;
le32 cmd_per_lun;
le32 event_info_size;
le32 sense_size;
le32 cdb_size;
le16 max_channel;
le16 max_target;
le32 max_lun;
};
里面的字段都很熟悉,不一一介绍了。
设备操作: request
下面是 scsi req 的命令结构,挑最重要的分析。
其他都是 SCSI 的 SAM 定义的。
struct virtio_scsi_req_cmd {
// Device-readable part
u8 lun[8];
le64 id;
u8 task_attr;
u8 prio;
u8 crn;
u8 cdb[cdb_size];
// The next two fields are only present if VIRTIO_SCSI_F_T10_PI
// is negotiated.
le32 pi_bytesout;
le32 pi_bytesin;
u8 pi_out[pi_bytesout];
u8 dataout[];
// Device-writable part
le32 sense_len;
le32 residual;
le16 status_qualifier;
u8 status;
u8 response;
u8 sense[sense_size];
// The next two fields are only present if VIRTIO_SCSI_F_T10_PI
// is negotiated
u8 pi_in[pi_bytesin];
u8 datain[];
};
lun:
8 字节的 lun 字段用于定位 一个 target 和 lun,格式如下:
| Byte 0| Byte 1| Byte 2| Byte 3| Byte 4| Byte 5| Byte 6 | Byte 7|
|<- 1 ->|<- t ->|<- lun id ->|<- 0 ->|
根据协议规定,可以支持 256 个 target(2^8) 和 16384(2^16)个 lun。
下面是 lun 的解析。
vhost_scsi_task_init_target(struct spdk_vhost_scsi_task *task, const __u8 *lun)
{
struct spdk_vhost_scsi_session *svsession = task->svsession;
struct spdk_scsi_dev_session_state *state;
uint16_t lun_id = (((uint16_t)lun[2] << 8) | lun[3]) & 0x3FFF;
SPDK_LOGDUMP(SPDK_LOG_VHOST_SCSI_QUEUE, "LUN", lun, 8);
/* First byte must be 1 and second is target */
if (lun[0] != 1 || lun[1] >= SPDK_VHOST_SCSI_CTRLR_MAX_DEVS) {
return -1;
}
SPDK 限制一个 controller 的 target 数量为 8.
lun[1] 表示 target 的 id,如果 target id >= 8,那么就超过了 一个 controller 的上限。
设备操作: controlq
VIRTIO_SCSI_T_TMF(最重要)
- Task management function
VIRTIO_SCSI_T_AN_QUERY
- Asynchronous notification query
VIRTIO_SCSI_T_AN_SUBSCRIBE
- Asynchronous notification subscription
设备操作: eventq
#define VIRTIO_SCSI_T_EVENTS_MISSED 0x80000000
struct virtio_scsi_event {
// Device-writable part
le32 event;
u8 lun[8];
le32 reason;
}
VIRTIO_SCSI_T_NO_EVENT
VIRTIO_SCSI_T_TRANSPORT_RESET
VIRTIO_SCSI_T_ASYNC_NOTIFY
VIRTIO_SCSI_T_PARAM_CHANGE
其中 VIRTIO_SCSI_T_TRANSPORT_RESET 可以有如下 3 个 reason :
#define VIRTIO_SCSI_T_TRANSPORT_RESET 1
#define VIRTIO_SCSI_EVT_RESET_HARD 0
#define VIRTIO_SCSI_EVT_RESET_RESCAN 1
#define VIRTIO_SCSI_EVT_RESET_REMOVED 2
如果是添加 target,对应的事件 + reason 为:
VIRTIO_SCSI_T_TRANSPORT_RESET + VIRTIO_SCSI_EVT_RESET_RESCAN
。
如果是删除 target,对应的事件 + reason 为:
VIRTIO_SCSI_T_TRANSPORT_RESET + VIRTIO_SCSI_EVT_RESET_REMOVED
。
下面是 SPDK 对应的代码:
vhost_scsi_session_add_tgt:
if (vhost_dev_has_feature(vsession, VIRTIO_SCSI_F_HOTPLUG)) {
eventq_enqueue(svsession, scsi_tgt_num,
VIRTIO_SCSI_T_TRANSPORT_RESET, VIRTIO_SCSI_EVT_RESET_RESCAN);
vhost_scsi_session_remove_tgt:
/* Send a hotremove Virtio event */
if (vhost_dev_has_feature(vsession, VIRTIO_SCSI_F_HOTPLUG)) {
eventq_enqueue(svsession, scsi_tgt_num,
VIRTIO_SCSI_T_TRANSPORT_RESET, VIRTIO_SCSI_EVT_RESET_REMOVED);
}
继续看 eventq_enqueue 的实现。我们知道 virtio-scsi 至少有 3 个 virtqueue:
#define VIRTIO_SCSI_CONTROLQ 0
#define VIRTIO_SCSI_EVENTQ 1
#define VIRTIO_SCSI_REQUESTQ 2
事件通知是放在 virtqueue 1 上。
注意:eventq virtqueue 是由 guest driver 放置一个空 的 buffer 在 queue 里,然后有新的事件后,device 取出这个描述符,填充事件,然后 guest driver 在完成回调里进行处理。
/* 拿到 desc 对应的 va */
vhost_vq_avail_ring_get
vhost_vq_get_desc
vhost_gpa_to_vva
/* 设置 event */
desc_ev->event = event;
desc_ev->lun[0] = 1;
desc_ev->lun[1] = scsi_dev_num;
/* virtio LUN id 0 can refer either to the entire device
* or actual LUN 0 (the only supported by vhost for now)
*/
desc_ev->lun[2] = 0 >> 8;
desc_ev->lun[3] = 0 & 0xFF;
/* virtio doesn't specify any strict format for LUN id (bytes 2 and 3)
* current implementation relies on linux kernel sources
*/
memset(&desc_ev->lun[4], 0, 4);
desc_ev->reason = reason;
/* 放到 used ring,guest driver 可以处理事件了*/
vhost_vq_used_ring_enqueue(vsession, vq, req, req_size);
virtio-scsi 事件处理:(virtscsi_handle_event)
我们主要关心 VIRTIO_SCSI_T_PARAM_CHANGE
。
如果 virtio-scsi 的容量或者 cache type 变化,就会触发此事件。
如果要支持此事件,Feature bits 需要支持:VIRTIO_SCSI_F_CHANGE
调用 scsi_rescan_device,然后通过发送 SCSI 命令 SPDK_SBC_SAI_READ_CAPACITY_16
来获取更新后的大小。
总结
给 SPDK 添加 resize vhost-scsi 的支持非常直接,按照 virtio-scsi spec 的要求实现对应的功能即可。
想看具体的实现,可以点击这里。