LINUX.ORG.RU

Блокировка каналов ввода-вывода в линукс

 , , ,


0

2

Пишу небольшую утилиту для тестирования для своих нужд и чтобы разобраться в механизмах работы с потоками ввода вывода. Функционал программы примерно следующий:

  1. Запустить дочерный процесс, в котором несколько раз требуется что-то ввести в стандартный ввод(STDIN) и на этот ввод программа реагирует выводом в стандартный вывод(STDOUT)
  2. Записать данные в стандартный ввод дочернего процесса (STDIN)
  3. Считать данные из стандартного вывода дочернего процесса (STDOUT)
  4. Перейти на шаг 2 Примерный код:

main.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>

void error(char *s);
char *data[2] = {"hello. How are you?\n", "lacrimosa\n"};

int main(void) {
  int in[2], out[2], n, pid;
  char buf[255];

  if (pipe(in) < 0) error("pipe in");
  if (pipe(out) < 0) error("pipe out");

  if ((pid = fork()) == 0) {
    /* This is the child process */
    /* Close stdin, stdout, stderr */
    close(0);
    close(1);
    close(2);
    /* make our pipes, our new stdin,stdout and stderr */
    dup2(in[0], 0);
    dup2(out[1], 1);
    dup2(out[1], 2);

    close(in[1]);
    close(out[0]);
    close(in[0]);
    close(out[1]);

    execl("/bin/sh", "sh", "-c", argv[1], (char *)NULL);
    error("Could not exec hexdump");
  }

  /* This is the parent process */
  
  close(in[0]);
  close(out[1]);
  for (int i = 0; i < 2; i++) {
    printf("<- %s", data[i]);
    /* Write some data to the childs input */
    write(in[1], data[i], strlen(data[i]));

    /* Read back any output */
    n = read(out[0], buf, 250);
    buf[n] = 0;
    printf("-> %s", buf);
    // Анализируем вывод дочернего процесса.
  }
  wait(NULL);
  exit(0);
}

void error(char *s) {
  perror(s);
  exit(1);
}

sample.c:

#include <stdio.h>

int main(inc argc, char *argv[]) {
  char buf[1024];
  for (int i = 0; i < 5; i++) {
    scanf("%s", buf);
    if (strstr(buf, "hello")) {
      printf("Hello!\n");
    } else {
      printf("Bye!\n");
    }
  }
  return 0; 
}

Проблема возникает, когда пытаюсь считать данные из потока вывода (STDOUT), потому что программа блокируется не доходя до туда. В примерах советуют просто закрыть поток ввода(STDIN), вот только проблема в том, что мне необходимо дальше записывать в него какие-то данные. Буду также крайне признателен, если поделитесь ссылкой на какие-либо материалы по теме pipes и межпроцессорного взаимодействия. Спасибо.

P.S. Я ввидимо ввел вас содержанием программы. Эта программа является просто примером выполняемых мной действий и не имеет отношение к реальному проекту. (Я не буду использовать hexdump). Поэтому я отредактировал код.

P.S. 2 Прилагаю ссылку на репозиторий, чтобы уже совсем понятно стало, что я пытаюсь сделать: https://github.com/pro100ren4/mtest



Последнее исправление: pro100ren4 (всего исправлений: 2)
  1. hexdump ожидает EOF чтоб понять что отправка данных уже завершена. Чтоб отправить ему EOF достаточно просто закрыть трубу после записи в нее.
  2. так же не лишним будет добавить -v hexdump-у чтоб он процессил данные как только они начнут поступать.

Полный пример:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

void error(char *s);
char *data = "Some input data\n";

int main(void) {
  int in[2], out[2], n, pid;
  char buf[255];

  if (pipe(in) < 0) error("pipe in");
  if (pipe(out) < 0) error("pipe out");

  if ((pid = fork()) == 0) {
    close(0);
    close(1);
    close(2);
    dup2(in[0], 0);
    dup2(out[1], 1);
    dup2(out[1], 2);
    close(in[1]);
    close(out[0]);
    execl("/usr/bin/hexdump", "hexdump", "-C", "-v", (char *)NULL);
    error("Could not exec hexdump");
  }

  printf("Spawned 'hexdump -C' as a child process at pid %d\n", pid);

  close(in[0]);
  close(out[1]);

  printf("<- %s", data);
  write(in[1], data, strlen(data));
  close(in[1]);

  while ((n = read(out[0], buf, 250)) > 0) {
    buf[n] = 0;
    printf("-> %s", buf);
  }

  wait(NULL);
  exit(0);
}

