【问题标题】:high performance udp server. blocking or non-blocking? c高性能 udp 服务器。阻塞还是非阻塞? C
【发布时间】:2023-06-12 23:45:01
【问题描述】:

我一直在阅读大量有关 udp 的阻塞与非阻塞套接字的内容,但我很难理解其中一个的优点。互联网上绝大多数的 cmets 似乎表明非阻塞更好,但对于它们在哪些情况下会更好,并且我没有找到关于何时首选阻塞的参考资料。我对这个问题的希望是社区可以对这个主题有所了解。

我自己的问题集的一些背景知识,以便可以将答案具体应用于问题的一般性质。我有一个我正在编写的 udp 服务器,它将在本地 lan 上有 40 个连接,因此将有恒定的数据流流入。数据速率平均约为 250MB/s,平均峰值为 500+Mb/s数据报大小约为 1400 字节。数据报的处理是轻量级的,但由于大量的 msgs 效率和性能是高优先级,以防止丢包。

由于我无法真正找到与此特定问题集类似的任何上下文信息,因此我不得不根据我收集到的有关阻塞与非阻塞的信息进行一些猜测。我将用我目前的假设来结束这个,然后将它开放给你的输入。基本上,由于这将是每个连接上几乎恒定的数据包流,我认为阻塞套接字会更可取,因为任何 recv 函数实际花费阻塞的时间与使用事件相比非常非常少基于模型的模型在异步模式下会有大量的触发器。我觉得我真正的问题集很可能是我计划用来从套接字读取的 40 个线程的优先级管理......确保每个线程都获得他们的 CPU 时间份额。我的方法和想法可能不正确,所以我希望并且如果社区可以帮助阐明这个问题,我将非常感激。

~编辑~

虽然我关心线程设计将如何影响/集成阻塞/非阻塞问题。我真的很关心从我的问题集的角度应该如何看待阻塞/非阻塞。如果线程确实成为一个问题,我可以使用线程池解决方案。

~edit2~

首先,我想说的是到目前为止的回复。你们中的一些人提到具有这么多套接字的单线程/套接字模型可能是一个坏主意,我承认我自己对解决方案是试探性的。然而,在 nikolai 的回复中的一个链接中,作者讨论了单线程/套接字模型并链接到一篇非常有趣的论文,我认为我会链接到这里,因为它消除了我持有的关于线程与事件的许多神话基于模型:why events are a bad idea

【问题讨论】:

  • 你在做netflow收集,不是吗? ;

标签: c sockets udp


【解决方案1】:

不是答案,只是一些链接,如果您的书签中还没有它们:

The C10K problem by Dan Kegel,
High-Performance Server Architecture by Jeff Darcy,
高级投票 API:epoll(4)kqueue(2)

编辑:

虽然听起来很愚蠢,但我完全想念您正在使用 UDP,所以......

由于 UDP 中没有协议级别的连接,除非您必须在不同的端口上工作,您不需要服务器上的 40 个套接字。只需 一个 UDP“服务器”套接字即可用于所有客户端。您可以随意阻塞这个套接字,只要确保套接字接收缓冲区足够大以适应流量峰值,并且不要花费太多时间处理每次读取。

【讨论】:

  • 您好,感谢 nikolai 的回复并感谢您提供的链接,现在将查看它们。我提到的 40 个不同的连接是一个 ip 40 端口。所以是的,不幸的是,我正在处理需要 40 个不同套接字的不同端口。
【解决方案2】:

我不知道阻塞或非阻塞有显着的性能优势;更多的是你的网络 I/O 事件循环想要做什么样的事情的问题:

  • 如果您的网络 I/O 线程要做的唯一事情是在单个套接字上侦听传入的 UDP 数据包,那么阻塞 I/O 可能会正常工作并且更容易编程。

  • 如果您的网络 I/O 线程需要处理多个套接字,那么阻塞 I/O 就会出现问题,因为如果它在套接字 A 上阻塞,它就不会被唤醒来处理到达的数据插座 B,反之亦然。在这种情况下,首选非阻塞 I/O,因为您可以在 select() 或 poll() 中进行阻塞,只要任何被监视的套接字上的数据可用,它们就会返回。

请注意,即使在非阻塞情况下,您也不希望在数据包之间进行繁忙循环,因为在线程 A 中消耗 CPU 周期意味着它们对线程 B 不可用,这会损害性能。因此,如果您没有在 recv() 中阻塞,请务必改为在 select() 或 poll() 中阻塞。

