2018-01-24

占个坑,准备下写vhost-user的东西


vhost-user是vhost-kernel又回到用户空间的实现,其基本思想和vhost-kernel很类似,不过之前在内核的部分现在有另外一个用户进程代替,可能是snapp或者dpdk等。在网上看相关资料较少,就简单介绍下。虽然和vhost-kernel实现的目标一致,但是具体的实现方式却有所不同。vhost-user下,UNIX本地socket代替了之前kernel模式下的设备文件进行进程间的通信(qemu和vhost-user app),而通过mmap的方式把ram映射到vhost-user app的进程空间实现内存的共享。其他的部分和vhost-kernel原理基本一致。这种情况下一般qemu作为client,而vhost-user app作为server如DPDK。而本文对于vhost-user server端的分析主要也是基于DPDK源码。本文主要分析涉及到的三个重要机制:qemu和vhost-user app的消息传递,guest memory和vhost-user app的共享,guest和vhost-user app的通知机制。

      

一、qemu和vhost-user app的消息传递

qemu和vhost-user app的消息传递是通过UNIX本地socket实现的,对应于kernel下每个ioctl的实现,这里vhost-user app必须对每个ioctl 提供自己的处理,DPDK下在vhost-user.c文件下的vhost_user_msg_handler函数,这里有一个核心的数据结构:VhostUserMsg,该结构是消息传递的载体,整个结构并不复杂

typedef struct VhostUserMsg {
union {
VhostUserRequest master;//qemu
VhostUserSlaveRequest slave;//dpdk
} request; #define VHOST_USER_VERSION_MASK 0x3
#define VHOST_USER_REPLY_MASK (0x1 << 2)
#define VHOST_USER_NEED_REPLY (0x1 << 3)
uint32_t flags;
uint32_t size; /* the following payload size */
union {
#define VHOST_USER_VRING_IDX_MASK 0xff
#define VHOST_USER_VRING_NOFD_MASK (0x1<<8)
uint64_t u64;
struct vhost_vring_state state;
struct vhost_vring_addr addr;
VhostUserMemory memory;
VhostUserLog log;
struct vhost_iotlb_msg iotlb;
} payload;
int fds[VHOST_MEMORY_MAX_NREGIONS];
} __attribute((packed)) VhostUserMsg;

既然是传递消息,其中必须包含消息的种类、消息的内容、消息内容的大小。而这些也是该结构的主要部分,首个union便标志该消息的种类。接下来的Flags表明该消息本身的一些性质,如是否需要回复等。size就是payload的大小,接下来的union是具体的消息内容,最后的fds是关联每一个memory RAM的fd数组。消息种类如下:

typedef enum VhostUserRequest {
VHOST_USER_NONE = ,
VHOST_USER_GET_FEATURES = ,
VHOST_USER_SET_FEATURES = ,
VHOST_USER_SET_OWNER = ,
VHOST_USER_RESET_OWNER = ,
VHOST_USER_SET_MEM_TABLE = ,
VHOST_USER_SET_LOG_BASE = ,
VHOST_USER_SET_LOG_FD = ,
VHOST_USER_SET_VRING_NUM = ,
VHOST_USER_SET_VRING_ADDR = ,
VHOST_USER_SET_VRING_BASE = ,
VHOST_USER_GET_VRING_BASE = ,
VHOST_USER_SET_VRING_KICK = ,
VHOST_USER_SET_VRING_CALL = ,
VHOST_USER_SET_VRING_ERR = ,
VHOST_USER_GET_PROTOCOL_FEATURES = ,
VHOST_USER_SET_PROTOCOL_FEATURES = ,
VHOST_USER_GET_QUEUE_NUM = ,
VHOST_USER_SET_VRING_ENABLE = ,
VHOST_USER_SEND_RARP = ,
VHOST_USER_NET_SET_MTU = ,
VHOST_USER_SET_SLAVE_REQ_FD = ,
VHOST_USER_IOTLB_MSG = ,
VHOST_USER_MAX
} VhostUserRequest;

到目前为止并不复杂,我们下面看下消息本身的初始化机制,socket-file的路径会作为参数传递进来,在main函数中examples/vhost/,调用us_vhost_parse_socket_path对参数中的socket-fiile参数进行解析,保存在静态数组socket_files中,而后在main函数中有一个for循环,针对每个socket-file,会调用rte_vhost_driver_register函数注册vhost 驱动,该函数的核心功能就是为每个socket-fie创建本地socket,通过create_unix_socket函数。vhost中的socket结构通过create_unix_socket描述。在注册驱动之后,会根据具体的特性设置features。在最后会通过rte_vhost_driver_start启动vhost driver,该函数倒是值得一看:

