使用管道,tee()和splice()将数据发送到多个套接字

时间:2013-01-07 06:04:18

标签: c linux pipe zero-copy

我正在使用tee()复制一个“master”管道,使用splice()写入多个套接字。当然,这些管道将以不同的速率清空,具体取决于我可以拼接()到目标套接字的程度。因此,当我接下来将数据添加到“主”管道然后再次发送()时,我可能会遇到这样的情况,即我可以将64KB写入管道,但只有4KB到“从”管道之一。我猜测如果我将所有“主”管道拼接到套接字,我将永远无法将剩余的60KB传送给该管道。真的吗?我想我可以跟踪一个tee_offset(从0开始),我设置为“unteed”数据的开头,然后不要拼接()过去。因此,在这种情况下,我将tee_offset设置为4096并且不会拼接更多,直到我能够将其发送给所有其他管道。我在这里走在正确的轨道上吗?对我有任何提示/警告吗?

1 个答案:

答案 0 :(得分:21)

如果我理解正确,您可以获得一些想要多路复用到多个套接字的实时数据源。你有一个单一的来源"管道连接到产生你的数据的任何东西,你已经有一个"目的地"您希望在其上发送数据的每个套接字的管道。您正在做的是使用tee()将数据从源管道复制到每个目标管道,并splice()将其从目标管道复制到套接字本身。

你要点击的根本问题是,如果其中一个插座根本无法跟上 - 如果你生成数据的速度超过发送速度,那么你就可以了。会有问题。这与您使用管道无关,这只是一个根本问题。所以,你想要选择一个策略来应对这种情况 - 我建议你处理这个问题,即使你不希望这个问题很常见,因为这些事情经常会让你后来咬人。您的基本选择是关闭违规套接字,或者跳过数据直到它清除其输出缓冲区 - 例如,后一种选择可能更适合音频/视频流。

与管道使用相关的问题是,在Linux上,管道缓冲区的大小有些不灵活。自Linux 2.6.11(2.6.17中添加了tee()调用)以来默认为64K - 请参阅pipe manpage。自2.6.35起,此值可以通过F_SETPIPE_SZ选项更改为fcntl()(请参阅fcntl manpage),直至/proc/sys/fs/pipe-size-max指定的限制,但缓冲仍然更多在用户空间中动态分配方案的按需改变是不方便的。这意味着您应对慢速套接字的能力将受到一定限制 - 这是否可接受取决于您希望接收和能够发送数据的速率。

假设这种缓冲策略是可以接受的,那么您可以假设您需要跟踪每个目标管道从源中消耗了多少数据,并且它只能安全​​丢弃所有目标管道已消耗的数据。由于tee()没有偏移的概念,所以这有点复杂 - 你只能从管道的起点复制。这样做的结果是,您只能以最慢的套接字的速度进行复制,因为您无法使用tee()复制到目标管道,直到某些数据已从源中消耗,并且你不能这个直到所有套接字都有你要消费的数据。

如何处理这取决于数据的重要性。如果你真的需要tee()splice()的速度,并且你确信慢速插座将是一个非常罕见的事件,你可以做这样的事情(我已经假设您使用非阻塞IO和单个线程,但类似的东西也适用于多个线程):

  1. 确保所有管道都是非阻塞的(使用fcntl(d, F_SETFL, O_NONBLOCK)使每个文件描述符都是非阻塞的。)
  2. 将每个目标管道的read_counter变量初始化为零。
  3. 使用类似epoll()之类的内容,等待源管道中的某些内容。
  4. 循环read_counter为零的所有目标管道,调用tee()将数据传输到每个目标管道。确保在标志中传递SPLICE_F_NONBLOCK
  5. read_counter传输的金额为每个目标渠道增加tee()。跟踪最低结果值。
  6. 找到read_counter的最低结果值 - 如果这不是零,则丢弃来自源管道的数据量(使用splice()调用并在{{1}上打开目标}, 例如)。丢弃数据后,减去从所有管道上的/dev/null中丢弃的金额(因为这是最低值,因此不会导致其中任何一个变为负数)。
  7. 从步骤 3 重复。
  8. 注意:过去绊倒我的一件事是read_counter影响管道上的SPLICE_F_NONBLOCKtee()操作是否是非阻塞的,并且您使用splice()设置的O_NONBLOCK会影响与其他来电(例如fnctl()read())的互动是否为非阻止。如果您希望所有内容都是非阻塞的,请同时设置它们。还要记得让你的套接字无阻塞,或者write()调用它们来传输数据可能会阻塞(除非你想要的是什么,如果你正在使用线程方法)。

    正如您所看到的,此策略存在一个主要问题 - 只要一个套接字阻塞,一切都停止 - 该套接字的目标管道将填满,然后源管道将停滞不前。因此,如果您在步骤 4 中到达splice()返回tee()的阶段,那么您将要关闭该套接字,或者至少"断开连接&#34} #34;它(即将它从循环中取出),这样你就不会向它写任何其他内容,直到它的输出缓冲区为空。您选择哪种方法取决于您的数据流是否可以从跳过的位恢复。

    如果你想更优雅地应对网络延迟,那么你将需要做更多的缓冲,这将涉及用户空间缓冲区(这相当于否定了EAGAIN的优势和tee())或者可能是基于磁盘的缓冲区。基于磁盘的缓冲几乎肯定会明显慢于用户空间缓冲,因此,由于您已经选择了splice()tee(),因此您可能需要大量的速度。第一名,但我提到它是完整的。

    如果您最终在任何时候从用户空间插入数据,那么值得注意的一件事是splice()调用可以执行"收集输出"从用户空间到管道,与vmsplice()调用类似。如果您正在进行足够的缓冲,以便在多个不同的已分配缓冲区之间拆分数据(例如,如果您正在使用池分配器方法),这可能很有用。

    最后,您可以想象在" fast"之间交换套接字。使用writev()tee()的方案,如果它们无法跟上,则将它们移动到较慢的用户空间缓冲。这会使您的实施变得复杂,但如果您处理大量连接并且只有很小一部分连接速度很慢,那么您仍然会减少复制到用户空间的数量。有点涉及。然而,这只是应对瞬态网络问题的短期措施 - 正如我原先所说,如果你的插座比你的插座慢,你就会遇到根本问题。您最终会遇到一些缓冲限制,需要跳过数据或关闭连接。

    总的来说,我会仔细考虑为什么你需要splice()tee()的速度,以及对于你的用例,在内存或磁盘上简单地用户空间缓冲是否更合适。但是,如果你确信速度总是很高,并且有限的缓冲是可以接受的,那么我上面概述的方法应该可行。

    另外,我应该提到的一点是,这将使您的代码非常特定于Linux - 我不知道这些调用是否支持其他Unix变体。 splice()调用比sendfile()更受限制,但可能更具可移植性。如果你真的想要携带物品,请坚持用户空间缓冲。

    请告诉我是否有任何我想了解更多细节的内容。