DPDK rte_mbuf

mbuf表示memory buffer或message buffer, 是DPDK中非常重要的数据结构, 一般用于存放网卡收发的报文, 但也可以用于存储控制消息, 事件等各种数据.

本文基于DPDK 17.11版本的mbuf实现进行描述与编码.

1 设计思想

1.1 存储

mbuf主要由元信息和数据两部分组成, 这两部分存放在一个连续的内存块中. 元信息和数据的关系类似于TCP/IP协议首部与协议数据的关系, 其中元信息描述了数据的属性, 如报文类型, 数据长度与起始地址, RSS hash值等等.

mbuf元信息是需要被频繁访问的部分, 它位于mbuf头部, 且被设计地足够小, 目前占用两个cache lines, 其中访问最频繁的信息位于第一个cache line. mbuf元信息由数据结构rte_mbuf表示. 如果用户还需要存储业务相关的其他数据, 可以放在mbuf的headroom中, 它是 rte_mbuf与数据之间的一块内存区域.

mbuf一般存放在元素大小固定的内存池(rte_mempool)中, 而DPDK内存池对内存多channel和多rank有优化, 使得其中的mbuf首部可以在channel和rank间交错分布, 有助于提升访存性能. 当需要存放的数据长度大于mbuf内存块的固定长度时, 比如要处理巨帧(jumbo frame), 可存放在多个mbuf中, 然后通过next指针构成链表, 即所谓mbuf chain.

1.2 Direct与Indirect mbuf

为了避免某些场景(如复制报文, IP分片等)下的内存拷贝, DPDK引入了indirect mbuf的概念. Direct mbuf就是普通的mbuf, 实际持有数据; 而indirect mbuf不实际持有数据, 而是附着(attach)在direct mbuf上, 它的数据指针指向direct mbuf的数据. 当direct mbuf被attach时, 它的引用计数+1; 反之当被detach时, 引用计数-1, 当减为0时, direct mbuf被释放.

Indirect mbuf不能实质上attach到另一个indirect mbuf, 这么做最终会attach到后者attach的direct mbuf. 所以indirect mbuf的引用计数只能是1. Indirect mbuf也不能重新attach到direct mbuf, 除非先detach. 使用mbuf的attach/detach接口可以执行相应操作, 但推荐使用clone接口, 因为它可以正确处理indirect mbuf的初始化, 以及mbuf chain.

由于indirect mbuf不实际持有数据, 因此可以调整存储它的内存池的参数, 如元素大小, 以便减少内存占用.

2 数据结构

mbuf的结构如下图所示:
在这里插入图片描述
具体地说, mbuf从前至后主要由 rte_mbuf 结构体, headroom, 实际数据和tailroom构成. 用户还可以在 rte_mbuf 结构体和headroom之前加入一定长度的私有数据(private data). headroom的大小在DPDK编译配置文件config/common_base中指定,如CONFIG_RTE_PKTMBUF_HEADROOM=128 . 每个mbuf的总长度由以下公式计算:

mbuf_size = sizeof(rte_mbuf) + private_size + headroom_size + data_size

在一些接口中有data_room_size的概念, 它其实不等于上面的data_size, 而等于headroom_size + data_size, 那么还有以下公式:

mbuf_size = sizeof(rte_mbuf) + private_size + data_room_size

在使用 rte_pktmbuf_pool_create 这样的接口时, 要求传入data_room_size, 这时候一定要清楚这个值的含义, 它是包含headroom的:

struct rte_mempool *
rte_pktmbuf_pool_create(const char *name, unsigned n,
	unsigned cache_size, uint16_t priv_size, uint16_t data_room_size,
	int socket_id);

在rte_mbuf.h中, 有一个 RTE_MBUF_DEFAULT_BUF_SIZE 定义, 它就是data_room_size, 它的定义如下:

#define	RTE_MBUF_DEFAULT_DATAROOM	2048
#define	RTE_MBUF_DEFAULT_BUF_SIZE	\
	(RTE_MBUF_DEFAULT_DATAROOM + RTE_PKTMBUF_HEADROOM)

