LCP中接口创建命令如下。

vpp# lcp create <sw_if_index>|<if-name> host-if <host-if-name> netns <namespace> [tun]

将创建下图结构的三个虚拟网络设备,分别为VPP中的virtio接口,内核中的vhost-net设备和tap后端设备(backend)。其中virtio设备和tap设备可分别在VPP和linux中通过命令查看,vhost设备可通过内核创建的vhost处理线程确定。

            |---------------------|
            |                     |
            |   VPP      virtio   |
            |               |     |
            |---------------|-----|
 userspace                  |
----------------------------|-------------------
 kernel                     |
            |---------------|-----|
            |               |     |
            |  tap <-> vhost-net  |
            |                     |
            |---------------------|

在创建接口对的函数lcp_itf_pair_create中,由函数tap_create_if执行实际的接口创建工作。如下为tap/tun接口创建所使用的参数。host_if_name为所要创建的tap/tun接口的名称,id值为VPP中对应的硬件接口的索引值。tap/tun接口使用标志TAP_FLAG_TUN来区分,默认创建tap接口。

      tap_create_if_args_t args = {
        .num_rx_queues = clib_max (1, vlib_num_workers ()),
        .num_tx_queues = 1,
        .id = hw->hw_if_index,
        .sw_if_index = ~0,
        .rx_ring_sz = 256,
        .tx_ring_sz = 256,
        .host_if_name = host_if_name,
        .host_namespace = 0,
      };
      ethernet_interface_t *ei;
      if (host_if_type == LCP_ITF_HOST_TUN)
        args.tap_flags |= TAP_FLAG_TUN;
      els {
          ei = pool_elt_at_index (ethernet_main.interfaces, hw->hw_instance);
          mac_address_copy (&args.host_mac_addr, &ei->address.mac);
      }

如下为tap_create_if函数。除了创建linux中对应的tap/tun接口之后,还需创建VPP中的virtio接口,两者关联,以实现报文在VPP和linux之间的交互。

linux接口请求结构ifr默认设置IFF_NO_PI标志,与tap/tun接口的交互报文不需携带额外的协议信息(Protocol Info);标志IFF_VNET_HDR表示交互报文携带virtio网络头部结构(struct virtio_net_hdr),这是virtio规范所需要的。

