vpp中dpdk接口注册流程分析

6 篇文章 4 订阅

vpp是一个优秀的包处理转发框架,可以采用非常多的接口类型来进行收发包,应用最多的就是dpdk了,因此本篇博客主要探讨被dpdk接管的网卡是如何注册到vpp中的。vpp代码版本是1904.本文代码贴的比较少,只描述了一些函数调用关系,最好可以参照源码阅读。

关于dpdk就不再赘述,简单的理解dpdk就是一个开发组件,提供了网卡驱动,可以接管原先由内核管理的网卡,实现kernel bypass。同时dpdk也提供了一些辅助机制,可以加快网卡报文的收发。而在vpp中,dpdk是作为一个插件来实现的。在编译vpp,执行make install-ext-deps的时候会自动下载dpdk源码进行编译安装,vpp代码中/src/plugins/dpdk则实现参数解析、dpdk初始化、网卡绑定、网卡注册到管理中心、报文收发等操作。

1.dpdk初始化

dpdk初始化的相关函数在文件/src/plugins/dpdk/device/init.c中,有兴趣的读者可以阅读下源码。

熟悉dpdk的读者应该知道,dpdk的初始化主要是由rte_eal_init函数来实现。在vpp中,调用关系是dpdk_config->rte_eal_init.而dpdk_config是由宏定义VLIB_CONFIG_FUNCTION (dpdk_config, "dpdk");将dpdk_config函数到相关链表中,vpp启动的时候会进行调用。dpdk_config函数主要是对启动参数进行解析,然后将解析后的参数传入rte_eal_init中,从而实现dpdk的eal环境初始化

2.关于vpp的接口管理

2.1.vpp接口层

vpp中有一个接口层的概念,所谓接口层,就是硬件驱动和上层软件之间一层抽象代码,屏蔽硬件的差异,为上层软件提供一些统一的操作接口。上层软件调用接口层的操作进行报文的读入与发出,同时可以进行硬件设备的设置以及相关信息(比如统计数据)的读取。
vpp的接口层又分为了硬件接口层hw和软件接口层sw。在进行hw和sw分析之前,先理清vpp代码中的设备类和接口类(device class和interface class):
device class,由宏定义VNET_DEVICE_CLASS进行定义。这个宏定义定义了这个class的添加和删除函数。对于这个class的理解,我理解的是这描述了硬件驱动。比如两个网卡类型不同,但是都是使用dpdk驱动,那么这两个网卡都会使用到这个结构体。

interface class,我认为这个描述了链路层。对于不同的网络设备,其链路层工作原理并不相同,有的是Ethernet设备,有的是vlan设备,因此,vpp增加了interface结构体用来描述链路层,其结构体是vnet_hw_interface_class_t。

硬件驱动可以理解成网络结构的第一层,链路层是第二层,vpp采用这种分层的定义方式是非常符合分层思想的。
                
关于硬件层接口定义:对于同一个驱动,可能会有绑定多个硬件设备,vpp中使用结构体vnet_hw_interface_t来描述具体某一个硬件设备,结构体中包含了device_class_index,hw_interface_class_index用来表示这个硬件设备属于上述哪个驱动类和链路层类。在代码中用hw或者hi来标识。
关于软件层接口定义:有时候我们会为某个硬件设备设置子接口,使用了软件层interface来描述这些子接口,结构体是vnet_sw_interface_t,在代码中用sw来标识。

vpp为了统一管理这些接口,使用了vnet_interface_main_t这个结构体,其包含了hw_interface_t和sw_interface_t的数组和个数,硬件名称与索引的哈希表等。这个结构体定义的变量是一个全局变量,vnm->interface_main。后续所有的接口都要填充到这个结构体中。

2.2.interface_main的初始化

前面所述,网卡最终是需要注册到vnm->interface_main中,那vnm->interface_main是何时初始化的呢?

VLIB_INIT_FUNCTION (vnet_interface_init);interface_main的初始化在vnet_interface_init函数中进行。

vnet_interface_init这个函数除了对vnet_interface_main_t结构体的相关参数进行初始化之外,还有一个很重要的操作,就是对(vnet_device_class_t)device class的tx_function进行赋值。这个赋值决定了后续网卡发包的执行函数。在第四章进行分析。

2.3.dpdk接口关于sw、hw结构体的初始化

dpdk对接口层的初始化是在dpdk_lib_init这个函数中完成的。dpdk_lib_init这个函数比较长,简单的来说主要做了三件事情。

1.dpdk_device_t *xd结构体的初始化

