数据包在服务器的处理分接收和发送两个方向,收包方向因为我们自己本身的业务场景涉及收包数据很少,后续另行介绍。
本文主要介绍F-Stack发包方向上当前的零拷贝处理方案、效果和应用场景的选择,发包方向上的数据拷贝目前主要为两个阶段,一是协议栈数据拷贝到DPDK的rte_mbuf
中,二是应用层调用socket发送接口时会将数据从应用层拷贝到FreeBSD协议栈,下面将分别进行介绍。
该过程的零拷贝实现由 @jinhao2 提交的Pull Request #364 合并到F-Stack主线中,相关实现细节可以参考相关代码,这里仅对实现方案进行简要介绍。
config.ini
中通过参数memsz_MB
修改默认配置。ff_mmap()/ff_munmap()
的实际mmap行为,而BSD协议栈调用kmem_malloc()/kmem_free()
时调用ff_mmap()/ff_munmap()
来获取内存页。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
锁定内存,防止交换)来达到可以跨进程使用的目的,但是对应的地址保存和查找结构也需要进行变更,一般应用建议避免跨进程使用即可,不建议进行修改。FF_USE_PAGE_ARRAY
使用,也可以与零拷贝发送接口FF_ZC_SEND
一起开启使用。通过提供单独的零拷贝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,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 } #endifff_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
一起开启使用。