大数跨境
0
0

Intel dpdk系列专题一:dpdk的mbuf管理

Intel dpdk系列专题一:dpdk的mbuf管理 通信行业搬砖工
2023-06-24
0
导读:本文由开水厂数据转面资深研发专家木木大佬讲解Intel dpdk(数据面开发套件)的mbuf管理机制,如果您觉得文章有帮助,欢迎关注公众号。

本文章摘选自知乎:

https://zhuanlan.zhihu.com/p/616314276?utm_id=0

作者:楠佬,系头部互联网厂商,开水厂数据转发面研发专家。了解更多楠佬著作欢迎访问楠佬知乎链接。

了解更多dpdk和网络虚拟化知识,欢迎关注公众号:通信行业搬砖工

1 前言

使用DPDK开发项目小伙伴对DPDK的Mbuf一定不陌生,我们千辛万苦的都在考虑针对它的各种优化,其实不管是基于Linux的内核转发或是DPDK的用户态开发我们一定是关注对报文的处理,不管是接收还是发送,网络上所有涉及的动作基本有离不开报文管理。


2 Mbuf结构

为了高效访问数据,DPDK将内存封装在Mbuf(struct rte_mbuf)结构体内。Mbuf主要用来封装网络帧缓存,也用来封装通用控制信息缓存。Mbuf 库提供了分配和释放缓冲区 (mbufs) 的能力,DPDK 应用程序可以使用这些缓冲区来存储消息缓冲区。消息缓冲区使用内存池库存储在内存池中。

rte_mbuf 结构通常承载网络数据包缓冲区,但它实际上可以是任何数据(控制数据、事件……)。 rte_mbuf 头结构保持尽可能小,目前只使用两个缓存行,最常用的字段位于两个缓存行中的第一个。原则上将基础性、频繁访问的数据放在第一个Cache Line字节,将功能性扩展的数据放在第二个Cache Line字节。


Mbuf报头包含包处理所需的所有数据,对于单个Mbuf存发不下的巨型帧(Jumbo Frame),Mbuf还有指向下一个Mbuf结构的指针来形成帧链表结构。所有应用都应该使用Mbuf结构来传输网络帧。


对于网络帧的封装和处理有两种方式:

1.将元数据嵌入单个内存缓冲区中,该结构后跟固定大小的数据包数据区域。

2.为元数据结构和数据包数据使用单独的内存缓冲区。


前者的好处是高效,它只需要一个指令来分配/释放数据包的整个内存,缺点是因为缓存长度固定而网络帧的大小不一,大部分帧只能填0(padding)的方式填满整个缓存,较为浪费内存空间。后者的优先相对灵活自由,数据帧的大小可以任意,同时对元数据和网络帧的缓存可以分开申请及释放,当然了缺点就是效率低。无法保证数据存储存在一个Cache Line中,可能造成Hit Miss。


为了高效,DPDK 选择了第一种方法。网络帧的元数据的一部分内容由DPDK网卡驱动写入。这些内容包含VLAN标签、RSS哈希值、网络帧入口端口号以及巨型帧所占的mbuf个数等等。对于巨型帧,网络帧元数据仅出现在第一个帧的Mbuf结构中,其他的帧该信息为空。

单帧Mbuf结构

如上图,包含了一个Mbuf的基本组成,其中Mbuf头部大小为两个Cache Line,在Mbuf头部和实际的数据包之间还有一段控制头信息(headroom),用来存储和系统中其他实体交互的信息,比如控制信息、帧内容、事件等,headroom的长度由RTE_PKTMBUF_HEADROOM控制。


headroom的起始地址保存在Mbuf的buf_addr 指针中,数据帧的起始指针可以通过调用rte_pktmbuf_mtod获得。

巨型帧Mbuf结构

数据帧的长度可通过调用rte_pktmbuf_pktlen(Mbuf)或者rte_pktmbuf_datalen(Mbuf)获得,但这只限于单帧Mbuf。巨型帧的单帧长度只由rte_pktmbuf_datalen(Mbuf)返回,而rte_pktmbuf_pktlen(Mbuf)用于访问巨型帧所有帧长度的总和,如图上所示。


关于Mbuf内存的申请释放我们放到下一节讨论,除此之外Mbuf提供可操作的API有,具体的使用方法可以参考(mbuf/rte_mbuf.h)的注释内容和用法以及使用手册:


rte_pktmbuf_datalen:获得帧数据长度

rte_pktmbuf_mtod:获得指向数据的指针

