文章目录
前言 数据结构 protocol VFIOUserHdr vfio_user_command VFIOUserHdr flags
设备模型 VFIODevice
VFIOPCIDevice VFIOKernPCIDevice VFIOUserPCIDevice
proxy
流程详解
前言
数据结构
protocol
VFIO User PCI设备,其PCI配置空间信息及BAR空间的数据都由远端Server提供,可以说VFIO User PCI设备由Client和Server共同模拟,向Guest提供一个完正的PCI设备。Client和Server之间传输的主要信息分为两类,一类是控制面的信息,包括DMA、Region相关配置,另一类是数据面信息,包括Region的读写和DMA的读写。 VFIO User协议如下所示:
VFIOUserHdr
VFIO User协议的每条消息,无论请求还是回复,都有相同格式的头部信息,其格式如下:
typedef struct {
uint16_t id; /* 标识一条请求或者回复消息 */
uint16_t command; /* 命令,enum vfio_user_command */
uint32_t size; /* 消息长度,包括消息头和消息体整个长度 */
uint32_t flags; /* 消息标志,见VFIOUserHdr flags */
uint32_t error_reply; /* 包含报错的回复消息 */
} VFIOUserHdr;
vfio_user_command
vfio_user_command是整个VFIO规范最核心的数据结构,它定义了Client从Server获取一个PCI设备信息所需要的所有命令,对于kernel驱动,由kernel提供这些信息,对于user驱动,由远端的socket服务进程提供这些信息,如下:
/* VFIOUserHdr commands */
enum vfio_user_command {
VFIO_USER_VERSION = 1,
VFIO_USER_DMA_MAP = 2,
VFIO_USER_DMA_UNMAP = 3,
VFIO_USER_DEVICE_GET_INFO = 4,
VFIO_USER_DEVICE_GET_REGION_INFO = 5,
VFIO_USER_DEVICE_GET_REGION_IO_FDS = 6,
VFIO_USER_DEVICE_GET_IRQ_INFO = 7,
VFIO_USER_DEVICE_SET_IRQS = 8,
VFIO_USER_REGION_READ = 9,
VFIO_USER_REGION_WRITE = 10,
VFIO_USER_DMA_READ = 11,
VFIO_USER_DMA_WRITE = 12,
VFIO_USER_DEVICE_RESET = 13,
VFIO_USER_DIRTY_PAGES = 14,
VFIO_USER_MAX,
};
VFIOUserHdr flags
VFIOUserHdr的头部标志位用于指明一条消息是请求类型的还是回复类型(request/reply),而且可以指示一条请求类型的消息是否需要回复(no_reply),或者一条消息是否携带错误信息,如果error位被置位,表明头部信息中error字段携带有错误信息。
/* VFIOUserHdr flags */
#define VFIO_USER_REQUEST 0x0
#define VFIO_USER_REPLY 0x1
#define VFIO_USER_TYPE 0xF
#define VFIO_USER_NO_REPLY 0x10
#define VFIO_USER_ERROR 0x20
设备模型
VFIO-User是VFIO协议的用户态实现(TYPE_VFIO_USER_PCI),它与内核态的实现(TYPE_VFIO_PCI)在协议上相同,因此可以共享协议相关的代码,Qemu在设计时将此部分抽象为TYPE_VFIO_PCI_BASE,作为两者的基类,如下图所示: VFIO-User的设备模型较简单,它在Qemu中是一个简单设备,其热插拔都通过device_add和device_del实现,命令行如下:
-device vfio-user-pci,socket=/var/run/xxx/cntrl,x-enable-migration=off,bus=pci.0,addr=0x9,id=ram-disk3
从上面可以看到,VFIO-User设备的属性相比传统的VFIO-PCI设备(Kernel Driver),增加了一个socket的属性,用于指定Server侧监听的unix socket路径,同时它也是一个PCI设备,因此可以配置PCI设备的BDF号,其它未使能的新增属性如下:
static Property vfio_user_pci_dev_properties[] = {
DEFINE_PROP_STRING("socket", VFIOUserPCIDevice, sock_name),
DEFINE_PROP_BOOL("secure-dma", VFIOUserPCIDevice, secure_dma, false),
DEFINE_PROP_BOOL("x-send-queued", VFIOUserPCIDevice, send_queued, false),
DEFINE_PROP_BOOL("x-no-posted-writes", VFIOUserPCIDevice, no_post, false),
DEFINE_PROP_UINT32("x-msg-timeout", VFIOUserPCIDevice, wait_time, 0),
DEFINE_PROP_END_OF_LIST(),
};
VFIODevice
VFIODevice定义了实现VFIO协议所必须的信息,它将kernel和user两种驱动在具体实现上有所不同,这里QEMU通过定义两个接口(Interface)来实现,分别是VFIODevIO(设备接口)和VFIOContIO(容器接口)。
typedef struct VFIODevice {
QLIST_ENTRY(VFIODevice) next;
struct VFIOGroup *group;
......
bool enable_migration; /* 是否使能迁移 */
VFIODeviceOps *ops; /* VFIO设备相关接口,kernel和user两种驱动有具体的实现,参考VFIODevIO结构体的注释 */
VFIODevIO *io_ops; /* VFIO容器相关接口,kernel和user两种驱动有具体的实现,同上 */
......
VFIOMigration *migration; /* 迁移相关信息 */
Error *migration_blocker; /* 版本是否支持迁移,如果blocker被设置,标记不支持 */
OnOffAuto pre_copy_dirty_page_tracking;
VFIOProxy *proxy; /* vfio-user驱动将基于socket通信实现的protocol封装到proxy中,这个字段是vfio-user特有的 */
struct vfio_region_info **regions; /* vfio设备的region信息 */
......
} VFIODevice;
VFIODevIO
Qemu向后端获取PCI设备信息,通过此接口实现,对于kernel驱动,接口通过ioctl实现,对于user驱动,接口通过socket通信实现。
/*
* The next 2 ops vectors are how Devices and Containers
* communicate with the server. The default option is
* through ioctl() to the kernel VFIO driver, but vfio-user
* can use a socket to a remote process.
*/
struct VFIODevIO {
/* 获取pci设备信息,对应vfio-user VFIO_USER_DEVICE_GET_INFO命令 */
int (*get_info)(VFIODevice *vdev, struct vfio_device_info *info);
/* 获取pci region信息,对应vfio-user VFIO_USER_DEVICE_GET_REGION_INFO命令 */
int (*get_region_info)(VFIODevice *vdev, struct vfio_region_info *info, int *fd);
int (*get_irq_info)(VFIODevice *vdev, struct vfio_irq_info *irq);
int (*set_irqs)(VFIODevice *vdev, struct vfio_irq_set *irqs);
int (*region_read)(VFIODevice *vdev, uint8_t nr, off_t off, uint32_t size,
void *data);
int (*region_write)(VFIODevice *vdev, uint8_t nr, off_t off, uint32_t size,
void *data, bool post);
};
/* 不同driver实现了不同的VFIODevIO:
* kernel-> vfio_dev_io_ioctl
* user->vfio_dev_io_sock
*/
#define VDEV_GET_INFO(vdev, info) \
((vdev)->io_ops->get_info((vdev), (info)))
#define VDEV_GET_REGION_INFO(vdev, info, fd) \
((vdev)->io_ops->get_region_info((vdev), (info), (fd)))
#define VDEV_GET_IRQ_INFO(vdev, irq) \
((vdev)->io_ops->get_irq_info((vdev), (irq)))
#define VDEV_SET_IRQS(vdev, irqs) \
((vdev)->io_ops->set_irqs((vdev), (irqs)))
#define VDEV_REGION_READ(vdev, nr, off, size, data) \
((vdev)->io_ops->region_read((vdev), (nr), (off), (size), (data)))
#define VDEV_REGION_WRITE(vdev, nr, off, size, data, post) \
((vdev)->io_ops->region_write((vdev), (nr), (off), (size), (data), (post)))
VFIOContIO
与VFIODevIO类似,容器的IO接口,kernel和user驱动有不同实现。
struct VFIOContIO {
int (*dma_map)(VFIOContainer *container, MemoryRegion *mr,
struct vfio_iommu_type1_dma_map *map);
int (*dma_unmap)(VFIOContainer *container,
struct vfio_iommu_type1_dma_unmap *unmap,
struct vfio_bitmap *bitmap);
int (*dirty_bitmap)(VFIOContainer *container, MemoryRegion *mr,
struct vfio_iommu_type1_dirty_bitmap *bitmap,
struct vfio_iommu_type1_dirty_bitmap_get *range);
void (*wait_commit)(VFIOContainer *container);
};
/* 不同driver实现了不同的VFIOContIO:
* kernel-> vfio_cont_io_ioctl
* user->vfio_cont_io_sock
*/
#define CONT_DMA_MAP(cont, mr, map) \
((cont)->io_ops->dma_map((cont), (mr), (map)))
#define CONT_DMA_UNMAP(cont, unmap, bitmap) \
((cont)->io_ops->dma_unmap((cont), (unmap), (bitmap)))
#define CONT_DIRTY_BITMAP(cont, mr, bitmap, range) \
((cont)->io_ops->dirty_bitmap((cont), (mr), (bitmap), (range)))
#define CONT_WAIT_COMMIT(cont) ((cont)->io_ops->wait_commit(cont))
VFIOPCIDevice
VFIOPCIDevice作为vfio-kernel和vfio-user的基类,提供PCI设备的标准信息(比如bar信息),呈现到guest,
typedef struct VFIOPCIDevice {
PCIDevice pdev; /* PCI设备基类 */
VFIODevice vbasedev; /* 包含实现kernel和user驱动所需的VFIO框架必需的信息,TODO */
VFIOINTx intx; /* 中断相关信息,TODO */
unsigned int config_size; /* 配置空间大小 */
......
/* 设备bar信息,不同driver,获取bar信息方式不同,kernel通过ioctl实现,user通过socket通信实现 */
VFIOBAR bars[PCI_NUM_REGIONS - 1]; /* No ROM */
......
/* pci设备ID信息 */
uint32_t vendor_id;
uint32_t device_id;
uint32_t sub_vendor_id;
uint32_t sub_device_id;
......
}
VFIOKernPCIDevice
VFIOKernPCIDevice抽象了vfio-kernel设备,这里直接继承自VFIOPCIDevice且没有多余的字段,kernel驱动相关信息在父类的vbasedev
成员中被描述。
typedef struct VFIOKernPCIDevice {
VFIOPCIDevice device;
}VFIOKernPCIDevice;
VFIOUserPCIDevice
VFIOUserPCIDevice抽象了vfio-user设备,与Kernel驱动一样,继承自VFIOPCIDevice,但多了几个vfio-user设备相关属性的字段,user驱动的通过socket实现,在父类的vbasedev
成员中,增加了proxy字段描述user驱动。
typedef struct VFIOUserPCIDevice {
VFIOPCIDevice device;
char *sock_name;
bool secure_dma; /* disable shared mem for DMA */
bool send_queued; /* all sends are queued */
bool no_post; /* all regions write are sync */
uint32_t wait_time; /* timeout for message replies */
} VFIOUserPCIDevice;
proxy
vfio-user设备引入proxy概念,用于封装基于socket通信的vfio-user协议的实现细节,具体实现包括:
基于socket的信息传输 基于IOThread实现的poll逻辑 基于链表的队列缓存
qemu将通过vfio-user协议传输的消息抽象为三类,分别如下:
async message: 异步消息,这一类消息发送完调用者无需等待回复,可以直接返回,承载消息的消息体会被标记为不再使用。 nowait message: 异步消息,这一类消息发送完后调用者可以返回,不用等待,行为和async message相同,但nowait mesage需要获取消息的回复。一旦proxy接收到消息的回复,承载消息的消息体会被标记为不再使用。 wait message: 同步消息,这一类消息发送完后调用者会一直等待直到回复的消息到达,proxy接收到该消息后会将request和reply一起返回给调用者,由调用者标记为不再使用。
/*
* Messages are queued onto the proxy's outgoing list.
*
* It handles 3 types of messages:
*
* async messages - replies and posted writes
*
* There will be no reply from the server, so message
* buffers are freed after they're sent.
*
* nowait messages - map/unmap during address space transactions
*
* These are also sent async, but a reply is expected so that
* vfio_wait_reqs() can wait for the youngest nowait request.
* They transition from the outgoing list to the pending list
* when sent, and are freed when the reply is received.
*
* wait messages - all other requests
*
* The reply to these messages is waited for by their caller.
* They also transition from outgoing to pending when sent, but
* the message buffer is returned to the caller with the reply
* contents. The caller is responsible for freeing these messages.
*
* As an optimization, if the outgoing list and the socket send
* buffer are empty, the message is sent inline instead of being
* added to the outgoing list. The rest of the transitions are
* unchanged.
*
* returns 0 if the message was sent or queued
* returns -1 on send error
*/
proxy在实现协议时定义了4个链表,每个链表的元素是VFIOUserMsg
,代表一个封装vfio-user协议消息体,4个链表分别是:
free: 空闲链表,存放提前申请好内存的消息体,当proxy要发送消息时,首先从free链表中查询是否有空闲消息体,有则直接取出VFIOUserMsg
用于发送消息,没有则从内存分配。 outgoing: 待发送链表,存放将要通过proxy发送的消息体。 pending: 等待链表,存放等待server回复的已发送的消息体。当消息体为同步等待-wait
消息时,需要等待server回复。这一类消息在发送完成后,消息体中的信息不会被释放,而是从outgoing链表中移出,被添加到pending链表中,等到proxy接收到对端reply并通过id成功匹配pending链表中的request消息时,将request和reply一起交由调用者来释放。 incoming: 接收链表,顾名思义,proxy将从socket收到的来自server侧的请求(request)放到incoming链表中。
VFIOUserMsg
typedef struct {
int send_fds;
int recv_fds;
int *fds;
} VFIOUserFDs;
enum msg_type {
VFIO_MSG_NONE,
VFIO_MSG_ASYNC, /* 异步请求,消息发送完成后立即返回,消息体回收到free链表中 */
VFIO_MSG_WAIT, /* 同步请求,消息发送完成后线程睡眠在信号量上,等待server处理完成 */
/* reply到达后被唤醒。消息体的在等待发送过程中首先放入outgoing队列,发送完成后放入pending队列 */
VFIO_MSG_NOWAIT,
VFIO_MSG_REQ,
};
typedef struct VFIOUserMsg {
QTAILQ_ENTRY(VFIOUserMsg) next;
VFIOUserHdr *hdr; /* vfio-user消息头 */
VFIOUserFDs *fds; /* IO向量包含的文件描述符组 */
uint32_t rsize;
/* 当同步等待的消息请求发送后,如果回复的消息到达,通过此id匹配reply和request */
uint32_t id;
/* 发送消息后,需要同步等待回复消息的线程,在此信号量上等待回复消息 */
QemuCond cv;
bool complete;
enum msg_type type; /* 标识消息类型 */
} VFIOUserMsg;
VFIOProxy
typedef struct VFIOProxy {
QLIST_ENTRY(VFIOProxy) next;
char *sockname;
struct QIOChannel *ioc; /* unix socket对应的IO通道,vfio-user协议基于此收发数据 */
void (*request)(void *opaque, VFIOUserMsg *msg);
void *req_arg;
int flags;
uint32_t wait_time;
QemuCond close_cv;
AioContext *ctx; /* IOThread上下文 */
QEMUBH *req_bh;
/*
* above only changed when BQL is held
* below are protected by per-proxy lock
*/
QemuMutex lock; /* 消息发送和信号量更新的同步锁 */
VFIOUserMsgQ free; /* 空闲可用的消息链表 */
VFIOUserMsgQ pending; /* nowait message在发送完成后链入的链表*/
VFIOUserMsgQ incoming; /* client侧接收的来自server侧的requst */
VFIOUserMsgQ outgoing; /* 即将发送出去的message链表 */
VFIOUserMsg *last_nowait;
VFIOUserMsg *part_recv;
size_t recv_left;
enum proxy_state state;
} VFIOProxy;
流程详解
消息发松流程
DMA映射流程