1

是否有一种可移植的方式来丢弃来自套接字的大量传入字节而不将它们复制到用户空间?在常规文件上,我可以使用lseek(),但在套接字上,这是不可能的。我有两种情况可能需要它:

  1. 记录流到达文件描述符(可以是 TCP、SOCK_STREAM 类型的 UNIX 域套接字或可能是管道)。每条记录前面都有一个固定大小的标头,指定其类型和长度,然后是可变长度的数据。我想先读取标题,如果它不是我感兴趣的类型,我只想丢弃下面的数据段,而不将它们转移到用户空间到一个虚拟缓冲区中。

  2. 长度可变且不可预测的记录流到达文件描述符。由于异步性质,当 fd 变得可读时,记录可能仍然不完整,或者它们可能是完整的,但是当我尝试将固定数量的字节读入缓冲区时,下一条记录可能已经存在。我想在记录之间的确切边界处停止读取 fd,因此我不需要管理我不小心从 fd 读取的部分加载的记录。因此,我使用recv()withMSG_PEEK标志读入缓冲区,解析记录以确定其完整性和长度,然后再次正确读取(从而实际上从套接字中删除数据)到确切的长度。这会复制数据两次——我想通过简单地丢弃套接字中缓冲的数据来避免这种情况。

在 Linux 上,我认为可以通过使用splice()数据并将其重定向到/dev/null而不将它们复制到用户空间来实现这一点。但是,splice()它仅适用于 Linux,并且sendfile()在更多平台上支持的类似内容不能使用套接字作为输入。我的问题是:

  1. 有没有一种便携的方式来实现这一点?可以在其他 UNIX(主要是 Solaris)上工作的东西以及没有的东西splice()

  2. 是在splice()Linux/dev/null上使用一种有效的方法来执行此操作,还是会浪费精力?

理想情况下,我希望有一个简单地从内核中的文件描述符fdssize_t discard(int fd, size_t count)中删除可读字节(即不向用户空间复制任何内容),阻塞可阻塞 fd 直到丢弃请求的字节数,或返回成功的字节数就像在非阻塞 fd 上丢弃的字节或 EAGAIN 一样。并在常规文件上推进查找位置:)read()

4

1 回答 1

4

简短的回答是不,没有便携的方法可以做到这一点

sendfile()方法是特定于 Linux 的,因为在大多数其他实现它的操作系统上,源必须是文件或共享内存对象。(我什至没有检查是否/在哪个 Linux 内核版本中,sendfile()从套接字描述符到/dev/null受支持。老实说,我会非常怀疑这样做的代码。)

查看例如 Linux 内核源代码,并考虑到 assize_t discard(fd, len)与标准的差异很小ssize_t read(fd, buf, len),显然可以添加这样的支持。甚至可以通过 ioctl(例如SIOCISKIP)添加它,以便于检测支持。

但是,问题在于您设计了一种低效的方法,而不是在算法级别修复该方法,您正在寻找可以使您的方法表现更好的拐杖。

你看,很难证明“额外副本”(从内核缓冲区到用户空间缓冲区)是一个实际的性能瓶颈。系统调用(用户空间和内核空间之间的上下文切换)的数量有时是。如果您向上游发送了一个补丁,例如ioctl(socketfd, SIOCISKIP, bytes)为 TCP 和/或 Unix 域流套接字实现,他们会指出,希望实现的性能提升最好通过不尝试获取您不需要的数据来获得。(换句话说,您尝试做事的方式本质上是低效的,与其创建拐杖来使该方法更好地工作,您应该选择一种性能更好的方法。)

在您的第一种情况下,接收由类型和长度标识符构成的结构化数据的进程希望跳过不需要的帧,最好通过修复传输协议来修复。例如,接收方可以通知发送方它对哪些帧感兴趣(即基本过滤方法)。如果你被一个由于外部原因而无法替换的愚蠢协议所困,那么你只能靠自己了。(FLOSS 开发者社区不是,也不应该因为有人为此而哀叹而维护愚蠢的决定。任何人都可以自由地这样做,但他们需要以一种不需要其他人额外工作的方式来做也。)

在第二种情况下,您已经读取了数据。不要那样做。相反,使用足够大的用户空间缓冲区来容纳两个完整大小的帧。每当您需要更多数据,但帧的开头已经超过缓冲区的中间时,memmove()帧首先从缓冲区的开头开始。

当您有一个部分读取的帧,并且您N从左侧有您不感兴趣的未读字节时,请将它们读入缓冲区的未使用部分。总是有足够的空间,因为你可以覆盖当前帧已经使用的部分,并且它的开始总是在缓冲区的前半部分。

如果帧很小,例如最大 65536 字节,则应使用可调整的最大缓冲区大小。在大多数具有高带宽流套接字的台式机和服务器机器上,2 MiB(2097152 字节或更多)更合理。这并没有浪费太多的内存,但你很少做任何内存副本(当你这样做时,它们往往很短)。(您甚至可以优化内存移动,以便仅复制、对齐完整的缓存线,因为在缓冲区的开头留下几乎一个垃圾缓存线是微不足道的。)

我使用大型数据集(包括文本形式的分子数据,其中记录由换行符分隔,并且使用用于转换十进制整数或浮点值的自定义解析器以获得更好的性能)进行 HPC,并且这种方法在实践中效果很好。简单地说,跳过缓冲区中已经存在的数据不是您需要优化的;与简单地避免做你不需要的事情相比,这是微不足道的开销。

还有一个问题是您希望通过这样做来优化什么:使用的 CPU 时间/资源,或整个任务中使用的挂钟。它们是完全不同的东西。

例如,如果您需要对某个文件中的大量文本行进行排序,那么您只需将整个数据集读取到内存中,构造一个指向每行的指针数组,对指针进行排序,最后写入,那么您使用的 CPU 时间最少每行(使用内部缓冲和/或 POSIX writev(),这样您就不需要write()为每个单独的行执行系统调用)。

但是,如果您希望最小化所使用的挂钟时间,您可以使用二叉堆或平衡二叉树来代替指针数组,并堆化或按顺序插入每行完全读取,这样当最后一行终于读完了,你已经按照正确的顺序排列了这些行。这是因为存储 I/O(对于除了病态输入情况之外的所有情况,例如单字符行)比使用任何强大的排序算法对它们进行排序需要更长的时间!内联工作(当数据进入时)的排序算法的 CPU 效率通常不如离线工作(在完整数据集上)的排序算法,因此最终会使用更多的 CPU 时间;但是因为 CPU 工作是在等待整个数据集加载到内存中浪费的时间完成的,所以它在更短的挂钟时间内完成!


如果有需要和兴趣,我可以提供一个实际的例子来说明这些技术。但是,绝对没有魔法,任何 C 程序员都应该能够自己实现这些(缓冲方案和排序方案)。(我确实考虑使用在线 Linux 手册页和 Wikipedia 文章和伪代码等资源,例如二进制堆“自行”完成。只要您不只是复制粘贴现有代码,我认为它是“在您的拥有”,即使有人或某些资源可以帮助您找到好的、强大的方法来做到这一点。)

于 2018-07-06T23:43:54.627 回答