是否有一种可移植的方法可以从类似套接字的文件描述符中丢弃一些可读字节?

Ang*_*gro 6 c unix sockets io zero-copy

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

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

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

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

  1. 有没有一种可移植的方法来实现这一目标?是否也可以在其他没有 UNIX(主要是 Solaris)的 UNIX 上运行splice()

  2. splice()-ing是/dev/null在 Linux 上执行此操作的有效方法,还是浪费精力?

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

Nom*_*mal 4

简短的回答是否定的,没有可移植的方法可以做到这一点

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

例如,查看 Linux 内核源代码,并考虑ssize_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 在线手册页、维基百科文章和伪代码等资源,例如“自己”执行二进制。只要您不只是复制粘贴现有代码,我认为它“在您的计算机上”执行此操作。自己的”,即使某人或某些资源可以帮助您找到良好、可靠的方法来做到这一点。)