DPDK之graph库源码分析

 


前言

DPDK在20.11版本(以下分析也是基于20.11版本)增加了图库librte_graph,可作为数据面的转发框架,类似vpp的图,各个不同功能的节点按照一定的顺序连接起来即可完成一个数据的完整处理功能,此功能就构成了一个图;

利用图做数据转发框架的好处在vpp已经体现出来,比如可以做到矢量处理网络数据帧,从而可以优化cpu的I-cache和D-cache,因此DPDK作为数据面高性能处理的开发套件,也有了类似的架构以迎合这些新技术,下面就该架构对应的代码库librte_graph进行分析;


1. DPDK图总体内存结构概览

DPDK的每个图可分为两部分,一部分是struct graph结构管理的,一部分是struct rte_graph结构管理的,struct graph结构对应的图管理真正的节点,即node_list队列,通过包裹节点graph_node管理,struct rte_graph结构对应的图管理运行时(这也是rte开头的原因)的节点,即graph指针指向的rte(runtime environment)开头的结构,这部分节点是真正节点的复制品;

下面从librte_graph库里的图创建函数rte_graph_create为入口开始分析,这个函数是理解DPDK图库的关键,在分析rte_graph_create函数前先看一下图总体内存结构的概览图,这也是rte_graph_create函数正常处理后的结果,如下:

 

首先看一下rte_graph_create函数的原型,如下:

参数name表示创建的图的名字,后续也是搜索匹配图的关键字,参数prm为创建图指定的一些图的参数,是一个结构体,如下:

socket_id:表示图所用到的内存所属的numa

nb_node_patterns:表示匹配节点的类型的个数

node_patterns:匹配节点的类型

后续分析会详细说明这些参数;

下面进入rte_graph_create函数内部(只分析主要的部分);

2. struct graph结构的图创建

2.1 检查图队列里有没有和给定的name重复的图,如果有则退出,即不能创建同名的图

 

2.2 申请一个图大小的内存块作为新建图的存储空间,并对图结构里的节点队列初始化,用于后续节点入队

结果如下图所示:

2.3 在节点队列里搜索匹配符合指定类型的节点,然后挂接到graph->node_list

expand_pattern_to_node如下,fnmatch会去匹配节点node的名字,包含通配符等,匹配到后会调用graph_node_add把节点node添加到graph->node_list:

graph_node_add如下:

首先防止把重复的节点挂接:

然后会申请一个graph_node类型的内存块,真正的node节点会先挂接到graph_node->node,然后graph_node作为node的包裹结构挂接到graph->node_list:

这里说一下graph_node结构,如下:

其中next用于连接下一个graph_node结构,node上面已经说明,visited主要是用于BFS算法作标记用的,后面会说到,adjacency_list用于存储指向该节点的邻居节点对应的graph_node结构指针,后面会说到;

结果如下图所示:

 

2.4 把上述挂接到graph->node_list队列的节点的邻居节点挂接到graph->node_list

前面说过,真正的节点是被包裹了一层,下面看一下真正的节点的结构,如下:

可以看到nb_edges和next_nodes存储了该节点对应的邻居节点的信息,即名字,知道了这个结构之后,再来分析graph_node_edges_add函数,如下:

正是使用每个真正节点存储的邻居名字,通过node_from_name函数来找到邻居节点(真正的节点)的,然后通过graph_node_add函数把邻居节点挂接到graph->node_list;

这里需要注意的地方是邻居节点挂接到graph->node_list后,该邻居节点也可能会有自己的邻居节点,这些邻居节点同样会挂接到graph->node_list,相当于一个递归过程,最终会把所有牵扯到节点都挂接到graph->node_list;

结果如下图所示:

2.5 根据实际节点的邻居关系,在实际节点的包裹节点graph_node里创建一份对等的邻居关系

graph_adjacency_list_update函数如下:

可以看到第一个红框的内容跟之前graph_node_edges_add函数一样,先找到节点的邻居节点(真正的节点),然后通过node_to_graph_node函数找到邻居节点的包裹节点graph_node,然后用adjacency_list指针指向该graph_node节点,即复制了一份实际节点的邻居关系;

这里需要注意adjacency_list指针,这是个指针数组,之前已经看到过graph_node结构,adjacency_list是在该结构的最后定义的,为struct graph_node *adjacency_list[],相当于struct graph_node *adjacency_list[0],这是个特殊的数组,数组大小为0,在内存不占空间,那么是怎么获得到内存空间的,前面分析graph_node_add函数时可以看到,该函数里有下面的操作:

申请内存大小时包括graph_node结构本身大小和nb_edges(邻居个数)个指针大小,因为calloc的内存空间是连续的,所以nb_edges个指针大小的空间自动赋予了adjacency_list指针数组,后续也可以用于存储指向邻居节点的指针了;

