sendto
。整个环形缓冲区的内容都会被发送到套接字上,这应该比在内存中使用缓冲区并为需要发送的缓冲区中的每个帧重复调用sendto
来获得更高的性能。当不使用PACKET_MMAP时,在调用
sendto
时,单个帧从用户空间内存中的缓冲区复制到内核内存中的SK buf,然后内核必须将数据包复制到NIC进行DMA访问的内存中,并向NIC发出信号以将帧传输到自己的硬件缓冲区并排队进行传输。当使用PACKET_MMAP套接字选项时,应用程序分配了mmapped内存并将其链接到原始套接字。应用程序将数据包放入mmapped缓冲区,调用sendto
,内核无需将数据包复制到SK buf中即可直接从mmapped缓冲区读取它们。此外,可以从环形缓冲区中读取“块”而不是单个数据包/帧。因此,性能提高了一次系统调用以复制多个帧,并且每个帧都少了一个复制操作,以将其放入NIC硬件缓冲区中。当我比较使用PACKET_MMAP的套接字和“普通”套接字(一个带有单个数据包的char缓冲区)的性能时,根本没有任何性能优势。为什么会这样?在Tx模式下使用PACKET_MMAP时,每个环块只能放入一个帧(而不是像Rx模式一样每个环块可以放入多个帧),但我正在创建256个块,所以我们应该在一个单独的sendto调用中发送256个帧,对吗?
使用PACKET_MMAP的性能,main()调用packet_tx_mmap():
bensley@ubuntu-laptop:~/C/etherate10+$ sudo taskset -c 1 ./etherate_mt -I 1
Using inteface lo (1)
Running in Tx mode
1. Rx Gbps 0.00 (0) pps 0 Tx Gbps 17.65 (2206128128) pps 1457152
2. Rx Gbps 0.00 (0) pps 0 Tx Gbps 19.08 (2385579520) pps 1575680
3. Rx Gbps 0.00 (0) pps 0 Tx Gbps 19.28 (2409609728) pps 1591552
4. Rx Gbps 0.00 (0) pps 0 Tx Gbps 19.31 (2414260736) pps 1594624
5. Rx Gbps 0.00 (0) pps 0 Tx Gbps 19.30 (2411935232) pps 1593088
没有使用PACKET_MMAP的性能,main()
调用 packet_tx()
:
bensley@ubuntu-laptop:~/C/etherate10+$ sudo taskset -c 1 ./etherate_mt -I 1
Using inteface lo (1)
Running in Tx mode
1. Rx Gbps 0.00 (0) pps 0 Tx Gbps 18.44 (2305001412) pps 1522458
2. Rx Gbps 0.00 (0) pps 0 Tx Gbps 20.30 (2537520018) pps 1676037
3. Rx Gbps 0.00 (0) pps 0 Tx Gbps 20.29 (2535744096) pps 1674864
4. Rx Gbps 0.00 (0) pps 0 Tx Gbps 20.26 (2533014354) pps 1673061
5. Rx Gbps 0.00 (0) pps 0 Tx Gbps 20.32 (2539476106) pps 1677329
packet_tx()
函数似乎比 packet_tx_mmap()
函数稍微快一点,但也稍微短一些,所以我认为最小的性能提升仅仅是在 packet_tx
中存在较少的代码行数。因此,在我的看来,这两个函数的性能几乎相同,为什么呢?按照我的理解,PACKET_MMAP 应该更快,因为它应该有更少的系统调用和复制操作。
void *packet_tx_mmap(void* thd_opt_p) {
struct thd_opt *thd_opt = thd_opt_p;
int32_t sock_fd = setup_socket_mmap(thd_opt_p);
if (sock_fd == EXIT_FAILURE) exit(EXIT_FAILURE);
struct tpacket2_hdr *hdr;
uint8_t *data;
int32_t send_ret = 0;
uint16_t i;
while(1) {
for (i = 0; i < thd_opt->tpacket_req.tp_frame_nr; i += 1) {
hdr = (void*)(thd_opt->mmap_buf + (thd_opt->tpacket_req.tp_frame_size * i));
data = (uint8_t*)(hdr + TPACKET_ALIGN(TPACKET2_HDRLEN));
memcpy(data, thd_opt->tx_buffer, thd_opt->frame_size);
hdr->tp_len = thd_opt->frame_size;
hdr->tp_status = TP_STATUS_SEND_REQUEST;
}
send_ret = sendto(sock_fd, NULL, 0, 0, NULL, 0);
if (send_ret == -1) {
perror("sendto error");
exit(EXIT_FAILURE);
}
thd_opt->tx_pkts += thd_opt->tpacket_req.tp_frame_nr;
thd_opt->tx_bytes += send_ret;
}
return NULL;
}
请注意下面的函数调用了
setup_socket()
而不是setup_socket_mmap()
:void *packet_tx(void* thd_opt_p) {
struct thd_opt *thd_opt = thd_opt_p;
int32_t sock_fd = setup_socket(thd_opt_p);
if (sock_fd == EXIT_FAILURE) {
printf("Can't create socket!\n");
exit(EXIT_FAILURE);
}
while(1) {
thd_opt->tx_bytes += sendto(sock_fd, thd_opt->tx_buffer,
thd_opt->frame_size, 0,
(struct sockaddr*)&thd_opt->bind_addr,
sizeof(thd_opt->bind_addr));
thd_opt->tx_pkts += 1;
}
}
下面是套接字设置函数的唯一区别,但本质上它们都需要设置SOCKET_RX_RING或SOCKET_TX_RING:
// Set the TPACKET version, v2 for Tx and v3 for Rx
// (v2 supports packet level send(), v3 supports block level read())
int32_t sock_pkt_ver = -1;
if(thd_opt->sk_mode == SKT_TX) {
static const int32_t sock_ver = TPACKET_V2;
sock_pkt_ver = setsockopt(sock_fd, SOL_PACKET, PACKET_VERSION, &sock_ver, sizeof(sock_ver));
} else {
static const int32_t sock_ver = TPACKET_V3;
sock_pkt_ver = setsockopt(sock_fd, SOL_PACKET, PACKET_VERSION, &sock_ver, sizeof(sock_ver));
}
if (sock_pkt_ver < 0) {
perror("Can't set socket packet version");
return EXIT_FAILURE;
}
memset(&thd_opt->tpacket_req, 0, sizeof(struct tpacket_req));
memset(&thd_opt->tpacket_req3, 0, sizeof(struct tpacket_req3));
//thd_opt->block_sz = 4096; // These are set else where
//thd_opt->block_nr = 256;
//thd_opt->block_frame_sz = 4096;
int32_t sock_mmap_ring = -1;
if (thd_opt->sk_mode == SKT_TX) {
thd_opt->tpacket_req.tp_block_size = thd_opt->block_sz;
thd_opt->tpacket_req.tp_frame_size = thd_opt->block_sz;
thd_opt->tpacket_req.tp_block_nr = thd_opt->block_nr;
// Allocate per-frame blocks in Tx mode (TPACKET_V2)
thd_opt->tpacket_req.tp_frame_nr = thd_opt->block_nr;
sock_mmap_ring = setsockopt(sock_fd, SOL_PACKET , PACKET_TX_RING , (void*)&thd_opt->tpacket_req , sizeof(struct tpacket_req));
} else {
thd_opt->tpacket_req3.tp_block_size = thd_opt->block_sz;
thd_opt->tpacket_req3.tp_frame_size = thd_opt->block_frame_sz;
thd_opt->tpacket_req3.tp_block_nr = thd_opt->block_nr;
thd_opt->tpacket_req3.tp_frame_nr = (thd_opt->block_sz * thd_opt->block_nr) / thd_opt->block_frame_sz;
thd_opt->tpacket_req3.tp_retire_blk_tov = 1;
thd_opt->tpacket_req3.tp_feature_req_word = 0;
sock_mmap_ring = setsockopt(sock_fd, SOL_PACKET , PACKET_RX_RING , (void*)&thd_opt->tpacket_req3 , sizeof(thd_opt->tpacket_req3));
}
if (sock_mmap_ring == -1) {
perror("Can't enable Tx/Rx ring for socket");
return EXIT_FAILURE;
}
thd_opt->mmap_buf = NULL;
thd_opt->rd = NULL;
if (thd_opt->sk_mode == SKT_TX) {
thd_opt->mmap_buf = mmap(NULL, (thd_opt->block_sz * thd_opt->block_nr), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED | MAP_POPULATE, sock_fd, 0);
if (thd_opt->mmap_buf == MAP_FAILED) {
perror("mmap failed");
return EXIT_FAILURE;
}
} else {
thd_opt->mmap_buf = mmap(NULL, (thd_opt->block_sz * thd_opt->block_nr), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED | MAP_POPULATE, sock_fd, 0);
if (thd_opt->mmap_buf == MAP_FAILED) {
perror("mmap failed");
return EXIT_FAILURE;
}
// Per bock rings in Rx mode (TPACKET_V3)
thd_opt->rd = (struct iovec*)calloc(thd_opt->tpacket_req3.tp_block_nr * sizeof(struct iovec), 1);
for (uint16_t i = 0; i < thd_opt->tpacket_req3.tp_block_nr; ++i) {
thd_opt->rd[i].iov_base = thd_opt->mmap_buf + (i * thd_opt->tpacket_req3.tp_block_size);
thd_opt->rd[i].iov_len = thd_opt->tpacket_req3.tp_block_size;
}
}
更新1:对物理接口的测试结果
有人提到我使用PACKET_MMAP时没有看到性能差异的原因可能是因为我向环回接口发送流量(首先,它没有QDISC)。由于运行packet_tx_mmap()
或packet_tx()
中的任一例程都可以生成超过10Gbps的速度,而我只有两个10Gbps的接口可用,因此我将它们绑定在一起,并得到了以下结果,这几乎与上面的结果相同,两个函数之间的速度差异很小:
packet_tx()
发送至20G bond0
- 1线程:平均10.77Gbps / 889kfps~
- 2线程:平均19.19Gbps / 1.58Mfps~
- 3线程:平均19.67Gbps / 1.62Mfps~(这是bond能够达到的最快速度)
packet_tx_mmap()
发送至20G bond0:
- 1线程:平均11.08Gbps / 913kfps~
- 2线程:平均19.0Gbps / 1.57Mfps~
- 3线程:平均19.66Gbps / 1.62Mfps~(这是连接速度的最大值)
上述所有测试中,帧大小为1514字节(与上面的原始环回测试保持一致)。
在以上所有测试中,软中断的数量大致相同(使用this script进行测量)。当一个线程运行packet_tx()
时,每秒有大约40k个中断在CPU核心上。当运行2和3个线程时,分别在2和3个核心上有40k。使用packet_tx_mmap()
时的结果相同。单个线程在一个CPU核心上的软中断约为40k。运行2和3个线程时,每个核心有40k个中断。
更新2:完整源代码
我已经上传了完整的源代码,我还在编写这个应用程序,所以它可能有很多缺陷,但它们超出了本问题的范围: https://github.com/jwbensley/EtherateMT
packet_tx_mmap
函数应该与内核共享缓冲区,这意味着在单个sendto()
系统调用中从用户空间复制多个数据包到内核空间,所以向回路接口发送流量意味着我们专门测试该方面,而不用担心将数据包DMA到NIC上,因为这对于packet_tx
和packet_tx_mmap
来说都是相同的处理过程,因为它们都在内核栈中处于较低层。 - jwbensleymemcpy
。 - kfx