数据包在服务器的处理分接收和发送两个方向,收包方向因为我们自己本身的业务场景涉及收包数据很少,后续另行介绍。
本文主要介绍F-Stack发包方向上当前的零拷贝处理方案、效果和应用场景的选择,发包方向上的数据拷贝目前主要为两个阶段,一是协议栈数据拷贝到DPDK的rte_mbuf
中,二是应用层调用socket发送接口时会将数据从应用层拷贝到FreeBSD协议栈,下面将分别进行介绍。
协议栈到DPDK
该过程的零拷贝实现由 @jinhao2 提交的Pull Request #364 合并到F-Stack主线中,相关实现细节可以参考相关代码,这里仅对实现方案进行简要介绍。
方案介绍
- 进程初始化时,通过mmap 为 BSD 堆栈分配指定大小的内存(目前默认256M),可以通过在
config.ini
中通过参数memsz_MB
修改默认配置。 - 通过 mlock() 固定物理内存,防止被换出到交换分区造成内存虚拟地址和物理地址对应关系的变化。
- 计算每个页面的起始地址并保存,包括虚拟地址和物理地址,物理地址的计算可以通过DPDK的提供的相关接口进行。
- 初始化一个堆栈结构来管理所有分配的页面。
- 通过从已经初始化的堆栈结构中获取/释放一页来替换
ff_mmap()/ff_munmap()
的实际mmap行为,而BSD协议栈调用kmem_malloc()/kmem_free()
时调用ff_mmap()/ff_munmap()
来获取内存页。 - 在将BSD协议栈
mbuf
的数据地址赋值给DPDK的rte_mbuf
时用于判断是否为初始化申请的内存池中的地址,并通过虚拟地址查找对应的物理地址,分别赋值给rte_buf
结构的buf_addr/buf_physaddr
,而不再实际进行内存拷贝。 - 使用一个循环队列保存发送的
mbuf
的指针,队列的长度应该与NIC的tx_queue_length
相同。在队列中的一项被推入新值之前,旧的mbuf
必须由 NIC 处理并且可以安全地释放。 - 如果
mbuf
是ext_cluster
类型,其中包括一个rte_mbuf
,表示是收包时零拷贝附加的数据地址,则使用rte_pktmbuf_clone()
代替。
使用方式及注意事项
使用方式
该功能默认并未开启,需要通过在lib/Makefile
中打开编译选项FF_USE_PAGE_ARRAY
,并重新编译F-Stack lib 库和应用程序后才能生效。
其他应用编程及使用方式与常规拷贝模式没有区别,对应用层透明。
注意事项
- 内存池初始化时在本进程通过
mmap
和mlock
申请,为进程私有地址空间,相关内存不能传递到其他进程使用。- 可以考虑在初始化时映射大页内存或者使用共享内存(同样需要
SHM_LOCk
或mlock
锁定内存,防止交换)来达到可以跨进程使用的目的,但是对应的地址保存和查找结构也需要进行变更,一般应用建议避免跨进程使用即可,不建议进行修改。
- 可以考虑在初始化时映射大页内存或者使用共享内存(同样需要
- 协议栈到DPDK的零拷贝功能可以单独开启
FF_USE_PAGE_ARRAY
使用,也可以与零拷贝发送接口FF_ZC_SEND
一起开启使用。 - 此处减少的内存拷贝是否对应用性能有提升还需要结合具体的应用进行实际测试,数据包在一定大小且使用方式合适时则可以有一定的性能优化效果,但优化效果并不一定很明显,比如只有2-3%左右的提升。
应用层到协议栈
通过提供单独的零拷贝API,使应用层在通过socket接口发送数据时,避免应用层到BSD协议栈的数据拷贝,具体细节见提交e12886c,下面将进行较为具体的介绍。
方案介绍
- 提供单独的零拷贝结构体
ff_zc_mbuf
,用于应用层缓存结构,后续应用层的数据操作和发送都应该使用该结构体,具体类型如下所示:struct ff_zc_mbuf {
void *bsd_mbuf; /* 指向BSD mbuf链的头节点 */
void *bsd_mbuf_off; /* 指向BSD mbuf链中偏移off后的当前节点 */
int off; /* mbuf链中的偏移量,应用层不应该直接修改 */
int len; /* 申请的mbuf链缓存的总长度,小于等于mbuf链实际能承载的数据长度 */
}; - 提供接口
ff_zc_mbuf_get()
,用于应用提前申请包含可以由内核直接使用的mbuf
的结构体作为应用层数据缓存,接口声明如下。int ff_zc_mbuf_get(struct ff_zc_mbuf *m, int len);该接口输入struct ff_zc_mbuf *
指针和需要申请的缓存总长度,内部将通过m_getm2()
分配mbuf
链,首地址保存在ff_zc_mbuf
结构的bsd_mbuf
变量中,后续可以传递给ff_write()
接口。其中m_getm2()
为标准socket接口拷贝应用层数据到协议栈时分配mbuf
链的接口,所以使用该接口范围的mbuf
链作为应用层缓存,可以在发送数据时完全兼容。 - 提供了缓存数据写入函数
ff_zc_mbuf_write()
,函数声明如下, - int ff_zc_mbuf_write(struct ff_zc_mbuf *m, const char *data, int len); 应用层在保存待发送的数据时,应通过接口
ff_zc_mbuf_wirte()
直接将数据写到ff_zc_mbuf
指向的mbuf
链的缓存中,ff_zc_mbuf_wirte()
接口可以多次调用写入缓存数据,接口内部自动处理缓存的偏移情况,但多次总的写入长度不能超过初始申请的缓存长度。 - 应用调用
ff_write()
接口时指定传递ff_zc_mubf.bsd_mbuf
为buf
参数,示例如下所示,ff_write(clientfd, zc_buf.bsd_mbuf, buf_len);在m_uiotombuf()
函数中,直接使用传递的mbuf
链的首地址,不再额外进行mbuf
链的分配和数据拷贝,如下所示,#ifdef FSTACK_ZC_SEND
if (uio->uio_segflg == UIO_SYSSPACE && uio->uio_rw == UIO_WRITE) {
m = (struct mbuf *)uio->uio_iov->iov_base; /* 直接使用应用层的mbuf链首地址 */
uio->uio_iov->iov_base = (char *)(uio->uio_iov->iov_base) + total;
uio->uio_iov->iov_len = 0;
uio->uio_resid = 0;
uio->uio_offset = total;
progress = total;
} else {
#endif
m = m_getm2(NULL, max(total + align, 1), how, MT_DATA, flags); /* 拷贝模式分配mbuf链*/
if (m == NULL)
return (NULL);
m->m_data += align;
/* Fill all mbufs with uio data and update header information. */
for (mb = m; mb != NULL; mb = mb->m_next) {
length = min(M_TRAILINGSPACE(mb), total – progress);
error = uiomove(mtod(mb, void *), length, uio); /* 拷贝模式拷贝应用层数据到协议栈 */
if (error) {
m_freem(m);
return (NULL);
}
mb->m_len = length;
progress += length;
if (flags & M_PKTHDR)
m->m_pkthdr.len += length;
}
#ifdef FSTACK_ZC_SEND
}
#endif - 在
ff_write()
函数成功返回后,之前申请的ff_zc_mbuf
结构内部mbuf
链数据不需要释放,该结构可以在函数ff_zc_mbuf_get()
中复用重新分配BSD的mbuf
链。- 不能够再次直接在
ff_zc_mbuf_wirte()
使用,必须重新调用ff_zc_mbuf_get()
分配新的mbuf
链之后才可以继续使用
- 不能够再次直接在
使用方式及注意事项
使用方式
该功能默认并未开启,需要通过在lib/Makefile
中打开编译选项FF_ZC_SEND
,并重新编译F-Stack lib 库和应用程序后才能生效。
零拷贝发送接口的使用方式与标准socket接口也有区别,具体可以参考前面的方案介绍及示例代码。
注意事项
- 使用零拷贝发送接口需要对原有应用进行修改才能接入,且并不一定有很明显的性能提升,所以默认不开启。
- 零拷贝发送接口可以单独开启
FF_ZC_SEND
使用,也可以与FF_USE_PAGE_ARRAY
一起开启使用。 - 与协议栈到DPDK的零拷贝类似,此处减少的内存拷贝是否对应用性能有提升还需要结合具体的应用进行实际测试,在特定应用场景下才会有一定的性能提升,但效果并不一定很明显,比如只有2-3%左右的提升。
- 目前
struct ff_zc_mbuf *
的结构是对外暴露给应用层的,可以更方便的进行测试使用,后续不排除隐藏该数据结构的可能。