自己实现一个简单的vhost-net

news2024/11/17 13:24:27

框架

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

结果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1025582.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

期权是什么?一分钟带你玩转期权策略!

很多人问我期权是什么&#xff0c;这个问题怎么回答呢&#xff1f;首先期权是一种交易模式&#xff0c;如同股票期货一样&#xff0c;但它又不同于股票和期货&#xff0c;因为它有自己的交易规则和特性&#xff0c;期权更多是一种工具&#xff0c;可以做空大盘对冲下跌风险&…

0018Java程序设计-springboot智慧环卫养管作业平台

文章目录 摘 要目 录系统设计开发环境 摘 要 本智慧环卫养管作业平台就是建立在充分利用现在完善科技技术这个理念基础之上&#xff0c;并使用IT技术进行对环卫养管作业的管理&#xff0c;从而保证环卫养管作业能够高效的进行&#xff0c;可以实现环卫养管作业的在线管理&…

健康云HIS系统源码,满足基层医疗机构业务需求,提供挂号支持、病患问诊、电子病历、开药发药、会员管理、统计查询、医生站和护士站等功能

云his系统源码 二级医院HIS系统全套源代码 自主研发&#xff0c;自主版权 一款满足基层医疗机构各类业务需要的健康云HIS系统。该系统能帮助基层医疗机构完成日常各类业务&#xff0c;提供病患挂号支持、病患问诊、电子病历、开药发药、会员管理、统计查询、医生站和护士站等一…

wabp.m 代码注释(便于算法快速理解)

