vhost_virtqueue
struct vhost_virtqueue:用于描述vhost设备对应的virtqueue,这部分内容可以参考之前virtqueue机制分析,本质上是将Qemu中virtqueue处理机制下沉到了Kernel中。
/* The virtqueue structure describes a queue attached to a device. */
struct vhost_virtqueue {
struct vhost_dev *dev;
/* The actual ring of buffers. */
struct mutex mutex;
unsigned int num;
struct vring_desc __user *desc;
struct vring_avail __user *avail;
struct vring_used __user *used;
struct file *kick;
struct file *call;
struct file *error;
struct eventfd_ctx *call_ctx;
struct eventfd_ctx *error_ctx;
struct eventfd_ctx *log_ctx;
struct vhost_poll poll;
/* The routine to call when the Guest pings us, or timeout. */
vhost_work_fn_t handle_kick;
/* Last available index we saw. */
u16 last_avail_idx;
/* Caches available index value from user. */
u16 avail_idx;
/* Last index we used. */
u16 last_used_idx;
/* Used flags */
u16 used_flags;
/* Last used index value we have signalled on */
u16 signalled_used;
/* Last used index value we have signalled on */
bool signalled_used_valid;
/* Log writes to used structure. */
bool log_used;
u64 log_addr;
struct iovec iov[UIO_MAXIOV];
struct iovec *indirect;
struct vring_used_elem *heads;
/* We use a kind of RCU to access private pointer.
* All readers access it from worker, which makes it possible to
* flush the vhost_work instead of synchronize_rcu. Therefore readers do
* not need to call rcu_read_lock/rcu_read_unlock: the beginning of
* vhost_work execution acts instead of rcu_read_lock() and the end of
* vhost_work execution acts instead of rcu_read_unlock().
* Writers use virtqueue mutex. */
void __rcu *private_data;
/* Log write descriptors */
void __user *log_base;
struct vhost_log *log;
};
关键成员:
-
desc、avail、used
VRing 是virtio 前后端数据传输的核心机制,virtqueue基于VRing实现,本质上是共享内存,guest/host 都能访问这些内存,参考virtio spec的定义,VRing由三部分组成, vring_desc,vring_avail,vring_used。
** Descriptor Table,存放Guest Driver提供的buffer的指针,每个条目指向一个Guest Driver分配的收发数据buffer。注意,VRing中buffer空间的分配永远由Guest Driver负责,Guest Driver发数据时,还需要向buffer填写数据,Guest Driver收数据时,分配buffer空间后通知Host向buffer中填写数据
** Avail Ring,存放Decriptor Table索引,指向Descriptor Table中的一个entry。当Guest Driver向Vring中添加buffer时,可以一次添加一个或多个buffer,所有buffer组成一个Descriptor chain,Guest Driver添加buffer成功后,需要将Descriptor chain头部的地址记录到Avail Ring中,让Host端能够知道新的可用的buffer是从VRing的哪个地方开始的。Host查找Descriptor chain头部地址,需要经过两次索引Buffer Adress = Descriptor Table[Avail Ring[last_avail_idx]],last_avail_idx是Host端记录的Guest上一次增加的buffer在Avail Ring中的位置。Guest Driver每添加一次buffer,就将Avail Ring的idx加1,以表示自己工作在Avail Ring中的哪个位置。Avail Rring是Guest维护,提供给Host用
** Used Ring,同Used Ring一样,存放Decriptor Table索引。当Host根据Avail Ring中提供的信息从VRing中取出buffer,处理完之后,更新Used Ring,把这一次处理的Descriptor chain头部的地址放到Used Ring中。Host每取一次buffer,就将Used Ring的idx加1,以表示自己工作在Used Ring中的哪个位置。Used Ring是Host维护,提供给Guest用
vhost_virtqueue 中就包含着三部分。
VRing中buffer空间的分配永远由guest负责,guest发数据时,还需要向buffer填写数据,guest收数据时,分配buffer空间后通知Host向buffer中填写数据。所以,vhost_virtqueue中的desc、avail、used都使用__user宏定义,说明指针地址必须在用户地址空间。qemu通过 vring_ioctl 的 VHOST_SET_VRING_ADDR 下发用户态地址设置。前后端使用共享这些内存。
long vhost_vring_ioctl(struct vhost_dev *d, int ioctl, void __user *argp)
{
......
case VHOST_SET_VRING_ADDR:
if (copy_from_user(&a, argp, sizeof a)) {
r = -EFAULT;
break;
}
......
vq->log_used = !!(a.flags & (0x1 << VHOST_VRING_F_LOG));
vq->desc = (void __user *)(unsigned long)a.desc_user_addr;
vq->avail = (void __user *)(unsigned long)a.avail_user_addr;
vq->log_addr = a.log_guest_addr;
vq->used = (void __user *)(unsigned long)a.used_user_addr;
break;
......
}
kick和call都是是eventfd文件,涉及到virtio前后端通知机制实现的重要部分:ioeventfd和irqfd。
- ioeventfd,提供了一种机制,用文件来模拟接收到guest的信号。vhost中对应就是kick,就是用来发送kick信号,通知host,guest已经准备好了要发送的报文;
- irqfd,提供了一种机制,用文件来模拟向guest注入中断。vhost中对应call,在host准备好接收的报文后,发送信号给kvm,kvm模拟中断触发guest接收报文。
- kick & handle_kick:
qemu通过VHOST_SET_VRING_KICK设置。
guest 发送报文时,将报文缓存区写入virtqueue后,就会执行vp_notify(),相当于执行一次port I/O(或者mmio),虚拟机则会退出guest mode。这里假设使用的是intel的vmx,当检测到pio或者mmio会设置vmcs中的exit_reason,内核态kvm执行vmx_handle_exit(),最终调用ioeventfd_write 产生了一次kick信号,唤醒vhost内核线程,调用 vhost的handle_kick(handle_tx_kick)完成vhost 数据发送流程。
static int
ioeventfd_write(struct kvm_io_device *this, gpa_t addr, int len,
const void *val)
{
struct _ioeventfd *p = to_ioeventfd(this);
if (!ioeventfd_in_range(p, addr, len, val))
return -EOPNOTSUPP;
eventfd_signal(p->eventfd, 1);
return 0;
}
- call & call_ctx:
qemu通过VHOST_SET_VRING_CALL设置。
vhost接收报文(handle_rx),将报文写入vring_desc,并记录desc索引到vring_used后,调用vhost_signal,通知guest接收报文。另外vhost发送完报文(handle_tx),也会调用vhost_signal,通知guest回收desc buffer。
如vhost_signal函数的注释,使用 eventfd 向guest发出信号。
/* This actually signals the guest, using eventfd. */
void vhost_signal(struct vhost_dev *dev, struct vhost_virtqueue *vq)
{
/* Signal the Guest tell them we used something up. */
if (vq->call_ctx && vhost_notify(dev, vq))
eventfd_signal(vq->call_ctx, 1);
}
private_data
私有数据设置为vhost_virtqueue 关联的tap接口的socket,qemu通过 VHOST_NET_SET_BACKEND设置。vhost_poll
见下面vhost_poll章节
vhost_poll
理解vhost_poll是理解vhost_net的工作机制很重要的一环。其代表vhost poll机制中的一个等待实体。
poll,字面意思轮询,可以理解为,当某一事件发生的时候(tap收到报文),轮询遍历关心此事件的所有对象(tap socket遍历socket等待列表中的所有等待实体 vhost_poll.wait),唤醒此对象的工作线程/进程(调用vhost_poll.wait.func=vhost_poll_wakeup唤醒vhost内核线程),调用次事件的处理函数处理事件(vhost_poll.work.fn)。
vhost_poll 存在于两个位置:
- vhost_net 中,用于关联tap设备,tap设备收发报文时触发poll事件处理;
- vhost_virtqueue中,用于关联eventfd 文件,eventfd收到kick信号后触发poll事件处理。
/* Poll a file (eventfd or socket) */
/* Note: there's nothing vhost specific about this structure. */
struct vhost_poll {
// table 是包含一个函数指针,在驱动的poll函数中被调用
poll_table table;
// wqh是一个等待队列头,没太多用处,用来判断poll是否已经挂载过。
wait_queue_head_t *wqh;
// wait是一个等待实体,其包含一个函数作为唤醒函数,vhost_poll_wakeup
wait_queue_t wait;
// vhost_work是poll机制处理的核心任务,处理网络数据包,其中函数指针指向用户设置的处理函数,这里就是handle_tx_net和handle_rx_net
struct vhost_work work;
unsigned long mask;
struct vhost_dev *dev;
};
vhost_poll 包含:
1、 poll_table table
包含一个回调函数,挂载的vhost_poll_func,外部通过poll_table完成等待队列的挂载,为poll挂载(poll_wait挂等待队列)通用流程所需通用数据结构。
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
/*
* Do not touch the structure directly, use the access functions
* poll_does_not_wait() and poll_requested_events() instead.
*/
typedef struct poll_table_struct {
poll_queue_proc _qproc;
unsigned long _key;
} poll_table;
vhost_poll_func函数 通过传入的 poll_table 参数获取到vhost_poll,将其代表的等待实体加入到传入的等待队列中,这里的等待队列有tap socket和eventfd的等待队列。vhost_poll_func 被 poll_wait调用,而poll_wait会被各种设备驱动的poll函数调用。
vhost中涉及两处调用:
- qemu通过VHOST_NET_SET_BACKEND 设置 vhost_virtqueue 与 tap设备的关联,并调用 tap设备的poll函数tun_chr_poll 将vhost_net 对应的等待实体加入到了tap设备socket等待队列中,后续tap设备收发报文时触发poll事件处理。
流程: vhost_net_ioctl (VHOST_NET_SET_BACKEND) --> vhost_net_set_backend --> vhost_net_enable_vq --> vhost_poll_start --> file->f_op->poll --> tun_chr_poll - qemu通过VHOST_SET_VRING_KICK设置vhost的kick文件,用以响应kvm的kick信号,其中会调用 eventfd的poll函数eventfd_poll,将 vhost_virtqueue 的对应的等待实体加入到 eventfd文件的等待队列中,eventfd文件收到信号后,会触发poll事件。
流程:vhost_vring_ioctl (VHOST_SET_VRING_KICK) --> vhost_poll_start --> file->f_op->poll --> eventfd_poll
static void vhost_poll_func(struct file *file, wait_queue_head_t *wqh,
poll_table *pt)
{
struct vhost_poll *poll;
poll = container_of(pt, struct vhost_poll, table);
poll->wqh = wqh;
add_wait_queue(wqh, &poll->wait);
}
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && p->_qproc && wait_address)
p->_qproc(filp, wait_address, p);
}
static unsigned int tun_chr_poll(struct file *file, poll_table *wait)
{
struct tun_file *tfile = file->private_data;
struct tun_struct *tun = __tun_get(tfile);
struct sock *sk;
unsigned int mask = 0;
if (!tun)
return POLLERR;
sk = tfile->socket.sk;
tun_debug(KERN_INFO, tun, "tun_chr_poll\n");
poll_wait(file, &tfile->wq.wait, wait);
......
}
2、struct vhost_work work;
vhost_work 代表vhost poll 被触发后,要做的工作,其中函数指针fn就是任务的回调函数,在vhost中有 handle_tx_net/handle_rx_net 用来处理网络数据包,handle_tx_kick/handle_rx_kick用来处理kick事件。
vhost_work 在vhost poll 被唤醒后,会被挂载到vhost内核线程的任务列表中等待处理。
struct vhost_work;
typedef void (*vhost_work_fn_t)(struct vhost_work *work);
struct vhost_work {
struct list_head node;
vhost_work_fn_t fn;
wait_queue_head_t done;
int flushing;
unsigned queue_seq;
unsigned done_seq;
};
3、 wait_queue_t wait;
wait_queue_t 表示一个等待实体,包含一个等待节点task_list,用来加入设备等待队列,就是在上面vhost_poll_func 中所做的事情。包含一个回调函数 func,挂载的vhost_poll_wakeup,用来唤醒vhost内核线程处理响应的事件。
vhost_poll_wakeup
typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);
int default_wake_function(wait_queue_t *wait, unsigned mode, int flags, void *key);
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
vhost_poll_wakeup 做的事情就是将vhost_poll 中的vhost_work 加入到vhost设备的工作列表,然后唤醒vhost内核线程,其会遍历vhost设备的工作列表调用其上挂载的vhost_work,调用其回调函数处理。
static int vhost_poll_wakeup(wait_queue_t *wait, unsigned mode, int sync,
void *key)
{
struct vhost_poll *poll = container_of(wait, struct vhost_poll, wait);
if (!((unsigned long)key & poll->mask))
return 0;
vhost_poll_queue(poll);
return 0;
}
void vhost_poll_queue(struct vhost_poll *poll)
{
vhost_work_queue(poll->dev, &poll->work);
}
void vhost_work_queue(struct vhost_dev *dev, struct vhost_work *work)
{
unsigned long flags;
spin_lock_irqsave(&dev->work_lock, flags);
if (list_empty(&work->node)) {
list_add_tail(&work->node, &dev->work_list);
work->queue_seq++;
wake_up_process(dev->worker);
}
spin_unlock_irqrestore(&dev->work_lock, flags);
}
vhost_poll_wakeup 被调用的地方:
- 从tap设备发送报文时,调用 tun_net_xmit ,其中调用 wake_up_interruptible_poll,最终调用__wake_up_common,根据上文可以知道,tfile->wq.wait 中挂载的是vhost_poll 的 wait,所以最终调用了vhost_poll_wakeup,唤醒了vhost内核线程。
static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct tun_struct *tun = netdev_priv(dev);
int txq = skb->queue_mapping;
struct tun_file *tfile;
......
wake_up_interruptible_poll(&tfile->wq.wait, POLLIN |
POLLRDNORM | POLLRDBAND);
......
}
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
if (curr->func(curr, mode, wake_flags, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
- kvm 通过eventfd 发kick信号时调用ioeventfd_write->eventfd_signal->__wake_up_common,唤醒vhost内核线程处理 kick事件。
__u64 eventfd_signal(struct eventfd_ctx *ctx, __u64 n)
{
unsigned long flags;
spin_lock_irqsave(&ctx->wqh.lock, flags);
if (ULLONG_MAX - ctx->count < n)
n = ULLONG_MAX - ctx->count;
ctx->count += n;
if (waitqueue_active(&ctx->wqh))
wake_up_locked_poll(&ctx->wqh, POLLIN);
spin_unlock_irqrestore(&ctx->wqh.lock, flags);
return n;
}
vhost_net_virtqueue
用于描述Vhost-Net设备对应的virtqueue,封装的struct vhost_virtqueue。
struct vhost_dev
描述通用的vhost设备,可内嵌在基于vhost机制的其他设备结构体中,比如struct vhost_net,struct vhost_scsi等。关键字段如下:1)vqs指针,指向已经分配好的struct vhost_virtqueue,对应数据传输;2)work_list,任务链表,用于放置需要在vhost_worker内核线程上执行的任务;3)worker,用于指向创建的内核线程,执行任务列表中的任务;
struct vhost_net
用于描述Vhost-Net设备。它包含几个关键字段:1)struct vhost_dev,通用的vhost设备,可以类比struct device结构体内嵌在其他特定设备的结构体中;2)struct vhost_net_virtqueue,实际上对struct vhost_virtqueue进行了封装,用于网络包的数据传输;3)struct vhost_poll,用于tap socket或者eventfd文件的poll机制,以便在数据包接收与发送时进行任务调度;
vhost_dev,vhost_virtqueue是vhost机制通用部分的实现。vhost_net,vhost_net_virtqueue则是采用vhost机制的网络功能的实现。还有vhost-blk,对block设备的模拟,以及vhost-scsi,对scsi设备的模拟。