int
rte_vhost_driver_start(const char *path)
{
struct vhost_user_socket *vsocket;
static pthread_t fdset_tid; pthread_mutex_lock(&vhost_user.mutex);
vsocket = find_vhost_user_socket(path);
pthread_mutex_unlock(&vhost_user.mutex); if (!vsocket)
return -;
/*创建一个线程监听fdset*/
if (fdset_tid == ) {
int ret = pthread_create(&fdset_tid, NULL, fdset_event_dispatch,
&vhost_user.fdset);
if (ret < )
RTE_LOG(ERR, VHOST_CONFIG,
"failed to create fdset handling thread");
} if (vsocket->is_server)
return vhost_user_start_server(vsocket);
else
return vhost_user_start_client(vsocket);
}

函数参数是对应的socket-file的路径,进入函数内部,首先便是根据路径通过find_vhost_user_socket函数找到对应的vhost_user_socket结构,所有的vhost_user_socket以一个数组的形式保存在vhost_user数据结构中。接下来如果该socket确实存在,就创建一个线程,处理vhost-user的fd,这个作用我们后面再看,该线程绑定的函数为fdset_event_dispatch。这些工作完成后,就启动该socket了,起始qemu和vhost可以互做server和client,一般情况下vhsot是作为server存在。所以这里就调用了vhost_user_start_server。这里就是我们常见的socket编程操作了,调用bind……然后listen……,没什么好说的。后面调用了fdset_add函数,这是就是vhost处理消息fd的一个单独的机制,

int
fdset_add(struct fdset *pfdset, int fd, fd_cb rcb, fd_cb wcb, void *dat)
{
int i; if (pfdset == NULL || fd == -)
return -; pthread_mutex_lock(&pfdset->fd_mutex);
i = pfdset->num < MAX_FDS ? pfdset->num++ : -;
if (i == -) {
fdset_shrink_nolock(pfdset);
i = pfdset->num < MAX_FDS ? pfdset->num++ : -;
if (i == -) {
pthread_mutex_unlock(&pfdset->fd_mutex);
return -;
}
} fdset_add_fd(pfdset, i, fd, rcb, wcb, dat);
pthread_mutex_unlock(&pfdset->fd_mutex); return ;
}

简单来说就是该函数为对应的fd注册了一个处理函数,当该fd有信号时,就调用该函数,这里就是vhost_user_server_new_connection。具体是如何实现的呢?看下fdset_add_fd函数

static void
fdset_add_fd(struct fdset *pfdset, int idx, int fd,
fd_cb rcb, fd_cb wcb, void *dat)
{
struct fdentry *pfdentry = &pfdset->fd[idx];
struct pollfd *pfd = &pfdset->rwfds[idx]; pfdentry->fd = fd;
pfdentry->rcb = rcb;
pfdentry->wcb = wcb;
pfdentry->dat = dat; pfd->fd = fd;
pfd->events = rcb ? POLLIN : ;
pfd->events |= wcb ? POLLOUT : ;
pfd->revents = ;
}

这里分成了两部分,一个是fdentry,一个是pollfd。前者保存具体的信息,后者用作poll操作,方便线程监听fd。参数中函数指针为第三个参数,所以这里pfd->events就是POLLIN。那么在会到处理线程的处理函数fdset_event_dispatch中,该函数会监听vhost_user.fdset中的rwfds,当某个fd有信号时,则进入处理流程

if (rcb && pfd->revents & (POLLIN | FDPOLLERR))
rcb(fd, dat, &remove1);
if (wcb && pfd->revents & (POLLOUT | FDPOLLERR))
wcb(fd, dat, &remove2);

这里的rcb便是前面针对fd注册的回调函数。再次回到vhost_user_server_new_connection函数中,当某个fd有信号时,这里指对应socket-file的fd,则该函数被调用,建立连接,然后调用vhost_user_add_connection函数。既然连接已经建立,则需要对该连接进行vhost的一些设置了,包括创建virtio_net设备附加到连接上,设置device名字等等。而关键的一步是为该fd添加回调函数,刚才的回调函数用于建立连接,在连接建立后就需要设置函数处理socket的msg了,这里便是vhost_user_read_cb。到这里正式进入msg的部分。该函数中调用了vhost_user_msg_handler,而该函数正是处理socket msg的核心函数。到这里消息处理的部分便介绍完成了。