rte_mbuf 结构体中与数据偏移或长度相关的成员有buf_addr, data_off, buf_len, pkt_len, data_len等好几个, 还有一些辅助宏定义或函数用于快速取得特定值, 如rte_pktmbuf_mtod(m)等, 这些东西容易使人头晕, 下表对此简单总结了一下:
在这里插入图片描述
当通过next指针形成mbuf chain时, 数据结构如下图所示:
在这里插入图片描述
对于mbuf链, 元信息仅保存在链首第一个mbuf中.

3 元信息

rte_mbuf结构体中利用2个cache lines存放了很多元信息, 这里举两个例子: 报文类型和RSS hash.

3.1 报文类型

报文类型就是DPDK中所谓的packet type, 当mbuf用于收包时, 它是网卡硬件解析的报文协议类型, 不同的网卡解析的能力不同. 报文类型是一个32bit的bit mask, 它被设计为union:

/*
 * The packet type, which is the combination of outer/inner L2, L3, L4
 * and tunnel types. The packet_type is about data really present in the
 * mbuf. Example: if vlan stripping is enabled, a received vlan packet
 * would have RTE_PTYPE_L2_ETHER and not RTE_PTYPE_L2_VLAN because the
 * vlan is stripped from the data.
 */
RTE_STD_C11
union {
	uint32_t packet_type; /**< L2/L3/L4 and tunnel information. */
	struct {
		uint32_t l2_type:4; /**< (Outer) L2 type. */
		uint32_t l3_type:4; /**< (Outer) L3 type. */
		uint32_t l4_type:4; /**< (Outer) L4 type. */
		uint32_t tun_type:4; /**< Tunnel type. */
		RTE_STD_C11
		union {
			uint8_t inner_esp_next_proto;
			/**< ESP next protocol type, valid if
			 * RTE_PTYPE_TUNNEL_ESP tunnel type is set
			 * on both Tx and Rx.
			 */
			__extension__
			struct {
				uint8_t inner_l2_type:4;
				/**< Inner L2 type. */
				uint8_t inner_l3_type:4;
				/**< Inner L3 type. */
			};
		};
		uint32_t inner_l4_type:4; /**< Inner L4 type. */
	};
};

32bit的packet type构成如下所示:

0               4               8               12              16
+---------------+---------------+---------------+---------------+
| outer_L2_type | outer_L3_type | outer_L4_type |  tunnel_type  |
+---------------+---------------+---------------+---------------+
| inner_L2_type | inner_L3_type | inner_L4_type |               |
+---------------+---------------+---------------+---------------+

为了方便, 这32bit可以使用packet_type成员来一次性访问. 不同网卡对同一个报文的报文类型的识别结果是不同的.

下面是两个例子. 以下封装的报文:

<'ether type'=0x0800
| 'version'=4, 'protocol'=0x29
| 'version'=6, 'next header'=0x3A
| 'ICMPv6 header'>

在i40e网卡上解析的报文类型如下:

RTE_PTYPE_L2_ETHER |
RTE_PTYPE_L3_IPV4_EXT_UNKNOWN |
RTE_PTYPE_TUNNEL_IP |
RTE_PTYPE_INNER_L3_IPV6_EXT_UNKNOWN |
RTE_PTYPE_INNER_L4_ICMP

以下封装的报文:

<'ether type'=0x86DD
| 'version'=6, 'next header'=0x2F
| 'GRE header'
| 'version'=6, 'next header'=0x11
| 'UDP header'>

在i40e网卡上解析的报文类型如下:

RTE_PTYPE_L2_ETHER |
RTE_PTYPE_L3_IPV6_EXT_UNKNOWN |
RTE_PTYPE_TUNNEL_GRENAT |
RTE_PTYPE_INNER_L3_IPV6_EXT_UNKNOWN |
RTE_PTYPE_INNER_L4_UDP

