LinuxCP插件virtio与内核vhost

以下为LCP创建的接口对,VPP侧为物理接口port7,映射到Linux侧的为虚拟接口hostap1,接口hostap1作为vhost的后端存在。VPP侧接口tap1为前端的virtio接口。

vpp# show lcp
itf-pair: [0] port7 tap1 hostap1 24 type tap
vdp#
vdp# show interface
          Name       Idx    State  MTU (L3/IP4/IP6/MPLS)     Counter          Count
port7                 5      up          9000/0/0/0
tap1                  9      up          9000/0/0/0     
vpp#
vpp# quit
/ #
/ # ip -d link show hostap1
24: hostap1: <NO-CARRIER,BROADCAST,MULTICAST,PROMISC,UP> mtu 9000 qdisc mq state DOWN mode DEFAULT group default qlen 1000
    tun addrgenmode eui64 numtxqueues 256 numrxqueues 256

Linux内核发送报文的流程如下:

Linux kernel(hostap1) --> virtio-input(tap1) --> ethernet-input
                                                       |
                                                       |
               port7-output <-- linux-cp-xc-ip4 <-- ip4-input
                    |
                    |
                 port7-tx

VPP LCP发送报文到Linux内核:

dpdk-input(port7) --> ethernet-input --> ip4-input-no-checksum --> ip4-lookup
                                                                       |
                                                                       |
              |--- ip4-dvr-dpo <--ip4-punt-redirect <-- ip4-punt <-- ip4-local
              |
              |
       ip4-dvr-reinject --> tap1-output --> tap1-tx --> Linux kernel(hostap1) 

以下内容分三个部分:virtio/vhost相关初始化,发送和接收流程。

一. virtio/vhost相关初始化

VPP LCP插件中函数tap_create_if创建以上用到的所有设备并进行相应的初始化。首先,打开设备文件/dev/net/tun,创建Linux内核中的tap类型设备hostap1。

tap_create_if
    tfd = open ("/dev/net/tun", O_RDWR | O_NONBLOCK); //获得描述符29
	
    ioctl(tfd=29,TUNGETFEATURES);  //特性协商:必须的特性- IFF_VNET_HDR

    ifr.ifr_flags |= IFF_TAP;
    ifr.ifr_name = "hostap1";
    ioctl (tfd, TUNSETIFF, (void *) &ifr);   //创建LInux TAP设备hostap1.
    //设置virtio网络头部大小
    ioctl (tfd, TUNSETVNETHDRSZ, sizeof (virtio_net_hdr_v1_t))

    //设置发送缓存大小
    ioctl(vif->tap_fds[i], TUNSETSNDBUF,  INT_MAX)

tun设备函数tun_chr_open处理open操作,分配结构tun_file,进行初始化,最终保存在文件结构file的成员private_data中。

在这里插入图片描述

内核函数__tun_chr_ioctl处理TUNSETIFF调用,创建网络设备。tun设备此时具有一个队列(numqueues),tun->tfiles数组大小为256,最多支持256个队列。可再打开/devnet/tun设备,创建tun_file结构,添加到tun->tfiles数组,扩充tun设备的队列数量。

__tun_chr_ioctl
    tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr)
        alloc_netdev_mqs(sizeof(struct tun_struct), name, tun_setup, MAX_TAP_QUEUES=256);
        tun_attach(tun, file, false)
        register_netdevice(tun->dev)

如下图,增加了tun_struct结构。

在这里插入图片描述

其次,打开vhost-net设备文件,创建vhost网络设备。可多次打开vhost-net设备,获得多个文件描述符,对应vhost设备的多个队列。

tap_create_if
    /* open as many vhost-net fds as required and set ownership */
    num_vhost_queues = clib_max (vif->num_rxqs, vif->num_txqs);
    for (i = 0; i < num_vhost_queues; i++) {
        vfd = open ("/dev/vhost-net", O_RDWR | O_NONBLOCK);
        vec_add1 (vif->vhost_fds, vfd);
		//内核将创建vhost内核线程,名称:vhost-$pid,pid为VPP的进程ID号。
		//多队列,或者多设备的情况,会创建多个相同名称的内核线程。
        ioctl(vfd, VHOST_SET_OWNER, 0);  
    }
    ioctl(vif->vhost_fds[0], VHOST_GET_FEATURES, &vif->remote_features);  //特性需要支持VIRTIO_F_VERSION_1