这里使用sizeof(struct node *)形式而不是sizeof(struct graph_node *)应该是为了强调邻居节点是真正的节点,就像前面分析的是通过真正的节点找到邻居节点的,只不过管理节点时包裹了一层节点;

结果如下图所示:

2.6 对新建的图进行检查

2.6.1 确保图里至少有一个源节点

通过标志位判断:

2.6.2 确保没有节点指向源节点

可以看到是通过判断节点的邻居节点是否是源节点来间接判断的,即邻居节点肯定是有其他节点(可以认为是该邻居节点的上一级节点)指向的,如下图,如果邻居节点是源节点,则说明有节点指向了源节点;

2.6.3 不允许节点自循环

因为节点之间连接是通过邻居关系形成的,所以是通过判断一个节点的邻居节点是否包含自己来判断该节点是否自循环的(即自己指向自己);

2.6.4 不能存在孤立节点

这里说的孤立节点是指在连接关系上是孤立的,包含两种情况:

  1. 对于源节点来说是指没有邻居节点;
  2. 对于非源节点来说是指不是任何节点的邻居节点;

如下图所示,比较直观:

以下红框对应上面说的两种情况

第一种情况比较简单,检查源节点的邻居个数是否为0即可,第二种情况相对复杂一点,是通过BFS算法(宽度优先搜索算法)来判断的,即通过源节点为入口递归遍历所有的邻居节点,最后检查所有的节点遍历标志,如果存在没有遍历到的节点,则说明存在孤立节点,具体流程如下:

1. 把所有节点的访问(遍历)标志置为false,表示还没有被访问                                                                                

       

2. 把源节点访问标志置为true,并把源节点入队到临时队列

3. 递归遍历源节点的所有邻居节点,并把访问标志置为true,然后入队到临时队列

可以看到临时队列的作用就是用于下次while循环时作为新的出发节点,然后遍历该新出发节点的邻居节点,按照循环逻辑各个节点访问顺序如下:

4. 最后通过node_list队列检查所有的节点,查看是否存在访问标志为false的节点,如果存在说明有节点没有被遍历到,即存在孤立节点;

2.7 对新建的图结构graph的一些参数进行初始化

socket:图所属的numa

src_node_count:源节点个数

node_count:所有节点个数

id:图的id,从0开始累计

3. struct rte_graph结构的图创建

创建函数主要是:

下面进入该函数进行分析:

3.1 计算rte结构所需内存大小并申请该大小的内存

结果如下:

其中需要注意的是rte_node结构里最后一个变量struct rte_node *nodes[] __rte_cache_min_aligned; /**< Next nodes. */,前面也用到过,是一个0长度的指针数组,graph_fp_mem_calc_size函数里的:

相当于为指针数组nodes申请了空间,指针个数正好是节点的邻居个数,如上图中的rte_node *:

3.2 graph结构关联rte结构

最终结果如下:

3.3 填充rte结构

3.3.1 ​​​填充rte_graph结构

rte_graph结构如下:

graph_header_popluate函数相对比较简单,直接对rte_graph结构的各个变量进行赋值,结果如下:

3.3.2 填充rte_node结构

rte_node结构如下:
struct rte_node {
	/* Slow path area  */
	uint64_t fence;		/**< Fence. */
	rte_graph_off_t next;	/**< Index to next node. */
	rte_node_t id;		/**< Node identifier. */
	rte_node_t parent_id;	/**< Parent Node identifier. */
	rte_edge_t nb_edges;	/**< Number of edges from this node. */
	uint32_t realloc_count;	/**< Number of times realloced. */

	char parent[RTE_NODE_NAMESIZE];	/**< Parent node name. */
	char name[RTE_NODE_NAMESIZE];	/**< Name of the node. */

	/* Fast path area  */
#define RTE_NODE_CTX_SZ 16
	uint8_t ctx[RTE_NODE_CTX_SZ] __rte_cache_aligned; /**< Node Context. */
	uint16_t size;		/**< Total number of objects available. */
	uint16_t idx;		/**< Number of objects used. */
	rte_graph_off_t off;	/**< Offset of node in the graph reel. */
	uint64_t total_cycles;	/**< Cycles spent in this node. */
	uint64_t total_calls;	/**< Calls done to this node. */
	uint64_t total_objs;	/**< Objects processed by this node. */
	RTE_STD_C11
		union {
			void **objs;	   /**< Array of object pointers. */
			uint64_t objs_u64;
		};
	RTE_STD_C11
		union {
			rte_node_process_t process; /**< Process function. */
			uint64_t process_u64;
		};
	struct rte_node *nodes[] __rte_cache_min_aligned; /**< Next nodes. */
}