RTE_PTYPE_XXX定义在 lib/librte_mbuf/rte_mbuf_ptype.h 中. 使用时要注意, 对于不同的报文内容, 解析出的同一协议类型的掩码是不同的, 比如不带选项的IPv4, 带选项的IPv4, 不清楚带不带选项的IPv4, 内层还是外层IPv4… 都是不同的. 比如对于外层IPv4报文来说, 有以下几个定义:

// 不包含选项, 00010000
#define RTE_PTYPE_L3_IPV4                   0x00000010
// 包含选项, 00110000
#define RTE_PTYPE_L3_IPV4_EXT               0x00000030
// 可能包含,也可能不包含选项, 10010000
#define RTE_PTYPE_L3_IPV4_EXT_UNKNOWN       0x00000090

可以看到, 无论是否包含选项, 只要是外层IPv4, bit 4总是1, 因此可以用它来判断是否外层IPv4, DPDK提供了这个宏定义:

/**
* Check if the (outer) L3 header is IPv4. To avoid comparing IPv4 types one by
* one, bit 4 is selected to be used for IPv4 only. Then checking bit 4 can
* determine if it is an IPv4 packet.
*/
#define  RTE_ETH_IS_IPV4_HDR(ptype) ((ptype) & RTE_PTYPE_L3_IPV4)

3.2 RSS hash

RSS是现代多队列网卡的一个重要功能, 可以对收到的包计算hash值, 然后分到多个队列. 然后用户程序就可以让CPU的多个核心分别处理这些队列, 利用多核CPU优势提升处理性能. DPDK RSS相关介绍与计算见我的文章.

rte_mbuf结构体使用 hash.rss 来存放网卡计算的RSS hash值, 它是一个32bit数. 注意要配置网卡参数, 启用RSS功能后这个值才有效.

4 应用

这里通过示例代码来介绍mbuf的一般使用, 代码见https://github.com/zzqcn/storage/tree/master/code/c/mbuf.

代码中一些常数定义:

#define MBUF_COUNT  (1024-1)
#define PRIV_SIZE   16
// ETH_MAX_len = 1518
// ETH_MTU = ETH_MAX_LEN - ETH_HDR_LEN - ETHER_CRC_LEN = 1518 - 14 - 4 = 1500
#define MBUF_DATAROOM_SIZE (RTE_PKTMBUF_HEADROOM + ETHER_MAX_LEN)
#define MBUF_SIZE   (sizeof(struct rte_mbuf) + PRIV_SIZE + MBUF_DATAROOM_SIZE)
#define CACHE_SIZE  32

4.1 分配

如上所述, mbuf一般存储在内存池中. 有两种方法创建mbuf内存池:

  • 直接使用 rte_mempool_create
  • 使用 rte_pktmbuf_pool_create

这两种方法最终都会创建 rte_mempool 数据结构, 但前者参数较多较复杂, 后者参数少简单. 另外直接使用 rte_mempool_create 还可以使用 flags 精细控制内存池的行为, 如单消费者标志 MEMPOOL_F_SC_GET , rte_pktmbuf_pool_create 不具备这个能力.

通过两种方式创建mbuf内存池:

    struct rte_mempool* mpool;
    struct rte_mbuf *m, *m2, *m3;
    struct rte_mempool_objsz objsz;
    uint32_t mbuf_size;

#ifdef RAW_MEMPOOL
    struct rte_pktmbuf_pool_private priv;
    priv.mbuf_data_room_size = MBUF_DATAROOM_SIZE;
    priv.mbuf_priv_size = PRIV_SIZE;

    mpool = rte_mempool_create("test_pool",
                               MBUF_COUNT,
                               MBUF_SIZE,
                               CACHE_SIZE,
                               sizeof(struct rte_pktmbuf_pool_private),
                               rte_pktmbuf_pool_init,
                               &priv,
                               rte_pktmbuf_init,
                               NULL,
                               SOCKET_ID_ANY,
                               MEMPOOL_F_SC_GET);