void error(char *s) {
  perror(s);
  exit(1);
}
iron ★★★★★
()
Ответ на: комментарий от iron

Ты всё правильно написал о том как это исправить, но во-первых если б я не знал и так ответ я б возможно не понял твой ответ, а во-вторых ты неправильно указал причину проблемы. hexdump никаких EOF в данном случае не ожидает. А ещё EOF нельзя «отправить», это не символ, а состояние пайпа.

hexdump, если его запустить в консоли и прислать указанную строку, сразу печатает ответ без каких-либо проблем. Проблема начинается если stdout hexdump-а перенаправлен в другую прогу, например так: hexdump -C | cat. Проблема эта широко известна, она из-за того что stdio-обёртка, которой он пользуется для вывода ответа, если замечает что stdout это не консоль, начинает копить выводимые строки у себя в памяти и слать только когда у неё переполняется буфер (4096 байт, вроде, но это не точно и может меняться), либо в конце работы программы. В этом примере именно так и происходит, hexdump принимает строку (она 16 байт как раз, если меньше то может быть будет ждать 16-и), выводит ответ но не шлёт его в настоящий stdout. Если же закрыть in[1] то hexdump завершается и шлёт всё что осталось.

Это всё очень мешает использовать стандартные утилиты (они все печатают через stdio) в интерактивной потоковой обработке чего-то - всё в итоге приходит с задержкой на размер этого буфера. Нормального способа отключить эту буферизацию не трогая исходник утилиты нет. Есть два костыльных: 1) подгрузить через LD_PRELOAD свою .so которая настроит stdio не небуферизованный вывод в начале работы, 2) запустить утилиту в фейковом терминале (чтоб она думала что она в терминале), тогда stdio буферизацию не включает.

firkax ★★★★★
()

В примерах советуют просто закрыть поток ввода(STDIN)

Бесполезный и, я бы даже сказал - вредный совет.

вот только проблема в том, что мне необходимо дальше записывать в него какие-то данные

flush() на SDTOUT спасёт отца русской демократии.

bugfixer ★★★★★
()
Ответ на: комментарий от iron

Я видимо неясно изъяснился. Я не могу закрыть трубу, так как мне необходимо что-то записывать в этом поток, после того как произведу чтение из STDOUT. Из-за этого возникает проблема, что на этапе записи в поток ввода программа блокируется.

pro100ren4
() автор топика
Ответ на: комментарий от firkax

Программа в вопросе просто пример реализации. Вместо hexdump может быть что угодно (чаще самописная С-программа). Правильно ли я понял, что проблема блокировки программы которая возникла непосредственно из-за этого буфера? Я сейчас отредактирую вопрос, чтобы было понятнее что мне необходимо.

pro100ren4
() автор топика
Ответ на: комментарий от bugfixer

Не совсем понял совет. Объясни подробнее пожалуйста. Как я понимаю понимаю надо сделать flush() для STDOUT. Но если я не ошибаюсь проблема возникает во время записи данных в STDIN, потому что программа блокируется ожидая дальнейшего ввода.

pro100ren4
() автор топика
Ответ на: комментарий от bugfixer

Еще на всякий случай я не имел ввиду непосредственно STDIN, а имел в виду поток который обозначен в коде in[1] и который воспринимается дочерним процессом как STDIN.

pro100ren4
() автор топика
Ответ на: комментарий от pro100ren4

Не совсем понял совет.

Это я погорячился - просто выше разговор про отключение буферизации, ну и ОП я прочёл по диагонали.

не имел ввиду непосредственно STDIN

Давайте договоримся (а) называть вещи своими именами и (б) - что никто и никогда не пишет в STDIN и не читает из STDOUT.

Если хочется общаться с потомком в две стороны (а не только от него читать, например) - наверное придётся перевести оба fd в non-blocking и делать IO в select-loop’е. Или я вопрос не до конца понял.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Окей я понял вас и постараюсь проиллюстрировать ситуацию максимально понятно. У меня есть небольшая программа как в программа, как в вопросе, которая в цикле считывает данные с помощью scanf и что-то печатает с помощью printf. Мне необходимо из другой программы:

  1. Запустить первую программу.
  2. Передать ей данные для ее scanf, чтобы она их считала.
  3. Считать вывод, который она выдала с помощью printf (Не считывать вывод с помощью printf)
  4. Повторить все с шага 2. Но у меня возникает проблема, потому что программа sample блокируется на шаге 2, как я понял из-за того, что demo ожидает еще данных(хотя при взаимодействии через консоль, после получения '\n' она продолжает выполнение). Я не понимаю как решить эту проблему, так как не знаком со всеми тонкостями взаимодействия с FIFO каналами. Прилагаю ссылку на репозиторий с проектом. Думаю так будет сильно лучше: https://github.com/pro100ren4/mtest
