vhost-net 2 -- 重要数据结构

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。


    image.png

    ** 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接收报文。

eventfd和irqfd都是qemu分别在vhost和kvm注册。
image.png
  • 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设备的模拟。

你可能感兴趣的:(vhost-net 2 -- 重要数据结构)