#else
    mpool = rte_pktmbuf_pool_create("test_pool",
                                    MBUF_COUNT,
                                    CACHE_SIZE,
                                    PRIV_SIZE,
                                    MBUF_DATAROOM_SIZE,
                                    SOCKET_ID_ANY);
#endif
    if(NULL == mpool)
        return -1;

可见 rte_mempool_create 的参数确实多不少. 对它的调用要注意几点:

  • 可提供两个回调函数, 一个在内存池初始化阶段, 池中元素初始化之前调用, 用于给内存池加私有数据. 即上面的 rte_pktmbuf_pool_init 函数, 它可带一个用户指针, 指针需指向 rte_pktmbuf_pool_private 结构体起始地址. 内存池私有数据的大小是第5个参数, 它可以大于 rte_pktmbuf_pool_private 结构体的大小
  • 另一个回调函数在初始化内存池中每一个元素时调用, 主要用于初始化一些在mbuf创建后就再也无需改动的成员, 如内存池指针, 数据起始地址等. 这里传入 rte_pktmbuf_init 函数. 此回调函数也可传入一个参数, 但在这里我们传入NULL, 因为 rte_pktmbuf_init 函数不使用这个参数
  • 内存池私有数据和mbuf私有数据是两个东西, 不要混淆. 前者位于内存池管理结构中, 用于记录内存池相关的私有信息; 后者位于内存池中每一个mbuf中, 用于存放和每个mbuf相关的私有信息

接下来我们考察内存池中mbuf的实际大小:

mbuf_size = rte_mempool_calc_obj_size(MBUF_SIZE, 0, &objsz);
printf("mbuf_size: %u\n", mbuf_size);
printf("elt_size: %u, header_size: %u, trailer_size: %u, total_size: %u\n",
    objsz.elt_size, objsz.header_size, objsz.trailer_size, objsz.total_size);

我们本来定义的mbuf整体大小:

MBUF_SIZE = rte_mbuf结构体大小 + private数据大小 + headroom大小 + 以太网报文大小
          = 128 + 16 + 256 + 1518
          = 1918

但程序打印的是:

mbuf_size: 1984
elt_size: 1920, header_size: 64, trailer_size: 0, total_size: 1984

可见内存池中每个元素大小所占内存大于1918字节, 这是因为每个元素由header, mbuf, trailer构成. 其中header为 rte_mempool_objhdr 结构体, 在不指定 MEMPOOL_F_NO_CACHE_ALIGN 标志的情况下, 它还需要对cache line对齐; mbuf需要8字节对齐(1918->1920); trailer则由 RTE_IBRTE_MEMPOOL_DEBUG 控制, 用于调试, 如果打开调试选项, 它也需要根据 MEMPOOL_F_NO_CACHE_ALIGN 标志的情况进行调整, 我们现在没设置这些选项和标志, 因此trailer为0. 总之最后每个元素的总大小是1984.

创建好内存池之后, 就可以通过以下接口来获得mbuf:

  • rte_mempool_get , rte_mempool_get_bulk
  • rte_pktmbuf_alloc , rte_pktmbuf_alloc_bulk

这里要注意:

  • mbuf的获取支持批量操作, 以提升性能
  • rte_pktmbuf_alloc 系列函数会在成功取得mbuf后, 调用 rte_pktmbuf_reset 对它进行重置, rte_mempool_get 系列函数则没有此操作, 需要用户手动重置:
    static inline void rte_pktmbuf_reset(struct rte_mbuf *m)
    {
    	m->next = NULL;
    	m->pkt_len = 0;
    	m->tx_offload = 0;
    	m->vlan_tci = 0;
    	m->vlan_tci_outer = 0;
    	m->nb_segs = 1;
    	m->port = MBUF_INVALID_PORT;
    
    	m->ol_flags = 0;
    	m->packet_type = 0;
    	rte_pktmbuf_reset_headroom(m);
    
    	m->data_len = 0;
    	__rte_mbuf_sanity_check(m, 1);
    }
    

