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

Elo*_*off 11 c linux pipe zero-copy

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

Car*_*roo 22

如果我理解正确,那么您需要将多个实时数据源复用到多个套接字.你有一个单独的"源"管道连接到产生你的数据的任何东西,并且你有一个"目标"管道,用于你希望发送数据的每个套接字.您正在做的是使用tee()将数据从源管道复制到每个目标管道,splice()并将其从目标管道复制到套接字本身.

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

该问题关系到你使用的管道,然而,就是在Linux上管道缓冲区的大小是不太灵活.自Linux 2.6.11以来默认为64K(tee()调用在2.6.17中添加) - 请参阅管道联机帮助页.从2.6.35开始,这个值可以通过F_SETPIPE_SZ选项fcntl()(参见fcntl联机帮助页)更改到指定的限制/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()打开目标的调用/dev/null).丢弃数据后,减去丢弃的量read_counter的所有管道(因为这是最低的值,那么这不会导致其中的任何变负).
  7. 从第3步开始重复.

注意:过去绊倒我的一件事是SPLICE_F_NONBLOCK影响管道上的tee()splice()操作是否是非阻塞的,并且O_NONBLOCK你设置的因素fnctl()会影响与其他调用(例如read()write())的交互是否是非阻塞的.如果您希望所有内容都是非阻塞的,请同时设置它们.还要记住让你的套接字无阻塞或者splice()向它们传输数据的调用可能会阻塞(除非你正在使用线程方法,这就是你想要的).

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

如果要应付网络延迟更优雅,那么你会需要做更多的缓冲,这将涉及任何用户空间的缓冲区(其中相当否定的优点tee()splice())或者是基于磁盘的缓存.基于磁盘的缓冲几乎肯定会明显慢于用户空间缓冲,因此不适合,因为你可能想要自从你选择tee()并且splice()首先获得很多速度,但我提到它是为了完整性.

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

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

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

此外,我应该提到的一件事是,这将使您的代码非常特定于Linux - 我不知道这些调用是否支持其他Unix变体.该sendfile()呼叫比更多的限制splice(),但可能是相当更加便于携带.如果你真的想要携带东西,那就坚持用户空间缓冲.

如果有任何我想要了解的内容,请告诉我.

  • 我希望我可以+10你的答案.是的,你已经很好地描述了我的问题,如果一个套接字无法跟上你的问题你是对的.除非一个收件人失败,否则每个套接字应该以相同的速度随时间推移.在这种情况下,唯一明智的做法是将其从复制集中删除.但你错过的是,虽然默认情况下管道是64KB,你可以将它们fcntl高达1MB(一个限制本身可以通过修改/ proc/sys/fs/pipe-max-size来提高.)我有足够的内存,我可以为每个管道分配多达64MB.你怎么看? (4认同)