重复的recv()调用是否昂贵?

时间:2011-02-24 10:20:16

标签: c sockets tcp

我对我经常遇到的情况有疑问。我不时要实现各种基于TCP的协议。它们中的大多数定义了以公共头([包ID,长度,有效载荷]或类似的东西)开头的可变长度数据包。显然,可以有两种方法来读取这些数据包:

  1. 读取标头(因为标头长度通常是固定的),提取有效负载长度,读取有效负载
  2. 读取所有可用数据并将其存储在缓冲区中;之后解析缓冲区
  3. 显然,第一种方法很简单,但需要两次调用read()(或者更多)。第二个稍微复杂一点,但需要较少的电话。

    问题是:第一种方法是否会严重影响性能以至于担心它?

5 个答案:

答案 0 :(得分:9)

是的,与内存副本相比,系统调用通常很昂贵。恕我直言,在x86架构上尤其如此,并且可以在RISC机器(arm,mips,......)上论证。

老实说,除非你必须每秒处理数百或数千个请求,否则你几乎不会注意到差异。

根据协议的具体情况,混合方法可能是最好的。当协议使用大量小数据包而不是大数据包时,您可以读取标头和部分数据。当它是一个小数据包时,你通过避免一个大的memcpy获胜,当数据包很大时,你只通过发出第二个系统调用就赢了。

答案 1 :(得分:4)

如果您的应用程序是一个能够同时处理多个客户端的服务器,并且非阻塞套接字用于在一个线程中处理多个客户端,那么您只能在套接字准备就绪时只发出一个recv()系统调用读取。

原因是如果你继续在循环中调用recv()并且客户端发送大量数据,那么可能发生的事情是你的recv()循环可能会长时间阻止线程做其他事情。例如,recv()从套接字读取一些数据,确定缓冲区中现在有一条完整的消息,并将该消息转发给回调。回调以某种方式处理消息并返回。如果再次调用recv(),则在回调处理上一条消息时可能会有更多消息到达。这导致一个套接字上的繁忙recv()循环,阻止线程处理任何其他挂起事件。

如果应用程序中的套接字读取缓冲区小于内核套接字接收缓冲区,则会加剧此问题。换句话说,在一次recv()调用中无法读取内核接收缓冲区的全部内容。轶事证据是,当一个2Mb内核套接字接收缓冲区有一个16Kb的用户空间缓冲区时,我在繁忙的生产系统上遇到了这个问题。连续发送许多消息的客户端会阻塞该recv()循环中的线程几分钟,因为在处理刚刚读取的消息时会有更多消息到达,从而导致服务中断。

在这种事件驱动的体系结构中,最好让用户空间读缓冲区等于内核套接字接收缓冲区的大小(或最大的消息大小,以较大者为准),以便所有可用的数据可以在一次recv()调用中读取内核缓冲区。这通过执行一次recv()调用,处理用户空间读取缓冲区中的所有完整消息然后将控制返回到事件循环来工作。这样,与大量传入数据的连接不会阻止线程处理其他事件和连接,而是循环处理所有可用传入数据的连接。

答案 2 :(得分:3)

获得答案的最佳方法是衡量。 strace程序适合测量系统调用时间。使用它本身会增加很多开销,但是如果你只是为了这个目的比较一个recv的成本和两个的成本,它应该是合理有意义的。使用-tt选项获取时间。或者您可以使用-c选项来概述花费在其上的系统调用所花费的时间。

更好的衡量方法是oprofile

另请注意,如果您确定缓冲是值得的,您可以使用fdopen和stdio函数来为您处理。这非常简单,如果您只处理单个连接或者每个连接有一个线程/进程,但如果您想使用select / {{ 1}} - 基于模型。

答案 3 :(得分:1)

请注意,您通常必须“将所有可用数据读入缓冲区并随后对其进行处理”,以便考虑recv()调用仅返回部分标题的(不太可能但可能)的情况 - 所以你不妨去吃整个猪并使用选项2.

答案 4 :(得分:-2)

是的,根据情况,read / recv调用可能很昂贵。例如,如果您发出大量的recv()调用以在每个小间隔内读取非常少量的数据,那么这将是性能损失。在这种情况下,您可以发出一个带有相当大缓冲区的recv(),比方说4k,然后解析该4k缓冲区。它可能包含多个标头+数据组合。通过首先读取标题,您可以找到数据及其长度。为了避免将数据的mem副本放入新的缓冲区,您可以只使用实际数据开始的偏移量,并存储该指针。