dpdk_device_t 是用来描述dpdk设备的结构体,非常重要。rte_eal_init这个函数执行完成之后,我们并没有对网卡本身做配置。参考dpdk提供的例子,在eal环境初始化完成之后,我们需要对网卡的参数,比如报文描述符个数,link speed,rxmode,txmode,网卡队列个数等进行配置,dpdk_device_t 这个结构体就是为了后续配置网卡的。当然,这个结构体还包含了vpp为了做结构管理所添加的参数,比如hw_if_index和sw_if_index等。结构体定义在/src/plugins/dpdk/device/dpdk.h文件中。

2.调用ethernet_register_interface来进行设备的注册。ethernet_register_interface这个函数是vpp向外提供ethernet设备注册的api,在这个函数中会继续调用vnet_register_interface,向接口管理中心(vnm->interface_main)注册一个接口。同时填充hw结构和sw结构。函数实现在/src/vnet/ethernet/interface.c文件中

3.调用dpdk_device_setup来对网卡进行配置以及给接口分配指定的收包线程,入参是前面所述的dpdk_device_t *xd。这个函数会继续调用rte_eth_dev_configure,即dpdk提供了网卡配置api。调用rte_eth_rx_queue_setup函数来分配收包线程,每个队列一个线程。

 


3.dpdk收包

dpdk是作为一个插件来运行的,收包是由一个input node实现。

VLIB_REGISTER_NODE (dpdk_input_node);

VLIB_NODE_FN (dpdk_input_node) (vlib_main_t * vm, vlib_node_runtime_t * node,
				vlib_frame_t * f)
{
  dpdk_main_t *dm = &dpdk_main;
  dpdk_device_t *xd;
  uword n_rx_packets = 0;
  vnet_device_input_runtime_t *rt = (void *) node->runtime_data;
  vnet_device_and_queue_t *dq;
  u32 thread_index = node->thread_index;

  /*
   * Poll all devices on this cpu for input/interrupts.
   */
  /* *INDENT-OFF* */
  foreach_device_and_queue (dq, rt->devices_and_queues)
    {
      xd = vec_elt_at_index(dm->devices, dq->dev_instance);
      if (PREDICT_FALSE (xd->flags & DPDK_DEVICE_FLAG_BOND_SLAVE))
	continue;	/* Do not poll slave to a bonded interface */
      n_rx_packets += dpdk_device_input (vm, dm, xd, node, thread_index,
					 dq->queue_id);
    }
  /* *INDENT-ON* */
  return n_rx_packets;
}

dpdk_device_input:完成收包操作,将报文传入下一个node,默认为ethernet_input node

4.dpdk发包

我觉得dpdk的发包逻辑还是比较复杂的,我原本以为会在插件中实现一个dpdk output之类的node,后来发现事情不是这么简单。

4.1.接口tx function的赋值

前面2.2说到vnet_interface_init这个函数会对(vnet_device_class_t)device class的tx_function进行赋值。这个tx_function就是最终的接口发包函数。

