【问题标题】:Capturing stdout/stderr separately and simultaneously from child process results in wrong total order (libc/unix)从子进程单独和同时捕获 stdout/stderr 会导致错误的总顺序(libc/unix)
【发布时间】:2021-03-11 04:38:43
【问题描述】:

我正在编写一个库,它应该在子进程中执行程序,捕获输出,并以逐行(字符串向量)的方式使输出可用。 STDOUT 有一个向量,STDERR 有一个向量,“STDCOMBINED”有一个向量,即所有输出都按照程序打印的顺序。子进程通过两个管道连接到父进程。一根管子用于 STDOUT,一根管子用于 STDERR。在父进程中我从管道的读取端读取,在子进程中我dup2()'ed STDOUT/STDERR 到管道的写入端。

我的问题: 我想捕获 STDOUT、STDERR、和“STDCOMBINED”(=两者都按它们出现的顺序)。但是组合向量中的顺序与原来的顺序不同。

我的方法: 我迭代直到两个管道都显示 EOF 并且子进程退出。在每次迭代中,我从 STDOUT 中准确读取一行(或 EOF),从 STDERR 中准确读取一行(或 EOF)。到目前为止,这有效。但是当我在父进程中捕获这些行时,STDOUT 和 STDERR 的顺序不一样,就像我在 shell 中执行程序并查看输出一样。

为什么会这样,我该如何解决这个问题?这可能吗?我知道在子进程中我可以将 STDOUT 和 STDERR 都重定向到一个管道,但我需要分别使用 STDOUT 和 STDERR,以及“STDCOMBINED”。


PS:我熟悉 libc/unix 系统调用,如dup2()pipe() 等。因此我没有发布代码。我的问题是关于一般方法,而不是特定语言的编码问题。我在 Rust 中针对原始 libc 绑定进行此操作。

PPS:我做了一个简单的测试程序,它混合了 5 个标准输出和 5 个标准错误消息。这足以重现问题。

【问题讨论】:

  • 没有定义发出标准输出和标准错误的顺序。它们是独立的流。
  • 有什么办法可以解决这个问题吗?例如禁用子进程的标准输出缓冲区?
  • 至少,父级需要selectpoll 管道来检测何时有新数据可用。您只需通过读取 “从 STDOUT 中准确地读取一行(或 EOF)和从 STDERR 中准确地读取一行(或 EOF)。”

标签: c unix pipe posix libc


【解决方案1】:

在每次迭代中,我从 STDOUT 中准确读取一行(或 EOF),从 STDERR 中准确读取一行(或 EOF)。

这就是问题所在。这只会捕获正确的顺序,如果这正是子进程中的输出顺序。

您需要捕捉野兽的异步特性:使您的管道端点无阻塞,select* 在管道上,并在 select 返回时读取存在的任何数据。然后,您将捕获输出的正确顺序。当然现在你不能读“正好一行”:你必须读任何可用的数据,不要再读了,这样你就不会阻塞,并维护一个每个管道缓冲区,你可以在其中添加新数据,提取任何存在的行,将未处理的输出推到开头,然后重复。您也可以使用循环缓冲区来保存一点memcpy-ing,但这可能不是很重要。

由于您在 Rust 中执行此操作,我认为您已经可以利用一个很好的异步反应模式(我猜我已经被 go 宠坏了,并将希望寄托在毫无戒心的人身上)。

*总是更喜欢特定平台的高性能原语,例如epoll on Linux/dev/poll on Solarispollset &c. on AIX

另一种可能性是使用 LD_PRELOAD 启动目标进程,使用专用库接管 glibc 的 POSIX write,检测对管道的写入,并通过预先将此类写入(并且仅这些写入)封装在数据包中它带有一个标头,其中存储了一个(原子更新的)进程范围的递增计数器,以及写入的大小。这样的标头可以在管道的另一端轻松解码,从而以更高的成功机会重新排序写入。

【讨论】:

  • 我会检查是否能找到 Rust 抽象。但特别是为了学习效果,我很想对 libc 做这件事。我会试试这个。谢谢。
  • 即使这样也可能无法可靠地工作;如果父母被安排了一段时间,当它再次运行时,孩子可能已经写入了两个 fd。 select 会告诉您两个管道上都有可用的数据,并且无法知道哪个先出现。
  • @NateEldredge 贝壳并没有做得更好,不是吗? linux 中是否有任何机制允许您相互锁定管道写入,以便另一个进程可以在不跟踪进程的情况下观察确切的顺序?
  • @UnslanderMonica:我不知道。对于套接字,有SO_TIMESTAMP,它可以让您查看接收到每个数据块的确切时间,但我不知道管道有任何此类事情。
  • 一种解决方案是使用多线程读取器,一个线程用于stdout,一个线程用于stderr,每个线程读取其流,当读取整行时,将其写入公共流一种确保数据不交错的方法。如果原始过程搞砸了它的写作,那么这样的读者无论如何也无能为力。
【解决方案2】:

我认为不可能严格按照自己的意愿去做。

如果您考虑在交互式 shell 中运行命令时它是如何完成的,会发现 stdout 和 stderr 都指向同一个文件描述符(TTY),因此通过与同一个文件。

为了说明,想象一下如果子进程有 2 个完全独立的线程,一个只写到 stderr,另一个只写到 stdout,会发生什么。总排序将取决于调度程序决定调度这些线程的方式,如果您想捕获它,您需要将这些线程与某些东西同步。

当然,在向 stderr 写入任何内容之前,某些内容可以向 stdout 写入数千行。

有两种方法可以将您的要求放松为可行的:

  1. 让用户传递一个标志,放弃单独的 stdout 和 stderr 流以支持正确的 stdcombined,然后将两者重定向到单个文件描述符。在执行该过程之前,您可能需要更改缓冲设置(如 stdbuf 所做的那样)。

  2. 假设 stdout 和 stderr 是“合理交错的”,@Nate Eldredge 指出的假设,在这种情况下,您可以使用 @Unslander Monica 的答案。

【讨论】:

  • 我认为“交错”是 Andrew Henle 的建议,而不是我的建议。
  • 嗨!这正是我昨天所做的工作:) 用户现在可以在库函数中选择两种策略。一是通过合理准确的排序分别捕获STDOUT/STDERR。另一种方法是通过相同的文件描述符/管道捕获两者,同时丢失仅 stdout 或仅 stderr 行,但总顺序是正确的。 PS:我发现对于(单线程子进程情况),如果每个交替的 stdout/stderr 输出之间的时间> 100ms,则准确度为 99%。我通过 2 个线程为“单独”策略捕获 stdout/stderr。我存储了订购的时间戳
猜你喜欢
  • 2013-09-02
  • 2018-10-31
  • 2020-06-23
  • 1970-01-01
  • 2015-10-28
  • 1970-01-01
相关资源
最近更新 更多