内核函数vhost_net_open分配vhost_net结构,进行相应初始化,最终保存到文件结构file的成员private_data中。

     vhost_dev_init   初始化vhost_net->dev结构
              vhost_poll_init 初始化vhost_net->vqs[0/1].vq.poll
      vhost_poll_init初始化vhost_net->poll[0/1]
     file->private_data = vhost_net.

如下为分配的vhost_net结构,其具有接收/发送两套队列结构vhost_net_virtqueue:
在这里插入图片描述
接下来,LCP进行发送和接收vring环的初始化。

tap_create_if
  for (i = 0; i < num_vhost_queues; i++) {
    if (i < vif->num_rxqs && (
	   args->error = virtio_vring_init (vm, vif, RX_QUEUE (i), args->rx_ring_sz)))
      goto error;

    if (i < vif->num_txqs && (
	   args->error = virtio_vring_init (vm, vif, TX_QUEUE (i), args->tx_ring_sz)))
      goto error;

发送和接收环使用相同的数据结构virtio_vring_t。如下为初始化的vif->rxq_vrings结构。vring->queue_id 标识rx和tx队列的索引,其中偶数为rx队列,奇数为tx队列。queue_id的最低1位对应于内核中vhost驱动中的vhost_net_virtqueue的索引:VHOST_NET_VQ_RX=0, VHOST_NET_VQ_TX=1。

在这里插入图片描述

如下为初始化的vif->txq_vrings结构。对于发送换,没有分配call_fd。

在这里插入图片描述
根据当前环境的配置情况(两个VPP线程:主线程和工作线程),tap_create_if初始化了3个队列,其中2个发送:vif->txq_vrings[2];一个接收:vif->rxq_vrings[1]。不同配置,发送和接收队列不相同。以下将所有队列的信息同步到内核的vhost-net驱动中。

以下为将vring数量同步到内核vhost驱动。

VHOST_SET_VRING_NUM(描述符30/31,TX/RX)
      描述符30对应RX和TX两个队列;描述符31仅有一个TX队列。
      vhost_net<30>->vqs[VHOST_NET_VQ_TX/VHOST_NET_VQ_RX].vq.num = 256/256;
      vhost_net<31>->vqs[VHOST_NET_VQ_TX].vq.num = 256;

将接收/发送vring的三个环结构desc/avail/used地址同步给内核vhost驱动。

VHOST_SET_VRING_ADDR
       描述符30对应两个队列id:0,1;描述符31对应一个队列id:2。
       vif->rxq_vrings[0].queue_id == 0
       vif->txq_vrings[0].queue_id == 1
       vif->txq_vrings[1].queue_id == 2

       addr.flags = 0;
       addr.desc_user_addr = pointer_to_uword (rxq/txq_vring->desc);
       addr.avail_user_addr = pointer_to_uword (rxq/txq_vring->avail);
       addr.used_user_addr = pointer_to_uword (rxq/txq_vring->used);

       将vif接口三个vring分配的desc/avail/used地址下发到内核vhost。
       vhost_net<30>->vqs[VHOST_NET_VQ_TX/VHOST_NET_VQ_RX].vq.<desc/avail/used> = txq/rxq_vring->desc/avail/used;
       vhost_net<31>->vqs[VHOST_NET_VQ_TX].vq.<desc/avail/used> = txq_vring->desc/avail/used;

以下将创建的call_fd和kick_fd同步给内核vhost驱动。

VHOST_SET_VRING_CALL
    tap_create_if中为发送vif->rxq_vrings[0]创建了call_fd和kick_fd,描述符分别为32和33。

    vhost_net<30>->vqs[VHOST_NET_VQ_RX].vq.call_ctx.ctx = eventfd_ctx_fdget(32)
    vhost_net<30>->vqs[VHOST_NET_VQ_RX].vq.kick = eventfd_fget(33)
             vhost_poll_start(&vq->poll, vq->kick);  内核vhost开始监听kick描述符。
        vhost_net<30/31>->vqs[VHOST_NET_VQ_TX].vq.call_ctx.ctx = NULL/NULL;

VHOST_SET_VRING_KICK
    vif->txq_vrings[0/1]两个发送vring不接收内核中断,没有创建call_fd(等于-1),创建的kick_fd描述符分别为34和35

    vhost_net<30>->vqs[VHOST_NET_VQ_TX].vq.kick = eventfd_fget(34)
            vhost_poll_start(&vq->poll, vq->kick);
    vhost_net<31>->vqs[VHOST_NET_VQ_TX].vq.kick = eventfd_fget(35)
            vhost_poll_start(&vq->poll, vq->kick);

以下将vhost_net与tap设备关联起来。vhost_net与tap设备建立了两个关联:a) vhost_net子结构保存了tap设备描述符;b) vhost_net的poll挂载在tap设备的等待队列上。