VNET_DEVICE_CLASS_TX_FN (dpdk_device_class) (vlib_main_t * vm,
					     vlib_node_runtime_t * node,
					     vlib_frame_t * f)
{
  dpdk_main_t *dm = &dpdk_main;
  vnet_interface_output_runtime_t *rd = (void *) node->runtime_data;
  dpdk_device_t *xd = vec_elt_at_index (dm->devices, rd->dev_instance);
  u32 n_packets = f->n_vectors;
  u32 n_left;
  u32 thread_index = vm->thread_index;
  int queue_id = thread_index;
  u32 tx_pkts = 0, all_or_flags = 0;
  dpdk_per_thread_data_t *ptd = vec_elt_at_index (dm->per_thread_data,
						  thread_index);
  struct rte_mbuf **mb;
  vlib_buffer_t *b[4];

  ASSERT (n_packets <= VLIB_FRAME_SIZE);

  /* calculate rte_mbuf pointers out of buffer indices */
  vlib_get_buffers_with_offset (vm, vlib_frame_vector_args (f),
				(void **) ptd->mbufs, n_packets,
				-(i32) sizeof (struct rte_mbuf));

  n_left = n_packets;
  mb = ptd->mbufs;

  while (n_left >= 8)
    {
      u32 or_flags;

      dpdk_prefetch_buffer (vm, mb[4]);
      dpdk_prefetch_buffer (vm, mb[5]);
      dpdk_prefetch_buffer (vm, mb[6]);
      dpdk_prefetch_buffer (vm, mb[7]);

      b[0] = vlib_buffer_from_rte_mbuf (mb[0]);
      b[1] = vlib_buffer_from_rte_mbuf (mb[1]);
      b[2] = vlib_buffer_from_rte_mbuf (mb[2]);
      b[3] = vlib_buffer_from_rte_mbuf (mb[3]);

      or_flags = b[0]->flags | b[1]->flags | b[2]->flags | b[3]->flags;
      all_or_flags |= or_flags;

      VLIB_BUFFER_TRACE_TRAJECTORY_INIT (b[0]);
      VLIB_BUFFER_TRACE_TRAJECTORY_INIT (b[1]);
      VLIB_BUFFER_TRACE_TRAJECTORY_INIT (b[2]);
      VLIB_BUFFER_TRACE_TRAJECTORY_INIT (b[3]);

      if (or_flags & VLIB_BUFFER_NEXT_PRESENT)
	{
	  dpdk_validate_rte_mbuf (vm, b[0], 1);
	  dpdk_validate_rte_mbuf (vm, b[1], 1);
	  dpdk_validate_rte_mbuf (vm, b[2], 1);
	  dpdk_validate_rte_mbuf (vm, b[3], 1);
	}
      else
	{
	  dpdk_validate_rte_mbuf (vm, b[0], 0);
	  dpdk_validate_rte_mbuf (vm, b[1], 0);
	  dpdk_validate_rte_mbuf (vm, b[2], 0);
	  dpdk_validate_rte_mbuf (vm, b[3], 0);
	}

      if (PREDICT_FALSE ((xd->flags & DPDK_DEVICE_FLAG_TX_OFFLOAD) &&
			 (or_flags &
			  (VNET_BUFFER_F_OFFLOAD_TCP_CKSUM
			   | VNET_BUFFER_F_OFFLOAD_IP_CKSUM
			   | VNET_BUFFER_F_OFFLOAD_UDP_CKSUM))))
	{
	  dpdk_buffer_tx_offload (xd, b[0], mb[0]);
	  dpdk_buffer_tx_offload (xd, b[1], mb[1]);
	  dpdk_buffer_tx_offload (xd, b[2], mb[2]);
	  dpdk_buffer_tx_offload (xd, b[3], mb[3]);
	}

      if (PREDICT_FALSE (node->flags & VLIB_NODE_FLAG_TRACE))
	{
	  if (b[0]->flags & VLIB_BUFFER_IS_TRACED)
	    dpdk_tx_trace_buffer (dm, node, xd, queue_id, b[0]);
	  if (b[1]->flags & VLIB_BUFFER_IS_TRACED)
	    dpdk_tx_trace_buffer (dm, node, xd, queue_id, b[1]);
	  if (b[2]->flags & VLIB_BUFFER_IS_TRACED)
	    dpdk_tx_trace_buffer (dm, node, xd, queue_id, b[2]);
	  if (b[3]->flags & VLIB_BUFFER_IS_TRACED)
	    dpdk_tx_trace_buffer (dm, node, xd, queue_id, b[3]);
	}

      mb += 4;
      n_left -= 4;
    }
  while (n_left > 0)
    {
      b[0] = vlib_buffer_from_rte_mbuf (mb[0]);
      all_or_flags |= b[0]->flags;
      VLIB_BUFFER_TRACE_TRAJECTORY_INIT (b[0]);

      dpdk_validate_rte_mbuf (vm, b[0], 1);
      dpdk_buffer_tx_offload (xd, b[0], mb[0]);

      if (PREDICT_FALSE (node->flags & VLIB_NODE_FLAG_TRACE))
	if (b[0]->flags & VLIB_BUFFER_IS_TRACED)
	  dpdk_tx_trace_buffer (dm, node, xd, queue_id, b[0]);

      mb++;
      n_left--;
    }

  /* transmit as many packets as possible */
  tx_pkts = n_packets = mb - ptd->mbufs;
  n_left = tx_burst_vector_internal (vm, xd, ptd->mbufs, n_packets);

  {
    /* If there is no callback then drop any non-transmitted packets */
    if (PREDICT_FALSE (n_left))
      {
	tx_pkts -= n_left;
	vlib_simple_counter_main_t *cm;
	vnet_main_t *vnm = vnet_get_main ();

	cm = vec_elt_at_index (vnm->interface_main.sw_if_counters,
			       VNET_INTERFACE_COUNTER_TX_ERROR);

	vlib_increment_simple_counter (cm, thread_index, xd->sw_if_index,
				       n_left);

	vlib_error_count (vm, node->node_index, DPDK_TX_FUNC_ERROR_PKT_DROP,
			  n_left);

	while (n_left--)
	  rte_pktmbuf_free (ptd->mbufs[n_packets - n_left - 1]);
      }
  }

  return tx_pkts;
}

