测量Unix域套接字的延迟

11

我想比较两个进程之间使用Unix域套接字的性能与另一种IPC的性能。

我有一个基本程序,创建一个套接字对,然后调用fork。然后,它测量将8192字节发送到其他进程并返回的往返时间(每次迭代都不同)。

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, char **argv) {
    int i, pid, sockpair[2];
    char buf[8192];
    struct timespec tp1, tp2;

    assert(argc == 2);

    // Create a socket pair using Unix domain sockets with reliable,
    // in-order data transmission.
    socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair);

    // We then fork to create a child process and then start the benchmark.
    pid = fork();

    if (pid == 0) { // This is the child process.
        for (i = 0; i < atoi(argv[1]); i++) {
            assert(recv(sockpair[1], buf, sizeof(buf), 0) > 0);
            assert(send(sockpair[1], buf, sizeof(buf), 0) > 0);
        }
    } else { // This is the parent process.
        for (i = 0; i < atoi(argv[1]); i++) {
            memset(buf, i, sizeof(buf));
            buf[sizeof(buf) - 1] = '\0';
            assert(clock_gettime(CLOCK_REALTIME, &tp1) == 0);
            assert(send(sockpair[0], buf, sizeof(buf), 0) > 0);
            assert(recv(sockpair[0], buf, sizeof(buf), 0) > 0);
            assert(clock_gettime(CLOCK_REALTIME, &tp2) == 0);
            printf("%lu ns\n", tp2.tv_nsec - tp1.tv_nsec);
        }
    }

    return 0;
}

然而,我注意到对于每次重复测试,第一次运行的经过时间(i = 0)总是一个异常值:

79306 ns
18649 ns
19910 ns
19601 ns
...

我想知道内核在第一次调用send()时是否需要进行一些最终设置,例如,在内核中分配8192字节以缓冲在send()recv()之间的数据?


可能是这样,或者可能与调度程序中的时间有关。 - Barmar
1
另外,计算struct timespec差异时,您可能不想忘记tv_sec。当秒增加并且您将负纳秒差异格式化为%lu时,那将看起来像一个很大的数字。 - pilcrow
3个回答

2

这并不是数据复制需要80微秒,那样会非常慢(仅100MB/s),而是因为您正在使用两个进程。当父进程第一次发送数据时,这些数据需要等待子进程完成分叉并开始执行。

如果您一定要使用两个进程,您应该先在另一个方向上进行发送,以便父进程可以等待子进程准备好后再开始发送。

例如:

子进程:

  send();
  recv();
  send();

父级:

  recv();
  gettime();
  send();
  recv();
  gettime();

你需要明白,你的测试很大程度上取决于进程在各个CPU核心中的位置,如果在同一个核心上运行,会导致任务切换。

基于这个原因,我强烈建议你使用单个进程进行测量。即使没有轮询或其他任何东西,只要保持相对较小的块,适合套接字缓冲区,你就可以这样做:

gettime();
send();
recv();
gettime();

为确保分配了缓冲区,您应首先执行非测量的往返。我相信你在这里会得到更小的时间。


1
我猜测涉及内核代码的指令缓存错过是第一次执行变慢的主要原因,可能还包括内核数据结构的数据缓存错过来跟踪操作。
懒惰设置也是一种可能性。
您可以尝试在测试之间(包括第一次测试之前)进行sleep(10)。在每次测试之间做一些将使用所有CPU缓存的事情,例如刷新网页。 如果是懒惰设置,则第一次调用会特别慢。否则,当缓存未被使用时,所有调用都将同样缓慢。

1
在Linux内核中,您可以找到___sys_sendmsg函数,该函数被send使用。请查看此处以查看代码。
该函数必须将用户消息(在您的情况下为8KB的buf)从用户空间复制到内核空间。之后,recv可以将接收到的消息从子进程的用户空间复制回内核空间。
这意味着您需要对send()recv()成对操作进行2次memcpy和一次kmalloc
第一个memcpy很特殊,因为未分配用于存储用户消息的空间。这也意味着数据缓存中也没有该空间。因此,第一个send() - recv()对将分配用于存储buf的内核内存,并且该内存还将被缓存。随后的调用将只是使用函数原型中的used_address参数使用该内存。

所以你的假设是正确的。第一次运行在内核中分配了8KB,并使用冷缓存,而其他运行则只使用先前分配和缓存的数据。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接