【发布时间】:2019-02-08 17:14:19
【问题描述】:
我一直在分析 TCP 延迟(特别是小消息从用户空间到内核空间的write),以便对write 的延迟有一些直觉(承认这可能是上下文-具体的)。我注意到在我看来相似的测试之间存在很大的不一致,我很想知道差异来自哪里。我知道微基准测试可能会出现问题,但我仍然觉得我缺少一些基本的理解(因为延迟差异约为 10 倍)。
设置是我有一个 C++ TCP 服务器,它接受一个客户端连接(来自同一 CPU 上的另一个进程),并且在与客户端连接时,对套接字进行 20 次系统调用 write,发送一个字节一次。服务器的完整代码复制在本文末尾。这是使用boost/timer 对每个write 计时的输出(这会增加约1 个麦克风的噪音):
$ clang++ -std=c++11 -stdlib=libc++ tcpServerStove.cpp -O3; ./a.out
18 mics
3 mics
3 mics
4 mics
3 mics
3 mics
4 mics
3 mics
5 mics
3 mics
...
我确实发现第一个write 比其他的慢得多。如果我将 10,000 个 write 呼叫包装在一个计时器中,则平均每个 write 需要 2 微秒,但第一个呼叫始终是 15 个以上的麦克风。为什么会出现这种“升温”现象?
与此相关,我进行了一个实验,在每个 write 调用之间,我做了一些阻塞 CPU 工作(计算一个大素数)。这会导致 所有write 调用变慢:
$ clang++ -std=c++11 -stdlib=libc++ tcpServerStove.cpp -O3; ./a.out
20 mics
23 mics
23 mics
30 mics
23 mics
21 mics
21 mics
22 mics
22 mics
...
鉴于这些结果,我想知道在将字节从用户缓冲区复制到内核缓冲区的过程中是否发生了某种批处理。如果多个write 调用快速连续发生,它们是否会合并为一个内核中断?
我特别想知道write 将缓冲区从用户空间复制到内核空间需要多长时间。如果在我连续 10,000 次时,有某种合并效应允许平均 write 只占用 2 个麦克风,那么得出 write 延迟为 2 个麦克风的结论是不公平的乐观;看来我的直觉应该是每个write 需要20 微秒。对于您可以获得的最低延迟(一个字节上的原始write 调用),这似乎非常慢。
最后一条数据是,当我在我的计算机上的两个进程(一个 TCP 服务器和一个 TCP 客户端)之间设置一个乒乓测试时,我平均每次往返 6 个麦克风(其中包括一个read, write,以及通过 localhost 网络移动)。这似乎与上面看到的单次写入的 20 个麦克风延迟不一致。
TCP 服务器的完整代码:
// Server side C/C++ program to demonstrate Socket programming
// #include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <boost/timer.hpp>
#include <unistd.h>
// Set up some blocking work.
bool isPrime(int n) {
if (n < 2) {
return false;
}
for (int i = 2; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
// Compute the nth largest prime. Takes ~1 sec for n = 10,000
int getPrime(int n) {
int numPrimes = 0;
int i = 0;
while (true) {
if (isPrime(i)) {
numPrimes++;
if (numPrimes >= n) {
return i;
}
}
i++;
}
}
int main(int argc, char const *argv[])
{
int server_fd, new_socket, valread;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
// Create socket for TCP server
server_fd = socket(AF_INET, SOCK_STREAM, 0);
// Prevent writes from being batched
setsockopt(server_fd, SOL_SOCKET, TCP_NODELAY, &opt, sizeof(opt));
setsockopt(server_fd, SOL_SOCKET, TCP_NOPUSH, &opt, sizeof(opt));
setsockopt(server_fd, SOL_SOCKET, SO_SNDBUF, &opt, sizeof(opt));
setsockopt(server_fd, SOL_SOCKET, SO_SNDLOWAT, &opt, sizeof(opt));
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
// Accept one client connection
new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
char sendBuffer[1] = {0};
int primes[20] = {0};
// Make 20 sequential writes to kernel buffer.
for (int i = 0; i < 20; i++) {
sendBuffer[0] = i;
boost::timer t;
write(new_socket, sendBuffer, 1);
printf("%d mics\n", int(1e6 * t.elapsed()));
// For some reason, doing some blocking work between the writes
// The following work slows down the writes by a factor of 10.
// primes[i] = getPrime(10000 + i);
}
// Print a prime to make sure the compiler doesn't optimize
// away the computations.
printf("prime: %d\n", primes[8]);
}
TCP 客户端代码:
// Server side C/C++ program to demonstrate Socket programming
// #include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int sock, valread;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
// We'll be passing uint32's back and forth
unsigned char recv_buffer[1024] = {0};
// Create socket for TCP server
sock = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(sock, SOL_SOCKET, TCP_NODELAY, &opt, sizeof(opt));
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// Accept one client connection
if (connect(sock, (struct sockaddr *)&address, (socklen_t)addrlen) != 0) {
throw("connect failed");
}
read(sock, buffer_pointer, num_left);
for (int i = 0; i < 10; i++) {
printf("%d\n", recv_buffer[i]);
}
}
我尝试使用和不使用标志 TCP_NODELAY、TCP_NOPUSH、SO_SNDBUF 和 SO_SNDLOWAT,并认为这可能会阻止批处理(但我的理解是这种批处理发生在内核缓冲区和网络之间,而不是在用户缓冲区和内核缓冲区之间)。
这是乒乓球测试的服务器代码:
// Server side C/C++ program to demonstrate Socket programming
// #include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <boost/timer.hpp>
#include <unistd.h>
__inline__ uint64_t rdtsc(void)
{
uint32_t lo, hi;
__asm__ __volatile__ (
"xorl %%eax,%%eax \n cpuid"
::: "%rax", "%rbx", "%rcx", "%rdx");
__asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
return (uint64_t)hi << 32 | lo;
}
// Big Endian (network order)
unsigned int fromBytes(unsigned char b[4]) {
return b[3] | b[2]<<8 | b[1]<<16 | b[0]<<24;
}
void toBytes(unsigned int x, unsigned char (&b)[4]) {
b[3] = x;
b[2] = x>>8;
b[1] = x>>16;
b[0] = x>>24;
}
int main(int argc, char const *argv[])
{
int server_fd, new_socket, valread;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
unsigned char recv_buffer[4] = {0};
unsigned char send_buffer[4] = {0};
// Create socket for TCP server
server_fd = socket(AF_INET, SOCK_STREAM, 0);
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
// Accept one client connection
new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
printf("Connected with client!\n");
int counter = 0;
unsigned int x = 0;
auto start = rdtsc();
boost::timer t;
int n = 10000;
while (counter < n) {
valread = read(new_socket, recv_buffer, 4);
x = fromBytes(recv_buffer);
toBytes(x+1, send_buffer);
write(new_socket, send_buffer, 4);
++counter;
}
printf("%f clock cycles per round trip (rdtsc)\n", (rdtsc() - start) / double(n));
printf("%f mics per round trip (boost timer)\n", 1e6 * t.elapsed() / n);
}
这是乒乓测试的客户端代码:
// #include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>
#include <boost/timer.hpp>
#include <unistd.h>
// Big Endian (network order)
unsigned int fromBytes(unsigned char b[4]) {
return b[3] | b[2]<<8 | b[1]<<16 | b[0]<<24;
}
void toBytes(unsigned int x, unsigned char (&b)[4]) {
b[3] = x;
b[2] = x>>8;
b[1] = x>>16;
b[0] = x>>24;
}
int main(int argc, char const *argv[])
{
int sock, valread;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
// We'll be passing uint32's back and forth
unsigned char recv_buffer[4] = {0};
unsigned char send_buffer[4] = {0};
// Create socket for TCP server
sock = socket(AF_INET, SOCK_STREAM, 0);
// Set TCP_NODELAY so that writes won't be batched
setsockopt(sock, SOL_SOCKET, TCP_NODELAY, &opt, sizeof(opt));
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// Accept one client connection
if (connect(sock, (struct sockaddr *)&address, (socklen_t)addrlen) != 0) {
throw("connect failed");
}
unsigned int lastReceived = 0;
while (true) {
toBytes(++lastReceived, send_buffer);
write(sock, send_buffer, 4);
valread = read(sock, recv_buffer, 4);
lastReceived = fromBytes(recv_buffer);
}
}
【问题讨论】:
-
旁注: 在 linux 下,您可以使用
PACKET_MMAP将内核缓冲区直接映射到用户空间内存。请参阅:kernel.org/doc/Documentation/networking/packet_mmap.txt IIRC,缺点是您必须自己处理数据包级别(即PF_PACKET) -
您可以将时间与写入
/dev/null、管道、ramdisk 上的文件等进行比较,以查看用户空间到内核的复制需要多长时间(应该大致相当于@ 987654355@) -
嘿克雷格,感谢您的提示。我对 PACKET_MMAP 的理解是否正确,它处于原始数据包的级别,所以我需要在它之上实现 TCP? (例如重新排序消息、检查数据丢失、解码标头等?)
-
是的。您将获得以太网标头、IP 标头,然后是 TCP 和/或 UDP 标头 [取决于],您必须解码/剥离它们。这不是太难。我最近不得不在商业产品中这样做,因为发送者的低级硬件只是盲目地爆出数据包,所以我不得不这样做。为了提高速度,我准备做
PACKET_MMAP,但是在基准测试之后,使用recv就足够了。仅供参考,PF_PACKET是tcpdump和libpcap使用的。见:blog.cloudflare.com/kernel-bypassstackoverflow.com/questions/18343365/… -
你有 Spectre/Meltdown 更新吗?如果您的主要测试运行时间足够长以进行上下文切换,您可能会在代码和缓冲区上获得额外的 TLB 和缓存刷新。
标签: c performance tcp linux-kernel