void  
tap_create_if (vlib_main_t * vm, tap_create_if_args_t * args)
{
  vlib_thread_main_t *thm = vlib_get_thread_main ();
  vlib_physmem_main_t *vpm = &vm->physmem_main;
  virtio_main_t *vim = &virtio_main;
  tap_main_t *tm = &tap_main;
  vnet_hw_if_caps_change_t cc;
  struct ifreq ifr = {.ifr_flags = IFF_NO_PI | IFF_VNET_HDR };
  struct ifreq get_ifr = {.ifr_flags = 0 };
  vhost_memory_t *vhost_mem = 0;
  virtio_if_t *vif = 0;

这里检查id值是否已经被使用,在没有指定id值时,在位图中找到第一个可用的位作为id值,最大支持1024个id(TAP_MAX_INSTANCE)。对于linux-cp插件,其将VPP硬件接口的索引值作为id值,一般情况下不会超出1024的限制值,而且不会造成重复。

如下可见,此id作为virtio接口的标识值。

  if (args->id != ~0) {
      if (clib_bitmap_get (tm->tap_ids, args->id)) {
        args->error = clib_error_return (0, "interface already exists");
        return;
      }
  } else {
      args->id = clib_bitmap_first_clear (tm->tap_ids);
  }
  if (args->id > TAP_MAX_INSTANCE) {
      args->error = clib_error_return (0, "cannot find free interface id");
      return;
  }

由pool中分配virtio_if_t结构的变量vif,用于创建VPP侧的virtio虚拟接口,如下对其进行初始化。

  pool_get_zero (vim->interfaces, vif);

根据参数tap/tun标志,对于tun设备,vif的类型设置为VIRTIO_IF_TYPE_TUN,为避免使用XDP数据路径,发送缓存(sndbuf)大小要小于INT_MAX。对于tap设备,vif类型设置为VIRTIO_IF_TYPE_TAP。

这里同时为linux接口请求结构ifreq设置tap/tun类型标志,内核根据标志决定创建tap/tun类型设备。

  if (args->tap_flags & TAP_FLAG_TUN) {
      vif->type = VIRTIO_IF_TYPE_TUN;
      ifr.ifr_flags |= IFF_TUN;
      /* From kernel 4.20, xdp support has been added in tun_sendmsg.
       * If sndbuf == INT_MAX, vhost batches the packet and processes
       * them using xdp data path for tun driver. It assumes packets
       * are ethernet frames (It needs to be fixed).
       * To avoid xdp data path in tun driver, sndbuf value should be < INT_MAX.
       */
      sndbuf = INT_MAX - 1;
  } else {
      vif->type = VIRTIO_IF_TYPE_TAP;
      ifr.ifr_flags |= IFF_TAP;
      sndbuf = INT_MAX;
  } 

vif的成员dev_instance设置为其在pool中的索引值,vif的id设置为以上逻辑选择的id值。发送队列的数量取值为参数中传入的发送队列数量与线程数量之间的最大值,这里的线程包括VPP的主线程和worker线程;接收队列数量为参数中传入的接收队列数量,至少为1。

对于linux-cp插件,传入的接收队列数量仅为worker线程数量(不包括主线程),所以其相较发送队列数量少一个。

  vif->dev_instance = vif - vim->interfaces;
  vif->id = args->id;
  vif->num_txqs = clib_max (args->num_tx_queues, thm->n_vlib_mains);
  vif->num_rxqs = clib_max (args->num_rx_queues, 1);

如果参数中指定了TAP_FLAG_ATTACH标志,所要绑定的linux主机接口名称不能为空。否则,可不指定名称,由内核自动分配。

  if (args->tap_flags & TAP_FLAG_ATTACH)
    {
      if (args->host_if_name == NULL)
      {
      err = clib_error_return (0, "host_if_name is not provided");
      goto error;
      } 
    } 

如果参数中指定了命名空间,保存现有命名空间,切换到指定的命名空间中。以下操作都在指定的命名空间中进行,创建的接口存在于指定命名空间中。操作完成之后,切换回保存的命名空间old_netns_fd。

  /* if namespace is specified, all further netlink messages should be executed
   * after we change our net namespace */
  if (args->host_namespace)
    {
      old_netns_fd = clib_netns_open (NULL /* self */);
      if ((nfd = clib_netns_open (args->host_namespace)) == -1)
        goto error;
      if (clib_setns (nfd) == -1)
        goto error;
    }

如果参数中指定的host_if_name不为空,LCP插件传入的为非空值,将其拷贝到linux接口请求结构ifreq的成员ifr_name中,之后在linux中创建此名称的设备。

  if (args->host_if_name != NULL)
    {
      host_if_name = (char *) args->host_if_name;
      clib_memcpy (ifr.ifr_name, host_if_name,
           clib_min (IFNAMSIZ, vec_len (host_if_name)));
    }

打开tun设备文件,获取支持的特性值,当前内核(5.10版本)中无论tap/tun设备驱动都支持IFF_VNET_HDR特性,参见内核tap/tun驱动程序,此特性作为vhost-net后端与VPP的virtio接口前端一并使用。如果没有此特性,LCP插件不能正常工作。

打开tun设备文件,将创建一个新的tun_file文件结构及关联的socket结构,返回文件描述符。将得到的文件描述符tfd添加到VPP virtio虚拟接口的tap_fds向量中。

  if ((tfd = open ("/dev/net/tun", O_RDWR | O_NONBLOCK)) < 0)
      goto error;

  vec_add1 (vif->tap_fds, tfd);

  _IOCTL (tfd, TUNGETFEATURES, &tap_features);
  if ((tap_features & IFF_VNET_HDR) == 0)
    {
      args->error = clib_error_return (0, "vhost-net backend not available");
      goto error;
    }

当前tap/tun驱动都是支持IFF_MULTI_QUEUE多队列特性的,如果不支持,将接收队列数量和发送队列数量固定为1。否则,为linux接口请求结构ifr添加IFF_MULTI_QUEUE标志,之后使能tap/tun的多队列特性。

  if ((tap_features & IFF_MULTI_QUEUE) == 0)
    {
      if (vif->num_rxqs > 1) {
        args->error = clib_error_return (0, "multiqueue not supported");
        goto error;
      }
      vif->num_rxqs = vif->num_txqs = 1;
    }
  else
    ifr.ifr_flags |= IFF_MULTI_QUEUE;

TUNSETIFF命令到内核中创建指定名称和属性的tap/tun接口,对于指定IFF_MULTI_QUEUE的情况,linux内核默认创建一个256(MAX_TAP_QUEUES)个队列的tap/tun网络设备,并且,将之前open的文件描述符绑定为此设备的一个队列,此时实际的队列数量为1。

获取新创建tap/tun接口的索引值,保存在VPP virtio虚拟接口结构中。当前LCP插件没有设置TAP_FLAG_GSO标志,也没有设置TAP_FLAG_CSUM_OFFLOAD标志。

  if (args->tap_flags & TAP_FLAG_GSO)
    {
      offload = TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6;
      vif->gso_enabled = 1;
    }
  else if (args->tap_flags & TAP_FLAG_CSUM_OFFLOAD)
    {
      offload = TUN_F_CSUM;
      vif->csum_offload_enabled = 1;
    }
  _IOCTL (tfd, TUNSETIFF, (void *) &ifr);

  vif->ifindex = if_nametoindex (ifr.ifr_ifrn.ifrn_name);

如果参数中没有指定linux主机接口名称,这里将其指定为内核自动分配的名称,对于tap类型设备,内核分配的名称格式为tap%d,对于tun类型设备,名称格式为tun%d。

  if (!args->host_if_name)
    host_if_name = ifr.ifr_ifrn.ifrn_name;
  else
    host_if_name = (char *) args->host_if_name;

对于绑定到已有接口(TAP_FLAG_ATTACH)的情况,首先关闭persist模式。如果参数中设置了TAP_FLAG_PERSIST,再使能persist模式,内核将增加对tap/tun模块的引用计数,以防被卸载。

linux-cp没有指定TAP_FLAG_ATTACH和TAP_FLAG_PERSIST标志。

  /* unset the persistence when attaching to existing interface */
  if (args->tap_flags & TAP_FLAG_ATTACH) {
      _IOCTL (tfd, TUNSETPERSIST, (void *) (uintptr_t) 0);
  }
  if (args->tap_flags & TAP_FLAG_PERSIST)
  {
      _IOCTL (tfd, TUNSETPERSIST, (void *) (uintptr_t) 1);
      _IOCTL (tfd, TUNGETIFF, (void *) &get_ifr);
      if ((get_ifr.ifr_flags & IFF_PERSIST) == 0) {
        args->error = clib_error_return (0, "persistence not supported");
        goto error;
      }
  }

每次打开/dev/net/tun设备,得到的新文件描述符,通过TUNSETIFF绑定到以上创建的tap/tun网络设备上,作为设备的一个新队列。另外,所有的文件描述符都添加到了VPP virtio虚拟接口的成员tap_fds向量中。

由于以上代码已经创建并绑定了一个队列,以下遍历由1开始。注意这里文件描述符是同时作为发送和接收队列(可读写)。

  /* create additional queues on the linux side.
   * we create as many linux queue pairs as we have rx queues
   */
  for (i = 1; i < vif->num_rxqs; i++)
    {
      if ((qfd = open ("/dev/net/tun", O_RDWR | O_NONBLOCK)) < 0)
        goto error;

      vec_add1 (vif->tap_fds, qfd);
      _IOCTL (qfd, TUNSETIFF, (void *) &ifr);
    }

设置每个接收队列的TUNSETVNETHDRSZ、TUNSETSNDBUF和TUNSETOFFLOAD等值。

  hdrsz = sizeof (vnet_virtio_net_hdr_v1_t);

  for (i = 0; i < vif->num_rxqs; i++)
    {
      _IOCTL (vif->tap_fds[i], TUNSETVNETHDRSZ, &hdrsz);
      _IOCTL (vif->tap_fds[i], TUNSETSNDBUF, &sndbuf);
      _IOCTL (vif->tap_fds[i], TUNSETOFFLOAD, offload);

      if (fcntl (vif->tap_fds[i], F_SETFL, O_NONBLOCK) < 0)
        goto error;
    }

vhost-net设备

创建vhost网络设备。每次打开/dev/vhost-net将创建一个新的文件结构,及其关联私有vhost_net/vhost_dev设备结构。返回值为文件描述符。VHOST_SET_OWNER命令将新创建的vhost网络设备关联到当前进程。

将返回的文件描述符保存到VPP virtio虚拟接口成员vhost_fds向量中。

  /* 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++)
    {
      if ((vfd = open ("/dev/vhost-net", O_RDWR | O_NONBLOCK)) < 0)
        goto error;

      vec_add1 (vif->vhost_fds, vfd);
      _IOCTL (vfd, VHOST_SET_OWNER, 0);
    }

VHOST_SET_OWNER也将创建内核线程,名称为vhost-加上vpp的进程号,每个fd文件描述符创建一个内核线程,处理vhost设备的接收/发送。

/ # ps aux
298       0       0       S       /bin/vpp -c /etc/vpp/startup.conf
307       0       0       S       [vhost-298]
308       0       0       S       [vhost-298]
309       0       0       S       [vhost-298]

以下进行特性的协商。获取vhost驱动支持的特性,当前内核vhost-net驱动都支持接收缓存合并(VIRTIO_NET_F_MRG_RXBUF)、间接(二级)描述符(VIRTIO_RING_F_INDIRECT_DESC)和VIRTIO_F_VERSION_1三个特性。

  _IOCTL (vif->vhost_fds[0], VHOST_GET_FEATURES, &vif->remote_features);

  if ((vif->remote_features & VIRTIO_FEATURE (VIRTIO_NET_F_MRG_RXBUF)) == 0)
      goto error;
  if ((vif->remote_features & VIRTIO_FEATURE (VIRTIO_RING_F_INDIRECT_DESC)) == 0)
      goto error;
  if ((vif->remote_features & VIRTIO_FEATURE (VIRTIO_F_VERSION_1)) == 0)
      goto error;

  vif->features |= VIRTIO_FEATURE (VIRTIO_NET_F_MRG_RXBUF);
  vif->features |= VIRTIO_FEATURE (VIRTIO_F_VERSION_1);
  vif->features |= VIRTIO_FEATURE (VIRTIO_RING_F_INDIRECT_DESC);

设置virtio_net_hdr_sz的大小。对于支持VIRTIO_NET_F_MRG_RXBUF或者VIRTIO_F_VERSION_1特性的情况,virtio网络头部的大小为sizeof (vnet_virtio_net_hdr_v1_t);否则,大小设置为sizeof (vnet_virtio_net_hdr_t)。前者相较后者多2个字节,多出的为表示合并的缓存数量的遍历(u16 num_buffers)。

  virtio_set_net_hdr_size (vif);

tap/tun接口参数配置

对于tap类型接口,如果参数中未指定mac地址,生成随机mac地址,将mac地址设置到linux tap/tun接口。

如果参数设置了linux主机桥接口名称,将创建的tap接口设置为桥的子接口,linux-cp插件没有配置host_bridge功能。以下的操作全部都是通过linux的netlink接口下发。

  if (vif->type == VIRTIO_IF_TYPE_TAP)
    {
      if (ethernet_mac_address_is_zero (args->host_mac_addr.bytes))
        ethernet_mac_address_generate (args->host_mac_addr.bytes);
      args->error = vnet_netlink_set_link_addr (vif->ifindex, args->host_mac_addr.bytes);
      if (args->error)
        goto error;

      if (args->host_bridge) {
        args->error = vnet_netlink_set_link_master (vif->ifindex, (char *) args->host_bridge);
        if (args->error)
          goto error;
       }
    }

为新创建的tap/tun接口配置IPv4地址。

  if (args->host_ip4_prefix_len)
    {
      args->error = vnet_netlink_add_ip4_addr (vif->ifindex,
                           &args->host_ip4_addr, args->host_ip4_prefix_len);
      if (args->error)
        goto error;
    }

为新创建的tap/tun接口配置IPv6地址。

  if (args->host_ip6_prefix_len)
    {
      args->error = vnet_netlink_add_ip6_addr (vif->ifindex,
                           &args->host_ip6_addr, args->host_ip6_prefix_len);
      if (args->error)
        goto error;
    }

新创建的tap/tun接口配置为UP状态。

  args->error = vnet_netlink_set_link_state (vif->ifindex, 1 /* UP */ );
  if (args->error)
      goto error;

