框架
vhost在网络中的位置如图:
要学习具体的框架可以看我之前的文章vhost-net--------深入了解Virtio-networking和vhost-net
接下来,我们自己实现一个vhost.
vhost-net代码
在代码中写了详细注释,就直接上代码了
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdint.h>
#include <stddef.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <sys/poll.h>
#include <sys/eventfd.h>
/*
描述Virtio网络设备的数据包头部信息,其中包含了以下成员:
flags:标志位,用于指示是否需要计算校验和(checksum)。如果设置了 VIRTIO_NET_HDR_F_NEEDS_CSUM 标志,表示需要计算校验和。
gso_type:GSO(Generic Segmentation Offload)类型,用于指示数据包的分段类型。可以是非GSO帧,IPv4 TCP(TSO),IPv4 UDP(UFO),IPv6 TCP等。
hdr_len:头部长度,表示Ethernet、IP和TCP/UDP头部的总长度。
gso_size:GSO帧中每帧追加到 hdr_len 的字节数。
csum_start:校验和开始位置,用于指示从何处开始计算校验和。
csum_offset:校验和偏移量,指示校验和放置的位置。
num_buffers:缓冲区数量,通常用于指示数据包的分段数量。
*/
struct virtio_net_hdr {
#define VIRTIO_NET_HDR_F_NEEDS_CSUM 1 /**< 使用 csum_start 和 csum_offset */
uint8_t flags;
#define VIRTIO_NET_HDR_GSO_NONE 0 /**< 非GSO(Generic Segmentation Offload)帧 */
#define VIRTIO_NET_HDR_GSO_TCPV4 1 /**< GSO帧,IPv4 TCP (TSO) */
#define VIRTIO_NET_HDR_GSO_UDP 3 /**< GSO帧,IPv4 UDP (UFO) */
#define VIRTIO_NET_HDR_GSO_TCPV6 4 /**< GSO帧,IPv6 TCP */
#define VIRTIO_NET_HDR_GSO_ECN 0x80 /**< TCP启用ECN(Explicit Congestion Notification) */
uint8_t gso_type;
uint16_t hdr_len; /**< Ethernet + IP + TCP/UDP头部长度 */
uint16_t gso_size; /**< 每帧追加到hdr_len的字节数 */
uint16_t csum_start; /**< 开始计算校验和的位置 */
uint16_t csum_offset; /**< 校验和放置的偏移量 */
uint16_t num_buffers; /**< 缓冲区数量 */
};
// virtio-v1.1-cs.pdf. page 21
/*
addr:这是一个 uint64_t 类型的字段,表示数据包的物理地址或者是指向数据包的指针。通常用于确定数据包在内存中的位置。
len:这是一个 uint32_t 类型的字段,表示数据包的长度,即数据包的字节数。
flags:这是一个 uint16_t 类型的字段,表示描述符的标志。它可以包含以下标志位:
VIRTQ_DESC_F_NEXT (1):表示该描述符有下一个描述符。
VIRTQ_DESC_F_WRITE (2):表示可以向该描述符写入数据。
VIRTQ_DESC_F_INDIRECT (4):表示该描述符是间接描述符,它指向一个描述符链。
next:这是一个 uint16_t 类型的字段,表示下一个描述符的索引。如果 VIRTQ_DESC_F_NEXT 标志被设置,该字段指示下一个描述符
的位置。
struct virtq_desc 通常用于虚拟队列中的描述符表,每个描述符都描述了一个数据包的相关信息,包括数据包的地址、长度和标志等。
描述符表的结构允许虚拟设备和主机之间进行高效的数据包传输和管理。
*/
struct virtq_desc {
uint64_t addr; // 数据包的物理地址或指向数据包的指针
uint32_t len; // 数据包的长度
uint16_t flags; // 描述符标志
uint16_t next; // 下一个描述符的索引(如果有)
};
/*
flags:这是一个 uint16_t 类型的字段,表示可用环的标志。如果设置了 VIRTQ_AVAIL_F_NO_INTERRUPT 标志(值为1),
则表示不应该触发中断来通知主机或虚拟机。这个标志用于控制是否应该触发中断。
idx:这是一个 uint16_t 类型的字段,表示下一个可用的描述符索引。它指示了在环中的哪个位置可以找到下一个可用的描述符。
虚拟机或主机可以根据这个索引来获取下一个要处理的数据包。
ring[0]:这是一个长度为0的数组,实际上它是一个柔性数组(flexible array member)。它用于存储可用环中的描述符索引。
具体的描述符索引的数量由 idx 字段指示。
used_event:这是一个 uint16_t 类型的字段,只有在存在 VIRTIO_F_EVENT_IDX 标志时才有效。它用于表示已使用环的事件索引,
用于在虚拟设备和主机之间同步事件。
struct virtq_avail 通常用于虚拟队列的可用环,它记录了哪些描述符是可供虚拟设备或主机使用的。描述符的索引存储在
ring 数组中,而 idx 字段则指示了下一个可用的描述符。标志字段用于控制中断触发行为。
*/
struct virtq_avail {
#define VIRTQ_AVAIL_F_NO_INTERRUPT 1 // 不触发中断的标志
uint16_t flags; // 可用环的标志
uint16_t idx; // 下一个可用的描述符索引
uint16_t ring[0]; // 可用环的数组,用于存储描述符索引
uint16_t used_event; /* 只有在 VIRTIO_F_EVENT_IDX 标志存在时有效 */
};
/* le32 is used here for ids for padding reasons. */
struct virtq_used_elem {
/* Index of start of used descriptor chain. */
uint32_t id;
/* Total length of the descriptor chain which was used (written to) */
uint32_t len;
};
/*
flags:这是一个 uint16_t 类型的字段,表示已使用环的标志。如果设置了 VIRTQ_USED_F_NO_NOTIFY 标志(值为1),
则表示不应该进行通知来通知主机或虚拟机。这个标志用于控制通知行为。
idx:这是一个 uint16_t 类型的字段,表示下一个可用的描述符索引。它指示了在环中的哪个位置可以找到下一个可用的描述符。
ring[0]:这是一个长度为0的数组,实际上它是一个柔性数组(flexible array member)。它用于存储已使用环中的描述符信息,
具体的描述符信息存储在 struct virtq_used_elem 结构体数组中。
avail_event:这是一个 uint16_t 类型的字段,只有在存在 VIRTIO_F_EVENT_IDX 标志时才有效。它用于表示可用环的事件索引,
用于在虚拟设备和主机之间同步事件。
struct virtq_used 通常用于虚拟队列的已使用环,它记录了哪些描述符已经被虚拟设备或主机使用。每个描述符的信息存储在
ring 数组中,而 idx 字段则指示了下一个可用的描述符。标志字段用于控制通知行为。
*/
struct virtq_used {
#define VIRTQ_USED_F_NO_NOTIFY 1 // 不进行通知的标志
uint16_t flags; // 已使用环的标志
uint16_t idx; // 下一个可用的描述符索引
struct virtq_used_elem ring[0]; // 已使用环的数组,用于存储已使用的描述符信息
uint16_t avail_event; /* 只有在 VIRTIO_F_EVENT_IDX 标志存在时有效 */
};
/*
desc:这是一个指向 struct virtq_desc 结构体的指针,用于表示描述符表。描述符表包含了用于描述数据包的数据结构,
通常包括数据包的起始地址、长度等信息。
avail:这是一个指向 struct virtq_avail 结构体的指针,用于表示可用环(Available Ring)。可用环包含了一个或多个描述符
的索引,表示哪些描述符可以被使用。
used:这是一个指向 struct virtq_used 结构体的指针,用于表示已使用环(Used Ring)。已使用环包含了已经处理的描述符
的信息,例如描述符的索引和长度。
log:这是一个指向日志数据的指针,用于记录虚拟队列的日志信息。这个字段通常用于调试或性能分析。
kickfd:触发中断的文件描述符。当有新的数据包可用时,该文件描述符可能会被用来通知虚拟机或主机处理新的数据包。
callfd:通知文件描述符。通常用于通知虚拟机或主机进行处理或其他操作。
num:队列的描述符数量,表示虚拟队列中描述符的总数。
last_used_idx:最后使用的描述符索引,用于跟踪已经处理的描述符。
struct virtqueue 通常用于虚拟设备的数据包传输和管理。通过这个结构体,程序可以访问描述符表、可用环和已使用环等信息,以实现虚拟设备和主机之间的数据包传输和协作
*/
struct virtqueue {
struct virtq_desc *desc; // 指向描述符表的指针
struct virtq_avail *avail; // 指向可用环的指针
struct virtq_used *used; // 指向已使用环的指针
void *log; // 指向日志数据的指针
int kickfd; // 触发中断的文件描述符
int callfd; // 通知文件描述符
uint32_t num; // 队列的描述符数量
uint32_t last_used_idx; // 最后使用的描述符索引
};
// vpp , dpdk , virtio -->
// ---> virtio
#define VIRTIO_NET_F_CSUM 0
#define VIRTIO_NET_F_GUEST_CSUM 1
#define VIRTIO_NET_F_CTRL_GUEST_OFFLOADS 2
#define VIRTIO_NET_F_MTU 3
#define VIRTIO_NET_F_MAC 5
#define VIRTIO_NET_F_GUEST_TSO4 7
#define VIRTIO_NET_F_GUEST_TSO6 8
#define VIRTIO_NET_F_GUEST_ECN 9
#define VIRTIO_NET_F_GUEST_UFO 10
#define VIRTIO_NET_F_HOST_TSO4 11
#define VIRTIO_NET_F_HOST_TSO6 12
#define VIRTIO_NET_F_HOST_ECN 13
#define VIRTIO_NET_F_HOST_UFO 14
#define VIRTIO_NET_F_MRG_RXBUF 15
#define VIRTIO_NET_F_STATUS 16
#define VIRTIO_NET_F_CTRL_VQ 17
#define VIRTIO_NET_F_CTRL_RX 18
#define VIRTIO_NET_F_CTRL_VLAN 19
#define VIRTIO_NET_F_GUEST_ANNOUNCE 21
#define VIRTIO_NET_F_MQ 22
#define VIRTIO_NET_F_CTRL_MAC_ADDR 23
#define VIRTIO_F_VERSION_1 32
#define VIRTIO_NET_F_RSC_EXT 61
#define VIRTIO_NET_F_STANDBY 62
#if 0
#define VHOST_SUPPORTED_FEATURES \
(1ULL << VIRTIO_NET_F_CSUM) | \
(1ULL << VIRTIO_NET_F_GUEST_CSUM)| \
(1ULL << VIRTIO_NET_F_CTRL_GUEST_OFFLOADS)| \
(1ULL << VIRTIO_NET_F_MTU) | \
(1ULL << VIRTIO_NET_F_MAC) | \
(1ULL << VIRTIO_NET_F_GUEST_TSO4) | \
(1ULL << VIRTIO_NET_F_GUEST_TSO6) | \
(1ULL << VIRTIO_NET_F_GUEST_ECN) | \
(1ULL << VIRTIO_NET_F_GUEST_UFO) | \
(1ULL << VIRTIO_NET_F_HOST_TSO4) | \
(1ULL << VIRTIO_NET_F_HOST_TSO6) | \
(1ULL << VIRTIO_NET_F_HOST_ECN) | \
(1ULL << VIRTIO_NET_F_HOST_UFO) | \
(1ULL << VIRTIO_NET_F_MRG_RXBUF) | \
(1ULL << VIRTIO_NET_F_STATUS) | \
(1ULL << VIRTIO_NET_F_CTRL_VQ) | \
(1ULL << VIRTIO_NET_F_CTRL_RX) | \
(1ULL << VIRTIO_NET_F_CTRL_VLAN) | \
(1ULL << VIRTIO_NET_F_GUEST_ANNOUNCE) | \
(1ULL << VIRTIO_NET_F_MQ) | \
(1ULL << VIRTIO_NET_F_CTRL_MAC_ADDR) | \
(1ULL << VIRTIO_NET_F_RSC_EXT) | \
(1ULL << VIRTIO_NET_F_STANDBY)
#else
#define VHOST_SUPPORTED_FEATURES \
((1ULL << VIRTIO_F_VERSION_1) | \
(1ULL << VIRTIO_NET_F_GUEST_CSUM) | \
(1ULL << VIRTIO_NET_F_GUEST_TSO4) | \
(1ULL << VIRTIO_NET_F_GUEST_TSO6))
#endif
#define VIRTIO_MAX_REGION 8
uint64_t vhost_supported_featrues = (VHOST_SUPPORTED_FEATURES);
/*
ROUNDON(x, y):这个宏将x按照y进行向下舍入。它的实现是通过对x和y取反后的位与操作来实现的。具体来说,
它将x的低位清零,保留了高位,使得x成为y的整数倍。这种操作通常用于对齐地址到某个边界。
ROUNDUP(x, y):这个宏将x按照y进行向上舍入。它的实现是通过将x和y相加后减去1,然后再对y取反后的位与操作来实现的。
这样可以确保x被向上舍入到了y的整数倍。这种操作通常用于计算内存分配的大小,以确保分配的内存大小是某个边界的整数倍。
这两个宏可以在编程中用于处理地址对齐或内存大小的舍入操作,以满足特定的要求。例如,如果需要将一个地址舍入到4字节边界,
可以使用ROUNDON(x, 4),如果需要将一个内存大小舍入到页大小的整数倍,可以使用ROUNDUP(x, PAGE_SIZE),其中PAGE_SIZE是页
的大小。
*/
#define ROUNDON(x, y) (x & (~(y - 1)))
#define ROUNDUP(x, y) (((x)+(y)-1) & (~((y)-1)))
#define min(x, y) (((x) <= (y))?(x):(y))
// ---> vhost
enum {
VHOST_USER_NONE = 0,
VHOST_USER_GET_FEATURES = 1,
VHOST_USER_SET_FEATURES = 2,
VHOST_USER_SET_OWNER = 3,
VHOST_USER_RESET_OWNER = 4,
VHOST_USER_SET_MEM_TABLE = 5,
VHOST_USER_SET_LOG_BASE = 6,
VHOST_USER_SET_LOG_FD = 7,
VHOST_USER_SET_VRING_NUM = 8,
VHOST_USER_SET_VRING_ADDR = 9,
VHOST_USER_SET_VRING_BASE = 10,
VHOST_USER_GET_VRING_BASE = 11,
VHOST_USER_SET_VRING_KICK = 12,
VHOST_USER_SET_VRING_CALL = 13,
VHOST_USER_SET_VRING_ERR = 14,
VHOST_USER_GET_PROTOCOL_FEATURES = 15,
VHOST_USER_SET_PROTOCOL_FEATURES = 16,
VHOST_USER_GET_QUEUE_NUM = 17,
VHOST_USER_SET_VRING_ENABLE = 18,
VHOST_USER_SEND_RARP = 19,
VHOST_USER_NET_SET_MTU = 20,
VHOST_USER_MAX = VHOST_USER_NET_SET_MTU,
};
#define VHOST_USER_VERSION_MASK 0x3
#define VHOST_USER_REPLY_MASK 0x1 << 2
#define VHOST_USER_VERSION 0x1
#define MAX_MULTI_QUEUE 256
/*
guest_address:这是一个 uint64_t 类型的字段,表示客户机(虚拟机)内存区域的起始地址。它指定了虚拟机中的内存区域
在客户机内存中的位置。
size:这是一个 uint64_t 类型的字段,表示内存区域的大小,即内存区域包含的字节数。
user_address:这是一个 uint64_t 类型的字段,表示用户空间内存区域的起始地址。它指定了内存区域在用户空间的位置,
允许用户空间程序访问该内存区域。
mmap_offset:这是一个 uint64_t 类型的字段,通常用于内存映射操作。它表示内存映射的偏移量,用于在用户空间将内存
区域映射到虚拟机内存。
struct vhost_user_region 通常用于描述虚拟机用户空间内存区域的配置和映射信息。通过这个数据结构,可以指定虚拟机
内存区域在客户机和用户空间中的位置和大小,以便进行内存访问和管理。
*/
struct vhost_user_region {
uint64_t guest_address; // 客户机(虚拟机)内存区域的起始地址
uint64_t size; // 内存区域的大小
uint64_t user_address; // 用户空间内存区域的起始地址
uint64_t mmap_offset; // 内存映射的偏移量
};
/*
nregions:这是一个 uint32_t 类型的字段,表示内存区域的数量。它指定了 regions 数组中存储的内存区域的数量。
padding:这是一个 uint32_t 类型的字段,通常用于内存对齐的填充。它确保数据结构的对齐。
regions[VIRTIO_MAX_REGION]:这是一个包含 struct vhost_user_region 结构体的数组,用于存储内存区域的信息。
VIRTIO_MAX_REGION 表示数组的最大长度。每个 struct vhost_user_region 结构体通常包含了描述一个内存区域的信息,
例如内存的起始地址和大小等。
struct vhost_user_mem 通常用于表示虚拟机用户空间内存的配置和管理信息。通过这个数据结构,可以指定一个或多个
内存区域的信息,以便虚拟机可以访问和管理这些内存区域。
*/
struct vhost_user_mem {
uint32_t nregions; // 内存区域的数量
uint32_t padding; // 内存对齐填充
struct vhost_user_region regions[VIRTIO_MAX_REGION]; // 存储内存区域信息的数组
};
// 虚拟环(vring)状态信息结构体,用于描述虚拟环的状态
struct vhost_vring_state {
uint32_t index; // 虚拟环的索引,表示不同的虚拟环
uint32_t num; // 虚拟环中的描述符数量,表示可用于数据传输的描述符数量
};
// 虚拟环(vring)地址信息结构体,用于描述虚拟环的地址布局
struct vhost_vring_address {
uint32_t index; // 虚拟环的索引,表示不同的虚拟环
uint32_t flags; // 标志位,用于附加信息(未使用的字段)
uint64_t desc; // 描述符(Descriptor)的起始地址
uint64_t used; // 已使用环的起始地址,用于存储已经使用的描述符信息
uint64_t avail; // 可用环的起始地址,用于存储可用的描述符信息
uint64_t log; // 日志(Log)的地址,用于记录环的状态信息(未使用的字段)
};
/*
request字段用于表示消息的类型或用途,flags字段用于携带附加信息,size字段表示消息体的大小。union部分可以根据消息类型
的不同来存储不同的数据,例如数值型数据、内存信息、vring状态或地址信息等。fds数组用于传递文件描述符,最多可以
传递VIRTIO_MAX_REGION个文件描述符。
这个结构体通常在虚拟化或设备驱动程序中用于在用户空间和内核空间之间进行通信,以便进行设备的配置、控制和数据传输等操作
不同的request值表示不同的操作类型,而union部分根据具体操作的需要来存储相应的数据。
*/
// vhost用户消息结构体,用于在用户空间和虚拟机之间传递消息
struct vhost_user_msg {
uint32_t request; // 请求类型,表示消息的用途
uint32_t flags; // 标志位,用于消息的附加信息
uint32_t size; // 消息体的大小
union {
uint64_t num; // 64位整数,用于传递数值型数据
struct vhost_user_mem memory; // vhost用户内存信息
struct vhost_vring_state state; // vring状态信息
struct vhost_vring_address addr; // vring地址信息
// uint64_t unset[VIRTIO_MAX_REGION * 2]; // 未设置的数据
};
int fds[VIRTIO_MAX_REGION]; // 用于传递文件描述符的数组,最多可以传递VIRTIO_MAX_REGION个文件描述符
} __attribute__((packed)); // 使用packed属性确保结构体紧凑排列,防止字节对齐
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
/*
vq[MAX_MULTI_QUEUE]:这是一个数组,用于存储虚拟设备的多个虚拟队列(virtqueue)。每个虚拟队列提供了一种在虚拟设备
和主机之间传输数据的机制。MAX_MULTI_QUEUE 表示最大的虚拟队列数量,它定义了数组的大小。每个虚拟队列都有其自己的数据结构,
包含与队列相关的信息。
mem:这是一个指向 struct vhost_user_mem 结构体的指针。struct vhost_user_mem 通常包含了虚拟设备的内存信息,
例如内存的地址、大小等。通过这个指针,可以访问虚拟设备的内存配置,以便进行数据包传输和内存管理。
struct virtio_dev 通常用于在程序中表示虚拟设备的状态和配置信息。通过这个结构体,程序可以管理虚拟队列以及
虚拟设备的内存等资源。在实际使用中,程序会初始化和配置 struct virtio_dev 的字段,以便与虚拟设备进行通信和数据传输。
*/
struct virtio_dev {
struct virtqueue vq[MAX_MULTI_QUEUE]; // 用于存储多个虚拟队列的数组
struct vhost_user_mem *mem; // 指向虚拟设备内存信息的指针
};
struct virtio_dev *virtiodev = NULL;
/*
创建一个 TAP 设备并返回相关的文件描述符,以便程序可以使用该描述符与 TAP 设备进行交互。函数的参数 dev
可以用于指定 TAP 设备的名称,如果不指定,则会由系统自动分配一个名称。函数中使用了 open、ioctl 等系统调用
来完成 TAP 设备的创建和配置。创建 TAP 设备后,可以使用返回的文件描述符来读取或写入网络数据包
*/
// 创建一个 TAP 设备并返回相关的文件描述符
int tun_alloc(char *dev)
{
struct ifreq ifr;
int fd, err;
// 初始化 ifr 结构体
memset(&ifr, 0, sizeof(ifr));
// 打开 /dev/net/tun 设备
if ((fd = open("/dev/net/tun", O_RDWR)) < 0)
return -1;
// 配置 TAP 设备的属性,包括标志和名称
ifr.ifr_flags = IFF_TAP | IFF_NO_PI; // 使用 TAP 设备,不包括包信息
if (*dev)
memcpy(ifr.ifr_name, dev, strlen(dev)); // 拷贝设备名称到 ifr 结构体
// 使用 ioctl 进行 TUNSETIFF 操作,将 TAP 设备配置为 ifr 所指定的属性,这里相当于创建安vent0
if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) {
close(fd); // 失败时关闭文件描述符
return err;
}
// 返回 TAP 设备的文件描述符
return fd;
}
int vhost_user_set_owner(void) {
}
/*
+------------------+ +------------------+ +------------------+ <->
| Virtual-Machine | | Virtual-Machine | | Virtual-Machine | |
+------------------+ +------------------+ +------------------+ |
| qemu-kvm | | qemu-kvm | | qemu-kvm | |
+------------------+ +------------------+ +------------------+ | User Space
---------------------------------------------------------------- |
| Host Virtual Address Space (HVA) | |
================================================================ <->
| | |
| Host OS | |
| | | Kernel
---------------------------------------------------------------- |
| Host Physical Address Space (HPA) | |
---------------------------------------------------------------- <->
*/
/*
+-----------------------------------+
| Guest Virtual Address (GVA) |
+-----------------------------------+
| Guest OS |
+-----------------------------------+
| Guest Physical Address (GPA) |
+-----------------------------------+
=====================================
+-----------------------------------+
| Host Virtual Address (HVA) |
+-----------------------------------+
| Host OS |
+-----------------------------------+
| Host Physical Address (HPA) |
+-----------------------------------+
+-----------------------------------+
| guest adress
| user address
| size
| mmap_offset
--> region->mmap_offset = mmap_addr + msg->mmap_offset - regions->guest_addr
--> gpa_to_hva()
--> hva = gpa_addr + region->mmap_offset
--> gva_to_hva()
--> hva = gva_addr + region->mmap_offset + region->guest_addr - region->user_addr
--> gva_to_gpa
--> guest virtual address, guest physical address
*/
uint64_t gpa_to_hva(struct virtio_dev *dev, uint64_t gpa_addr) {
int i = 0;
struct vhost_user_region *region;
for (i = 0; i < dev->mem->nregions; i++) {
region = &dev->mem->regions[i];
// 检查给定的 GPA 是否在当前内存区域的范围内
if (gpa_addr <= region->guest_address + region->size &&
gpa_addr >= region->guest_address) {
// 如果在范围内,返回对应的 HVA,加上 mmap_offset 偏移量
return gpa_addr + region->mmap_offset;
}
}
// 如果 GPA 不在任何内存区域的范围内,返回0表示转换失败
return 0;
}
/*
将全局虚拟地址(通常是与设备通信的地址)转换为本地虚拟地址,以便在本地系统中进行访问。它通过遍历设备的内存区域,
检查GVA地址是否位于某个内存区域的有效范围内,如果是,则根据映射关系计算出对应的HVA地址
*/
// 将全局虚拟地址(GVA)转换为本地虚拟地址(HVA)
uint64_t gva_to_hva(struct virtio_dev *dev, uint64_t gva_addr) {
int i = 0;
struct vhost_user_region *region;
// 遍历设备的内存区域
for (i = 0;i < dev->mem->nregions;i ++) {
region = &dev->mem->regions[i];
// 如果GVA地址在当前内存区域的有效范围内
if (gva_addr <= region->user_address + region->size &&
gva_addr >= region->user_address) {
// 计算HVA地址:GVA地址 + 区域的映射偏移 + 区域的guest_address - 区域的user_address
return gva_addr + region->mmap_offset +
region->guest_address - region->user_address;
}
}
// 如果未找到匹配的内存区域,返回0表示无效地址
return 0;
}
// msg -->
// 设置设备的内存表
int vhost_user_set_mem_table(struct virtio_dev *dev, struct vhost_user_msg *msg) {
// 如果设备的内存表为空,分配内存
if (!dev->mem) {
dev->mem = (struct vhost_user_mem*)malloc(sizeof(struct vhost_user_mem));
memset(dev->mem, 0, sizeof(struct vhost_user_mem));
}
// 从消息中获取内存信息
struct vhost_user_mem *memory = &msg->memory; // -->
// 设置设备的内存区域数量
dev->mem->nregions = memory->nregions; // 2
printf("内存区域数量:memory->nregions: %d\n", memory->nregions);
int i = 0;
for (i = 0;i < memory->nregions;i ++) {
// 复制内存区域信息
memcpy(&dev->mem->regions[i], &memory->regions[i], sizeof(struct vhost_user_region));
// 打印文件描述符和内存大小
printf("文件描述符: %d, 大小: %lx\n", msg->fds[i], dev->mem->regions[i].size);
printf("映射偏移: %lx\n", memory->regions[i].mmap_offset);
// 计算映射的总大小,并向上对齐到2^20字节的倍数
size_t size = dev->mem->regions[i].size + dev->mem->regions[i].mmap_offset;
size = ROUNDUP(size, 2 << 20);
// 使用mmap将文件描述符映射到内存
void *mmap_addr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED,
msg->fds[i], 0);
// 计算偏移地址
dev->mem->regions[i].mmap_offset = (uint64_t)mmap_addr + memory->regions[i].mmap_offset
- memory->regions[i].guest_address;
}
return 0;
}
int vhost_user_msg_handler(int connfd, struct vhost_user_msg *msg) {
switch (msg->request) {
case VHOST_USER_GET_FEATURES:
// 获取设备支持的特性
printf("获取特性:0x%lx\n", vhost_supported_featrues);
msg->num = vhost_supported_featrues;
msg->size = sizeof(vhost_supported_featrues);
msg->flags &= ~VHOST_USER_VERSION_MASK;
msg->flags |= VHOST_USER_VERSION;
msg->flags |= VHOST_USER_REPLY_MASK;
size_t count = offsetof(struct vhost_user_msg, num) + msg->size;
send(connfd, msg, count, 0);
break;
case VHOST_USER_SET_FEATURES:
// 设置设备特性
vhost_supported_featrues = msg->num;
printf("设置特性:0x%lx\n", vhost_supported_featrues);
break;
case VHOST_USER_SET_OWNER: //
// 设置设备所有者
printf("设置所有者:%d\n", msg->fds[0]);
virtiodev = (struct virtio_dev *)malloc(sizeof(struct virtio_dev));
memset(virtiodev, 0, sizeof(struct virtio_dev));
pthread_mutex_lock(&mtx);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);
//
// vhost_net
break;
case VHOST_USER_RESET_OWNER:
// 重置设备所有者
printf("重置所有者\n");
break;
case VHOST_USER_SET_MEM_TABLE: // 72
// 设置内存表
printf("设置内存表\n");
// backend --> virtio_dev
// qemu --> vhost_user_msg
vhost_user_set_mem_table(virtiodev, msg);
break;
case VHOST_USER_SET_LOG_BASE:
// 设置日志基地址
printf("设置日志基地址\n");
break;
case VHOST_USER_SET_LOG_FD:
// 设置日志文件描述符
printf("设置日志文件描述符\n");
break;
case VHOST_USER_SET_VRING_NUM:
// 设置虚拟环数量
printf("设置虚拟环数量:%d\n", msg->state.num);
virtiodev->vq[msg->state.index].num = msg->state.num;
break;
case VHOST_USER_SET_VRING_ADDR: { // msg -->
// 设置虚拟环地址
printf("设置虚拟环地址\n");
struct virtqueue *vq = &virtiodev->vq[msg->state.index];
vq->desc = (struct virtq_desc*)gva_to_hva(virtiodev, msg->addr.desc);
vq->avail = (struct virtq_avail*)gva_to_hva(virtiodev, msg->addr.avail);
vq->used = (struct virtq_used*)gva_to_hva(virtiodev, msg->addr.used);
//vq->log = (void*)gva_to_hva(virtiodev, msg->addr.log);
printf("索引:%d\n", msg->state.index);
printf("gva 描述:%lx,可用:%lx,已使用:%lx,日志:%lx\n",
msg->addr.desc, msg->addr.avail, msg->addr.used, msg->addr.log);
printf("hva 描述:%p,可用:%p,已使用:%p,描述地址:%lx\n",
vq->desc, vq->avail, vq->used, vq->desc->addr);
break;
}
case VHOST_USER_SET_VRING_BASE: {
// 设置虚拟环基地址
printf("设置虚拟环基地址:%d\n", msg->state.num);
virtiodev->vq[msg->state.index].last_used_idx = msg->state.num;
break;
}
case VHOST_USER_GET_VRING_BASE:
// 获取虚拟环基地址
printf("获取虚拟环基地址\n");
break;
/*
Kick事件:Kick事件通常与虚拟机监视器(后端)通知虚拟机(前端)有数据包或其他需要处理的事件相关。
这个事件的名称来自于前端需要"踢"或唤醒虚拟机以处理新到达的数据包。Kick事件通常包括一个文件描述符(FD),
该FD用于发送中断通知给虚拟机。当前端收到Kick事件时,它会检查vring以查看是否有新的数据包等待处理
*/
case VHOST_USER_SET_VRING_KICK: {
int index = msg->num & 0x00ff; //
int fd = 0;
if (msg->num & 0x100) { //
fd = -1;
} else {
fd = msg->fds[0];
}
printf("设置虚拟环Kick事件:%d,索引:%d\n", msg->fds[0], index);
if (virtiodev->vq[index].kickfd > 0) {
close(virtiodev->vq[index].kickfd);
}
virtiodev->vq[index].kickfd = fd;
break;
}
/*
Call事件:Call事件与Kick事件不同,它通常用于前端通知后端执行某种特定操作,而不是通知有新数据包到达。
这可以是需要协商或协调的操作,例如网络配置更改等。与Kick事件类似,Call事件也可以包括一个文件描述符,
用于发送通知给虚拟机监视器。当前端收到Call事件时,它执行所需的操作或通知后端执行操作
*/
case VHOST_USER_SET_VRING_CALL: {
int index = msg->num & 0x00ff; //
int fd = 0;
if (msg->num & 0x100) { //
fd = -1;
} else {
fd = msg->fds[0];
}
printf("设置虚拟环Call事件:%d,索引:%d\n", msg->fds[0], index);
if (virtiodev->vq[index].callfd > 0) {
close(virtiodev->vq[index].callfd);
}
virtiodev->vq[index].callfd = fd;
break;
}
case VHOST_USER_SET_VRING_ERR:
// 设置虚拟环错误事件
printf("设置虚拟环错误事件\n");
break;
case VHOST_USER_GET_PROTOCOL_FEATURES:
// 获取协议特性
printf("获取协议特性\n");
break;
case VHOST_USER_SET_PROTOCOL_FEATURES:
// 设置协议特性
printf("设置协议特性\n");
break;
case VHOST_USER_GET_QUEUE_NUM:
// 获取队列数量
printf("获取队列数量\n");
break;
case VHOST_USER_SET_VRING_ENABLE:
// 获取虚拟环启用状态
printf("获取虚拟环启用状态\n");
break;
case VHOST_USER_SEND_RARP:
// 发送RARP请求
printf("发送RARP请求\n");
break;
case VHOST_USER_NET_SET_MTU:
// 设置网络接口最大传输单元(MTU)
printf("设置网络接口最大传输单元(MTU)\n");
break;
}
}
#define MAX_PKT_BURST 256
#define MBUF_DATA_LENGTH 1024
/*
data:一个指向数据包实际内容的指针。这个指针通常指向数据包的起始位置,允许对数据包的内容进行读取和处理。
len:表示数据包的长度,即数据包的字节数。它记录了数据包内容的大小。
hdr:一个struct virtio_net_hdr类型的结构体,用于存储虚拟网络头部信息。虚拟网络头部通常包含了一些与网络相关的元数据,例如源和目标MAC地址、协议类型等信息。
*/
struct mbuf {
unsigned char *data; // 数据指针,指向数据包的内容
uint32_t len; // 数据包的长度
struct virtio_net_hdr hdr; // 虚拟网络头部
};
struct mbuf *vhost_new_mbuf(void) {
// 分配并初始化一个 mbuf 结构
struct mbuf *m = (struct mbuf*)malloc(sizeof(struct mbuf));
if (!m) {
return NULL; // 分配失败,返回 NULL
}
// 为 mbuf 中的数据字段分配内存
m->data = (unsigned char*)malloc(MBUF_DATA_LENGTH);
if (!m->data) {
free(m); // 分配失败,释放之前分配的 mbuf 结构内存
return NULL;
}
// 初始化 mbuf 结构的数据字段长度和其他属性
m->len = MBUF_DATA_LENGTH;
return m; // 返回指向新创建的 mbuf 结构的指针
}
void vhost_free_mbuf(struct mbuf *m) {
if (m->data) free(m->data);
free(m);
return ;
}
/*
作用是从虚拟队列中的描述符复制数据包的头部和内容到 mbuf 结构中,以便进一步处理数据包。具体步骤如下:
获取指定 desc_idx 的描述符(struct virtq_desc)。
计算数据包头部的长度,通常由 hdrlen 表示。
使用 gpa_to_hva 函数将描述符的物理地址转换为虚拟地址,以便读取描述符的内容。
计算数据包的长度,即描述符的总长度减去头部长度。
使用 memcpy 函数将数据包的头部复制到 mbuf->hdr 中。
如果数据包长度大于0,则设置 mbuf->len 为数据包长度,并将数据包的内容复制到 mbuf->data 中。
如果数据包长度为0,可以根据需要执行适当的处理。
最后,打印相关信息,包括描述符地址、长度等,用于调试和分析
*/
int copy_desc_to_mbuf(struct virtio_dev *dev, struct virtqueue *vq,
struct mbuf *mbuf, uint16_t desc_idx) {
#if 0
// 这部分代码似乎被注释掉了,用于示例目的
memset(mbuf->data, 'A', 14);
mbuf->len = 14;
#else
struct virtq_desc *desc = &vq->desc[desc_idx]; // 获取描述符
uint32_t hdrlen = sizeof(struct virtio_net_hdr); // 获取头部长度
void *desc_addr = (void*)gpa_to_hva(dev, desc->addr); // 获取描述符的地址
uint32_t desc_pkt_len = desc->len - hdrlen; // 计算数据包的长度
memcpy(&mbuf->hdr, desc_addr, hdrlen); // 复制数据包头部到 mbuf 中
if (desc_pkt_len > 0) {
mbuf->len = desc_pkt_len; // 设置 mbuf 的长度为数据包长度
memcpy(mbuf->data, ((unsigned char *)desc_addr) + hdrlen, desc_pkt_len); // 复制数据包内容到 mbuf 中
} else if (desc_pkt_len == 0) {
// 数据包长度为0,可以执行适当的处理
}
printf("desc->addr: %lx, %p, desc->len: %d, len: %d\n",
desc->addr, desc_addr, desc->len, mbuf->len);
#endif
}
/*
从虚拟网络设备的队列中传输数据包到pkts数组中的mbuf结构中,并更新已使用环的信息。
具体的实现可能需要依赖一些外部函数和结构体,例如vhost_new_mbuf和copy_desc_to_mbuf
这些函数的实现应该在其他地方提供。
*/
int vhost_tx(struct virtio_dev *dev, struct mbuf *pkts[], uint16_t npkts) {
// 如果虚拟设备的内存未初始化,返回0
if (!dev->mem) return 0;
// 设置队列索引,这里使用1
const int qidx = 1;
// 获取与队列相关的数据结构
struct virtqueue *vq = &dev->vq[qidx];
// 检查队列描述符、可用环以及已使用环是否为空,如果为空则返回0
if (vq->desc == NULL || vq->avail == NULL || vq->used == NULL) return 0;
// 根据可用环中的索引,获取对应的描述符索引
uint16_t desc_idx[MAX_PKT_BURST] = {0};
int i = 0;
for (i = 0; i < npkts; i++) {
desc_idx[i] = vq->avail->ring[(vq->last_used_idx + i) % vq->num];
}
// 遍历数据包,为每个数据包分配并配置一个mbuf
for (i = 0; i < npkts; i++) {
pkts[i] = vhost_new_mbuf();
if (!pkts[i]) break;
// 将描述符数据复制到mbuf中
copy_desc_to_mbuf(dev, vq, pkts[i], desc_idx[i]);
// 更新已使用环的索引和信息
uint32_t used_idx = (vq->last_used_idx++) % vq->num;
vq->used->ring[used_idx].id = desc_idx[i];
vq->used->ring[used_idx].len = 0;
}
// 更新已使用环的索引
vq->used->idx += i;
// 返回成功传输的数据包数量
return i;
}
/*
将一个网络数据包从 mbuf 结构复制到虚拟队列中的描述符中,以便在虚拟化网络设备中传输。具体步骤如下:
获取要操作的描述符(virtq_desc)。
计算网络数据包头部的长度,通常由 hdrlen 表示。
使用 gpa_to_hva 函数将描述符的物理地址转换为虚拟地址,以便写入数据。
使用 memset 函数清空描述符的头部,通常头部用于存储头部信息。
检查数据包长度是否等于头部长度。如果等于,表示数据包内容和头部都在同一个描述符中,需要处理下一个描述符。
获取下一个描述符的索引。
获取下一个描述符,并再次将其地址转换为虚拟地址。
使用 memcpy 函数将数据包的内容复制到描述符中。
*/
static int copy_mbuf_to_desc(struct virtio_dev *dev, struct virtqueue *vq,
struct mbuf *mbuf, uint32_t desc_idx) {
struct virtq_desc *desc;
// 获取网络数据包头部的长度
size_t hdrlen = sizeof(struct virtio_net_hdr);
// 获取要操作的描述符
desc = &vq->desc[desc_idx];
// 将描述符的地址转换为虚拟地址,以便写入数据
void *addr = (void *)gpa_to_hva(dev, desc->addr);
// 清空描述符的头部,通常是预留给头部信息的空间
memset(addr, 0, hdrlen);
// 检查数据包长度是否等于头部长度
if (desc->len - hdrlen == 0) {
// 如果数据包长度减去头部长度等于0,表示数据包内容和头部在同一个描述符中
// 获取下一个描述符的索引
desc_idx = desc->next;
// 获取下一个描述符
desc = &vq->desc[desc_idx];
// 再次将描述符的地址转换为虚拟地址
addr = (void *)gpa_to_hva(dev, desc->addr);
// 将数据包的内容复制到描述符中
memcpy(addr, mbuf->data, mbuf->len);
}
return 0; // 返回0表示操作成功
}
/*
从虚拟队列接收一定数量的数据包,将这些数据包存储在 pkts 数组中,并更新虚拟队列的状态以通知虚拟机。具体步骤如下:
获取虚拟队列 vq 的可用环中的索引以确定要处理的数据包数量。这是通过计算可用环中索引的差值来实现的。
限制要处理的数据包数量不超过可用的数据包数量和最大数据包数量 MAX_PKT_BURST。
如果没有要处理的数据包,直接返回0。
遍历要处理的数据包数量,从可用环中获取相应的描述符索引,并存储在 desc_idx 数组中。
循环处理每个数据包,将数据包从 mbuf 结构复制到描述符中,同时更新已使用的描述符和长度。
更新已使用的索引 vq->used->idx,表示已使用的描述符数量。
最后,使用 eventfd_write 函数触发虚拟队列的事件,通知虚拟机已有数据包可用。
*/
int vhost_rx(struct virtio_dev *dev, int qidx,
struct mbuf *pkts[], int npkts) {
struct virtqueue *vq = &dev->vq[qidx];
uint32_t desc_idx[MAX_PKT_BURST] = {0};
// 获取可用环中的索引以确定要处理的数据包数量
uint16_t avail_idx = *((uint16_t*)&vq->avail->idx);
int navail = avail_idx - vq->last_used_idx;
npkts = min(npkts, navail); // 确保不超过可用的数据包数量
npkts = min(npkts, MAX_PKT_BURST); // 限制在最大数据包数量内
if (npkts == 0) return 0; // 如果没有要处理的数据包,直接返回
int i = 0;
for (i = 0; i < npkts; i++) {
// 获取可用环中的描述符索引
desc_idx[i] = vq->avail->ring[(vq->last_used_idx + i) & (vq->num - 1)];
}
uint16_t used_idx = 0;
for (i = 0; i < npkts; i++) {
// 将数据包存储到描述符中
copy_mbuf_to_desc(dev, vq, pkts[i], desc_idx[i]);
// 更新已使用的描述符和长度
used_idx = (vq->last_used_idx++) & (vq->num - 1);
vq->used->ring[used_idx].id = desc_idx[i];
vq->used->ring[used_idx].len = pkts[i]->len + sizeof(struct virtio_net_hdr);
}
// 更新已使用的索引
vq->used->idx += i;
// 触发虚拟队列的事件,通知虚拟机已有数据包可用
eventfd_write(vq->callfd, 1);
return i; // 返回处理的数据包数量
}
static int recvfrom_peer(int fd, struct mbuf **mbuf) {
int rc;
struct mbuf *m;
// 创建一个新的 mbuf 结构来存储接收的数据
m = vhost_new_mbuf();
if (!m) {
printf("no mbuf\n");
exit(-1);
}
// 从文件描述符 fd 中读取数据到 mbuf 的数据字段中
rc = read(fd, m->data, m->len);
if (rc < 0) {
perror("read");
vhost_free_mbuf(m); // 释放 mbuf 结构内存
return 0;
}
// 更新 mbuf 结构中的数据长度字段
m->len = rc;
// 将 mbuf 指针存储到传递给函数的指针 mbuf 中
*mbuf = m;
return 1;
}
/*
在TUN/TAP接口和virtiodev之间实现数据的双向传输。它使用了多线程和异步I/O技术来处理网络数据。
需要注意的是,这段代码依赖于一些外部函数和结构体,例如vhost_tx、recvfrom_peer、vhost_rx和mbuf等,
这些函数和结构体的定义和实现应该在其他地方提供
*/
void *vhost_user_vnet_start(void *arg) {
printf("vhost_user_vnet_start --> \n");
// 加锁互斥量,等待virtiodev被初始化
pthread_mutex_lock(&mtx);
while (virtiodev == NULL) {
pthread_cond_wait(&cond, &mtx);
}
pthread_mutex_unlock(&mtx);
// 创建并配置TUN/TAP接口
int fd = tun_alloc("vnet0");
if (fd < 0) {
perror("tap");
}
// 配置用于poll的文件描述符,设置可读写属性
struct pollfd pfd = {0};
pfd.fd = fd;
pfd.events = POLLIN | POLLOUT;
printf("vhost_user_vnet_start --> \n");
while (1) { // 从fd读取数据并发送到virtiodev(qemu中的虚拟网卡)
int ret = poll(&pfd, 1, -1);
if (ret < 0) {
usleep(1); // 等待1微秒
continue;
}
// 如果pfd标记为POLLOUT,执行以下操作(发送数据到virtiodev)
if (pfd.revents & POLLOUT) {
struct mbuf *m;
int np = vhost_tx(virtiodev, &m, 1);
if (np == 0) {
usleep(1); // 等待1微秒
continue;
}
// 输出发送的数据
printf("mbuf: %s, len: %d\n", m->data, m->len);
// 将数据写入TUN/TAP接口
int ret = write(fd, m->data, m->len);
if (ret < 0) {
perror("write");
printf("errno: %d\n", errno);
usleep(1);
continue;
}
}
// 如果pfd标记为POLLIN,执行以下操作(接收数据)
if (pfd.revents & POLLIN) {
struct mbuf *m;
int np = recvfrom_peer(fd, &m);
if (np > 0) {
// 将接收到的数据传递给virtiodev(qemu中的虚拟网卡)
vhost_rx(virtiodev, 0, &m, 1);
vhost_free_mbuf(m);
}
}
usleep(1); // 等待1微秒
}
}
int main(int argc, char **argv) {
// 检查命令行参数是否足够
if (argc < 2) return -1;
// 创建Unix域套接字
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
// 初始化Unix域套接字地址结构
struct sockaddr_un sa;
memset(&sa, 0, sizeof(struct sockaddr_un));
sa.sun_family = AF_UNIX;
sprintf(sa.sun_path, "%s", argv[1]);// /tmp/vhost.sock
// 将套接字绑定到Unix域套接字地址
int rc = bind(sockfd, (struct sockaddr*)&sa, sizeof(struct sockaddr_un));
if (rc < 0) return -2;
// 监听套接字
listen(sockfd, 1024);
// 创建一个线程
pthread_t tid;
pthread_create(&tid, NULL, vhost_user_vnet_start, NULL);
// 打印信息
printf("/tmp/vhost.sock --> accept\n");
// 接受客户端连接
int clientfd = accept(sockfd, 0, 0);
// 打印客户端连接信息
printf("/tmp/vhost.sock --> accept clientfd: %d\n", clientfd);
// 定义消息头大小
size_t hdrsz = offsetof(struct vhost_user_msg, num);
// 进入主循环
while (1) {
struct vhost_user_msg msg = {0};
// 设置消息头的接收缓冲区
struct iovec iov;
iov.iov_base = &msg; // 缓冲区的起始地址
iov.iov_len = hdrsz; // 缓冲区的长度
// 设置消息头的接收控制信息
struct msghdr msgh;
size_t fdsize = sizeof(msg.fds);
char control[CMSG_SPACE(fdsize)];
memset(&msgh, 0, sizeof(struct msghdr));
msgh.msg_iov = &iov;
msgh.msg_iovlen = 1;
msgh.msg_control = control;
msgh.msg_controllen = sizeof(control);
// 接收消息
int rc = recvmsg(clientfd, &msgh, 0);
if (rc <= 0) {
perror("recvmsg");
break;
}
if (msgh.msg_flags & (MSG_TRUNC | MSG_CTRUNC)) {
break;
}
// 处理控制消息
struct cmsghdr *cmsg;
for (cmsg = CMSG_FIRSTHDR(&msgh);cmsg != NULL;cmsg = CMSG_NXTHDR(&msgh, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET && (cmsg->cmsg_type == SCM_RIGHTS)) {
memcpy(msg.fds, CMSG_DATA(cmsg), fdsize);
}
}
// 打印请求信息
printf("request: %d, flags: %d, size: %d\n",
msg.request, msg.flags, msg.size);
// 如果有数据需要接收
if (msg.size > 0) {
int rc = recv(clientfd, &msg.num, msg.size, 0);
if (rc != msg.size) {
perror("recv");
}
}
// 调用消息处理函数
vhost_user_msg_handler(clientfd, &msg);
}
}
vhost 启动命令
./vhost /tmp/vhost.sock
QUME启动虚拟机命令
qemu-system-x86_64 -enable-kvm -m 512 -object memory-backend-file,id=mem0,size=512M,mem-path=/mnt/huge/,share=on -numa node,memdev=mem0 -chardev socket,id=vhost0,path=/tmp/vhost.sock -netdev vhost-user,id=user0,chardev=vhost0 -device virtio-net-pci,id=net0,netdev=user0 -drive file=/home/king/share/ovs/img/tinycore.raw,format=raw -cdrom /home/king/share/ovs/img/TinyCore-current.iso