将mbuf内存池交给收包网卡时, 网卡 应该 会自动调用 rte_pktmbuf_reset .

4.2 回收

回收相关接口:

  • rte_mempool_put , rte_mempool_put_bulk 同样的, 这些裸的内存池接口并不会处理mbuf内部细节
  • rte_pktmbuf_free 回收mbuf或mbuf chain, 可以处理引用计数等内部细节
  • rte_pktmbuf_free_seg 回收单个mbuf, 但不处理mbuf chain. 可以处理引用计数等内部细节
  • rte_mbuf_raw_free 底层回收接口, 调用者必须保证被回收的mbuf是direct mbuf, 且引用计数为1等, 也不处理mbuf chain

当使用网卡发包时, 发送成功后网卡驱动会自动释放mbuf.

4.3 一般操作与报文链

我们创建两个mbuf m和m2, m数据长度1000字节, m2 500字节. 创建后输出其元信息:

m = rte_pktmbuf_alloc(mpool);
rte_pktmbuf_append(m, 1000);
mbuf_dump(m);

m2 = rte_pktmbuf_alloc(mpool);
rte_pktmbuf_append(m2, 500);
mbuf_dump(m2);

m的输出:

RTE_PKTMBUF_HEADROOM: 256
sizeof(mbuf): 128
m: 0x7fbfb601ffc0
m->buf_addr: 0x7fbfb6020050
m->data_off: 256
m->buf_len: 1774
m->pkt_len: 1000
m->data_len: 1000
m->nb_segs: 1
m->next: (nil)
m->buf_addr+m->data_off: 0x7fbfb6020150
rte_pktmbuf_mtod(m): 0x7fbfb6020150
rte_pktmbuf_data_len(m): 1000
rte_pktmbuf_pkt_len(m): 1000
rte_pktmbuf_headroom(m): 256
rte_pktmbuf_tailroom(m): 518
rte_pktmbuf_data_room_size(mpool): 1774
rte_pktmbuf_priv_size(mpool): 16

m2的输出(省略部分内容, 下同):

m: 0x7fbfb601f800
m->buf_addr: 0x7fbfb601f890
m->data_off: 256
m->buf_len: 1774
m->pkt_len: 500
m->data_len: 500
m->nb_segs: 1
m->next: (nil)
m->buf_addr+m->data_off: 0x7fbfb601f990
rte_pktmbuf_mtod(m): 0x7fbfb601f990
rte_pktmbuf_data_len(m): 500
rte_pktmbuf_pkt_len(m): 500
rte_pktmbuf_headroom(m): 256
rte_pktmbuf_tailroom(m): 1018
rte_pktmbuf_data_room_size(mpool): 1774
rte_pktmbuf_priv_size(mpool): 16

m与m2构成mbuf chain:

rte_pktmbuf_chain(m, m2);
mbuf_dump(m);

此时m成为mbuf chain首节点, 输出为:

m: 0x7fbfb601ffc0
m->buf_addr: 0x7fbfb6020050
m->data_off: 256
m->buf_len: 1774
m->pkt_len: 1500
m->data_len: 1000
m->nb_segs: 2
m->next: 0x7fbfb601f800
m->buf_addr+m->data_off: 0x7fbfb6020150
rte_pktmbuf_mtod(m): 0x7fbfb6020150
rte_pktmbuf_data_len(m): 1000
rte_pktmbuf_pkt_len(m): 1500
rte_pktmbuf_headroom(m): 256
rte_pktmbuf_tailroom(m): 518
rte_pktmbuf_data_room_size(mpool): 1774
rte_pktmbuf_priv_size(mpool): 16

通过 rte_pktmbuf_free 释放m即可释放整个mbuf chain:

printf("mempool count before free: %u\n", rte_mempool_avail_count(mpool));
rte_pktmbuf_free(m);
printf("mempool count after free: %u\n", rte_mempool_avail_count(mpool));

