vfio-ccw: 基础架构

简介

这里我们描述了 Linux/s390 对 I/O 子通道(subchannel)设备的 vfio 支持。vfio-ccw 的动机是将子通道直通(passthrough)到虚拟机,而 vfio 是实现这一目标的手段。

与其他硬件架构不同,s390 定义了一种统一的 I/O 访问方法,即所谓的通道 I/O(Channel I/O)。它有其自己的访问模式

  • 通道程序在一个独立的(协)处理器上异步运行。

  • 通道子系统将直接访问通道程序中由调用者指定的任何内存,即其中不涉及 iommu。

因此,当我们为这些设备引入 vfio 支持时,我们通过中介设备(mediated device, mdev)实现来完成。vfio mdev 将被添加到一个 iommu 组中,从而使其能够被 vfio 框架管理。并且我们为特殊的 vfio I/O 区域添加了读/写回调,以便将通道程序从 mdev 传递到其父设备(真实的 I/O 子通道设备),以进行进一步的地址转换并执行 I/O 指令。

本文并不打算详细解释 s390 I/O 架构的每一个细节。更多信息/参考资料可以在这里找到

  • 全面了解通道 I/O 的良好起点:https://en.wikipedia.org/wiki/Channel_I/O

  • s390 架构:s390 操作原理手册 (IBM Form. No. SA22-7832)

  • 实现了一个简单的模拟通道子系统的现有 QEMU 代码也可以作为很好的参考。这使得跟踪流程更加容易。qemu/hw/s390x/css.c

关于 vfio 中介设备框架:- VFIO Mediated devices

vfio-ccw 的动机

通常,在 s390 上通过 QEMU/KVM 虚拟化的客户机只能通过“通道 I/O 上的 Virtio (virtio-ccw)”传输看到半虚拟化的 virtio 设备。这使得 virtio 设备可以通过处理通道设备的标准操作系统算法被发现。

然而这还不够。在 s390 上,对于使用标准基于通道 I/O 机制的大多数设备,我们还需要提供将它们直通给 QEMU 虚拟机的功能。这包括没有 virtio 对应设备的设备(例如磁带驱动器)或者具有客户机想要利用的特定特征的设备。

为了将设备传递给客户机,我们希望使用与其他所有人相同的接口,即 vfio。我们通过 vfio 中介设备框架和子通道设备驱动程序“vfio_ccw”来实现对通道设备的这种 vfio 支持。

CCW 设备的访问模式

s390 架构实现了一个所谓的通道子系统,它提供了物理连接到系统的设备的统一视图。尽管 s390 硬件平台了解各种各样的不同外围连接设备,如磁盘设备(又称 DASD)、磁带、通信控制器等。它们都可以通过定义良好的访问方法进行访问,并且它们以统一的方式呈现 I/O 完成情况:I/O 中断。

所有的 I/O 都需要使用通道命令字(CCW)。CCW 是发给专用 I/O 通道处理器的指令。通道程序是由 I/O 通道子系统执行的 CCW 序列。为了向通道子系统发出通道程序,需要构建一个操作请求块(ORB),它可用于向系统指出 CCW 的格式和其他控制信息。操作系统发信号给 I/O 通道子系统,通过 SSCH(启动子通道)指令开始执行通道程序。然后中央处理器可以自由继续执行非 I/O 指令,直到被中断。I/O 完成结果由中断处理程序以中断响应块(IRB)的形式接收。

回到 vfio-ccw,简而言之

  • ORB 和通道程序在客户机内核中构建(带有客户机物理地址)。

  • ORB 和通道程序被传递到宿主机内核。

  • 宿主机内核将客户机物理地址转换为真实地址,并通过发出特权的通道 I/O 指令(例如 SSCH)来启动 I/O。

  • 通道程序在一个独立处理器上异步运行。

  • I/O 完成将通过 I/O 中断向宿主机发出信号。并且它将被复制为 IRB 到用户空间以传回给客户机。

物理 vfio ccw 设备及其子 mdev

如上所述,我们通过 mdev 实现来实现 vfio-ccw。

