以下为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结构变化如下,