pro100ren4
() автор топика
Последнее исправление: pro100ren4 (всего исправлений: 1)
Ответ на: комментарий от pro100ren4

Я не могу закрыть трубу, так как мне необходимо что-то записывать в этом поток, после того как произведу чтение из STDOUT.

Программа не начнет выполняться пока ты не закроешь трубу. Факт закрытия трубы сообщает программе о том, что данных больше не будет и нужно начинать их процессить. Следовательно, если ты не закрываешь трубу то программа не выполняется и когда ты пытаешься читать вывод то там ничего нету. Собственно, по этому программа и висит на read().

Из-за этого возникает проблема, что на этапе записи в поток ввода программа блокируется.

Не на этапе записи, а на этапе чтения. Забей программулину в gdb и убедись в том, что программа висит на read().

iron ★★★★★
()
Ответ на: комментарий от pro100ren4

что demo ожидает еще данных

Она ждёт 250 байт в blocking read(), как вы и попросили. Если хочется читать построчно - переводите fd в non-blocking и ждите данных в select на каждом wake-up выгребая всё что пришло.

bugfixer ★★★★★
()
Ответ на: комментарий от pro100ren4

Вместо hexdump может быть что угодно (чаще самописная С-программа)

А я тоже указал что hexdump только одна из многих прог которые пользуются stdio (printf/puts/fwrite итд) для вывода текста.

Правильно ли я понял, что проблема блокировки программы которая возникла непосредственно из-за этого буфера?

Я не знаю то за «проблема блокировки». У тебя проблема не в блокировке (она там происходит вполне закономерно и так и должно быть), а в том что дочерняя программа не шлёт ответ когда ты его хочешь прочитать.

Не шлёт из-за того что stdio не сразу всё отправляет а копит буфер.

firkax ★★★★★
()
Ответ на: комментарий от pro100ren4

Но если я не ошибаюсь проблема возникает во время записи данных в STDIN, потому что программа блокируется ожидая дальнейшего ввода.

С чего ты взял? Блокируется у тебя чтение ответа потому что ответ не прислали.

firkax ★★★★★
()
Ответ на: комментарий от iron

Программа не начнет выполняться пока ты не закроешь трубу

Да что ж такое? Я уже выше расписал что всё не так. Повторю ещё раз: hexdump не ждёт никаких EOF-ов и сразу обрабатывает все данные. Ну и, конечно, запускается сразу как ты его запустил, а не при закрытии чего-то.

firkax ★★★★★
()
Ответ на: комментарий от pro100ren4

потому что программа sample блокируется на шаге 2, как я понял из-за того, что demo ожидает еще данных(хотя при взаимодействии через консоль, после получения '\n' она продолжает выполнение).

Нет, неправильно понял. И у тебя есть main.c и sample.c, кто такой demo неизвестно.

firkax ★★★★★
()
Ответ на: комментарий от bugfixer

Нет, она ждёт любых данных и вернёт не больше чем 250. non-blocking тут ни при чём.

Нужда в non-blocking будет только если у автора начнёт переполняться ядерный буфер пайпа из родителя в обработчик за одну итерацию, с hexdump-ом этого точно не происходит (16 байт его не переполнят) а что там у него на самом деле неизвестно.

firkax ★★★★★
()
Ответ на: комментарий от pro100ren4

А то я не разбираюсь еще в работе с файловыми дескрипторами и режимах ввода

Там нечего разбираться и проблема у тебя не в чтении а в записи, и не в дескриптор а в stdio-шный FILE *stdout.

firkax ★★★★★
()
Ответ на: комментарий от firkax

Нет, она ждёт любых данных и вернёт не больше чем 250. non-blocking тут ни при чём.

И вы это несумнящеся на голубом глазу заявляете? Реально? В отсутствие EOF и прочих error conditions? А тут дети присутствуют, между прочим, постеснялись бы.

bugfixer ★★★★★
()
Ответ на: комментарий от pro100ren4

Можете дать ссылки на материалы по этой теме

Прямые ссылки - нет, но погуглите «Advanced Programming in the Unix Environment» by Stevens - я уверен найдёте где взять бесплатно.

