Как буферизовать стандартный вывод в памяти и записать его из выделенного потока

У меня есть приложение C со многими рабочими потоками. Важно, чтобы они не блокировались, поэтому, когда рабочие потоки должны записывать в файл на диске, я заставляю их записывать в циклический буфер в памяти, а затем выделяю выделенный поток для записи этого буфера на диск.

Рабочие потоки больше не блокируются. Выделенный поток может безопасно блокироваться при записи на диск, не затрагивая рабочие потоки (он не удерживает блокировку при записи на диск). Мой буфер памяти настроен так, чтобы он был достаточно большим, чтобы поток записи мог не отставать.

Все это отлично работает. Мой вопрос: как мне реализовать что-то подобное для stdout?

Я мог бы использовать макрос printf() для записи в буфер памяти, но у меня нет контроля над всем кодом, который может быть записан в стандартный вывод (некоторые из них находятся в сторонних библиотеках).

Мысли? НикБ


person NickB    schedule 05.06.2009    source источник
comment
Все решения здесь неверны, потому что запись в каналы может блокироваться. (И если вы установите их неблокирующими, FILE просто попадет в состояние неисправимой ошибки, если он заблокируется, когда ему нужно очистить.)   -  person R.. GitHub STOP HELPING ICE    schedule 19.02.2011


Ответы (7)


Мне нравится идея использования freopen. Вы также можете перенаправить stdout в канал, используя dup и dup2, а затем используйте 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;
}
person Nate Kohl    schedule 05.06.2009
comment
Похоже, это решает мою проблему. Я попробую. Спасибо! - person NickB; 05.06.2009
comment
Когда я попробовал такой подход, я обнаружил read зависания, если в трубе ничего нет. Но добавление long flags = fcntl(out_pipe[0], F_GETFL); flags |= O_NONBLOCK; fcntl(out_pipe[0], F_SETFL, flags); в раздел инициализации решило эту проблему. - person aschepler; 27.01.2011
comment
Именно то, что я искал! - person Marcin; 15.11.2014
comment
Хороший метод, но как сделать что-то подобное в Windows? - person konserw; 01.04.2016
comment
@NateKohl Эй, Нейт, отличный код, не могли бы вы привести пример для двух разных функций, одна из которых считывает вывод другой, и наоборот. - person Mohsin; 08.09.2016
comment
Спасибо за код. Однако у меня есть дополнительный вопрос. Когда я запускаю операторы печати в дочернем процессе, который я создал с помощью fork. Я всегда получаю 1 в конце строки, которую я читаю. Вы знаете, откуда это? - person Arwed Mett; 05.03.2019

Если вы работаете с GNU libc, вы можете использовать потоки памяти строковые потоки .

person diapir    schedule 05.06.2009
comment
Я думаю, что это действительно правильный ответ. Канальный подход включает в себя дорогостоящие системные вызовы и, безусловно, будет блокироваться. - person Seth Robertson; 19.08.2011
comment
Срок действия ссылки истек, используйте вместо этого gnu.org/software/ libc/manual/html_node/String-Streams.html. - person valfur03; 06.01.2021

Вы можете «перенаправить» stdout в файл, используя freopen().

man freopen говорит:

Функция freopen() открывает файл, именем которого является строка, на которую указывает path, и связывает с ним поток, на который указывает stream. Исходный поток (если он существует) закрывается. Аргумент режима используется так же, как и в функции fopen(). В основном функция freopen() используется для изменения файла, связанного со стандартным текстовым потоком (stderr, stdin или stdout).

Этот файл вполне может быть каналом — рабочие потоки будут писать в этот канал, а поток записи будет слушать.

person qrdl    schedule 05.06.2009
comment
Это не решает мою проблему. Я пытаюсь переместить запись на диск из потока, выполняющего вызов printf(). При использовании freopen() мои вызовы printf() по-прежнему будут записываться в файл, хотя и в файл, отличный от стандартного вывода. Могу ли я указать файл для freopen(), который не является файлом на диске? - person NickB; 05.06.2009
comment
Конечно. Используйте канал вместо файла. - person qrdl; 05.06.2009

Почему бы вам не обернуть все ваше приложение в другое? По сути, вам нужен умный cat, который копирует stdin в stdout, буферизуя по мере необходимости. Затем используйте стандартное перенаправление stdin/stdout. Это можно сделать вообще без изменения текущего приложения.

~MSalters/# YourCurrentApp | bufcat
person MSalters    schedule 05.06.2009

Вы можете изменить способ работы буферизации с помощью setvbuf() или setbuf(). Здесь есть описание: http://publications.gbdirect.co.uk/c_book/chapter9/input_and_output.html.

[Редактировать]

stdout действительно FILE*. Если существующий код работает с FILE*s, я не вижу, что ему мешает работать с stdout.

person Bastien Léonard    schedule 05.06.2009

Одним из решений (для обеих вещей, которые вы делаете) было бы использовать сбор записи через writev .

Каждый поток может, например, запустить sprintf в буфер iovec, а затем передать указатели iovec потоку записи и заставить его просто вызывать writev с помощью stdout.

Вот пример использования writev из Advanced Unix Programming.

В Windows вы должны использовать WSAsend для аналогичной функциональности.

person Robert S. Barnes    schedule 05.06.2009

Метод с использованием 4096 bigbuf будет работать лишь частично. Я пробовал этот код, и хотя он успешно перехватывает стандартный вывод в буфер, его нельзя использовать в реальном случае. У вас нет возможности узнать, какова длина захваченного вывода, поэтому нет возможности узнать, когда завершать строку '\0'. Если вы попытаетесь использовать буфер, вы получите 4000 символов мусора, если вы успешно захватили 96 символов вывода stdout.

В моем приложении я использую интерпретатор Perl в программе C. Я понятия не имею, сколько вывода будет выброшено из любого документа, переданного в программу C, и, следовательно, приведенный выше код никогда не позволит мне чисто распечатать этот вывод где бы то ни было.

person Bob    schedule 26.04.2010
comment
Если вы сначала заполните bigbuf нулями, то вам гарантируется, что строка будет завершаться нулем ... если только последний байт bigbuf не равен нулю. Но в этом случае вы можете обнаружить переполнение. - person Mark Lakata; 10.12.2014