有过网络编程经验的程序员都知道,一般在上层应用设计时,尽量保持数据包体的大小和MTU(最小传输单元)保持一致,这样就可以保证一包数据的传输中的完整性。减少IP层出现问题导致传输层的重传机制启动(主要是指UDP,TCP自己已经处理)。一般来说MTU在传输过程中饰面还会加上开头结尾校验等单元,这样,使用抓包工具时的范围在601514(MTU实际是461500)。那小于46个字节呢?老规矩,自动补齐呗。
DPDK做一种直接和IO打交道的框架,自然有收发两种情况,也就是说既有从上层传入的完整的数据流需要DPDK框架来拆解成分片大小(MTU可允许)又要可以将分片数据重组为完整的数据流。正反两个方向都是流畅清晰的,这是做为网络编程框架的一个基本要求。在这个分片和组装的过程中,就会用前面学习过的零拷贝技术。
再重复看一下mbuf的数据结构(全部数据定义请查看源码):
/**
* The generic rte_mbuf, containing a packet mbuf.
*/
struct rte_mbuf {
MARKER cacheline0;
void * buf_addr; /**< Virtual address of segment buffer. */
/**
* Physical address of segment buffer.
* Force alignment to 8-bytes, so as to ensure we have the exact
* same mbuf cacheline0 layout for 32-bit and 64-bit. This makes
* working on vector drivers easier.
*/
RTE_STD_C11
union {
rte_iova_t buf_iova;
rte_iova_t buf_physaddr; /**< deprecated */
} __rte_aligned(sizeof(rte_iova_t));
/* next 8 bytes are initialised on RX descriptor rearm * /
MARKER64 rearm_data;
uint16_t data_off;
......
/* second cache line - fields only used in slow path or on TX */
MARKER cacheline1 __rte_cache_min_aligned;
/** Size of the application private data. In case of an indirect
* mbuf, it stores the direct mbuf private data size.
*/
uint16_t priv_size;
/** Timesync flags for use with IEEE1588. */
uint16_t timesync;
/** Sequence number. See also rte_reorder_insert(). */
uint32_t seqn;
/** Shared data for external buffer attached to mbuf. See
* rte_pktmbuf_attach_extbuf().
*/
struct rte_mbuf_ext_shared_info * shinfo;
uint64_t dynfield1[2]; /**< Reserved for dynamic fields. * /
}
在这个数据结构体里有两个奇怪的标记MARKER cacheline1,和MARKER cacheline0,看这个MARKER定义,这类似于汇编语言中的标签。MARKER被定义为(void**),需要注意的是MARKER[0],是一个可扩展数组,是GCC支持的,但在VS编译器中会报错,可以使用1长度数组来代替。可以理解成一个数据结构里面有两个指针分别指向不同功能的域。
mbuf虽然是传输网络包的,但是理论上缓冲存储何种数据都是可以的。其实主要看利用效率,所以把它分为两个域也是可以理解的。经常使用的放在第一个域内,扩展的数据放在第二个域内。mbuf可以使用next指针指向下一个相同的数据结构形成链表。而mbuf本身也有两种方式来存储数据,一种是直接的,即元数据和数据在一个mbuf内,而另外一种是元数据和数据分别使用各自的缓冲区。两者的各自优势也很明显,前者类似于固定的内存池,操作容易但可能因为数据包大小不同导致有浪费;而后者则可能导致操作复杂,效率低。DPDK为了效率,肯定是选用第一种了。不过,在巨型帧中,元信息只在第一帧中体现,其后帧该部分为空。
需要说明的是在mbuf内部的头部和实际的数据包之间是有一段控制信息的,也就是headroom,用来存储一些交互信息,其起始的地址为buf_addr指针,可以用RTE_PKTMBUF_HEADROOM来调整大小。而数据帧的地址可以调用rte_pktmbuf_mtod宏来得到,看一下这个宏的定义就明白了,其实就是地址的偏移处理。
#define rte_pktmbuf_mtod_offset(m, t, o) \
((t)(void * )((char * )(m)->buf_addr + (m)->data_off + (o)))
另外其还还有一个尾部的tailroom,它们的定义有内核中的协议栈skb_buf有些类似。
相关的接口都是rte_mbuf.h中,包括创建、匹配、释放等:
static inline void
rte_mbuf_prefetch_part1(struct rte_mbuf *m)
{
rte_prefetch0(&m->cacheline0);
}
static inline void
rte_mbuf_prefetch_part2(struct rte_mbuf *m)
{
#if RTE_CACHE_LINE_SIZE == 64
rte_prefetch0(&m->cacheline1);
#else
RTE_SET_USED(m);
#endif
}
__rte_experimental
void rte_pktmbuf_free_bulk(struct rte_mbuf **mbufs, unsigned int count);
struct rte_mbuf *
rte_pktmbuf_clone(struct rte_mbuf *md, struct rte_mempool *mp);
static inline uint16_t rte_pktmbuf_headroom(const struct rte_mbuf *m)
{
__rte_mbuf_sanity_check(m, 0);
return m->data_off;
}
static inline uint16_t rte_pktmbuf_tailroom(const struct rte_mbuf *m)
{
__rte_mbuf_sanity_check(m, 0);
return (uint16_t)(m->buf_len - rte_pktmbuf_headroom(m) -
m->data_len);
}
static inline char *rte_pktmbuf_append(struct rte_mbuf *m, uint16_t len)
{
void * tail;
struct rte_mbuf * m_last;
__rte_mbuf_sanity_check(m, 1);
m_last = rte_pktmbuf_lastseg(m);
if (unlikely(len > rte_pktmbuf_tailroom(m_last)))
return NULL;
tail = (char * )m_last->buf_addr + m_last->data_off + m_last->data_len;
m_last->data_len = (uint16_t)(m_last->data_len + len);
m->pkt_len = (m->pkt_len + len);
return (char* ) tail;
}
static inline int rte_pktmbuf_trim(struct rte_mbuf *m, uint16_t len)
{
struct rte_mbuf * m_last;
__rte_mbuf_sanity_check(m, 1);
m_last = rte_pktmbuf_lastseg(m);
if (unlikely(len > m_last->data_len))
return -1;
m_last->data_len = (uint16_t)(m_last->data_len - len);
m->pkt_len = (m->pkt_len - len);
return 0;
}
上面只是举了几个典型的接口,更多的可以去查看相关源码。
在DPDK中,网络数据会被存储在一个环形(ring)缓冲区内,同时,在mbuf的环形缓冲区内创建一个mbuf对象。因为是内存池,所以这都不需要申请新的内存,意思就是速度非常快。二者之间通过通道或者RANK进行对齐(其实就是补0,有C++开发经验的都知道对齐)。
上面的内存池是在DPDK中已经创建好了,不过这里面还有一个问题,当多核CPU同时访问一个缓冲区时,仍然有竞争的问题,虽然使用CAS减少了锁的压力,但仍然导致效率会降低。这时,就需要通过DPDK对每个核心进行缓存,这样通过减少访问内存池的次数来降低竞争。
mbuf使用mempool来进行管理,在上面的数据结构定义中,可以看到一个指针向了mempool。当然,在这里面为了提高效率也大量使用了汇编汇合编程如:
static inline void rte_prefetch1(const volatile void *p)
{
asm volatile ("dcbt 0,%[p],0" : : [p] "r" (p));
}
更详细的内存池会在相关部分详细分析,此处只是带过。
在网络通信过程中,会产生很多控制数据,其实就是元数据,包括一些硬件和协议层的元数据,最典型的就是一些校验和,长度等等。另外,一些扩展数据可以在mbuf中自己定义,在其数据结构最后有一个dynfield1字段,它的说明就是“保留的动态域”。这些都可以是mbuf的一种动态适应机制。
从基础入手,继续分层剥离,越来越清晰的逼近DPDK的内部。