rte_pktmbuf_prepend:在帧数据前插入一段内容

rte_pktmbuf_append:在帧数据后插入一段内容

rte_pktmbuf_adj:在帧数据前删除一段内容

rte_pktmbuf_trim:在帧数据后截掉一段内容

rte_pktmbf_attach:连接两段缓存,此函数会连接两段属于不同缓存区的缓存,称为间接缓存(indirect buff)。对间接缓存的访问效率低于直接缓存(就是一段缓存包含完整Mbuf结构和帧数据),因此请仅将此函数用于网络帧的复制和分段。

rte_pktmbuf_detach:分开两段缓存。

rte_pktmbuf_clone:此函数作为rte_pktmbuf_attach的更高一级抽象,将正确设置连接后Mbuf的各个参数,相对于rte_pktmbuf_attach更为安全。

3 Mbuf的内存池管理

内存池的双环形缓存区结构

DPDK的内存管理与硬件关系紧密,并为应用的高效存取服务,在DPDK中,数据包的内存操作对象被抽象化为Mbuf结构,而有限的rte_mbuf结构对象则存储在内存池中。内存池使用环形缓冲区来保存空闲对象。逻辑如图上所示。


当一个网络帧被网卡接收时,DPDK的网卡驱动将其存储在一个高效的环形缓冲区中,同时在Mbuf的环形缓冲区中创建一个Mbuf对象。这两个行为都是不涉及申请内存的,这些内存实际上在创建内存池时就已经申请好了。Mbuf对象被创建好后,网卡驱动根据分析出的帧信息将其初始化,并将其和实际帧对象逻辑相连。对网络帧的分析处理都集中于Mbuf,仅在必要的时候访问实际网络帧。


为了增加对Mbuf的访问效率,内存池还拥有内存通道/Rank对其辅助方法。内存池还允许用户设置核心缓存区大小来调节环形内存块读写的频率。


实践证明,在内存对象之间补零,以确保每个对象和内存的一个通道和Rank起始处对齐,能大幅减少未命中的发生概率且增加存取效率。在L3转发和流分类应用中尤为如此,内存池以更大占有量为代价支持此项技术。在创建一个内存池,用户可以选择是否开启。


多核CPU访问同一个内存池或者同一个环形缓冲区时,因为每次读写时都要进行CAS(Compare-and-Set)操作来保证期间数据未被其他核心修改,所以存储效率较低。DPDK的解决方法是使用单核本地缓存一部分数据,实时对环形缓冲区进行块读写操作,以减少访问环形缓冲区的次数。单核CPU对自己缓存的操作无需中断,访问效率因而得到提升。当然使用这种方法的问题就是每个核都拥有私用缓存(大小可定义,或禁用),而这些缓存在绝大部分时间都没有得到百分之百运用,因此一部分空间将被浪费。


3.1 Mbuf存储

Mbuf的分配使用Mempool库。因此,它确保数据包报头在通道和等级之间以最佳方式交错,以进行 L3 处理。mbuf 包含一个字段,指示它源自的池。当调用 rte_pktmbuf_free(m) 时,mbuf 返回到它的原始池。


数据包 mbuf 构造函数由 API 提供。 rte_pktmbuf_init()函数初始化mbuf结构中的一些字段,这些字段一旦创建就不会被用户修改(mbuf类型、源池、缓冲区起始地址等)。该函数在池创建时作为 rte_mempool_create() 函数的回调函数提供。


举个例子,每个参数在rte_mempool_create使用含义请参考使用手册,不难理解我们创建了一个内存池,拥有一个私有数据结构struct_pktmub_pool_private, 默认的DEFAULT_MBUF_SUIZE一般是sizeof(struct rte_mbuf)+RTE_PKTBUF_HEADROOM+(关注的解析大小2048等),DEFAULT_MEMPOOL_CACHE_SIZE大小对齐,对象初始化之前使用rte_pktmbuf_pool_init函数初始化地址池,参数是arg1, 然后调用对象的初始化函数rte_pktmbuf_init参数是arg2。


struct rte_mempool *pool_mbuf = rte_mempool_create("mbuf_pool_0",

                                                    poolsize,

                                                    DEFAULT_MBUF_SIZE,

                                                    DEFAULT_MEMPOOL_CACHE_SIZE,

                                                    sizeof(struct rte_pktmuf_pool_private),

                                                    rte_pktmbuf_pool_init, arg1,

                                                    rte_pktmbuf_init, arg2,

                                                    socket0,

                                                    0);

                    

