LinuxCP插件virtio与内核vhost

news/2024/11/30 1:30:45/

以下为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 interfaceName       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 1000tun 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_iftfd = open ("/dev/net/tun", O_RDWR | O_NONBLOCK); //获得描述符29ioctl(tfd=29,TUNGETFEATURES);  //特性协商:必须的特性- IFF_VNET_HDRifr.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_ioctltun_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.pollvhost_poll_init初始化vhost_net->poll[0/1]file->private_data = vhost_net.

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

tap_create_iffor (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 == 0vif->txq_vrings[0].queue_id == 1vif->txq_vrings[1].queue_id == 2addr.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_CALLtap_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_KICKvif->txq_vrings[0/1]两个发送vring不接收内核中断,没有创建call_fd(等于-1),创建的kick_fd描述符分别为34和35vhost_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_rxstruct 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_bufsvhost_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_recvmsgvhost_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_nstart = 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_notifyeventfd_signal(vq->call_ctx, 1);

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

在这里插入图片描述

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

virtio_input_nodevirtio_device_input_inlinevirtio_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_inlinen_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_inlinevirtio_interface_tx_split_gso_inlineadd_buffer_to_slotvirtio_kickadd_buffer_to_slotvring_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_kickhandle_txvhost_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_deschead = 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_sendmsgvhost_add_used_and_signal(&net->dev, vq, head, 0);

内核vhost_virtqueue结构变化如下,

在这里插入图片描述


http://www.ppmy.cn/news/805862.html

相关文章

HTML特殊符号表示方法

个人总结&#xff0c;不便记忆&#xff0c;便于查找&#xff0c;希望帮到大家 &#xff08; &#xff09;空格符&#xff1a;&nbsp&#xff1b; &#xff08;<&#xff09;小于号: &It&#xff1b; &#xff08;>&#xff09;大于号&#xff1a; &gt&#…

‘font-weight’属性

字体粗细&#xff1a;‘font-weight’属性 名称&#xff1a; font-weight 取值&#xff1a; normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 初始&#xff1a; normal 适用于&#xff1a; 所有元素 继承&#xff1a; 是 百分比&a…

Latex符号对照表

感谢博主的总结&#xff0c;这里转载方便自己平时快速找到。 一些基本符号大全&#xff1a;https://blog.csdn.net/zgj926503/article/details/52757631?ops_request_misc%257B%2522request%255Fid%2522%253A%2522160189222519724836739518%2522%252C%2522scm%2522%253A%25222…

Html 中的特殊符号表示

Html 中的特殊符号表示 字符十进制转义字符"&quot;&&amp;<<<>>>不断开空格(non-breaking space) HTML特殊转义字符对照表 字符十进制转义字符字符十进制转义字符字符十进制转义字符?¡¡&Aacute;&aacute;&#xffe0;&#…

各种透明玻璃厚度测量,双边对射厚度测量,玻璃测厚传感器

深圳立仪科技有限公司成立于 2014 年&#xff0c;现位于深圳市国际低碳城&#xff0c;是一家以精密光学检测为主业的民营高科技企业&#xff0c;光谱共焦位移传感器及其应用配套为主推产品 立仪科技的关键研发人员在激光位移测量、3D 扫描领域有十几年的经验&#xff0c;发现光…

MarkDOWN常用符号

linux 如何写标题&#xff1f; #‘#’表示一级标题 ‘##’表示二级标题 ‘###’表示三级标题 ‘文字下面添加下划线_表示一级标题&#xff0c;添加表示二级标题’ 引用可以用> 大于号加上空格。 背景色名称AliceBlue颜色的RGB值F0F8FF颜色的RGB值rgb(240, 248, 255)背景色…

nm符号的类型

nm命令介绍的很多&#xff0c;但大多不介绍其函数符号标志的含义。 最近在调试动态库时常用到&#xff0c;其中用的最多的用法: nm -A * |grep “aaa” | cfilt // -A 为了显示文件&#xff0c; cfilt转换为可读风格&#xff0c;好像有个参数也能实现类似功能 其他内容整理如下…