二、guest memory和vhost-user app的共享

虽然qemu和vhost通过socket建立了联系,但是这信息量毕竟有限,重点是要传递的数据,难不成通过socket传递的??当然不是,如果这样模式切换和数据复制估计会把系统撑死……这里主要也是用到共享内存的概念。核心机制和vhost-kernel类似,qemu也需要把guest的内存布局通过MSG传递给vhost-user,那么我们就从这里开始分析,在函数vhost_user_msg_handler中

    case VHOST_USER_SET_MEM_TABLE:
ret = vhost_user_set_mem_table(dev, &msg);
break;

在分析函数之前我们先看下几个数据结构

/*对应qemu端的region结构*/
typedef struct VhostUserMemoryRegion {
uint64_t guest_phys_addr;//GPA of region
uint64_t memory_size; //size
uint64_t userspace_addr;//HVA in qemu process
uint64_t mmap_offset; //offset
} VhostUserMemoryRegion; typedef struct VhostUserMemory {
uint32_t nregions;//region num
uint32_t padding;
VhostUserMemoryRegion regions[VHOST_MEMORY_MAX_NREGIONS];//All region
} VhostUserMemory;

在vhsot端,对应的数据结构为

struct rte_vhost_mem_region {
uint64_t guest_phys_addr;//GPA of region
uint64_t guest_user_addr;//HVA in qemu process
uint64_t host_user_addr;//HVA in vhost-user
uint64_t size;//size
void *mmap_addr;//mmap base Address
uint64_t mmap_size;
int fd;//relative fd of region
};

意义都比较容易理解就不在多说,在virtio_net结构中保存有指向当前连接对应的memory结构rte_vhost_memory

struct rte_vhost_memory {
uint32_t nregions;
struct rte_vhost_mem_region regions[];
};

OK,下面看代码,代码虽然较多,但是意义都比较容易理解,只看核心部分吧:

dev->mem = rte_zmalloc("vhost-mem-table", sizeof(struct rte_vhost_memory) +
sizeof(struct rte_vhost_mem_region) * memory.nregions, );
if (dev->mem == NULL) {
RTE_LOG(ERR, VHOST_CONFIG,
"(%d) failed to allocate memory for dev->mem\n",
dev->vid);
return -;
}
/*region num*/
dev->mem->nregions = memory.nregions; for (i = ; i < memory.nregions; i++) {
/*fd info*/
fd = pmsg->fds[i];
reg = &dev->mem->regions[i];
/*GPA of specific region*/
reg->guest_phys_addr = memory.regions[i].guest_phys_addr;
/*HVA in qemu address*/
reg->guest_user_addr = memory.regions[i].userspace_addr;
reg->size = memory.regions[i].memory_size;
reg->fd = fd;
/*offset in region*/
mmap_offset = memory.regions[i].mmap_offset;
mmap_size = reg->size + mmap_offset; /* mmap() without flag of MAP_ANONYMOUS, should be called
* with length argument aligned with hugepagesz at older
* longterm version Linux, like 2.6.32 and 3.2.72, or
* mmap() will fail with EINVAL.
*
* to avoid failure, make sure in caller to keep length
* aligned.
*/
alignment = get_blk_size(fd);
if (alignment == (uint64_t)-) {
RTE_LOG(ERR, VHOST_CONFIG,
"couldn't get hugepage size through fstat\n");
goto err_mmap;
}
/*对齐*/
mmap_size = RTE_ALIGN_CEIL(mmap_size, alignment);
/*执行映射,这里就是本进程的虚拟地址了,为何能映射另一个进程的文件描述符呢?*/
mmap_addr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, fd, ); if (mmap_addr == MAP_FAILED) {
RTE_LOG(ERR, VHOST_CONFIG,
"mmap region %u failed.\n", i);
goto err_mmap;
} reg->mmap_addr = mmap_addr;
reg->mmap_size = mmap_size;
/*region Address in vhost process*/
reg->host_user_addr = (uint64_t)(uintptr_t)mmap_addr +
mmap_offset; if (dev->dequeue_zero_copy)
add_guest_pages(dev, reg, alignment); }

首先就是为dev分配mem空间,由此我们也可以得到该结构的布局