配置默认的IPv4和IPv6网关。linux-cp插件并没有设置这些参数。

  if (args->host_ip4_gw_set) {
      args->error = vnet_netlink_add_ip4_route (0, 0, &args->host_ip4_gw);
      if (args->error)
        goto error;
  }
  if (args->host_ip6_gw_set) {
      args->error = vnet_netlink_add_ip6_route (0, 0, &args->host_ip6_gw);
      if (args->error)
        goto error;                              
  } 

为新创建的tap/tun接口配置MTU值。

  if (args->host_mtu_set) { 
      args->error = vnet_netlink_set_link_mtu (vif->ifindex, args->host_mtu_size);
      if (args->error)
        goto error;
  } 
  else if (tm->host_mtu_size != 0) {
      args->error = vnet_netlink_set_link_mtu (vif->ifindex, tm->host_mtu_size);
      if (args->error)
        goto error;

      args->host_mtu_set = 1;
      args->host_mtu_size = tm->host_mtu_size;
  }

切换回原本的命名空间。

  /* switch back to old net namespace */
  if (args->host_namespace) {
      if (clib_setns (old_netns_fd) == -1) {
        args->rv = VNET_API_ERROR_SYSCALL_ERROR_2;
        args->error = clib_error_return_unix (0, "setns '%s'", args->host_namespace);
        goto error;
      }
  }

