如何将stdout缓存在内存中并从专用线程中写入

24

我有一个包含多个工作线程的C应用程序。这些线程不可阻塞,所以当工作线程需要写入磁盘文件时,我让它们将其写入内存中的循环缓冲区,然后再有专门的线程将该缓冲区中的内容写入磁盘。

工作线程现在不会再阻塞。专门的线程可以安全地阻塞,而不会影响工作线程(它在写入磁盘时不会持有锁)。我的内存缓冲区被调整得足够大,以便写入线程可以跟上。

这一切都运转得很好。我的问题是,如何为stdout实现类似的功能?

我可以编写一个宏printf(),使其写入内存缓冲区,但我无法控制所有可能写入stdout的代码(其中一些位于第三方库中)。

你有想法吗? NickB


1
所有这里的解决方案都是错误的,因为写入管道可能会阻塞。(如果将它们设置为非阻塞,则当需要刷新时,文件将进入不可恢复的错误状态。) - R.. GitHub STOP HELPING ICE
7个回答

37

我喜欢使用freopen的想法。你也可以尝试使用dupdup2stdout重定向到管道,然后使用read从管道中获取数据。

可以像这样:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define MAX_LEN 40

int main( int argc, char *argv[] ) {
  char buffer[MAX_LEN+1] = {0};
  int out_pipe[2];
  int saved_stdout;

  saved_stdout = dup(STDOUT_FILENO);  /* save stdout for display later */

  if( pipe(out_pipe) != 0 ) {          /* make a pipe */
    exit(1);
  }

  dup2(out_pipe[1], STDOUT_FILENO);   /* redirect stdout to the pipe */
  close(out_pipe[1]);

  /* anything sent to printf should now go down the pipe */
  printf("ceci n'est pas une pipe");
  fflush(stdout);

  read(out_pipe[0], buffer, MAX_LEN); /* read from pipe into buffer */

  dup2(saved_stdout, STDOUT_FILENO);  /* reconnect stdout for testing */
  printf("read: %s\n", buffer);

  return 0;
}

5
当我尝试了这样的方法后,我发现如果管道里没有数据,read函数会一直阻塞。但是在初始化部分添加long flags = fcntl(out_pipe[0], F_GETFL); flags |= O_NONBLOCK; fcntl(out_pipe[0], F_SETFL, flags);就可以解决这个问题。 - aschepler
不错的方法,但在Windows中如何实现类似的功能? - konserw
@NateKohl 嘿,Nate,代码写得很棒。你能否提供两个不同函数的示例,其中一个函数读取另一个函数的输出,反之亦然? - Mohsin
谢谢你的代码。不过我有一个后续问题。当我在通过fork创建的子进程中运行打印语句时,我总是在读取的字符串末尾得到一个1。你知道这是从哪里来的吗? - Arwed Mett

9

2
我认为这是真正正确的答案。管道方法涉及昂贵的系统调用,肯定会阻塞。 - Seth Robertson
2
链接已过期,请使用此链接 https://www.gnu.org/software/libc/manual/html_node/String-Streams.html。 - hintauh

4
您可以使用freopen()stdout“重定向”到文件中。 man freopen说:
“freopen()函数打开名称为指向路径字符串的字符串的文件,并将由流指针指向它。原始流(如果存在)将关闭。 mode参数与fopen()函数中一样使用。 freopen()函数的主要用途是更改与标准文本流(stderr,stdin或stdout)相关联的文件。”
该文件可以是管道 - 工作线程将写入该管道,而编写器线程将侦听该管道。

这并没有解决我的问题。我正在尝试将磁盘写入从调用printf()的线程中移出。使用freopen()仍然会使我的printf()调用写入一个文件,尽管是不同于stdout的文件。我是否可以指定一个不是磁盘文件的“文件”来进行freopen()操作? - NickB
当然可以。使用管道而不是文件。 - qrdl

2
为什么不用另一个应用程序包装你的整个应用程序呢?基本上,你需要一个智能的“猫”,将stdin复制到stdout,必要时缓冲。然后使用标准的stdin/stdout重定向。这可以在完全不修改当前应用程序的情况下完成。
~MSalters/# YourCurrentApp | bufcat

0

使用4096 bigbuf的方法只能部分地工作。我尝试过这段代码,虽然它成功地将stdout捕获到缓冲区中,但在实际情况下它是无用的。你不知道捕获的输出有多长,因此也就不知道何时终止字符串'\0'。如果你尝试使用缓冲区,你会得到4000个字符的垃圾输出,即使你成功捕获了96个字符的stdout输出。

在我的应用程序中,我在C程序中使用perl解释器。我不知道会有多少输出从C程序中抛出,因此上面的代码永远不会让我干净地打印输出到任何地方。


如果你先用零填充bigbuf,那么保证字符串以空字符结尾......除非bigbuf的最后一个字节不是零。但在这种情况下,你可以检测到溢出。 - Mark Lakata

0

0

一个解决方案(适用于你正在做的两件事情)是使用通过writev进行聚合写入。

例如,每个线程都可以sprintf到iovec缓冲区,然后将iovec指针传递给编写器线程,并让它简单地调用stdout的writev。

这里有一个使用Advanced Unix Programming中的writev的示例。

在Windows下,您可以使用WSAsend实现类似的功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接