在这里插入图片描述

VPP virtio信息与内核vhost同步之后,内核结构如下,变化主要体现在vhost_virtqueue结构中。

在这里插入图片描述

二. Linux vhost发送报文到VPP的virtio接口

tun设备发送函数如下,将报文添加到tun_files对应套接口的接收队列上(sk_receive_queue),唤醒等待队列中的wait项,这里有之前注册的vhost_net->poll[RX/TX].wait,发送和接口的wait都注册在这里。

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;

        tfile = rcu_dereference(tun->tfiles[txq]);
        skb_queue_tail(&tfile->socket.sk->sk_receive_queue, skb);

        wake_up_interruptible_poll(&tfile->wq.wait, POLLIN | POLLRDNORM | POLLRDBAND);

对于POLLIN/POLLOUT,处理程序统一为 vhost_poll_wakeup。这里为POLLIN事件,对应上vhost_net->poll[VHOST_NET_VQ_RX].wait。调用其vhost_work_queue将work添加到vhost_dev设备的work_list链表,唤醒内核处理线程(vhost-$pid)。

vhost_poll_queue(vhost_net->poll[VHOST_NET_VQ_RX])
         vhost_work_queue(poll->dev, &poll->work);
                 list_add_tail(&work->node, &dev->work_list);
                 wake_up_process(dev->worker);
内核处理线程,这里work的处理函数为handle_rx_net->handle_rx。
vhost_worker(void *data)
          work->fn(work);

这里实际处理函数为handle_rx。

handle_rx
     struct vhost_net_virtqueue *nvq = &net->vqs[VHOST_NET_VQ_RX];
     struct vhost_virtqueue *vq = &nvq->vq;
     struct msghdr msg = { .msg_iov = vq->iov,}
     vhost_disable_notify(&net->dev, vq);    //禁止linux-cp插件的kick操作

     struct socket *sock = vq->private_data;  (tun设备描述符对应的套接口)
            get_rx_bufs
                 vhost_get_vq_desc  返回descriptor的索引
                       __get_user(ring_head,  &vq->avail->ring[last_avail_idx % vq->num]) //第一个可用描述符的索引。
                       __copy_from_user(&desc, vq->desc + i, sizeof desc);    // 将索引对应的描述符结构内容拷贝到desc中(struct vring_desc)。

                      //将描述符中指定的缓存地址和长度转成内核iov结构
                      translate_desc(vq, vhost64_to_cpu(vq, desc.addr), vhost32_to_cpu(vq, desc.len), iov + iov_count,  )
                      vq->last_avail_idx++;   /* On success, increment avail index. */

            //get_rx_bufs函数返回值为vring_used_elem结构的heads,其成员id为描述符索引,len为描述符缓存大小,另外返回headcount为heads的数量。
            heads[headcount].id = cpu_to_vhost32(vq, d);
            heads[headcount].len = cpu_to_vhost32(vq, len);
            return headcount;
      至此,根据描述符内容填充完整了msghdr结构的iov,调用recvmsg结构tun设备的数据。
      msg.msg_iovlen = in;
      err = sock->ops->recvmsg(NULL, sock, &msg, sock_len, MSG_DONTWAIT | MSG_TRUNC);   //tun_recvmsg
      vhost_add_used_and_signal_n(&net->dev, vq, vq->heads,   headcount);   //通知linux-cp的virtio设备数据准备完毕。

函数vhost_add_used_and_signal_n通知linux-cp的virtio设备,数据准备完毕。

