我正在使用 tee() 复制一个“主”管道,以使用 splice() 写入多个套接字。自然,这些管道将以不同的速率清空,具体取决于我可以将多少 splice() 连接到目标套接字。因此,当我接下来将数据添加到“主”管道然后再次 tee() 时,我可能会遇到这样一种情况,即我可以将 64KB 写入管道,但只能将 4KB 写入“从属”管道之一。我猜如果我将所有“主”管道拼接()到套接字,我将永远无法将剩余的 60KB 发送到该从管道。真的吗?我想我可以跟踪一个 tee_offset (从 0 开始),我将它设置为“unteed”数据的开头,然后不要 splice() 越过它。所以在这种情况下,我会将 tee_offset 设置为 4096 并且不会拼接更多,直到我' 我能够将它全部连接到其他管道。我在正确的轨道上吗?对我有什么提示/警告吗?
1 回答
如果我理解正确,您有一些实时数据源,您希望多路复用到多个套接字。你有一个单一的“源”管道连接到生成数据的任何东西,并且你有一个“目标”管道用于每个要发送数据的套接字。您正在做的是使用tee()
将数据从源管道复制到每个目标管道,splice()
并将其从目标管道复制到套接字本身。
您将在这里遇到的基本问题是,如果其中一个套接字根本无法跟上 - 如果您生成数据的速度比发送数据的速度快,那么您就会遇到问题。这与您使用管道无关,这只是一个基本问题。因此,您需要选择一种策略来应对这种情况——我建议您处理这种情况,即使您不认为它很常见,因为这些事情以后经常会出现在您身上。您的基本选择是关闭有问题的套接字,或者跳过数据直到它清除其输出缓冲区 - 例如,后一种选择可能更适合音频/视频流。
然而,与您使用管道有关的问题是,在 Linux 上,管道缓冲区的大小有些不灵活。tee()
自 Linux 2.6.11(在 2.6.17 中添加调用)以来,它默认为 64K - 请参阅管道手册页。F_SETPIPE_SZ
从 2.6.35开始,可以通过选项fcntl()
(参见fcntl 手册页)将这个值更改为由 指定的限制/proc/sys/fs/pipe-size-max
,但是与用户空间中的动态分配方案相比,按需更改缓冲仍然更尴尬。这意味着您应对慢速套接字的能力将受到一定限制——这是否可以接受取决于您期望接收和能够发送数据的速率。
假设这种缓冲策略是可以接受的,那么您的假设是正确的,您需要跟踪每个目标管道从源中消耗了多少数据,并且只有丢弃所有目标管道已经消耗的数据才是安全的。tee()
由于没有偏移量的概念,这有点复杂- 您只能从管道的开头复制。这样做的结果是您只能以最慢套接字的速度进行复制,因为tee()
在从源消耗了一些数据之前,您不能使用复制到目标管道,并且您不能这样做,直到所有套接字都有您将要使用的数据。
你如何处理这取决于你的数据的重要性。如果你真的需要 and 的速度tee()
,splice()
并且你确信慢速套接字将是一个极其罕见的事件,你可以做这样的事情(我假设你使用的是非阻塞 IO 和单线程,但是类似的东西也适用于多个线程):
- 确保所有管道都是非阻塞的(用于
fcntl(d, F_SETFL, O_NONBLOCK)
使每个文件描述符非阻塞)。 read_counter
将每个目标管道的变量初始化为零。- 使用类似epoll()的东西等到源管道中有东西。
- 循环遍历所有
read_counter
为零的目标管道,调用tee()
以将数据传输到每个目标管道。确保您传递SPLICE_F_NONBLOCK
标志。 read_counter
每个目标管道的增量为 传输的数量tee()
。跟踪最低的结果值。- 找到 - 的最低结果值,
read_counter
如果它不为零,则丢弃源管道中的数据量(例如,使用在splice()
上打开目标的调用/dev/null
)。丢弃数据后,从所有read_counter
管道上减去丢弃的数量(因为这是最低值,所以这不会导致它们中的任何一个变为负数)。 - 从步骤3开始重复。
注意:过去让我感到困惑的一件事是SPLICE_F_NONBLOCK
影响管道上的tee()
andsplice()
操作是否是非阻塞的,而O_NONBLOCK
你设置的 withfnctl()
会影响与其他调用(例如read()
and write()
)的交互是否是非阻塞的。如果您希望所有内容都是非阻塞的,请同时设置。还要记住使您的套接字不阻塞,否则splice()
将数据传输到它们的调用可能会阻塞(除非这是您想要的,如果您使用的是线程方法)。
正如你所看到的,这种策略有一个主要问题——一旦一个套接字阻塞,一切都会停止——该套接字的目标管道将被填满,然后源管道将变得停滞不前。因此,如果您到达步骤4tee()
中返回的阶段,那么您将要关闭该套接字,或者至少“断开”它(即,将其从循环中取出),这样您就不会向它写入任何其他内容直到其输出缓冲区为空。您选择哪种取决于您的数据流是否可以从跳过的部分中恢复。EAGAIN
如果您想更优雅地处理网络延迟,那么您将需要做更多的缓冲,这将涉及用户空间缓冲区(这会否定tee()
and的优点splice()
)或者可能是基于磁盘的缓冲区。基于磁盘的缓冲几乎肯定会比用户空间缓冲慢得多,因此不合适,因为您可能想要很高的速度,因为您首先选择了tee()
,splice()
但我提到它是为了完整性。
如果您最终在任何时候从用户空间插入数据,那么值得注意的一件事是vmsplice()
调用,它可以执行从用户空间到管道的“收集输出”,其方式与writev()
调用类似。如果您正在做足够的缓冲以将数据拆分到多个不同的分配缓冲区(例如,如果您使用池分配器方法),这可能很有用。
最后,您可以想象在“快速”使用方案之间交换套接字,tee()
并且splice()
如果它们跟不上,则将它们移至较慢的用户空间缓冲。这将使您的实现复杂化,但是如果您正在处理大量连接并且只有很小一部分连接速度很慢,那么您仍然会减少复制到用户空间的数量,这在一定程度上涉及到。然而,这只是应对瞬态网络问题的短期措施——正如我最初所说的,如果你的套接字比你的源慢,你就会遇到一个根本问题。你最终会达到一些缓冲限制,需要跳过数据或关闭连接。
总的来说,我会仔细考虑为什么您需要速度,tee()
以及splice()
对于您的用例,简单地在内存或磁盘上进行用户空间缓冲是否更合适。但是,如果您确信速度将始终很高,并且可以接受有限的缓冲,那么我上面概述的方法应该可以工作。
另外,我应该提到的一件事是,这将使您的代码非常特定于 Linux - 我不知道这些调用在其他 Unix 变体中得到支持。sendfile()
调用比 更受限制,splice()
但可能更便携。如果您真的希望事物具有可移植性,请坚持使用用户空间缓冲。
让我知道是否有任何我已经介绍过的内容,您想了解更多详细信息。