virtio-net队列

初始化virtio-net的接收和发送队列。根据virtio规范,接收队列的队列id为偶数;发送队列的队列id为奇数。

#define TX_QUEUE(X) ((X*2) + 1)
#define RX_QUEUE(X) (X*2)

  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;
  }

初始化vhost内存结构vhost_memory_t的变量vhost_mem,物理地址使用当前vlib_main_t成员physmem_main中定义的地址。对于linux-cp插件而言,内存区的物理地址和映射的用户层地址是相同的,内存区间只有一个。VPP不同于qemu虚拟机。

  /* setup features and memtable */
  i = sizeof (vhost_memory_t) + sizeof (vhost_memory_region_t);
  vhost_mem = clib_mem_alloc (i);
  clib_memset (vhost_mem, 0, i);
  vhost_mem->nregions = 1;
  vhost_mem->regions[0].memory_size = vpm->max_size;
  vhost_mem->regions[0].guest_phys_addr = vpm->base_addr;
  vhost_mem->regions[0].userspace_addr = vhost_mem->regions[0].guest_phys_addr;

  for (i = 0; i < vhost_mem->nregions; i++)
    virtio_log_debug (vif, "memtable region %u memory_size 0x%lx "
              "guest_phys_addr 0x%lx userspace_addr 0x%lx", i,
              vhost_mem->regions[0].memory_size,
              vhost_mem->regions[0].guest_phys_addr,
              vhost_mem->regions[0].userspace_addr);