通道 I/O 没有 IOMMU 硬件支持,因此物理 vfio-ccw 设备没有 IOMMU 级别的转换或隔离。

子通道 I/O 指令全部是特权指令。在处理 I/O 指令拦截时,vfio-ccw 具有软件监督和转换功能,决定了通道程序在发送到硬件之前是如何被编排的。

在此实现中,我们针对两种类型的设备有两个驱动程序

  • 用于物理子通道设备的 vfio_ccw 驱动程序。这是一个针对真实子通道设备的 I/O 子通道驱动程序。它实现了一组回调函数,并作为父(物理)设备注册到 mdev 框架。因此,mdev 为 vfio_ccw 提供了一个通用接口(sysfs)来创建 mdev 设备。随后可以由 vfio_ccw 创建一个 vfio mdev 并将其添加到中介总线中。正是这个 vfio 设备被添加到了 IOMMU 组和 vfio 组中。vfio_ccw 还提供了一个 I/O 区域,用于接受来自用户空间的通道程序请求,并存储 I/O 中断结果供用户空间检索。为了通知用户空间 I/O 完成,它提供了一个接口来设置用于异步发信号的 eventfd 文件描述符(fd)。

  • 用于中介 vfio ccw 设备的 vfio_mdev 驱动程序。这是由 mdev 框架提供的。它是针对由 vfio_ccw 创建的 mdev 的 vfio 设备驱动程序。它实现了一组 vfio 设备驱动程序回调,将自己添加到一个 vfio 组中,并作为 mdev 驱动程序注册到 mdev 框架。它使用了一个 vfio iommu 后端,该后端使用现有的 map 和 unmap ioctl,但它不是将它们编程到设备的 IOMMU 中,而是简单地存储转换以供后续请求使用。这意味着在虚拟机中用客户机物理地址编程的设备可以由 vfio 内核一步到位地将其地址转换为进程虚拟地址,固定页面(pin page)并用宿主机物理地址对硬件进行编程。对于 mdev,vfio iommu 后端不会在 VFIO_IOMMU_MAP_DMA ioctl 期间固定页面。Mdev 框架在此操作中只会维护一个 iova<->vaddr 映射的数据库。并且它们从 vfio iommu 后端导出了 vfio_pin_pages 和 vfio_unpin_pages 接口,供物理设备按需固定和取消固定页面。

以下是高级架构图

+-------------+
|             |
| +---------+ | mdev_register_driver() +--------------+
| |  Mdev   | +<-----------------------+              |
| |  bus    | |                        | vfio_mdev.ko |
| | driver  | +----------------------->+              |<-> VFIO user
| +---------+ |    probe()/remove()    +--------------+    APIs
|             |
|  MDEV CORE  |
|   MODULE    |
|   mdev.ko   |
| +---------+ | mdev_register_parent() +--------------+
| |Physical | +<-----------------------+              |
| | device  | |                        |  vfio_ccw.ko |<-> subchannel
| |interface| +----------------------->+              |     device
| +---------+ |       callback         +--------------+
+-------------+

这些组件协同工作的过程。

  1. vfio_ccw.ko 驱动物理 I/O 子通道,并将物理设备(带回调)注册到 mdev 框架。当 vfio_ccw 探测子通道设备时,它将设备指针和回调注册到 mdev 框架。在 sysfs 中设备节点下的 mdev 相关文件节点将被创建,即 ‘mdev_create’、‘mdev_destroy’ 和 ‘mdev_supported_types’。

  2. 创建一个中介 vfio ccw 设备。使用 ‘mdev_create’ sysfs 文件,我们需要手动创建一个(在我们的情况下仅一个)中介设备。

  3. vfio_mdev.ko 驱动中介 ccw 设备。vfio_mdev 也是 vfio 设备驱动程序。它将探测 mdev 并将其添加到 iommu_group 和 vfio_group 中。然后我们可以将该 mdev 直通给客户机。

VFIO-CCW 区域

vfio-ccw 驱动程序暴露了 MMIO 区域,以接受来自用户空间的请求并向其返回结果。

vfio-ccw I/O 区域