VNET_DEVICE_CLASS_TX_FN (dpdk_device_class)这个宏定义将dpdk_device_class设备类的发送函数设置为上述代码,注册到dpdk_device_class.tx_fn_registrations链表中,在vnet_interface_init函数中进行遍历,最终的效果是将上述代码设置成dpdk_device_class.tx_function。

4.2.接口output node和tx node的初始化

vpp的所有功能都是由一个个node组成的,所以发送报文这个功能也是由一个node执行的。

在2.3中说到vnet_register_interface这个函数注册接口。除了对接口进行注册外,还有一个功能,那就是注册一个output node和tx node,这两个node就是最终接口的发送node


/* Register an interface instance. */
u32
vnet_register_interface (vnet_main_t * vnm,
			 u32 dev_class_index,
			 u32 dev_instance,
			 u32 hw_class_index, u32 hw_instance)
{
  //前面代码没有贴出来

//注册tx node
    r.type = VLIB_NODE_TYPE_INTERNAL;
      r.runtime_data = &rt;
      r.runtime_data_bytes = sizeof (rt);
      r.scalar_size = 0;
      r.vector_size = sizeof (u32);

      r.flags = VLIB_NODE_FLAG_IS_OUTPUT;
      r.name = tx_node_name;
      r.function = dev_class->tx_function;

      hw->tx_node_index = vlib_register_node (vm, &r);

      vlib_node_add_named_next_with_slot (vm, hw->tx_node_index,
					  "error-drop",
					  VNET_INTERFACE_TX_NEXT_DROP);

  //注册output node
      hw->output_node_index = vlib_register_node (vm, &r);

      vlib_node_add_named_next_with_slot (vm, hw->output_node_index,
					  "error-drop",
					  VNET_INTERFACE_OUTPUT_NEXT_DROP);
      vlib_node_add_next_with_slot (vm, hw->output_node_index,
				    hw->tx_node_index,
				    VNET_INTERFACE_OUTPUT_NEXT_TX);
}

所有的接口output node的入口函数都设置为vnet_interface_output_node。对于dpdk设备来说,tx node的function就是VNET_DEVICE_CLASS_TX_FN (dpdk_device_class)定义的函数。

4.3.发送流程

以ip4报文处理流程举例。当我们处理完ip4报文后,一般下一个节点会指定为ip4-lookup,来查找路由,后续的流程一般是ip4_rewirte,interface-output.

VLIB_REGISTER_NODE (vnet_per_buffer_interface_output_node) = {
  .name = "interface-output",
  .vector_size = sizeof (u32),
};

interface-output节点的入口函数是

VLIB_NODE_FN (vnet_per_buffer_interface_output_node) (vlib_main_t * vm,
						      vlib_node_runtime_t *
						      node,
						      vlib_frame_t * frame)
{
...
}

在这个函数中,会通过读取buffer->sw_if_index[VNET_TX]的值来确定是由哪个接口往外发送。下一个节点就是4.2中所说的接口output node。整体的报文发送流程就走完了。

4.4.interface output node如何获取到接口的output node index

前面说到interface output node的执行函数会读取buffer->sw_if_index[VNET_TX]的值来确定是由哪个接口往外发送。从buffer->sw_if_index[VNET_TX]的值我们可以获取到接口信息,那么在interface output node的执行函数中,是如何获取到这个接口的output node index的呢?

回答这个问题,就需要回头看看vpp的初始化过程。在src/vnet/interface_output.c文件中,有一处宏定义:

VNET_HW_INTERFACE_ADD_DEL_FUNCTION (vnet_per_buffer_interface_output_hw_interface_add_del);

先说下vnet_per_buffer_interface_output_hw_interface_add_del这个函数的作用。

clib_error_t *
vnet_per_buffer_interface_output_hw_interface_add_del (vnet_main_t * vnm,
						       u32 hw_if_index,
						       u32 is_create)
{
  vnet_hw_interface_t *hi = vnet_get_hw_interface (vnm, hw_if_index);
  u32 next_index;

  if (hi->output_node_index == 0)
    return 0;

  next_index = vlib_node_add_next
    (vnm->vlib_main, vnet_per_buffer_interface_output_node.index,
     hi->output_node_index);
  hi->output_node_next_index = next_index;

  return 0;
}

这个函数是先获取接口hw结构,然后将接口的output node放在interface output node后面,这样在interface output node执行函数中就可以获取到下一个节点的索引,即接口output node index。那么这个函数是什么时候执行的呢?