下面一个for循环对每个region先进行对应信息的复制,然后对该region的大小进行对其操作,接着通过mmap的方式对region关联的fd进行映射,这里便得到了region在vhost端的虚拟地址,但是region中GPA对应的虚拟地址还需要在mmap得到的虚拟地址上加上offset,该值也是作为参数传递进来的。到此,设置memory Table的工作基本完成,看下地址翻译过程呢?

/* Converts QEMU virtual address to Vhost virtual address. */
static uint64_t
qva_to_vva(struct virtio_net *dev, uint64_t qva)
{
struct rte_vhost_mem_region *reg;
uint32_t i; /* Find the region where the address lives. */
for (i = ; i < dev->mem->nregions; i++) {
reg = &dev->mem->regions[i]; if (qva >= reg->guest_user_addr &&
qva < reg->guest_user_addr + reg->size) {
return qva - reg->guest_user_addr +
reg->host_user_addr;
}
} return ;
}

相当简单把,核心思想是先使用QVA确定在哪一个region,然后取地址在region中的偏移,加上该region在vhost-user映射的实际有效地址即reg->host_user_addr字段。这部分还有一个核心思想是fd的使用,vhost_user_set_mem_table直接从MSG中获取到了fd,然后直接把FD进行mmap映射,这点一时间让我难以理解,FD不是仅仅在进程内部有效么?怎么也可以共享了??通过向开源社区请教,感叹自己的知识面实在狭窄,这是Unix下一种通用的传递描述符的方式,怎么说呢?就是进程A的描述符可以通过特定的调用传递给进程B,进程B在自己的描述符表中分配一个位置给该描述符指针,因此实际上进程B使用的并不是A的FD,而是自己描述符表中的FD,但是两个进程的FD却指向同一个描述符表,就像是增加了一个引用而已。后面会专门对该机制进行详解,本文仅仅了解该作用即可。

三、vhost-user app的通知机制。

这里的通知机制和vhost kernel基本一致,都是通过eventfd的方式。因此这里就比较简单了

qemu端的代码:

 file.fd = event_notifier_get_fd(virtio_queue_get_host_notifier(vvq));
r = dev->vhost_ops->vhost_set_vring_kick(dev, &file);
static int vhost_user_set_vring_kick(struct vhost_dev *dev,
struct vhost_vring_file *file)
{
return vhost_set_vring_file(dev, VHOST_USER_SET_VRING_KICK, file);
}
static int vhost_set_vring_file(struct vhost_dev *dev,
VhostUserRequest request,
struct vhost_vring_file *file)
{
int fds[VHOST_MEMORY_MAX_NREGIONS];
size_t fd_num = ;
VhostUserMsg msg = {
.request = request,
.flags = VHOST_USER_VERSION,
.payload.u64 = file->index & VHOST_USER_VRING_IDX_MASK,
.size = sizeof(msg.payload.u64),
}; if (ioeventfd_enabled() && file->fd > ) {
fds[fd_num++] = file->fd;
} else {
msg.payload.u64 |= VHOST_USER_VRING_NOFD_MASK;
} if (vhost_user_write(dev, &msg, fds, fd_num) < ) {
return -;
} return ;
}

可以看到这里实质上也是把eventfd的描述符传递给vhost-user。再看vhost-user端,在vhost_user_set_vring_kick中,关键的一句

vq->kickfd = file.fd;

其实这里的通知机制和kernel下没什么区别,不过是换到用户空间对eventfd进行操作而已,这里暂时不讨论了,后面有时间在补充!

以马内利!

参考资料:

qemu 2.7 源码

DPDK源码

