DPDK(数据平面开发套件)是一套用于快速处理数据包的库和驱动,广泛用于高性能和高吞吐量的网络程序设计。rte_mbuf是DPDK中一个核心的数据结构,用于表示网络数据包(mbuf 是 memory buffer 的缩写)。

rte_mbuf结构体不仅包含了数据包的内容,还包含了关于数据包的其他元数据,如数据包的长度、协议类型、接收时间戳等信息。rte_mbuf旨在保持小巧的数据结构大小以适应CPU缓存行,目前只使用两个缓存行,最常用的字段位于第一个缓存行中,从而提高处理速度。

对于新分配的mbuf,数据开始的区域是buffer之后 RTE_PKTMBUF_HEADROOM 字节的位置,这是缓存对齐的。 这个数据缓冲区地址如下图红色部分所示,可以通过rte_pktmbuf_mtod 计算得到,其是一个宏,用于将指向 rte_mbuf 结构的指针转换为指向其数据缓冲区的指针。

https://dpdk-docs.readthedocs.io/en/latest/_images/mbuf1.svg

下面是 rte_pktmbuf_mtod 宏的一个典型定义:

#define rte_pktmbuf_mtod(m, t) ((t)((char *)(m)->buf_addr + (m)->data_off))

这里,(m) 是指向 rte_mbuf 的指针,而 (t) 是转换的目标类型(通常是 void * 或特定的数据类型指针,如 char *)。

  • (m)->buf_addr 是指向分配给这个特定 rte_mbuf 的数据缓冲区的基地址。

  • (m)->data_off 是数据开始的偏移量,相对于 buf_addr。在 rte_mbuf 结构分配时,data_off 通常被初始化为RTE_PKTMBUF_HEADROOM(128),以保留空间用于协议头等。

当你使用这个宏时,你可以这样来获得数据缓冲区的地址:

struct rte_mbuf *mbuf = // ... 已经分配好的 rte_mbuf
void *data_ptr = rte_pktmbuf_mtod(mbuf, void *);

如果你要处理以太网帧,你可能会使用 struct ether_hdr *

struct ether_hdr *eth_hdr = rte_pktmbuf_mtod(mbuf, struct ether_hdr *);

这样,eth_hdr 就指向了数据缓冲区中以太网头部的开始位置。

在DPDK中,rte_mbuf数据结构包含一个数据缓冲区来存储实际的网络数据包内容。这个数据缓冲区分为几个部分,包括头部空间(headroom)、数据区域和尾部空间(tailroom)。rte_mbuf的设计允许在不复制数据的情况下高效地添加或删除头部和尾部的数据。

以下是headroom和tailroom的简单介绍:

  • Headroom:是数据缓冲区的开始部分,在数据之前预留的空间。它通常用于在不需要移动已有数据的情况下添加协议头部,例如在数据包前面添加以太网头部或IP头部。

  • Tailroom:是数据缓冲区的末尾部分,是在数据之后预留的空间。它可以用于添加尾部信息或在数据包末尾追加更多数据。

当你在rte_mbuf中解析或封装数据包时,你可能需要移动数据区域的开始或结束位置,这相当于改变headroom和tailroom的大小。这是通过调整data_off字段和data_len字段来实现的。下面是一些示例操作:

  1. 向数据包前添加头部(增加头部)
    在数据包的前面添加头部时,你需要减少headroom(通过减少
    data_off),同时增加data_len以确保整个数据包长度是正确的。

struct rte_mbuf *m; // 假设已经初始化并分配了mbuf
char *data_ptr = rte_pktmbuf_mtod(m, char*); // 获取数据区域的开始指针
size_t header_size = ...; // 新头部的大小
data_ptr -= header_size; // 移动指针以腾出空间
m->data_off -= header_size; // 调整data_off以反映新的数据开始位置
m->data_len += header_size; // 增加数据长度

  1. 从数据包前移除头部(减少头部)
    如果你需要解析并移除数据包前的头部,你会增加headroom(通过增加
    data_off),同时减少data_len

size_t header_size = ...; // 要移除的头部大小
m->data_off += header_size; // 移动数据区域的开始位置
m->data_len -= header_size; // 减少数据长度

  1. 向数据包末尾添加数据(增加尾部)
    在数据包的末尾添加数据时,你只需要增加
    data_len,因为tailroom已经预留在缓冲区的末尾。

size_t tail_data_size = ...; // 要添加的尾部数据大小
m->data_len += tail_data_size; // 增加数据长度

  1. 从数据包末尾移除数据(减少尾部)
    如果你需要从数据包末尾移除数据,你将减少
    data_len