输出:

mempool count before free: 1021
mempool count after free: 1023

4.4 Direct/Indirect mbuf

创建一个direct mbuf m, 再创建2个m的拷贝m2, m3:

m = rte_pktmbuf_alloc(mpool);
rte_pktmbuf_append(m, 1000);
mbuf_dump(m);
m2 = rte_pktmbuf_clone(m, mpool);
mbuf_dump(m2);
m3 = rte_pktmbuf_clone(m, mpool);
mbuf_dump(m3);

输出如下, 可见这3个mbuf的首地址不同, 但涉及到数据的偏移和指针(如buf_addr, data_off等)都是一样的:

// m
m: 0x7f56c8c1f800
m->refcnt: 1
m->buf_addr: 0x7f56c8c1f890
m->data_off: 256
m->buf_len: 1774
m->pkt_len: 1000
m->data_len: 1000
m->nb_segs: 1
m->next: (nil)
rte_pktmbuf_mtod(m): 0x7f56c8c1f990

// m2
m: 0x7f56c8c1ffc0
m->refcnt: 1
m->buf_addr: 0x7f56c8c1f890
m->data_off: 256
m->buf_len: 1774
m->pkt_len: 1000
m->data_len: 1000
m->nb_segs: 1
m->next: (nil)
rte_pktmbuf_mtod(m): 0x7f56c8c1f990

// m3
m: 0x7f56c8c1f040
m->refcnt: 1
m->buf_addr: 0x7f56c8c1f890
m->data_off: 256
m->buf_len: 1774
m->pkt_len: 1000
m->data_len: 1000
m->nb_segs: 1
m->next: (nil)
rte_pktmbuf_mtod(m): 0x7f56c8c1f990

此时原始direct mbuf m的refcnt是3. 依次对m, m2, m3, m调用释放接口, 并检查m的引用计数和mbuf内存池节点数量:

printf("mempool count before free: %u\n", rte_mempool_avail_count(mpool));
printf("m->refcnt: %u\n", m->refcnt);
rte_pktmbuf_free(m);
printf("mempool count after free: %u\n", rte_mempool_avail_count(mpool));
printf("m->refcnt: %u\n", m->refcnt);
rte_pktmbuf_free(m3);
printf("mempool count after free: %u\n", rte_mempool_avail_count(mpool));
printf("m->refcnt: %u\n", m->refcnt);
rte_pktmbuf_free(m2);
printf("mempool count after free: %u\n", rte_mempool_avail_count(mpool));
printf("m->refcnt: %u\n", m->refcnt);
rte_pktmbuf_free(m);
printf("mempool count after free: %u\n", rte_mempool_avail_count(mpool));
printf("m->refcnt: %u\n", m->refcnt);

输出:

mempool count before free: 1020
m->refcnt: 3
mempool count after free: 1020
m->refcnt: 2
mempool count after free: 1021
m->refcnt: 1
mempool count after free: 1023
m->refcnt: 1
mempool count after free: 1023
m->refcnt: 1

可以看到:

  • 第1次释放m, 由于还有m2,m3还在引用它, 所以并没有真正回收到内存池, 但引用计数-1 = 2
  • 释放m2, 它是indirect mbuf, 直接回收到内存池, 同时它引用的m的引用计数再-1 = 1
  • 释放m3, 它是indirect mbuf, 直接回收到内存池, 同时由于它引用的m的引用计数已经是1, 表示无人占占用, 所以将m也回收到内存池. 此时所有mbuf都已经还回到内存池
  • 第2次释放m, 这里已经是double free的错误操作了, 任何内存池元素都不应该重复回收. 不过也应该注意所有mbuf的引用计数必定>=1

5 参考

  • DPDK源码 lib/librte_mbuf/
  • DPDK源码 lib/librte_mempool/
  • DPDK开发手册: Mbuf Library

原文链接:https://www.yuque.com/zzqcn/opensource/oirzxh

  • 9
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值