vhost-user 分析1
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的更多相关文章
- SpringBoot 整合RabbitMQ错误记录
1. 控制台报错:Exception in thread "main" java.io.IOException…… Caused by: com.rabbitmq.client.S ...
- RabbitMQ的Vhost,Exchange,Queue原理分析
Vhost分析 RabbitMQ的Vhost主要是用来划分不同业务模块.不同业务模块之间没有信息交互. Vhost之间相互完全隔离,不同Vhost之间无法共享Exchange和Queue.因此Vhos ...
- [原] KVM虚拟机网络闪断分析
背景 公司云平台的机器时常会发生网络闪断,通常在10s-100s之间. 异常情况 VM出现问题时,表现出来的情况是外部监控系统无法访问,猜测可能是由于系统假死,OVS链路问题等等.但是在出现网络问题的 ...
- [dpdk] 熟悉SDK与初步使用 (三)(IP Fragmentation源码分析)
对例子IP Fragmentation的熟悉,使用,以及源码分析. 功能: 该例子的功能有二: 一: 将IP分片? 二: 根据路由表,做包转发. 路由表如下: IP_FRAG: Socket : ad ...
- 使用elk+redis搭建nginx日志分析平台
elk+redis 搭建nginx日志分析平台 logstash,elasticsearch,kibana 怎么进行nginx的日志分析呢?首先,架构方面,nginx是有日志文件的,它的每个请求的状态 ...
- 【原】Nginx添加Content-MD5头部压测分析
如需转载,必须注明原文地址,请尊重作者劳动成果. http://www.cnblogs.com/lyongerr/p/5048464.html 本文介绍了webbenck安装,但是最后使用的是ab工具 ...
- Django搭建及源码分析(三)---+uWSGI+nginx
每个框架或者应用都是为了解决某些问题才出现旦生的,没有一个事物是可以解决所有问题的.如果觉得某个框架或者应用使用很不方便,那么很有可能就是你没有将其使用到正确的地方,没有按开发者的设计初衷来使用它,当 ...
- 使用elk+redis搭建nginx日志分析平台(转)
logstash,elasticsearch,kibana 怎么进行nginx的日志分析呢?首先,架构方面,nginx是有日志文件的,它的每个请求的状态等都有日志文件进行记录.其次,需要有个队列,re ...
- vhost:一种 virtio 高性能的后端驱动实现
什么是 vhost vhost 是 virtio 的一种后端实现方案,在 virtio 简介中,我们已经提到 virtio 是一种半虚拟化的实现方案,需要虚拟机端和主机端都提供驱动才能完成通信,通常, ...
- 消息中间件选型分析——从Kafka与RabbitMQ的对比来看全局
一.前言 消息队列中间件(简称消息中间件)是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成.通过提供消息传递和消息排队模型,它可以在分布式环境下提供应用解耦 ...
随机推荐
- Ansible Playbook 使用循环语句
如下,with_items 是循环的对象,with_items 是 python list 数据结构,task 会循环读取 list 里面的值,key 的名称是 item [root@localhos ...
- hadoop应用开发技术详解
<大 数据技术丛书:Hadoop应用开发技术详解>共12章.第1-2章详细地介绍了Hadoop的生态系统.关键技术以及安装和配置:第3章是 MapReduce的使用入门,让读者了解整个开发 ...
- 使用java发送QQ邮件
使用java发送邮件的时候,需要先下载两个jar包,连接如下: JavaMail mail.jar 1.4.5 JAF(版本 1.1.1) activation.jar 然后将这连个包导入,博主用的是 ...
- iOS10个实用小技巧(总有你不知道的和你会用到的)
本文转载至 http://www.jianshu.com/p/a3156826c27c 在开发过程中我们总会遇到各种各样的小问题,有些小问题并不是十分容易解决.在此我就总结一下,我在开发中遇到的各种小 ...
- 设计模式初探-桥接(Bridge)模式
桥接(Bridge)模式,又称Handle/Body模式,属于对象结构型模式.用于将抽象部分与它的实现部分分离,使它们都可以独立地变化.比如常见的电脑窗口界面,不同的操作系统其窗口界面绘制的原理肯定不 ...
- Servlet 简单介绍
来源于菜鸟教程http://www.runoob.com/servlet/servlet-intro.html Servlet 简介 Servlet 是什么? Servlet(Server Apple ...
- Big Spatio temporal Data(R-tree Index and NN & RNN & Skyline)
一.简单介绍大数据技术产物 “大数据”一词首先出现在2008年9月<Nature>杂志发表的一篇名为“Big Data: Wikiomics”的文章上(Mitch,2008).“大数据科学 ...
- ASP.NET Request.Cookies获取某个Cookie的奇怪问题
公司的某个产品依赖一个Cookie的值,发现在某些情况下即使Request附带了该Cookie(通过Fiddler2监控),服务器端通过HttpContext的Request.Cookies访问该Co ...
- nagios监控报错 It appears as though you do not have permission to view...
今天在安装完nagios后,通过nagios网页界面点击主机.服务.问题页面时.均报错,报错的内容都差不多.如点击服务,报错: It appears as though you do not have ...
- Accelerated Failure Time Models加速失效时间模型AFT
Weibull distribution 或者 σ是未知的scale参数,独立于X的常量, σ>0 是服从某一分布的随机变量 残差(residuals)=