size_t tail_data_size = ...; // 要移除的尾部数据大小
m->data_len -= tail_data_size; // 减少数据长度

通常,DPDK提供了一系列非常有用的宏和函数来处理rte_mbuf中的数据,例如rte_pktmbuf_appendrte_pktmbuf_trimrte_pktmbuf_adj等,这些函数会适当地移动headroom和tailroom指针并调整相关字段,以确保正确地管理数据包内容。使用这些函数可以简化代码并减少出错的可能性。

  1. rte_pktmbuf_append
    rte_pktmbuf_append函数用于在rte_mbuf的数据区域末尾追加空间。这个函数会增加数据包的长度,并返回数据区域新末尾的指针以便写入数据。

void *rte_pktmbuf_append(struct rte_mbuf *m, uint16_t len);

  • m:指向rte_mbuf结构的指针。

  • len:希望在数据区域末尾追加的长度。

如果追加操作成功,该函数返回一个指向追加位置的指针;如果没有足够的尾部空间(tailroom)进行追加,它将返回NULL

  1. rte_pktmbuf_trim
    rte_pktmbuf_trim函数用于从rte_mbuf的数据区域末尾移除指定长度的数据。这个函数会减少数据包的长度,通常在确定数据包末尾的某些数据不需要时使用。

int rte_pktmbuf_trim(struct rte_mbuf *m, uint16_t len);

  • m:指向rte_mbuf结构的指针。

  • len:希望从数据区域末尾移除的长度。

该函数返回0表示成功,返回-1表示失败(例如,如果要移除的长度大于数据区域当前的长度)。

  1. rte_pktmbuf_adj
    rte_pktmbuf_adj函数用于移除rte_mbuf数据区域开头的一段数据。这个函数通常用于移除头部信息,如移除网络数据包的以太网头部。

void *rte_pktmbuf_adj(struct rte_mbuf *m, uint16_t len);

  • m:指向rte_mbuf结构的指针。

  • len:希望从数据区域开头移除的长度。

如果操作成功,该函数返回一个指向新数据开始位置的指针;如果要移除的长度大于数据区域的长度,它将返回NULL

这些函数简化了数据包处理的操作,使得开发者不必直接操作rte_mbuf中的data_offdata_len字段,从而减少了出错的可能性。通过使用这些辅助函数,可以更安全、更便捷地管理数据包的缓冲区。

re_mbuf的分配

在DPDK中,rte_mbuf结构体与数据缓冲区的关联是通过内存池(mempool)实现的。当你初始化DPDK时,你会创建一个或多个内存池,这些内存池用于分配rte_mbuf结构体及其关联的缓冲区。

这里是一个简化的例子,演示了如何创建一个内存池,并且如何从这个池中分配一个rte_mbuf

#include <rte_mempool.h>
#include <rte_mbuf.h>

// 创建内存池的例子
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL", // 池的名字
                                                        8192,        // 池中元素的数量
                                                        256,         // 缓存大小
                                                        0,           // 应用程序私有数据的大小
                                                        RTE_MBUF_DEFAULT_BUF_SIZE, // 每个mbuf的数据缓冲区大小
                                                        rte_socket_id()); // 当前CPU socket的ID

// 从内存池中分配一个rte_mbuf的例子
struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool);

在这个例子中,rte_pktmbuf_pool_create函数用于创建一个名为MBUF_POOL的内存池,该内存池能够存放8192个元素,每个元素包括一个rte_mbuf结构体和一个数据缓冲区。这个数据缓冲区的大小由RTE_MBUF_DEFAULT_BUF_SIZE决定。此外,我们还设置了一个per-core缓存,大小为256个mbuf,这有助于提高mbuf分配的性能。

rte_pktmbuf_alloc函数从内存池中分配一个rte_mbuf元素。分配的rte_mbuf已经关联了一个数据缓冲区,这个缓冲区紧跟在rte_mbuf结构体之后。你可以通过rte_pktmbuf_mtod宏来获取指向这个数据缓冲区的指针:

void *data_buffer = rte_pktmbuf_mtod(mbuf, void *);

这样,你就可以通过data_buffer指针来访问和操作实际的数据包内容。

每个rte_mbuf在内存池中是连续存储的,并且每个rte_mbuf包括两部分:

  1. rte_mbuf结构体自身,包含了元数据。

  2. 紧随其后的是数据缓冲区,用于实际存放数据报文。

由于DPDK使用了物理连续内存(通过hugepages机制),并且内存池机制保证了这种连续性,所以这使得包括网络适配器在内的硬件可以直接在不同的rte_mbuf之间进行高效的数据传输。

参考

https://dpdk-docs.readthedocs.io/en/latest/prog_guide/mbuf_lib.html