零拷贝功能
功能描述
提供零拷贝定制接口,支持用户调用零拷贝接口,避免应用于用户态协议栈之间的内存拷贝,显著降低CPU开销与内存带宽消耗。
零拷贝功能支持灵活部署方案:服务端和客户端双端配置使用可获得最佳网络传输性能;也支持单端配置使用,用户可根据实际场景需求选择。
使用示例
本章示例以iPerf3为例。 本章主要说明如何配置、使用API参考的零拷贝接口,提供零拷贝使用伪代码,指导用户使用。
业务在使用零拷贝接口时,必须遵循以下约束,以保证接口使用的安全性。
业务在声明struct knet_iovec类型的变量时应对其进行正确的初始化,确保knet_iovec中的iov_base字段由knet_mp_alloc申请而来,iov_len字段不大于申请的写缓冲区的实际长度,否则会导致发送失败,或者段错误(SegFault)的发生。
// 错误场景一:调用knet_zwritev时,入参iov的iov_base指向非法地址 struct knet_iovec iov = {0}; ... iov.iov_base = 0x1; // 填充非法的内存地址 iov.iov_len = iovlen; iov.free_cb = knet_mp_free; iov.opaque = NULL; knet_zwritev(sockfd, &iov, 1); // 出现非法地址访问的段错误! ... // 错误场景二:调用knet_zwritev时,入参iov的iov_base指向的地址不是由knet_mp_alloc申请而得来的 struct knet_iovec iov = {0}; iov.iov_base = malloc(iovlen); // 未使用knet_mp_alloc返回的内存 iov.iov_len = iovlen; iov.free_cb = knet_mp_free; iov.opaque = NULL; knet_zwritev(sockfd, &iov, 1); // 接口不会返回错误,但对端不会接收到正确的数据报文 // 正常场景 struct knet_iovec iov = {0}; iov.iov_base = knet_mp_alloc(iovlen); // 使用knet_mp_alloc申请写缓冲区内存 iov.iov_len = iovlen; iov.free_cb = knet_mp_free; iov.opaque = NULL; knet_zwritev(sockfd, &iov, 1); // 发送成功,对端可以收到正确的数据报文业务应自定义合理的iov的释放回调,或使用knet_mp_free来作为iov的free_cb,否则会导致内存泄漏,甚至是段错误。
// 错误场景一:调用knet_zwritev时,free_cb指向非法的地址 struct knet_iovec iov = {0}; iov.iov_base = knet_mp_alloc(iovlen); iov.iov_len = iovlen; iov.free_cb = 0x1; // free_cb 指向的地址非法 iov.opaque = NULL; knet_zwritev(sockfd, &iov, 1); // 不会立即返回错误,实际调用free_cb时发生段错误! // 错误场景二:调用knet_iov_free时,free_cb设置不合理 void FreeCb(void *addr, void *opaque) // 自定义的不合理的释放回调,实际上什么也没做 { return; } iov.iov_base = knet_mp_alloc(iovlen); iov.iov_len = iovlen; iov.free_cb = FreeCb; // 使用自定义的释放回调 iov.opaque = NULL; knet_zwritev(sockfd, &iov, 1); // 不会立即返回错误,会出现内存泄漏,写缓冲区不会被正常释放,内存泄漏!业务访问knet_mp_alloc或knet_zreadv两个函数填充的iov_base时,需自行保证内存访问的安全性。对iov_base进行访存时,不要超过iov_len的限制范围。如果越界访存,可能出现内存破坏,或者段错误的问题。
// 错误场景一:使用knet_iovec时读内存越界 struct knet_iovec iov = {0}; knet_zreadv(sockfd, &iov, 1); // 读取成功,knet_zreadv接口填充iov的各个字段 uint8_t *data = (uint8_t *)iov.iov_base; for (size_t i = 0; i < iov.iov_len + 1000; i++) { // 读越界,可能导致读取错误数据或者出现段错误! printf("%02X ", iov[i]); if ((i + 1) % 16 == 0) { printf("\n"); } } // 错误场景二:使用knet_iovec时写内存越界 struct knet_iovec iov = {0}; iov.iov_base = knet_mp_alloc(iovlen); // 使用knet_mp_alloc申请了写缓冲区 memcpy(iov.iov_base, data, iovlen + 1000); // 写越界!可能导致内存被破坏,或者出现段错误!
零拷贝功能指K-NET提供特殊的读写接口,使用零拷贝的读写接口可以消除用户缓冲区到mbuf间的内存拷贝,以提高读写的性能。 具有如下约束:
- 业务使用K-NET零拷贝接口,需保证配置项“zcopy_enable”为1,即已使能。
- 业务使用零拷贝写接口时的iov_len不得超过配置项“zcopy_sge_len”的值,该配置项建议设置为希望发送的最大iov_len的值。
- 业务单次调用零拷贝写接口写入的数据总长度不得大于“def_sendbuf”的值。
- “zcopy_sge_num”推荐按照用户态TCP/IP协议栈配置项中的zcopy_sge_num的计算公式配置。
- 业务使用零拷贝读接口读取数据并使用完毕后,需调用iov的free_cb来释放读缓冲区。
- 业务使用零拷贝写接口时,应该调用写缓冲区申请接口knet_mp_alloc来申请写缓冲区。
- 零拷贝写接口的发送操作具有原子性,要么全部发送成功,要么全部发送失败。
- 通过设置socket fd的属性可以设置零拷贝接口的阻塞模式和非阻塞模式。零拷贝读接口支持阻塞模式和非阻塞模式,零拷贝写接口仅支持非阻塞模式。
- 为保证零拷贝接口性能正常,建议开启配置中的tso和lro选项。
业务适配
K-NET 零拷贝接口。以下为业务使用K-NET零拷贝读接口的伪代码。
#include "knet_socket_api.h" // 零初始化 knet_iovec struct knet_iovec iov[iovcnt] = {0}; // 调用零拷贝读接口读取数据 ssize_t ret = knet_zreadv(sockfd, iov, iovcnt); if (ret <= 0) { // 读接口返回值小于等于零表示失败,进行错误处理 } // 业务处理读取到的数据 process_data(iov, iovcnt, ret); // 业务处理完数据后,调用iov的free_cb对读缓冲区进行释放 for(int i = 0; i< iovcnt; ++i) { if (iov[i].free_cb != NULL) { iov[i].free_cb(iov[i].iov_base, iov[i].opaque); } }以下为业务使用K-NET零拷贝写接口的伪代码。
#include "knet_socket_api.h" // 零初始化 knet_iovec struct knet_iovec iov[iovcnt] = {0}; // 调用knet_mp_alloc申请写缓冲区,释放回调设置为knet_mp_free,knet_mp_free不需要额外的自定义参数opaque,可以设置为NULL for(int i = 0; i < iovcnt; ++i) { iov[i].iov_base = knet_mp_alloc(iovlen); assert(iov[i].iov_base != NULL); iov[i].iov_len = iovlen; iov[i].free_cb = knet_mp_free; iov[i].opaque = NULL; } // 往写缓冲区中填充数据 for (int i = 0; i < iovcnt; ++i) { fill_data(iov[i].iov_base, iov[i].iov_len); } // 调用零拷贝写接口发送数据 ssize_t ret = knet_zwritev(sockfd, iov, iovcnt); if (ret <= 0) { // 写接口返回值小于等于零表示失败,进行错误处理,需手动调用iov的free_cb来保证写缓冲区的正常释放 for(int i = 0; i < iovcnt; ++i) { if (iov[i].free_cb != NULL) { iov[i].free_cb(iov[i].iov_base, iov[i].opaque); } } } // 发送成功后无需手动释放写缓冲区以下为业务使用K-NET零拷贝写接口,并使用自定义的释放回调的伪代码。
#include "knet_socket_api.h" // 业务调用knet_mp_alloc申请写缓冲区,并将其添加到业务自定义的内存池中 void *buf = NULL; for (int i = 0; i < MP_MAX_NUM; ++i) { buf = knet_mp_alloc(MP_SIZE); assert(buf != NULL); MempoolPut(poolId, buf); // 用户自定义内存池的put函数 } void FreeCb(void *addr, void *poolId) // 自定义释放回调,将写缓冲区放回用户自建的内存池中 { MempoolPut(poolId, addr); // 用户自定义内存池的put函数 } // 调用用户自建内存池的申请函数申请写缓冲区,free_cb和opaque设置为合理的内容,用于将写缓冲区释放回用户自定义的内存池中 int cnt; for(cnt = 0; i < iovcnt; ++cnt) { iov[i].iov_base = MempoolGet(poolId, iovlen); // 用户自定义内存池的get函数 assert(iov[i].iov_base != NULL); iov[i].iov_len = iovlen; iov[i].free_cb = FreeCb; // 用户自定义释放回调 iov[i].opaque = poolId; // 释放回调所需的自定义参数 } // 往写缓冲区中填充数据 for (int i = 0; i < cnt; ++i) { fill_data(iov[i].iov_base, iov[i].iov_len); } // 调用零拷贝写接口发送数据 ssize_t ret = knet_zwritev(sockfd, iov, cnt); if (ret <= 0) { // 写接口返回值小于等于零表示失败,进行错误处理,需手动调用iov的free_cb来保证写缓冲区的正常释放 for(int i = 0; i< iovcnt; ++i) { if (iov[i].free_cb != NULL) { iov[i].free_cb(iov[i].iov_base, iov[i].opaque); } } } // 发送成功后无需手动释放写缓冲区
业务编译。
添加编译选项:指定头文件搜索路径与链接的库名称,以iPerf3为例。 在iPerf3源码根目录下的Makefile.am文件中加入以下编译选项。
libiperf_la_LIBADD = -lknet_frame AM_CPPFLAGS = -I/usr/include/knet编译构建业务,以iPerf3为例,在iperf3目录下执行以下命令。
sh bootstrap.sh ./configure; make
修改K-NET配置文件。 以iPerf3为例,假设发送包长为65535,每次发送的iovcnt为1,进行单条TCP连接的打流。
```bash vi /etc/knet/knet_comm.conf ```将“zcopy_enable”设置为1,使能零拷贝;开启tso、lro选项,保证def_sendbuf的值大于单次发送的数据总长度,在本示例中大于65535 * 1即可;确保“zcopy_sge_len”为写缓冲区iov的iov_len的最大值,本示例中配置为发送包长的大小,即65535;按照推荐公式计算“zcopy_sge_num”的值,本示例中低于最小值8192,故配置为8192,当发送包长很小时,此配置项需要比较大的配置。
```json { "version": "2.0.0", "common": { ... "zcopy_enable": 1 // 使能零拷贝 }, ... "hw_offload": { // 开启 tso,lro,tcp_checksum "tso": 1, "lro": 1, "tcp_checksum": 1, ... }, "proto_stack": { ... "def_sendbuf": 1048576, // 必须大于单次发送的数据总长度,即 iovlen * iovcnt ... "zcopy_sge_len": 65535, // 设置为发送包长的大小 "zcopy_sge_num": 8192 // 根据公式计算推荐值,最小值为8192 }, "dpdk": { ... "socket_mem": "--socket-mem=1024", // 确保内存足够 "socket_limit": "--socket-limit=1024", ... } } ```启动业务。
以iperf3为例。
iperf3 -s -4 -p 10001 --bind 192.168.1.10启动业务后可进行业务操作。
Demo示例
以Tperf为例,参考tperf_knet.patch使用示例使用。