一种在 C 中非常有效(快速)的网络轮询的方法

A method for very efficient (fast) network polling in C

精简版: 我不想使用第三方库或框架,如 Netmap 或 DPDK,有没有比 poll() 或 [=13= 更快的东西],或者我可以提高这些调用的效率吗?

完整版:我有一个单线程应用程序,它使用单个套接字尽可能快地发送数据,我正在使用 valgrind/cachegrind/callgrind 来尝试增加效率(以及应用程序的吞吐量)。

目前,接收主机站点处于无限循环中,试图尽快检查接收到的数据(这需要是非阻塞的,当没有数据包要处理时 "other stuff" 已完成)。我在接收主机上使用 select(),因为 select() 使用提供微秒轮询频率的时间间隔,而 poll() 使用毫秒值。 Callgrind 向我展示 FD_SET() 执行的指令几乎是 select() 操作的两倍,select() 的一个缺点是我必须 运行 FD_SET()接收循环的每次迭代:

         .    .    .          .     .    .          .     .    .              // Poll for incoming frames
 4,361,236    1    1  2,180,618     0    0  2,180,618     0    0              TEST_INTERFACE->TV_SELECT_DELAY.tv_sec = 0;
 4,361,236    0    0  2,180,618     0    0  2,180,618     0    0              TEST_INTERFACE->TV_SELECT_DELAY.tv_usec = 000000;
56,696,068    2    2 15,264,326     0    0  2,180,618     0    0              FD_SET(TEST_INTERFACE->SOCKET_FD, &TEST_INTERFACE->FD_READS);
         .    .    .          .     .    .          .     .    .  
         .    .    .          .     .    .          .     .    .              TEST_INTERFACE->SELECT_RET_VAL = select(TEST_INTERFACE->SOCKET_FD_COUNT,
         .    .    .          .     .    .          .     .    .                                                      &TEST_INTERFACE->FD_READS,
         .    .    .          .     .    .          .     .    .                                                      NULL, NULL,
30,528,652    1    1 10,903,090     0    0 15,264,326     0    0                                                      &TEST_INTERFACE->TV_SELECT_DELAY);
         .    .    .          .     .    .          .     .    .  
         .    .    .          .     .    .          .     .    .  
 9,432,052    0    0  4,361,236     0    0          0     0    0              if (TEST_INTERFACE->SELECT_RET_VAL > 0 &&
 7,450,590    1    1  2,128,740     0    0          0     0    0                  FD_ISSET(TEST_INTERFACE->SOCKET_FD, &TEST_INTERFACE->FD_READS))
         .    .    .          .     .    .          .     .    .              {
         .    .    .          .     .    .          .     .    .  
         .    .    .          .     .    .          .     .    .                  RX_LEN = recvfrom(TEST_INTERFACE->SOCKET_FD,
         .    .    .          .     .    .          .     .    .                                    FRAME_HEADERS->RX_BUFFER,
         .    .    .          .     .    .          .     .    .                                    TEST_PARAMS->F_SIZE_TOTAL,
 5,321,850    1    1  2,128,740     0    0  2,838,320     0    0                                    0, NULL, NULL);
         .    .    .          .     .    .          .     .    .  

我使用 select() 获得平均 130-140Mpbs 的接收吞吐量。使用 poll() 我得到平均 150-160Mbps。

         .    .    .         .     .    .         .     .    .              // Poll for incoming frames
7,347,032    2    2 1,836,758     0    0 4,591,895     0    0  TEST_INTERFACE->SELECT_RET_VAL = poll(TEST_INTERFACE->fds, 1, 0);
         .    .    .         .     .    .         .     .    .  
         .    .    .         .     .    .         .     .    .  
 3,673,516    0    0 1,836,758     0    0         0     0    0  if (TEST_INTERFACE->SELECT_RET_VAL > 0)
         .    .    .         .     .    .         .     .    .  {
         .    .    .         .     .    .         .     .    .  
 2,142,186    0    0   714,062     0    0         0     0    0      if ( TEST_INTERFACE->fds[0].revents & POLLIN )
   714,062    0    0   357,031     0    0   357,031     0    0           TEST_INTERFACE->fds[0].revents = 0;
         .    .    .         .     .    .         .     .    .  
         .    .    .         .     .    .         .     .    .                  RX_LEN = recvfrom(TEST_INTERFACE->SOCKET_FD,
         .    .    .         .     .    .         .     .    .                                    FRAME_HEADERS->RX_BUFFER,
         .    .    .         .     .    .         .     .    .                                    TEST_PARAMS->F_SIZE_TOTAL,
 5,355,465    1    1 2,142,186     0    0 2,856,248     0    0                                    0, NULL, NULL);

使用 poll() 我传递了 0 的超时值所以希望它在任何时候都不会阻塞,但我不确定这是否正确并且它实际上比select().

在实验室中,我有几台带有 10Gbps NIC 的背对背服务器。使用 select() 和 Tx 主机以 10Gbps 的线路速率发送数据,Rx 主机可以处理大约 9.5Gbps 的流量。上面的结果只是在我的笔记本电脑上的虚拟机之间(实验室目前停止运行)以表明 poll() 使我的 Rx 主机略有增加。在上面的结果中,我们可以看到 poll() 更快,但是 10Gbps 是每几十纳秒一个数据包,所以我不确定(当实验室再次工作时)我是否会看到任何改进。

所以我想我有两个问题:

  1. 在我的 poll()select() 测试中,因为我使用的是单个套接字 poll() 稍快一些,但是这是否适用于更快的连接,10 /20/30/40Gbps,或者以这些速度 select() 会更快吗?

  2. 因为我只使用一个插座,如果我从select()/poll()切换到epoll(),速度会提高吗?或者我可以使用其他一些“内置”方法,它比 poll()select() 更快,或者我可以做些什么来减少它们的执行时间?

如果 select 的 fd(s) 不改变,您可以(通常)作弊并只使用预先计算的 int。

int   fds = 1 << fd;
int   fdp;

 do {
     fdp = fds;
     n = select(fd+1, &fdp, ...

如果 FD_SET 是瓶颈,这应该会有所帮助。

select 的低效之处之一是复制内核 space 的大 fd_sets in/out——确保 fd 足够低以适合 int 有帮助。


我假设线程已关闭 table。

您还可以通过使用非阻塞套接字获得一些帮助,并且只在它说 EAGAIN 时调用 poll/select(并且您有 none 个 "other work" 要做)。