bugfixer ★★★★★
()

Или используй fflush во второй программе или неблокирующий ввод/вывод в первой.

Вообще когда ты открываешь программу на ввод-вывод, то у тебя есть два дескриптора, и каждый из них может блокироваться в общем случае. Проще всего через неблокирующий ввод-вывод работать, другие варианты - select (или его модные замены типа epoll), многопоточность.

vbr ★★★★
()
Последнее исправление: vbr (всего исправлений: 1)
Ответ на: комментарий от firkax

Я прочитал все, что вы мне написали. Правильно ли я понял, что проблема с блокировкой программы возникает в тот момент когда я пытаюсь считать данные из out[0], в который, если я не ошибаюсь, перенаправлен stdout дочернего процесса, из-за того, что все данные поступающие на stdout не в терминал буферизуются и отдаются только после заполнения буфера или по закрытию канала. Все верно?

pro100ren4
() автор топика
Ответ на: комментарий от firkax

demo это программа-пример из репозитория, ссылку на который я дал позже в вопросе, чтобы лучше объяснить свою проблему и в котором хранится уже тот проект над которым я работаю, а не пример. Вот она: https://github.com/pro100ren4/mtest

pro100ren4
() автор топика
Ответ на: комментарий от firkax

Нет, она ждёт любых данных и вернёт не больше чем 250. non-blocking тут ни при чём.

Должен признать что вы таки правы - к моему большому удивлению blocking read() из pipe запросто может быть partial, что подтверждается и тестами, и man 2 read честно об этом говорит. Беру свои слова обратно. Век живи, век учись…

bugfixer ★★★★★
()
Ответ на: комментарий от pro100ren4

Почти верно. Неверно тут: «по закрытию канала». Не по закрытию канала а по завершению работы программы, которая писала в свой stdout. Конкретно с hexdump-ом она завершается когда ей закрывают ввод, да, но это частный случай.

И надо уточнить что stdout для всяких printf()/fwrite() это не то же самое что дескриптор 1 для write(). Дескриптор 1 это ядерный интерфейс, и в нём всё шлётся сразу, а вот функции из stdio буферизуют.

firkax ★★★★★
()
Ответ на: комментарий от vbr

«Неблокирующий ввод-вывод в первой» никак не поможет против того что вторая буферизует свой вывод.

Проще всего через неблокирующий ввод-вывод работать

Корректная реализация неблокирующего ввода-вывода сразу раздувает код с одной строчки (read/write) до страницы ответственного кода. Но часто это единственный пригодный вариант.

firkax ★★★★★
()
Ответ на: комментарий от firkax

Как я понял единственно решение моей проблемы это либо убрать буферизацию вывода в дочернем процессе путем использования в нем read и write, который не буферизует вывод и шлет все сразу, либо реализовать неблокирущий ввод-вывод. Правильно?

pro100ren4
() автор топика
Ответ на: комментарий от firkax

Буферизация вывода это нормально и это делает любая адекватная программа. Собственно в libc это добавили по умолчанию не просто так.

vbr ★★★★
()
Последнее исправление: vbr (всего исправлений: 2)
Ответ на: комментарий от pro100ren4

Убрать буферизацию можно и оставаясь с stdio-функциями, поставив в начало setbuf(stdout,NULL) либо вызывая fflush(stdout) каждый раз когда надо сбросить буфер в настоящий вывод.

Неблокирующий ввод-вывод тут ни при чём. Но он может понадобиться, если ты хочешь слать форкнутому процессу большие куски входных данных за 1 раз, а он начнёт слать ответы раньше чем всё примет и тоже большие. Не знаю может ли такое у тебя произойти, но если произойдёт - то всё зависнет. Родительский процесс будет ждать пока второй прочитает его входные данные, а тот будет пытаться послать ему ответ и тоже ждать, до чтения никто не дойдёт.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax

Можно ли сделать вызов setbuf в дочернем процессе до вызова execl или обязательно делать этот в кода дочерней программы. Просто я попытался сделать это в процессе до вызова execl и родительская программа перестала вообще что-либо считывать из дочернего процесса. Может я не разобрался в каких-то нюансах?

pro100ren4
() автор топика
Ответ на: комментарий от pro100ren4

До - нельзя, никак не повлияет. Это функция libc, а она грузится с нуля после exec со своим дефолтным состоянием.

перестала вообще что-либо считывать из дочернего процесса

Она вроде и так у тебя ничего не считывает.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)