分配一个新的 mbuf 需要用户指定应该从中获取 mbuf 的内存池。对于任何新分配的 mbuf,它包含一个长度为 0 的段。数据的偏移量被初始化为在缓冲区中有一些字节的净空 (RTE_PKTMBUF_HEADROOM)。


struct rte_pktmbuf_pool_private {

 uint16_t mbuf_data_room_size; /**< Size of data space in each mbuf. */

 uint16_t mbuf_priv_size; /**< Size of private area in each mbuf. */

 uint32_t flags; /**< reserved for future use. */

};

struct rte_pktmbuf_pool_private则作为每个内存池的私有数据被添加到每个mempool结构的后面。


释放 mbuf 意味着将其返回到其原始内存池中。 mbuf 的内容在存储在池中时不会被修改(作为空闲 mbuf)。由构造函数初始化的字段不需要在 mbuf 分配时重新初始化。


当释放包含多个段的数据包 mbuf 时,所有段都将被释放并返回到它们的原始内存池。


4 报文处理中的指令预取

我们在看DPDK的例子中能够看到读取报文内容时添加了指令预取命令:


 for (i = 0; i < nb; i++) {

  if (likely(i < nb - 1))

   rte_prefetch0(rte_pktmbuf_mtod(pkts[i+1], void *));

  mb = pkts[i];


  eth_hdr = rte_pktmbuf_mtod(mb, struct rte_ether_hdr *);


  /* Swap dest and src mac addresses. */

  rte_ether_addr_copy(&eth_hdr->dst_addr, &addr);

  rte_ether_addr_copy(&eth_hdr->src_addr, &eth_hdr->dst_addr);

  rte_ether_addr_copy(&addr, &eth_hdr->src_addr);


  mbuf_field_set(mb, ol_flags);

 }

能够看到它其实是一些缓存预取指令:

static inline void rte_prefetch0(const volatile void *p)
{
asm volatile ("prefetcht0 %[p]" : : [p] "m" (*(const volatile char *)p));
}

static inline void rte_prefetch1(const volatile void *p)
{
asm volatile ("prefetcht1 %[p]" : : [p] "m" (*(const volatile char *)p));
}

static inline void rte_prefetch2(const volatile void *p)
{
asm volatile ("prefetcht2 %[p]" : : [p] "m" (*(const volatile char *)p));
}

static inline void rte_prefetch_non_temporal(const volatile void *p)
{
asm volatile ("prefetchnta %[p]" : : [p] "m" (*(const volatile char *)p));
}
之前我们讲过缓存预期的原理与概念这里就不难理解:

prefetcht0 :预取数据到所有级别的缓存,包括L0。
prefetcht1 :预取数据到除L0外所有级别的缓存。
prefetcht2 :预取数据到除L0、L1外所有级别的缓存。
prefetchnta :预取数据到非临时缓冲结构中,可以最小化对缓存的污染。
如果在CPU操作数据之前,我们就已经将数据主动加载到缓存中,那么就减少了由于缓存不命中,需要从内存取数的情况,这样就可以加速操作,获得性能上提升。

5 元数据信息
一些信息由网络驱动程序检索并存储在mbuf中使处理更容易,比如VLAN、RSS散列结果和由硬件计算的校验和等,一个 mbuf 还包含输入端口(它从哪里来)和链中 mbuf段的数量。对于链式缓冲区中,只有链的第一个 mbuf 存储此元信息。例如,IEEE1588 数据包时间戳机制、VLAN 标记和 IP 校验和计算的 RX 端就是这种类型的信息。在 TX 端,如果硬件支持,应用程序也可以将某些处理委托给硬件。例如,RTE_MBUF_F_TX_IP_CKSUM 标志允许卸载 IPv4 校验和的计算。以下示例说明如何在 vxlan 封装的 tcp 数据包上配置不同的 TX 卸载:

计算out_ip的校验和
mb->l2_len = len(out_eth)
mb->l3_len = len(out_ip)
mb->ol_flags |= RTE_MBUF_F_TX_IPV4 | RTE_MBUF_F_TX_IP_CSUM
set out_ip checksum to 0 in the packet
这要求在硬件上支持RTE_ETH_TX_OFFLOAD_IPV4_CKSUM卸载。

计算 out_ip 和 out_udp 的校验和
mb->l2_len = len(out_eth)
mb->l3_len = len(out_ip)
mb->ol_flags |= RTE_MBUF_F_TX_IPV4 | RTE_MBUF_F_TX_IP_CSUM | RTE_MBUF_F_TX_UDP_CKSUM
set out_ip checksum to 0 in the packet
set out_udp checksum to pseudo header using rte_ipv4_phdr_cksum()