I/O 区域用于接受来自用户空间的通道程序请求,并存储 I/O 中断结果供用户空间检索。该区域的定义是

struct ccw_io_region {
#define ORB_AREA_SIZE 12
        __u8    orb_area[ORB_AREA_SIZE];
#define SCSW_AREA_SIZE 12
        __u8    scsw_area[SCSW_AREA_SIZE];
#define IRB_AREA_SIZE 96
        __u8    irb_area[IRB_AREA_SIZE];
        __u32   ret_code;
} __packed;

此区域始终可用。

在启动 I/O 请求时,orb_area 应填充客户机 ORB,scsw_area 应填充虚拟子通道的 SCSW。

irb_area 存储 I/O 结果。

ret_code 存储每次访问该区域的返回码。可能会出现以下值

0

操作成功。

-EOPNOTSUPP

ORB 指定了传输模式,或者 SCSW 指定了启动函数之外的功能。

-EIO

在设备未处于准备好接受请求的状态时发出了请求,或者发生了内部错误。

-EBUSY

子通道处于状态挂起或忙碌状态,或者请求已经处于活动状态。

-EAGAIN

请求正在处理中,调用者应当重试。

-EACCES

发现用于 I/O 的通道路径不可操作。

-ENODEV

发现设备不可操作。

-EINVAL

orb 指定的链长于 255 个 ccw,或者发生了内部错误。

vfio-ccw cmd 区域

vfio-ccw cmd 区域用于接受来自用户空间的异步指令

#define VFIO_CCW_ASYNC_CMD_HSCH (1 << 0)
#define VFIO_CCW_ASYNC_CMD_CSCH (1 << 1)
struct ccw_cmd_region {
       __u32 command;
       __u32 ret_code;
} __packed;

该区域通过区域类型 VFIO_REGION_SUBTYPE_CCW_ASYNC_CMD 暴露。

目前,CLEAR SUBCHANNEL(清除子通道)和 HALT SUBCHANNEL(暂停子通道)使用此区域。

command 指定要发出的命令;ret_code 存储每次访问该区域的返回码。可能会出现以下值

0

操作成功。

-ENODEV

发现设备不可操作。

-EINVAL

指定了 halt 或 clear 之外的命令。

-EIO

在设备未处于准备好接受请求的状态时发出了请求。

-EAGAIN

请求正在处理中,调用者应当重试。

-EBUSY

在处理暂停(halt)请求时,子通道处于状态挂起或忙碌状态。

vfio-ccw schib 区域

vfio-ccw schib 区域用于向用户空间返回子通道信息块(SCHIB)数据

struct ccw_schib_region {
#define SCHIB_AREA_SIZE 52
       __u8 schib_area[SCHIB_AREA_SIZE];
} __packed;

该区域通过区域类型 VFIO_REGION_SUBTYPE_CCW_SCHIB 暴露。

读取此区域会触发向关联硬件发出 STORE SUBCHANNEL(存储子通道)。

vfio-ccw crw 区域

vfio-ccw crw 区域用于向用户空间返回通道报告字(CRW)数据

struct ccw_crw_region {
       __u32 crw;
       __u32 pad;
} __packed;

该区域通过区域类型 VFIO_REGION_SUBTYPE_CCW_CRW 暴露。

如果存在与此子通道相关的挂起 CRW(例如报告通道路径状态变化的 CRW),读取此区域将返回该 CRW,否则返回全零。如果多个 CRW 处于挂起状态(可能包括链式 CRW),再次读取此区域将返回下一个 CRW,直到没有挂起的 CRW 并返回全零。这类似于 STORE CHANNEL REPORT WORD 的工作方式。

vfio-ccw 操作细节

