博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
vpp中dpdk接口注册流程分析
阅读量:4204 次
发布时间:2019-05-26

本文共 12035 字,大约阅读时间需要 40 分钟。

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. */u32vnet_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启动过程中执行顺序不同,关于不同宏定义执行顺序,后续再阅读相关代码。

参考文章:

转载地址:http://cyali.baihongyu.com/

你可能感兴趣的文章
yii2 php namespace 引入第三方非namespace库文件时候,报错:Class not found 的解决
查看>>
softlayer 端口开放
查看>>
操作1:mongodb安装
查看>>
操作2:mongodb使用语法
查看>>
如何给分类增加一个属性(后台)
查看>>
linux设置环境变量 临时设置 和 永久设置
查看>>
检查网站在世界各地的打开速度
查看>>
jquery 向上(顶部),向下(底部)滑动
查看>>
seo
查看>>
MySQL: InnoDB 还是 MyISAM?
查看>>
SQL语言的组成部分 ddl dcl dml
查看>>
mysql数据库从库同步延迟的问题
查看>>
1.mysql数据库主从复制部署笔记
查看>>
mysql数据库主从同步的问题解决方法
查看>>
mysql 配置 - on xFanxcy.com
查看>>
mysql一: 索引优化
查看>>
测试人员,今天再不懂BDD就晚了!
查看>>
害怕自动化(1)
查看>>
深圳市软件质量提升工程系列活动——安全测试百人大课堂
查看>>
LoadRunner如何在脚本运行时修改log设置选项?
查看>>