VHOST_SET_MEM_TABLE将vhost_mem指定的内存区间,添加到vhost设备的IOTLB映射中,所有的vhost_fds都使用相同的映射。将IOVA地址guest_phys_addr映射到地址userspace_addr。

同时,将协商的特性下发。

  for (i = 0; i < num_vhost_queues; i++)
  {
      int fd = vif->vhost_fds[i];
      _IOCTL (fd, VHOST_SET_FEATURES, &vif->features);
      _IOCTL (fd, VHOST_SET_MEM_TABLE, vhost_mem);
  }

关联vhost与tap/tun接口

在以下循环中,初始化发送和接收virtio vring队列组。这里同时处理发送和接收队列,循环次数加倍。奇数遍历处理发送队列,欧树遍历处理接收队列。

  /* finish initializing queue pair */
  for (i = 0; i < num_vhost_queues * 2; i++) {
      vhost_vring_addr_t addr = { 0 };
      vhost_vring_state_t state = { 0 };
      vhost_vring_file_t file = { 0 };
      vnet_virtio_vring_t *vring;
      u16 qp = i >> 1;
      int fd = vif->vhost_fds[qp];

      if (i & 1) {
        if (qp >= vif->num_txqs)
          continue;
        vring = vec_elt_at_index (vif->txq_vrings, qp);
      } else {
        if (qp >= vif->num_rxqs)
          continue;
        vring = vec_elt_at_index (vif->rxq_vrings, qp);
      }

设置vring的队列大小。linux-cp设置为256。

      addr.index = state.index = file.index = vring->queue_id & 1;
      state.num = vring->queue_size;
      virtio_log_debug (vif, "VHOST_SET_VRING_NUM fd %d index %u num %u", fd, state.index, state.num);
      _IOCTL (fd, VHOST_SET_VRING_NUM, &state);

下发virtio规范中定义的描述符desc/avail/used地址信息。

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

      virtio_log_debug (vif, "VHOST_SET_VRING_ADDR fd %d index %u flags 0x%x "
            "desc_user_addr 0x%lx avail_user_addr 0x%lx "
            "used_user_addr 0x%lx", fd, addr.index,
            addr.flags, addr.desc_user_addr, addr.avail_user_addr,
            addr.used_user_addr);
      _IOCTL (fd, VHOST_SET_VRING_ADDR, &addr);

设置call_fd和kick_fd描述符。call_fd用于vhost通知前端used描述符的变化;kick_fd用于前端通知vhost可用avail描述符的变化,vhost在监听kick_fd。

      file.fd = vring->call_fd;
      virtio_log_debug (vif, "VHOST_SET_VRING_CALL fd %d index %u call_fd %d", fd, file.index, file.fd);
      _IOCTL (fd, VHOST_SET_VRING_CALL, &file);

      file.fd = vring->kick_fd;
      virtio_log_debug (vif, "VHOST_SET_VRING_KICK fd %d index %u kick_fd %d", fd, file.index, file.fd);
      _IOCTL (fd, VHOST_SET_VRING_KICK, &file);

将新创建的tap/tun设备的描述符,设置为vhost-net设备的backend。vhost设备的发送对应tap/tun设备的sendmsg;vhost设备的接收对应tap/tun设备的recvmsg。

      file.fd = vif->tap_fds[qp % vif->num_rxqs];
      virtio_log_debug (vif, "VHOST_NET_SET_BACKEND fd %d index %u tap_fd %d", fd, file.index, file.fd);
      _IOCTL (fd, VHOST_NET_SET_BACKEND, &file);
  }