vhost_add_used_and_signal_n(&net->dev, vq, vq->heads,   headcount);  
     vhost_add_used_n(vq, heads, count);
     vhost_signal(dev, vq);

__vhost_add_used_n
     start = vq->last_used_idx % vq->num;
     used = vq->used->ring + start;

     __put_user(heads[0].id, &used->id)
     __put_user(heads[0].len, &used->len)
     vq->last_used_idx += count
     __put_user(cpu_to_vhost16(vq, vq->last_used_idx), &vq->used->idx)


vhost_signal(struct vhost_dev *dev, struct vhost_virtqueue *vq)
    vhost_notify
    eventfd_signal(vq->call_ctx, 1);

如下,内核vhost_virtqueue结构的变化。

在这里插入图片描述

VPP中函数virtio_input_node作为输入型节点处理接收到的报文。

virtio_input_node
     virtio_device_input_inline
            virtio_device_input_gso_inline    //接收处理报文
            virtio_refill_vring_split                  //重新填充接收描述符,当消耗的描述符数量超过总量1/8时,进行重新填充。

由于Linux内核将used->idx设置为1,vring记录的last_used_idx为0,表明内核使用了一个描述符。以下取出此描述符对应的vlib_buffer_t,进行处理。

virtio_device_input_gso_inline
      n_left = vring->used->idx - vring->last_used_idx;  
      slot = vring->used->ring[vring->last_used_idx & 255].id ;   //取出内核使用的vlib_buffer_t索引
       len = vring->used->ring[vring->last_used_idx & 255].len - hdr_sz;  //减去virtio头部长度,得到报文的实际长度。

       bi0 = vring->buffers[slot];
       vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0);   //得到报文数据所在的vlib_buffer_t,开始对报文进行处理。


       vring->desc_in_use--;
       vring->last_used_idx++;   //由于接收到一个报文,消耗了一个描述符,desc_in_use变为255,last_used_idx增加为1。

处理完成之后,virtio接口vif的rxq_vrings变化如下:

在这里插入图片描述

三. VPP virtio接口发送报文到Linux内核

virtio接口的发送函数virtio_interface_tx_inline如下,如同与上一节,这里设计到的vring都是指vif结构中的txq_vring。这里主要是获取发送描述符,并填充发送数据。

virtio_interface_tx_inline
    virtio_interface_tx_split_gso_inline
        add_buffer_to_slot
        virtio_kick

add_buffer_to_slot
    vring_desc_t *d = &vring->desc[vring->desc_next]; //获得可用的发送描述符
    d.addr = pointer_to_uword (vlib_buffer_get_current (b))) - hdr_sz;   //vlib_buffer结构数据地址,减去virtio头部长度
    d.len = b->current_length + hdr_sz;  //数据长度加上virtio头部长度

    vring->buffers[vring->desc_next] = bi;   //保存待发送vlib_buffer_t的索引bi。
    vring->avail->ring[vring->avail->idx & mask] = vring->desc_next;

发送之后txq_vring结构变化如下:
在这里插入图片描述
发送第一个报文的变化对比如下:
在这里插入图片描述

内核函数handle_tx_kick调用handle_tx接收VPP virtio接口发送来的数据,发送给tap接口。

handle_tx_kick
    handle_tx
        vhost_net_virtqueue *nvq = &net->vqs[VHOST_NET_VQ_TX];
        struct msghdr msg = { .msg_iov = vq->iov };
    
        vhost_net_tx_get_vq_desc(net, vq, vq->iov, ARRAY_SIZE(vq->iov), &out, &in)
             vhost_get_vq_desc
                      head = vq->avail->ring[vq->last_avail_idx % vq->num];  //获得可用的发送描述符的索引。
                      translate_desc函数将描述符中的缓存地址和长度转换为内核iovec结构
         s = move_iovec_hdr(vq->iov, nvq->hdr, hdr_size, out);   //virtio头部数据保存到nvq->hdr, 去掉vq->iov中的virtio头部数据,
         msg.msg_iovlen = out;        //发送描述符的数量
         sock->ops->sendmsg(NULL, sock, &msg, len);      //tun_sendmsg
         vhost_add_used_and_signal(&net->dev, vq, head, 0);

内核vhost_virtqueue结构变化如下,

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值