vhost-user 分析1的更多相关文章

  1. SpringBoot 整合RabbitMQ错误记录

    1. 控制台报错:Exception in thread "main" java.io.IOException…… Caused by: com.rabbitmq.client.S ...

  2. RabbitMQ的Vhost,Exchange,Queue原理分析

    Vhost分析 RabbitMQ的Vhost主要是用来划分不同业务模块.不同业务模块之间没有信息交互. Vhost之间相互完全隔离,不同Vhost之间无法共享Exchange和Queue.因此Vhos ...

  3. [原] KVM虚拟机网络闪断分析

    背景 公司云平台的机器时常会发生网络闪断,通常在10s-100s之间. 异常情况 VM出现问题时,表现出来的情况是外部监控系统无法访问,猜测可能是由于系统假死,OVS链路问题等等.但是在出现网络问题的 ...

  4. [dpdk] 熟悉SDK与初步使用 (三)(IP Fragmentation源码分析)

    对例子IP Fragmentation的熟悉,使用,以及源码分析. 功能: 该例子的功能有二: 一: 将IP分片? 二: 根据路由表,做包转发. 路由表如下: IP_FRAG: Socket : ad ...

  5. 使用elk+redis搭建nginx日志分析平台

    elk+redis 搭建nginx日志分析平台 logstash,elasticsearch,kibana 怎么进行nginx的日志分析呢?首先,架构方面,nginx是有日志文件的,它的每个请求的状态 ...

  6. 【原】Nginx添加Content-MD5头部压测分析

    如需转载,必须注明原文地址,请尊重作者劳动成果. http://www.cnblogs.com/lyongerr/p/5048464.html 本文介绍了webbenck安装,但是最后使用的是ab工具 ...

  7. Django搭建及源码分析(三)---+uWSGI+nginx

    每个框架或者应用都是为了解决某些问题才出现旦生的,没有一个事物是可以解决所有问题的.如果觉得某个框架或者应用使用很不方便,那么很有可能就是你没有将其使用到正确的地方,没有按开发者的设计初衷来使用它,当 ...

  8. 使用elk+redis搭建nginx日志分析平台(转)

    logstash,elasticsearch,kibana 怎么进行nginx的日志分析呢?首先,架构方面,nginx是有日志文件的,它的每个请求的状态等都有日志文件进行记录.其次,需要有个队列,re ...

  9. vhost:一种 virtio 高性能的后端驱动实现

    什么是 vhost vhost 是 virtio 的一种后端实现方案,在 virtio 简介中,我们已经提到 virtio 是一种半虚拟化的实现方案,需要虚拟机端和主机端都提供驱动才能完成通信,通常, ...

  10. 消息中间件选型分析——从Kafka与RabbitMQ的对比来看全局

    一.前言 消息队列中间件(简称消息中间件)是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成.通过提供消息传递和消息排队模型,它可以在分布式环境下提供应用解耦 ...

随机推荐

  1. Create maintenance backup plan in SQL Server 2008 R2 using the wizard

    You will need to identify how you want your maintenance plan to be setup. In this example the mainte ...

  2. RAC的搭建(二)--创建ASM磁盘

     1. 规划 表决磁盘: 1Gx3(3节点以下,建议都采用这种配置,三个磁盘加起来要大于1.8G,否则会报错) 数据磁盘: 10Gx1 闪回磁盘: 5Gx1 2. 创建共享磁盘 virtualBox上 ...

  3. 在taro中跳转页面的时候执行两遍componentDidMount周期的原因和解决方法

    在做taro跳转的时候,发现在跳转后的页面会走两遍componentDidMount周期,查看了github上的issues,发现是跳转路由带参为中文引起的,只要把中文参数进行urlencode解决 ...

  4. Python进阶 学习笔记(二)

    (涉及内容:面向对象,类的继承) 定义类并创建实例 在Python中,类通过 class 关键字定义.以 Person 为例,定义一个Person类如下: class Person(object): ...

  5. Android 之 布局训练

      1.线性布局 LinearLayout <?xml version="1.0" encoding="utf-8"?> <LinearLay ...

  6. echarts.js多图表数据展示使用小结

    echarts api文档: http://echarts.baidu.com/echarts2/doc/doc.html echarts demo示例: http://echarts.baidu.c ...

  7. STL 迭代器 iterator const

    STL迭代器很多时候可以当成指针来使用. 但是指针一般可以用const来控制访问. 那迭代器呢. #include <iostream> #include <vector> u ...

  8. 【Spring系列】Spring IoC

    前言 IoC其实有两种方式,一种是DI,而另一种是DL,即Dependency Lookup(依赖查找),前者是当前软件实体被动接受其依赖的其他组件被IOc容器注入,而后者是当前软件实体主动去某个服务 ...

  9. Webpack 备忘录

    Webpack 属于在项目中配置一次就很少改动的那种工具,但这样就导致新项目再配置 Webpack 时会有些生疏,所以将 Webpack 核心概念及常用配置记录如下. 1)核心概念 Webpack 4 ...

  10. 简单了解如何使用vue-router和vue-resource

    我们先来看看vue-router 1.npm install vue-router --save 2.调用vue-router: 第一种方法: 直接在main.js中调用 import vueRout ...