看宏定义的内容可以发现,这个宏定义的目的是为了在main函数执行之前,执行vnm->hw_interface_add_del_functions[VNET_ITF_FUNC_PRIORITY_LOW].fp=vnet_per_buffer_interface_output_hw_interface_add_del。这样在main函数执行之前,vnm->hw_interface_add_del_functions这个数组就完成了初始化。

vnet_register_interface
	->vnet_hw_interface_set_flags_helper
		->call_hw_interface_add_del_callbacks
			->call_elf_section_interface_callbacks
				->elt->fp(即调用函数vnet_per_buffer_interface_output_hw_interface_add_del)

在上述函数调用流程中可以看到,注册接口的时候,会调用到vnet_per_buffer_interface_output_hw_interface_add_del这个函数,从而将接口的output node放在interface output node后面。如果是多个接口,interface output node后面就会有多个接口output node,然后根据buffer->sw_if_index[VNET_TX]的值来确定是哪个接口。

 

5.思考

vpp的代码还是比较绕的,但是其设计思想值得借鉴。对于一个网络设备,其本身就是工作在分层架构中,在软件框架的实现上也遵循了分层架构,使得逻辑比较清晰。但是vpp代码中用了大量的宏定义,造成了代码阅读的不便。1904版本的源码相比之前的代码也有一些改动,最直观的就是节点注册VLIB_REGISTER_NODE中,不显式的定义node.function,而是通过VLIB_NODE_FN宏来实现。根据注释推测这可以给node设置不同优先级的function,但是我感觉这给源码阅读带来了很多的不便。关于接口发送节点,为什么不直接用VLIB_REGISTER_NODE宏来定义,而是通过vlib_register_node函数,这个我理解的是vpp要适配不同的驱动,用vlib_register_node函数来定义,不同的驱动可以共用一个函数。对于新加入的接口类型,可以直接以插件的方式,只需要调用vpp提供的相关类型的register函数,不用修改vpp源码。

vpp代码中不同的宏定义其在vpp启动过程中执行顺序不同,关于不同宏定义执行顺序,后续再阅读相关代码。

参考文章:https://segmentfault.com/a/1190000019400426?utm_source=tag-newest

  • 3
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要配置FD.io VPPDPDK,您可以按照以下步骤进行操作: 1. 安装DPDK:首先,确保您的系统符合DPDK的要求,并按照DPDK官方文档的说明进行安装。您可以从DPDK官方网站上下载DPDK的源代码,并按照提供的说明进行编译和安装。 2. 配置DPDK环境变量:设置DPDK环境变量,包括`RTE_SDK`和`RTE_TARGET`。`RTE_SDK`指向DPDK源代码目录的路径,`RTE_TARGET`指定您要构建的目标平台。例如,在bash shell,您可以使用以下命令设置环境变量: ``` export RTE_SDK=/path/to/dpdk export RTE_TARGET=<target> ``` 3. 配置VPP:安装FD.io VPP并启动VPP进程。您可以从FD.io VPP官方网站上获取安装说明,并根据指南进行安装。在启动VPP之前,确保您已正确配置了DPDK。 4. 配置VPPDPDK集成:编辑VPP的运行时配置文件,通常是位于`/etc/vpp/startup.conf`。将DPDK驱动程序与VPP绑定,指定所需的物理接口和CPU核心。 例如,要将DPDK绑定到VPP并配置两个物理接口(例如eth0和eth1),可以在配置文件添加以下内容: ``` dpdk { dev <DPDK_DEVICE_NAME> { num-rx-queues <NUM_RX_QUEUES> num-tx-queues <NUM_TX_QUEUES> socket-mem <SOCKET_MEM> } } interface <INTERFACE_NAME> { dpdk <DPDK_DEVICE_NAME> } ``` 在上述配置,您需要将`<DPDK_DEVICE_NAME>`替换为DPDK设备名称(例如`0000:00:00.0`),`<NUM_RX_QUEUES>`和`<NUM_TX_QUEUES>`分别是接收和发送队列的数量,`<SOCKET_MEM>`是用于DPDK内存的分配。 5. 启动VPP:使用VPP启动命令启动VPP进程,例如: ``` sudo vpp -c /etc/vpp/startup.conf ``` 在启动VPP后,它将根据配置文件的设置与DPDK集成。 请注意,这只是简单的配置示例,您可能需要根据您的特定需求进行更详细的配置。您可以参考FD.io VPPDPDK的官方文档以获取更多详细信息和配置选项。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值