Mbuf Library

mbuf 库提供分配和释放缓冲区 (mbuf) 的能力,DPDK 应用程序可以使用这些缓冲区来存储消息缓冲区。消息缓冲区使用内存池库存储在内存池中。

rte_mbuf 结构通常承载网络数据包缓冲区,但它实际上可以是任何数据(控制数据、事件……)。 rte_mbuf 头结构保持尽可能小,目前仅使用两个缓存行,最常用的字段位于两个缓存行中的第一个。

Design of Packet Buffers

对网络帧的封装及处理有两种方式:将网络帧元数据(metadata)和帧本身存放在固定大小的同一段缓存中;或将元数据和网络帧分开存放在两段缓存里。前者的好处是高效:对缓存的申请及释放均只需要一个指令,缺点是因为缓存长度固定而网络帧大小不一,大部分帧只能使用填0(padding)的方式填满整个缓存,较为耗费内存空间。后者的优点则是相对自由:帧数据的大小可以任意,同时对元数据和网络帧的缓存可以分开申请及释放;缺点是低效,因为无法保证数据存在于一个CacheLine中,可能造成HitMiss。

为保持包处理的效率,DPDK采用了前者。网络帧元数据的一部分内容由DPDK的网卡驱动写入。这些内容包括VLAN标签、RSS哈希值、网络帧入口端口号以及巨型帧所占的Mbuf个数等。对于巨型帧,网络帧元数据仅出现在第一个帧的Mbuf结构中,其他的帧该信息为空。

下图为单个mbuf的结构定义:

image-20220709131639142

一个Mbuf的基本组成。其中,Mbuf头部的大小为两个CacheLine,之后的部分为缓存内容,其起始地址存储在Mbuf结构的 buffer_addr指针中。在Mbuf头部和实际包数据之间有一段控制头空间(headroom),用来存储和系统中其他实体交互的信息,如控制信息、帧内容、事件等。headroom的长度可由RTE_PKTMBUF_HEADROOM定义。 headroom的起始地址保存在Mbuf的buff_addr指针中,在lib/librte_port/rte_port.h中也有实用的宏,用来获得从buff_addr起始特定偏移量的指针和数据,详情请参考rte_port.h源码中RTE_MBUF_METADATA_UINT8_PTR以及RTE_MBUF_METADATA_UINT8等宏。数据帧的起始指针可通过调用rte_pktmbuf_mtod(Mbuf)获得。 数据帧的实际长度可通过调用rte_pktmbuf_pktlen(Mbuf)或rte_pktmbuf_datalen(Mbuf)获得,但这仅限于单帧Mbuf。巨型帧的单帧长度只由rte_pktmbuf_datalen(Mbuf)返回,而rte_pktmbuf_pktlen(Mbuf)用于访问巨型帧所有帧长度的总和。 下图为多个mbuf的分段保存

image-20220709131639142

创建的函数为rte_pktmbuf_alloc()或rte_ctrlmbuf_alloc(),前者用来创建网络帧Mbuf,后者用来创建控制 信息Mbuf。初始化该Mbuf则由rte_pktmbuf_init()或rte_ctrlmbuf_init()函数完成。这两个函数用来初始化一些Mbuf的关键 信息,如Mbuf类型、所属内存池、缓存起始地址等。初始化函数被作为rte_mempool_create的回调函数。 释放一段Mbuf实际等于将其放回所属的内存池,其缓存内容在被重新创建前不会被初始化。

存储在内存池中的缓冲区

缓冲区管理器使用内存池库来分配缓冲区。因此,它确保数据包标头在通道和等级之间以最佳方式交织以进行 L3 处理。 mbuf 包含一个字段,指示它来自的池。当调用 rte_pktmbuf_free(m) 时,mbuf 返回到它的原始池。

构造函数

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

分配和释放 mbuf

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

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

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

操作 mbuf

该库提供了一些用于操作数据包 mbuf 中的数据的函数。例如:

获得帧数据长度——rte_pktmbuf_datalen() 获得指向数据的指针——rte_pktmbuf_mtod() 在帧数据前插入一段内容——rte_pktmbuf_prepend() 在帧数据后增加一段内容——rte_pktmbuf_append() 在帧数据前删除一段内容——rte_pktmbuf_adj() 将帧数据后截掉一段内容——rte_pktmbuf_trim() 连接两段缓存——rte_pktmbuf_attach() 此函数会连接两段属于不同缓存区的缓存,称为间接缓存(indirectbuffer)。对间接缓存的访问效率低于直接缓存(意为一段缓存包含完整Mbuf结构和帧数据),因此请仅将此函数用于网络帧的复制或分段。 分开两段缓存——rte_pktmbuf_detach() 克隆Mbuf——rte_pktmbuf_clone(),此函数作为rte_pktmbuf_attach的更高一级抽象,将正确设置连接后Mbuf的各个参数,相对rte_pktmbuf_attach更为安全。

直接和间接缓冲区

直接缓冲区是完全独立且自包含的缓冲区。间接缓冲区的行为类似于直接缓冲区,但其中的缓冲区指针和数据偏移量指的是另一个直接缓冲区中的数据。这在需要复制或分段数据包的情况下很有用,因为间接缓冲区提供了跨多个缓冲区重用相同数据包数据的方法。

当使用 rte_pktmbuf_attach() 函数“附加”到直接缓冲区时,缓冲区将变为间接缓冲区。每个缓冲区都有一个引用计数器字段,每当间接缓冲区附加到直接缓冲区时,直接缓冲区上的引用计数器就会递增。类似地,每当间接缓冲区被分离时,直接缓冲区上的引用计数器就会递减。如果结果引用计数器等于 0,则释放直接缓冲区,因为它不再使用。

处理间接缓冲区时需要记住几件事。首先,一个间接缓冲区永远不会附加到另一个间接缓冲区。尝试将缓冲区 A 附加到 的间接缓冲区 B,B又附件在C上时,使 rte_pktmbuf_attach() 自动将 A 附加到 C,有效地克隆 B。其次,对于成为间接缓冲区,其引用计数器必须等于 1,即它不能被另一个间接缓冲区引用。最后,不可能将间接缓冲区重新附加到直接缓冲区(除非先将其分离)。

虽然可以使用推荐的 rte_pktmbuf_attach() 和 rte_pktmbuf_detach() 函数直接调用附加/分离操作,但建议使用更高级别的 rte_pktmbuf_clone() 函数,该函数负责正确初始化间接缓冲区并可以克隆具有多个段的缓冲区。

由于间接缓冲区不应该实际保存任何数据,因此应配置间接缓冲区的内存池以指示减少的内存消耗。间接缓冲区的内存池初始化示例(以及间接缓冲区的用例示例)可以在几个示例应用程序中找到,例如 IPv4 多播示例应用程序。

image-20220709131639142

调试

在调试模式下,mbuf 库的函数在任何操作(例如缓冲区损坏、错误类型等)之前执行完整性检查。

用例

所有网络应用程序都应该使用 mbufs 来传输网络数据包。

http://doc.dpdk.org/guides/prog_guide/mbuf_lib.html

https://blog.csdn.net/yaochuh/article/details/88207553