【讨论】:

  • 另外,如果您想尽量减少丢弃的传入 UDP 数据包,我建议您执行以下操作:(1) 在您的 UDP 接收器线程中尽可能少地执行操作。理想情况下,只需接收数据包,然后将它们添加到队列中以供另一个线程处理,然后尽快返回 select()/recv() (2) 以高于处理线程的优先级运行接收器线程 (3)使用 setsockopt(fd, SOL_SOCKET, SO_RCVBUF, ...) 使 UDP 套接字的传入数据缓冲区尽可能大。
  • 你应该改写你的答案。因为我很想投反对票,因为 select 或 poll 在阻塞 I/O 上效果很好(这正是它们的用途,一次阻塞多个句柄,如果有数据则解除阻塞)。
  • @kriss select()poll() 仅在您的线程能够调用它们时才有效,并且如果您的线程在某个地方被 recv() 阻塞,则它无法调用它们(或做任何事情否则)直到在该套接字上接收到一些数据之后,这可能需要很长时间。
  • 如果线程可以调用 recv() ,为什么不能调用 select() 或 poll() ?在单个 fd 上使用这些以确保可以在没有阻塞的情况下调用 recv() 或控制允许的超时,这是一个很常见的习惯用法。如果 poll() 没有检测到准备好在所需时间内读取的数据,我们就不会调用 recv()。
  • 或者您的意思是您正在考虑不检查哪个 fd 引发了 READ 事件? 是邪恶的。
【解决方案3】:
  • 当使用阻塞 IO 时,在您的程序中的某个时刻,您应该在任何文件句柄(在您的情况下为套接字)上进行轮询或选择等待数据。因为如果您在没有确保数据准备就绪的情况下读取任何 fh,它将阻塞并且程序将停止管理其他套接字。为了避免这种情况并保持程序简单,使用阻塞 IO 的程序通常为每个 socket/fh 编写一个线程,从而避免了轮询或选择的需要。

  • 如果您使用非阻塞 IO,您的程序将在每次读取时运行并检查数据到达。无需轮询或选择。该程序仍然可以非常简单,也无需为此特定目的使用线程。

我相信最有效的方法是使用 poll 或 select 来一次管理多个 IO(如果您愿意,它可以是在线程之间拆分的所有文件句柄的子集)。它比没有 poll 或 select 的非阻塞 IO 更有效,因为这种方法基本上大部分时间都在无用地尝试读取每个套接字,而且这是有代价的。这三者之间最糟糕的方法是使用阻塞 IO,每个线程有一个 fh,因为与读取返回的 WOULDBLOCK 或轮询相比,线程管理的成本很高。

这就是说,非阻塞IO还有一个好处:你的程序除了IO之外可能还有计算要做,而阻塞等待IO你就做不到了。这可能会导致将 poll/select 与非阻塞 IO 一起使用,或者在较小的超时下使用它,或者甚至使用专用于 IO 和其他线程的小型专用线程来处理计算密集型部分或程序。

在某些情况下,您可能也别无选择。我碰巧不得不等待来自通过 NFS 挂载的文件句柄的数据。在这种情况下尝试设置非阻塞 IO 是没有用的,因为 NFS 层在内部使用阻塞 IO...

您也可以考虑使用异步 IO。它非常有效,您的程序变成了“事件驱动”。对于windows系统来说这很常见,我没有看过Linux异步IO的当前开发状态​​。上次我检查了一些人在为内核 API 添加异步 IO,但我不知道它是否稳定或达到了主流内核。

【讨论】:

    【解决方案4】:

    我不太确定为 40 个套接字使用 40 个线程是否是一个好主意...当然,当您有少量套接字时,每个套接字使用一个线程是有意义的,但是拥有这么多线程只是要求线程饥饿、死锁和丢失数据包。

    至于阻塞与非阻塞,请记住阻塞的成本相对较高……尽管在我看来它更容易使用。总体而言,异步触发器等可能比阻塞/唤醒线程更快。

    【讨论】:

    • 虽然死锁不会成为问题,因为每个连接都将被独立处理,丢失的数据包和线程饥饿可能是一个问题。我更关心为什么阻塞更昂贵,特别是因为它适用于这个例子。如果通话很少被阻塞,我不确定这比一连串的事件通知更好。