virtio接口配置

virtio接口的网络信息与linux中tap/tun接口的设置一致,如下设置virtio接口的mac地址信息。

  if (vif->type == VIRTIO_IF_TYPE_TAP) {
      if (!args->mac_addr_set)
        ethernet_mac_address_generate (args->mac_addr.bytes);

      clib_memcpy (vif->mac_addr, args->mac_addr.bytes, 6);
      if (args->host_bridge)
        vif->host_bridge = format (0, "%s%c", args->host_bridge, 0);
  }

初始化virtio接口的mtu、ipv4/ipv6等信息。

  vif->host_if_name = format (0, "%s%c", host_if_name, 0);
  if (args->host_namespace)
    vif->net_ns = format (0, "%s%c", args->host_namespace, 0);
  vif->host_mtu_size = args->host_mtu_size;
  vif->tap_flags = args->tap_flags;
  clib_memcpy (vif->host_mac_addr, args->host_mac_addr.bytes, 6);
  vif->host_ip4_prefix_len = args->host_ip4_prefix_len;
  vif->host_ip6_prefix_len = args->host_ip6_prefix_len;
  if (args->host_ip4_prefix_len)
    clib_memcpy (&vif->host_ip4_addr, &args->host_ip4_addr, 4);
  if (args->host_ip6_prefix_len)
    clib_memcpy (&vif->host_ip6_addr, &args->host_ip6_addr, 16);

VPP virtio设备