vfio-ccw 遵循 vfio-pci 在 s390 平台上的做法,使用 vfio-iommu-type1 作为 vfio iommu 后端。

  • CCW 转换 API 一组用于进行 CCW 转换的 API(以 cp_ 开头)。用户空间程序传入的 CCW 使用其客户机物理内存地址进行组织。这些 API 会将 CCW 复制到内核空间,并通过用相应的宿主机物理地址更新客户机物理地址,组装一个可运行的内核通道程序。注意,即使对于直接访问的 CCW,我们也必须使用 IDAL(间接数据地址列表),因为引用的内存可以位于任何地方,包括 2G 以上。

  • vfio_ccw 设备驱动程序 该驱动程序利用 CCW 转换 API 并引入了 vfio_ccw,它是您想要直通的 I/O 子通道设备的驱动程序。vfio_ccw 实现了以下 vfio ioctl

    VFIO_DEVICE_GET_INFO
    VFIO_DEVICE_GET_IRQ_INFO
    VFIO_DEVICE_GET_REGION_INFO
    VFIO_DEVICE_RESET
    VFIO_DEVICE_SET_IRQS
    

    这提供了一个 I/O 区域,以便用户空间程序可以将通道程序传递给内核,在将它们发出到真实设备之前进行进一步的 CCW 转换。这还提供了 SET_IRQ ioctl 来设置事件通知程序,以异步方式通知用户空间程序 I/O 完成。

vfio-ccw 的使用并不限于 QEMU,而 QEMU 绝对是理解这些补丁如何工作的良好示例。这里有关于 QEMU 客户机触发的 I/O 请求将如何被处理的更多详细信息(不包含错误处理)。

解释

  • Q1-Q7:QEMU 端处理流程。

  • K1-K5:内核端处理流程。

Q1.

在初始化期间获取 I/O 区域信息。

Q2.

设置事件通知程序和处理程序以处理 I/O 完成。

... ...

Q3.

拦截 ssch 指令。

Q4.

将客户机通道程序和 ORB 写入 I/O 区域。

K1.

从客户机复制到内核。

K2.

将客户机通道程序转换为宿主机内核空间通道程序,该程序对真实设备而言变为可运行的。

K3.

使用 QEMU 传入的 orb 中包含的必要信息,将 ccwchain 发出到设备。

K4.

返回 ssch 条件码 (CC code)。

Q5.

将条件码返回给客户机。

... ...

K5.

中断处理程序获取 I/O 结果并将结果写入 I/O 区域。

K6.

向 QEMU 发出信号以检索结果。

Q6.

获取信号,事件处理程序从 I/O 区域读出结果。

Q7.

为客户机更新 irb。

限制

当前的 vfio-ccw 实现侧重于仅支持实现 DASD/ECKD 设备的块设备功能(读/写)所需的的基本命令。某些命令在将来可能需要特殊处理,例如与路径分组相关的任何内容。

DASD 是一种存储设备。而 ECKD 是一种数据记录格式。有关 DASD 和 ECKD 的更多信息,可以在这里找到:https://en.wikipedia.org/wiki/Direct-access_storage_device https://en.wikipedia.org/wiki/Count_key_data

结合 QEMU 中的相应工作,我们现在可以使直通的 DASD/ECKD 设备在客户机中上线,并将其用作块设备。

当前的代码允许客户机通过 START SUBCHANNEL 启动通道程序,并发出 HALT SUBCHANNEL、CLEAR SUBCHANNEL 和 STORE SUBCHANNEL。

目前,所有的通道程序都会被预取(prefetch),而不管 ORB 中的 p 位设置如何。因此,不支持自修改通道程序。出于这个原因,IPL(初始程序加载)必须由用户空间/客户机程序作为特殊情况处理;从 QEMU 4.1 开始,这已经在 QEMU 的 s390-ccw bios 中实现。

vfio-ccw 仅支持经典(命令模式)通道 I/O。不支持传输模式(HPF)。

当前不支持 QDIO 子通道。除 DASD/ECKD 之外的经典设备可能可以工作,但尚未经过测试。

参考资料

  1. ESA/s390 操作原理手册 (IBM Form. No. SA22-7832)

  2. ESA/390 公共 I/O 设备命令手册 (IBM Form. No. SA22-7204)

  3. https://en.wikipedia.org/wiki/Channel_I/O

  4. 面向 S/390 和 zSeries 的 Linux

  5. VFIO - “虚拟功能 I/O”

  6. VFIO 中介设备 (Mediated devices)