算法效果: 波峰和起点检测效果: function [r,pk] = wabp(Araw, Offset,Scale, Fs) % r = wabp(Araw,Offset,Scale, Fs); % Input: Araw (125Hz sampled) waveform in wfdb-MIT format, % Offset, Scale % Araw = 血压波形 % Offset=偏移(信号减去或者加上偏移恢复成…

启山智软/O2O商城

文章目录 启山智软介绍一、O2O商业模式是什么二、启山智软O2O商城管理系统1.O2O商城系统2.多种商业形态的O2O商城系统1、类似蜜雪冰城的合作加盟模式2、类似优衣库的连锁直营模式3、类似京东到家的同城/本地服务平台 O2O商城开发具备的特色功能&#xff1a;部分源码分享 启山智…

MTBF、MTTR、MTTA 和 MTTF

了解一些最常见的事件指标 在当今永不停机的世界中&#xff0c;中断和技术事件比以往任何时候都更加重要。故障和停机期间会带来现实后果&#xff0c;错过截止时间、付款逾期、项目延迟。 这就是为什么公司必须量化和跟踪有关正常运行时间、停机期间以及团队解决问题的速度和…

改变latex单张页面宽度的正确做法

https://tex.stackexchange.com/questions/6834/change-paper-size-in-mid-document#comment115838_6838 首先注意&#xff0c;网上所有有关newgeometry的说明都是不可行的&#xff0c;因为 画红圈的地方大家自行阅读&#xff0c;这就是原因。 其次&#xff0c;更改页面宽度的…

一、Vuex相关概念和使用

目录 Vuex的概念核心概念Vuex 的使用场景1、组件之间的数据共享2、复杂状态的管理3、异步操作的处理 Vuex的概念 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态&#xff0c;并以相应的规则保证状态以一种可预测的方式发生变化…

数据研发“新人”如何快速落地?

作者&#xff1a;肖迪(墨诩) 一、前言 这个季度主推安全月构筑&夯实稳定性底盘&#xff0c;就组织了组里的同学对核心业务链路进行了稳定性的摸排。在摸排过程中&#xff0c;不断有个声音在问你摸排出来的问题就是全部问题么&#xff1f;你加的监控加全了么&#xff1f;你…

【Git】一个完整的git项目之代码管理

1.版本库初始化 1.1 git clone 这是一种较为简单的初始化方式&#xff0c;当项目已经有了一个远程的Git版本库&#xff0c;只需在本地克隆一份代码。如&#xff1a; git clone http://192.168.x.x/github.com/someone/some_project.git some_project 上面的命令是将 htt…

SpringBoot自动装配原理及分析

一、什么是自动装配 在使用SpringBoot的时候&#xff0c;会自动将Bean装配到IoC容器中。例如我们在使用Redis数据库的时候&#xff0c;会引入依赖spring-boot-starter-data-redis。在引入这个依赖后&#xff0c;服务初始化的时候&#xff0c;会将操作Redis需要的组件注入到IoC…

给你下单前,磨练你无数次的国外客户

前段时间给几个客户做了不少方案设计和报价后都没有下文&#xff0c;给我做项目的设计师都对我没信心了&#xff0c;多少有点抱怨&#xff0c;就说我是雷声大雨点小。再有要做设计的图&#xff0c;就不会像之前那样热心了。 说真的&#xff0c;多少有点受挫。前天那个咨询了无…

log4j2 日志保存至数据库

文章目录 概述一、springmvc工程1.创建数据库日志表2.log4j2.xml引入JDBCAppender3.定义日志管理类4.编写日志输出代码5.运行结果6.完整代码 二、springboot工程1. 创建数据库日志表2.log4j2.xml引入JDBCAppender3.定义日志管理类4. 遗留问题5. 解决办法6. 完整代码 概述 Apac…

求臻医学:结直肠癌患者必看的就诊指南及基因检测意义

结直肠癌是常见的消化道肿瘤之一&#xff0c;已跃居我国高发恶性肿瘤第2位&#xff0c;且其发病率、死亡率逐年上升。数据显示&#xff0c;2020年新发病例 55.5 万&#xff0c;死亡病例 28.6 万。本文系统归纳总结了结直肠癌患者应该选择哪些诊科室、相关检查、治疗方式、预后预…

数字IC设计系列----单端口RAM、双端口RAM、同步FIFO、异步FIFO

一、单端口RAM原理及实现 1.1、概念/原理 在内存空间中开辟出一段固定大小的内存用于存储数据&#xff0c;每一个数据所占的bit位称之为位宽&#xff0c;这段内存空间中数据的总数称之为深度。例如reg [7:0] mem [255:0]&#xff0c;这段内存空间中每一个数据的位宽为8bit&am…

VS2019中使用printf函数报错处理方法

VS2019中使用printf函数报错处理方法 在使用vs2019学习OpenCV的过程中&#xff0c;使用简单的printf函数&#xff0c;竟然编译不过去&#xff0c;VS2019报错&#xff1b; 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 C4996 ‘sprintf’: This function or variable may…

基础概念回顾:云原生应用交付

原文链接&#xff1a;基础概念回顾&#xff1a;云原生应用交付 转载来源&#xff1a;NGINX 开源社区 NGINX 唯一中文官方社区 &#xff0c;尽在 nginx.org.cn 尽管云原生应用开发诞生于 21 世纪初&#xff0c;但是在术语使用方面还是非常混乱。本文将带您了解常见的术语和问题。…

图像处理领域之►边缘检测大合集◄【应该是全网仅有的了吧】

图像处理领域之►边缘检测‧大合集◄ 概述 {\color{Brown}概述} 概述 数据集 {\color{Purple}数据集} 数据集 实践 {\color{Red}实践} 实践 深度学习方法 {\color{Blue} 深度学习方法} 深度学习方法 机器学习方法 {\color{Blue} 机器学习方法} 机器学习方法 基于传统方法 {\col…

如何隐藏或修改Docker容器中的Nginx响应头中的Server

背景介绍 现在大部分项目通过Nginx作为反向代理&#xff0c;实际由于安全审计要求需要隐藏或修改响应头的Server信息&#xff0c;传统的项目直接部署在nginx服务器中&#xff0c;只需要在nginx服务器安装ngx_http_headers_more_filter_module插件&#xff0c;然后通过修改ngin…

Linux服务器占用处理手记

磁盘占用定位处理 查看磁盘占用情况&#xff1a; df -h 查看每个目录的占用情况&#xff1a; du -h -x --max-depth1 查找大文件和目录 du -sh /* du -sh /home/* 可参考&#xff1a; Linux垃圾清理指北_linux 清理垃圾_智商二五零_的博客-CSDN博客 查看CPU和内存占用情…