对于tap类型,将virtio接口注册为VPP以太网设备。对于tun类型,将virtio接口注册为VPP tun-device类设备。设备类别(dev_class)都为virtio_device_class。但是,硬件类别前者为ethernet_hw_interface_class,后者为tun_device_hw_interface_class。

  if (vif->type != VIRTIO_IF_TYPE_TUN {
      vnet_eth_interface_registration_t eir = {};

      eir.dev_class_index = virtio_device_class.index;
      eir.dev_instance = vif->dev_instance;
      eir.address = vif->mac_addr;
      eir.cb.flag_change = virtio_eth_flag_change;
      eir.cb.set_max_frame_size = virtio_eth_set_max_frame_size;
      vif->hw_if_index = vnet_eth_register_interface (vnm, &eir);
  } else {
      vif->hw_if_index = vnet_register_interface (vnm, 
         virtio_device_class.index, vif->dev_instance /* device instance */ ,
         tun_device_hw_interface_class.index, vif->dev_instance);
  }

初始化virtio接口的能力集。

  tm->tap_ids = clib_bitmap_set (tm->tap_ids, vif->id, 1);
  sw = vnet_get_hw_sw_interface (vnm, vif->hw_if_index);
  vif->sw_if_index = sw->sw_if_index;
  args->sw_if_index = vif->sw_if_index;
  args->rv = 0;
  hw = vnet_get_hw_interface (vnm, vif->hw_if_index);
  cc.mask = VNET_HW_IF_CAP_INT_MODE | VNET_HW_IF_CAP_TCP_GSO |
        VNET_HW_IF_CAP_TX_TCP_CKSUM | VNET_HW_IF_CAP_TX_UDP_CKSUM;
  cc.val = VNET_HW_IF_CAP_INT_MODE;

  if (args->tap_flags & TAP_FLAG_GSO)
    cc.val |= VNET_HW_IF_CAP_TCP_GSO | VNET_HW_IF_CAP_TX_TCP_CKSUM |
          VNET_HW_IF_CAP_TX_UDP_CKSUM;
  else if (args->tap_flags & TAP_FLAG_CSUM_OFFLOAD)
    cc.val |= VNET_HW_IF_CAP_TX_TCP_CKSUM | VNET_HW_IF_CAP_TX_UDP_CKSUM;

  if ((args->tap_flags & TAP_FLAG_GSO) && (args->tap_flags & TAP_FLAG_GRO_COALESCE)
      virtio_set_packet_coalesce (vif);
  if (vif->type == VIRTIO_IF_TYPE_TUN) {
      hw->min_frame_size = TUN_MIN_PACKET_BYTES;
      vnet_hw_interface_set_mtu ( vnm, hw->hw_if_index, args->host_mtu_size ? args->host_mtu_size : TUN_DEFAULT_PACKET_BYTES);
  }
  vnet_hw_if_change_caps (vnm, vif->hw_if_index, &cc);

设置virtio前端接口的发送和接口virtqueue。

  virtio_pre_input_node_enable (vm, vif);
  virtio_vring_set_rx_queues (vm, vif);
  virtio_vring_set_tx_queues (vm, vif);

  vif->per_interface_next_index = ~0;
  vnet_hw_interface_set_flags (vnm, vif->hw_if_index, VNET_HW_INTERFACE_FLAG_LINK_UP);
  /*
   * Host tun/tap driver link carrier state is "up" at creation. The
   * driver never changes this unless the backend (VPP) changes it using
   * TUNSETCARRIER ioctl(). See tap_set_carrier().
   */
  vif->host_carrier_up = 1;

virtqueue分配

如下virtio规范中定义个virtqueue的三个部分的大小和对其方式。其中对齐方式规定的为最小值,以下代码可见,VPP中都是按照cache大小进行的对齐。

Virtqueue Part Alignment Size
Descriptor Table 16 16*(Queue Size)
Available Ring 2 6 + 2*(Queue Size)
Used Ring 4 6 + 8*(Queue Size)

VPP函数virtio_vring_init分配结构vnet_virtio_vring_t,这里使用virtio pci规范中使用的vring名称,与virtio规范中的virtqueue是相同。默认的virtqueue大小为256,最大值为32768。具体参加virtio规范。

clib_error_t *
virtio_vring_init (vlib_main_t * vm, virtio_if_t * vif, u16 idx, u16 sz)
{
  vnet_virtio_vring_t *vring;

  if (!is_pow2 (sz))
    return clib_error_return (0, "ring size must be power of 2");
  if (sz > 32768)
    return clib_error_return (0, "ring size must be 32768 or lower");
  if (sz == 0)
    sz = 256;

分配发送或者接收virtqueue结构。奇数索引为发送;偶数索引为接收。

#define TX_QUEUE_ACCESS(X) (X/2)
#define RX_QUEUE_ACCESS(X) (X/2)

  if (idx % 2)
    {
      vec_validate_aligned (vif->txq_vrings, TX_QUEUE_ACCESS (idx), CLIB_CACHE_LINE_BYTES);
      vring = vec_elt_at_index (vif->txq_vrings, TX_QUEUE_ACCESS (idx));
      clib_spinlock_init (&vring->lockp);
    } else {
      vec_validate_aligned (vif->rxq_vrings, RX_QUEUE_ACCESS (idx), CLIB_CACHE_LINE_BYTES);
      vring = vec_elt_at_index (vif->rxq_vrings, RX_QUEUE_ACCESS (idx));
    }

分配buffer描述符,大小为队列长度sz与结构体vnet_virtio_vring_desc_t长度(16)的乘积,如上表格。对其方式为按照cache大小对齐。

  i = sizeof (vnet_virtio_vring_desc_t) * sz;
  i = round_pow2 (i, CLIB_CACHE_LINE_BYTES);
  vring->desc = clib_mem_alloc_aligned (i, CLIB_CACHE_LINE_BYTES);
  clib_memset (vring->desc, 0, i);

分配可用buffer索引,大小为结构体vnet_virtio_vring_avail_t长度(6),加上队列长度sz与2的乘积,vring->avail->ring为u16类型。

标志VRING_AVAIL_F_NO_INTERRUPT表明在此可用描述符使用完成之后,vhost不发送中断事件(call_fd)。

  i = sizeof (vnet_virtio_vring_avail_t) + sz * sizeof (vring->avail->ring[0]);
  i = round_pow2 (i, CLIB_CACHE_LINE_BYTES);
  vring->avail = clib_mem_alloc_aligned (i, CLIB_CACHE_LINE_BYTES);
  clib_memset (vring->avail, 0, i);
  // tell kernel that we don't need interrupt
  vring->avail->flags = VRING_AVAIL_F_NO_INTERRUPT;

分配已使用buffer索引,大小为结构体vnet_virtio_vring_used_t长度(8),加上队列长度sz与结构体vnet_virtio_vring_used_elem_t程度(8)的乘积,

  i = sizeof (vnet_virtio_vring_used_t) + sz * sizeof (vnet_virtio_vring_used_elem_t);
  i = round_pow2 (i, CLIB_CACHE_LINE_BYTES);
  vring->used = clib_mem_alloc_aligned (i, CLIB_CACHE_LINE_BYTES);
  clib_memset (vring->used, 0, i);

分配sz数量的buffers缓存,这里并没有分配实际的buffer空间。

  vring->queue_id = idx;
  ASSERT (vring->buffers == 0);
  vec_validate_aligned (vring->buffers, sz, CLIB_CACHE_LINE_BYTES);

对于发送vring,在发送完成之后,不需要vhost去通知前端,不初始化call_fd。最后分配kick_fd。

  if (idx & 1) {
      clib_memset_u32 (vring->buffers, ~0, sz);
      // tx path: suppress the interrupts from kernel
      vring->call_fd = -1;
  } else
    vring->call_fd = eventfd (0, EFD_NONBLOCK | EFD_CLOEXEC);

  vring->queue_size = sz;
  vring->kick_fd = eventfd (0, EFD_NONBLOCK | EFD_CLOEXEC);

示例

如下,由于VPP的worker线程有3个,tap-fds有三个描述符为41,42,43。vhost-fds有4个描述符44,45,46,47。

vpp# show tap tap1
Interface: tap1 (ifindex 5)
  name "m2/1"
  host-ns "(nil)"
  host-mtu-size "9000"
  host-mac-addr: 00:60:e0:80:bc:83
  host-carrier-up: 1
  vhost-fds 44 45 46 47
  tap-fds 41 42 43
  gso-enabled 0
  csum-enabled 0
  packet-coalesce 0
  packet-buffering 0
  Mac Address: 02:fe:6b:f6:30:f3
  Device instance: 0
  flags 0x1
    admin-up (0)

协商的特性如下:

  features 0x110008000
    VIRTIO_NET_F_MRG_RXBUF (15)
    VIRTIO_RING_F_INDIRECT_DESC (28)
    VIRTIO_F_VERSION_1 (32)
  remote-features 0x13d008000
    VIRTIO_NET_F_MRG_RXBUF (15)
    VIRTIO_F_NOTIFY_ON_EMPTY (24)
    VHOST_F_LOG_ALL (26)
    VIRTIO_F_ANY_LAYOUT (27)
    VIRTIO_RING_F_INDIRECT_DESC (28)
    VIRTIO_RING_F_EVENT_IDX (29)
    VIRTIO_F_VERSION_1 (32)

接收队列为3个,偶数的0,2,4;发送队列为4个,奇数的1,3,5,7。

  Number of RX Virtqueue  3
  Number of TX Virtqueue  4
  Virtqueue (RX) 0
    qsz 256, last_used_idx 2, desc_next 0, desc_in_use 254
    avail.flags 0x1 avail.idx 256 used.flags 0x1 used.idx 2
    kickfd 49, callfd 48
  Virtqueue (RX) 2
    qsz 256, last_used_idx 4, desc_next 0, desc_in_use 252
    avail.flags 0x1 avail.idx 256 used.flags 0x1 used.idx 4
    kickfd 52, callfd 51
  Virtqueue (RX) 4
    qsz 256, last_used_idx 8, desc_next 0, desc_in_use 248
    avail.flags 0x1 avail.idx 256 used.flags 0x1 used.idx 8
    kickfd 55, callfd 54
  Virtqueue (TX) 1
    qsz 256, last_used_idx 0, desc_next 0, desc_in_use 0
    avail.flags 0x1 avail.idx 0 used.flags 0x0 used.idx 0
    kickfd 50, callfd -1
  Virtqueue (TX) 3
    qsz 256, last_used_idx 0, desc_next 0, desc_in_use 0
    avail.flags 0x1 avail.idx 0 used.flags 0x0 used.idx 0
    kickfd 53, callfd -1
  Virtqueue (TX) 5
    qsz 256, last_used_idx 0, desc_next 0, desc_in_use 0
    avail.flags 0x1 avail.idx 0 used.flags 0x0 used.idx 0
    kickfd 56, callfd -1
  Virtqueue (TX) 7
    qsz 256, last_used_idx 0, desc_next 0, desc_in_use 0
    avail.flags 0x1 avail.idx 0 used.flags 0x0 used.idx 0
    kickfd 57, callfd -1
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