这要求在硬件上支持RTE_ETH_TX_OFFLOAD_IPV4_CKSUM 和 RTE_ETH_TX_OFFLOAD_UDP_CKSUM

计算 in_ip 的校验和
mb->l2_len = len(out_eth + out_ip + out_udp + vxlan + in_eth)
mb->l3_len = len(in_ip)
mb->ol_flags |= RTE_MBUF_F_TX_IPV4 | RTE_MBUF_F_TX_IP_CSUM
set in_ip checksum to 0 in the packet
这与情况 1 类似,但 l2_len 不同。要求硬件支持 RTE_ETH_TX_OFFLOAD_IPV4_CKSUM ,请注意,它只有在外部 L4 校验和为 0 时才能工作。

计算 in_ip 和 in_tcp 的校验和
mb->l2_len = len(out_eth + out_ip + out_udp + vxlan + in_eth)
mb->l3_len = len(in_ip)
mb->ol_flags |= RTE_MBUF_F_TX_IPV4 | RTE_MBUF_F_TX_IP_CSUM | RTE_MBUF_F_TX_TCP_CKSUM
set in_ip checksum to 0 in the packet
set in_tcp checksum to pseudo header using rte_ipv4_phdr_cksum()
这与情况 2类似,但 l2_len 不同。它要求硬件支持 RTE_ETH_TX_OFFLOAD_IPV4_CKSUM 和 RTE_ETH_TX_OFFLOAD_TCP_CKSUM 。请注意,它只有在外部 L4 校验和为 0 时才能工作。

内部TCP段
mb->l2_len = len(out_eth + out_ip + out_udp + vxlan + in_eth)
mb->l3_len = len(in_ip)
mb->l4_len = len(in_tcp)
mb->ol_flags |= RTE_MBUF_F_TX_IPV4 | RTE_MBUF_F_TX_IP_CKSUM | RTE_MBUF_F_TX_TCP_CKSUM |
  RTE_MBUF_F_TX_TCP_SEG;
set in_ip checksum to 0 in the packet
set in_tcp checksum to pseudo header without including the IP payload length using rte_ipv4_phdr_cksum()
它要求硬件支持RTE_ETH_TX_OFFLOAD_TCP_TSO 。请注意,它只有在外部 L4 校验和为 0 时才能工作。

计算 out_ip、in_ip、in_tcp 的校验和
mb->outer_l2_len = len(out_eth)
mb->outer_l3_len = len(out_ip)
mb->l2_len = len(out_udp + vxlan + in_eth)
mb->l3_len = len(in_ip)
mb->ol_flags |= RTE_MBUF_F_TX_OUTER_IPV4 | RTE_MBUF_F_TX_OUTER_IP_CKSUM  | \
  RTE_MBUF_F_TX_IP_CKSUM |  RTE_MBUF_F_TX_TCP_CKSUM;
set out_ip checksum to 0 in the packet
set in_ip checksum to 0 in the packet
set in_tcp checksum to pseudo header using rte_ipv4_phdr_cksum()
它要求硬件支持RTE_ETH_TX_OFFLOAD_IPV4_CKSUM, RTE_ETH_TX_OFFLOAD_UDP_CKSUM 和RTE_ETH_TX_OFFLOAD_OUTER_IPV4_CKSUM

rte_mbuf.h 中描述了标志列表及其确切含义,大家有兴趣可以去获取更详细的信息。

5.1 动态字段和标志
mbuf的大小是有约束和限制的;而为每个数据包保存的元数据量是无限的。最基本的网络信息已经在现有的 mbuf 字段和标志中找到了它们的位置。如果需要添加新功能,新字段和标志应适合“动态空间”,方法是在 mbuf 结构中注册一些空间:

dynamic field:mbuf 结构中的命名区域,具有给定的大小(至少 1 个字节)和对齐约束。
dynamic flag:mbuf 结构中的命名位,存储在字段 ol_flags 中。
动态字段和标志由函数 rte_mbuf_dyn* 管理,无法注销字段或标志。

【声明】内容源于网络
0
0
通信行业搬砖工
14年通信研发经验,大厂搬砖,分享通信工程技术、经验、行业趋势等内容。
内容 503
粉丝 0
通信行业搬砖工 14年通信研发经验,大厂搬砖,分享通信工程技术、经验、行业趋势等内容。
总阅读1
粉丝0
内容503