graph_nodes_populate函数略微复杂一些,主要逻辑为通过node_list列表逐个找出真正的node,然后复制到rte_node结构,再用rte_node后续的rte_node*指针(即前面说过的*nodes[]指针数组)挨个指向该node的邻居节点的名字(这个只是临时的,用处下面分析),最后申请RTE_GRAPH_BURST_SIZE个void *的空间,用rte_node的objs指向该空间完成一个node的操作,然后循环进行下一个node的操作;

结果如下:

其中需要注意的是node->next = off;表示偏移量到下一个rte_node,如下图:

node->next会在下面的graph_node_nexts_populate函数里的rte_graph_foreach_node宏里逐个寻找rte_node时用到;

3.3.3 构建rte_node结构的邻居关系

graph_node_nexts_populate函数主要逻辑为利用node->next逐个找出rte_node节点,再利用该节点的邻居节点的名字(3.2填充rte_node结构里说过的*nodes[]指针数组,指向邻居节点的名字)找出该节点的邻居rte_node节点,并把该节点后续的rte_node *指针(即*nodes[]指针数组)逐个指向该节点的邻居rte_node节点(rte_node *指针由原来指向实际节点的名字改为指向邻居rte_node节点),正如前面说过的graph_node节点会按照真正的节点创建一份邻居关系一样,rte_node节点同样也会这么做,结果如下:

3.3.4 填充rte_node结构的源节点

graph_src_nodes_populate函数主要逻辑为逐个找到rte_node源节点,并把objs指向的ring结构大小调整为2*RTE_GRAPH_BURST_SIZE,然后把当前rte_node节点相对rte结构的首地址(rte_graph的首地址)的偏移量赋值给对应的源节点rte_graph_off_t变量,结果如下图:

3.4 初始化rte结构

主要是调用每个节点的init函数对rte结构进行初始化,init函数由用户定义;

4. 入队新建的图

可以看到把新建的图入队到全局变量graph_list,每个图都有一个id号,从0开始累计;

至此,一个图的创建算是完成了,最终总体内存结构如前文所示“DPDK图总体内存结构概览”,关于图的更多细节可参考代码进行分析;


总结

本文主要讲述了DPDK的graph库的rte_graph_create函数,理解了该函数基本就理解了graph库的精髓,关于graph库的使用可以参看test_graph.c测试代码,该测试代码对应编写了librte_node库,做了简单的数据转发示例,其实其基本思想也是类似vpp的图,避免了linux内核网络协议栈的深度嵌套以及帧的标量处理,提高了处理的效率,是现在数据平面高速转发处理的常用方式之一;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: DPDK是一种高性能数据包处理框架,能够在标准Intel处理器上实现高速数据包收发,其源码开放并有详细的开发者手册PDF可供参考。DPDK作为一种用户态数据包处理框架,利用整个计算机所有可用的CPU核心,实现了数据包的高吞吐量和低延迟。相比于传统的内核协议栈方式,DPDK可以在处理网络数据包时减少唤醒操作和内存复制等开销,提高网络数据包处理的效率。DPDK框架包括了四个核心模块:环形缓冲、内存管理、线程管理和设备驱动。其中,环形缓冲是DPDK最核心的模块之一,也是实现高吞吐量数据收发的关键。DPDK源码很庞大,其中包含设备管理、调度管理、协议栈等众多模块,需要有一定的网络编程基础和对硬件知识的理解才能够进行开发与调试。开发者手册PDF详细介绍了DPDK的架构、核心API、样例代码等内容,对于一些想要了解DPDK架构和开发的消息人士来说非常有用。然而,由于DPDK需要使用专有硬件进行加速,因此在实际的部署和应用过程中,需要较高的技能门槛和较大的硬件成本支持。 ### 回答2: DPDK是一个开源的数据平面开发套件,是一种优化高性能数据包处理的工具集。DPDK源码是开发者们在构建高性能网络应用时所使用的关键组件之一,它由一组通用和驱动程序组成,可让应用程序通过网络卡直接访问内存。DPDK的目标是加速数据包处理,减少处理延迟和CPU开销,从而提升系统的吞吐量、性能和响应能力。 DPDK开发者手册pdf包括官方文档、范例和指南等。它提供了对DPDK如何工作、如何使用和如何开发应用程序的深入了解。开发者手册包括DPDK核心和网络接口、存储接口、虚拟设备和安全功能等详细说明,涵盖了从安装和配置到开发和调试的全过程。同时,开发者手册中还包含大量的代码实例及应用场景,可以帮助开发者更快地上手,实现高效的数据平面处理。 总之,DPDK源码和开发者手册为网络开发人员提供了优秀的工具和文档支持,让开发者可以更加轻松和高效地构建高性能的网络应用程序。无论是在业务处理、数据中心或云环境中,DPDK都是一个可靠的数据平面开发框架,为网络应用程序提供了高速、可靠的运行环境。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值