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

Используя совет, найденный здесь: Восстановление stdout после использования dup Я попытался восстановить stdin и стандартный выход. Однако при использовании printf для проверки восстановления стандартного вывода я не смог прочитать вывод. Код выглядит следующим образом. После восстановления stdout как 1 я попытался напечатать «готово».

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

void main(int argv, char *argc) {
    int stdin_copy = dup(STDIN_FILENO);
    int stdout_copy = dup(STDOUT_FILENO);
    int testpipe[2];
    pipe(testpipe); 
    int PID = fork();
    if (PID == 0) { 
        dup2(testpipe[0], 0);
        close(testpipe[1]);
        execl("./multby", "multby", "3", NULL);// Just multiplies argument 3 with the number in testpipe.
    } else {
        dup2(testpipe[1], 1);
        close(testpipe[0]);
        printf("5");
        fclose(stdout);
        close(testpipe[1]);
        char initialval[100];
        read(testpipe[0], initialval, 100);
        fprintf(stderr, "initial value: %s\n", initialval);
        wait(NULL);
        dup2(stdin_copy, 0);
        dup2(stdout_copy, 1);
        printf("done");//was not displayed when I run code.
    }
}

Однако я не видел «готово», когда запускал код. (Должно быть сделано после 15). Это мой вывод: начальное значение: exec успешно a: 3 b: 5 multby успешно 15

Что я сделал не так при восстановлении stdout?


person Marine Biologist Kujo    schedule 18.02.2020    source источник


Ответы (2)


Вы вызываете fclose(stdout);, который закрывает объект FILE stdout, а также STDOUT_FILELO -- дескриптор файла stdout (это две разные вещи, связанные вместе). Поэтому, когда вы позже вызываете dup2(stdout_copy, 1);, это восстанавливает дескриптор файла, но объект FILE остается закрытым, поэтому вы не можете использовать его для вывода чего-либо. Невозможно повторно открыть объект FILE для ссылки на файловый дескриптор1, поэтому лучше всего просто удалить строку fclose. dup2 закроет дескриптор файла, который вы заменяете (поэтому вам не нужно отдельное закрытие), и вы сможете увидеть вывод


1Вы могли бы использовать freopen с /dev/fd на некоторых системах, но /dev/fd нельзя переносить

person Chris Dodd    schedule 18.02.2020
comment
Поскольку freopen() является частью стандартного C, портативный. Поскольку fdopen() является частью стандарта POSIX, он также достаточно переносим. — если что-нибудь POSIX доступно на целевой машине. - person Jonathan Leffler; 18.02.2020
comment
@JonathanLeffler, но /dev/fd не является стандартным - person Chris Dodd; 18.02.2020
comment
Ах я вижу. То, что вы утверждаете, является непереносимым, отличается от того, что я предполагал, что вы говорили. Может быть, это должно быть /dev/fd? - person Jonathan Leffler; 18.02.2020
comment
Но разве fclose() не нужен для сброса ввода (5) в канал? В противном случае я не получаю никаких данных от dup(testpipe[0], 0) - person Marine Biologist Kujo; 18.02.2020
comment
Зависит от вашего режима буферизации - используйте fflush(stdout) для сброса, если он буферизован. - person Chris Dodd; 18.02.2020
comment
@JonathanLeffler Обычно fclose вызывает close для дескриптора файла, хранящегося внутри файла FILE. Кроме того, сторона https port70.net использует устаревший сертификат Symantec, поэтому он выдает предупреждение для пользователей Chrome (и, возможно, другого браузера). - person S.S. Anne; 20.02.2020
comment
@SSAnne - что касается Chrome, я пробовал и 8.0.3987.106, и 8.0.3987.116 (и с тех пор обновил версию 106 до 116), и они действительно возражали. Я думаю (но уже не могу легко доказать), что пробовал 8.0.3987.87, и он не возражал. Во всяком случае, должным образом отмечено; Вместо этого я постараюсь не забывать использовать http-ссылки. Спасибо! - person Jonathan Leffler; 20.02.2020
comment
@JonathanLeffler Я все еще на 79 (Chrome OS), и я думаю, что он делает это некоторое время (вероятно, с 70 или даже раньше). - person S.S. Anne; 20.02.2020
comment
@S.S.Anne: Любопытно — у меня есть рабочая и домашняя машины (на расстоянии около 3 метров друг от друга), и я использую Chrome в основном на рабочей машине (и Firefox в основном на домашней машине) и до сих пор без проблем подключался к сайту. . Хммм: закладка URL не имеет видимого префикса на этом компьютере. Когда я добавляю https://, он жалуется; когда я добавляю http://, это не так. Я не уверен в значении этого. Обе машины теперь имеют http:// на этом URL-адресе, поэтому при копировании и вставке я буду указывать «небезопасный» URL-адрес, хотя я думаю, что риски довольно ограничены. Я не планирую модифицировать старые ответы. - person Jonathan Leffler; 20.02.2020

Основные пометки

Повторяю то, что уже говорил о SO в других ответах.

Вы не закрываете достаточно файловых дескрипторов в дочернем процессе.


Практическое правило: если вы dup2() один конец канала к стандартному вводу или стандартному выводу, закройте оба исходных файловых дескриптора, возвращенных pipe() как можно скорее. В частности, вы должны закрыть их перед использованием любого из семейства exec*(). функций.

Правило также применяется, если вы дублируете дескрипторы с помощью dup() или < a href="http://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html" rel="nofollow noreferrer">fcntl() с F_DUPFD или F_DUPFD_CLOEXEC.


Если родительский процесс не будет взаимодействовать ни с одним из своих дочерних процессов через канал, он должен убедиться, что он закрывает оба конца канала достаточно рано (например, перед ожиданием), чтобы его дочерние процессы могли получать индикацию EOF при чтении (или получать SIGPIPE). сигналы или ошибки записи при записи), а не блокировать на неопределенный срок. Даже если родитель использует канал без использования dup2(), в норме он должен закрывать по крайней мере один конец канала — чрезвычайно редко программа может читать и писать на обоих концах одного канала.

Обратите внимание, что параметр O_CLOEXEC соответствует open(), а параметры FD_CLOEXEC и F_DUPFD_CLOEXEC to fcntl() также может участвовать в этом обсуждении.

Если вы используете posix_spawn() и его обширное семейство вспомогательных функций (21 функция в целом), вам нужно будет просмотреть, как закрывать файловые дескрипторы в порожденном процессе (posix_spawn_file_actions_addclose() и т. д.).

Обратите внимание, что использование dup2(a, b) безопаснее, чем использование close(b); dup(a); по целому ряду причин. Во-первых, если вы хотите, чтобы дескриптор файла был больше обычного, dup2() — единственный разумный способ сделать это. Другой заключается в том, что если a совпадает с b (например, оба 0), то dup2() обрабатывает его правильно (он не закрывает b перед дублированием a), тогда как отдельные close() и dup() ужасно терпят неудачу. Это маловероятное, но не невозможное обстоятельство.


Анализ кода

Вопрос содержит код:

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

void main(int argv, char *argc) {
    int stdin_copy = dup(STDIN_FILENO);
    int stdout_copy = dup(STDOUT_FILENO);
    int testpipe[2];
    pipe(testpipe); 
    int PID = fork();
    if (PID == 0) { 
        dup2(testpipe[0], 0);
        close(testpipe[1]);
        execl("./multby", "multby", "3", NULL);// Just multiplies argument 3 with the number in testpipe.
    } else {
        dup2(testpipe[1], 1);
        close(testpipe[0]);
        printf("5");
        fclose(stdout);
        close(testpipe[1]);
        char initialval[100];
        read(testpipe[0], initialval, 100);
        fprintf(stderr, "initial value: %s\n", initialval);
        wait(NULL);
        dup2(stdin_copy, 0);
        dup2(stdout_copy, 1);
        printf("done");//was not displayed when I run code.
    }
}

Строка void main(int argv, char *argc) { должна быть int main(void), так как вы не используете аргументы командной строки. У вас также есть имена argv и argc, противоположные обычному соглашению — первый аргумент обычно называется argc (количество аргументов), а второй обычно называется argv (вектор аргументов). Кроме того, тип второго аргумента должен быть char **argv (или char **argc, если вы хотите запутать всех случайных читателей). См. также Что должен возвращать main() в C и C++?

Следующий блок кода, требующий обсуждения:

    if (PID == 0) { 
        dup2(testpipe[0], 0);
        close(testpipe[1]);
        execl("./multby", "multby", "3", NULL);// Just multiplies argument 3 with the number in testpipe.
    }

Это нарушает эмпирическое правило. Вы также должны поместить код обработки ошибок после execl().

if (PID == 0)
{
    dup2(testpipe[0], STDIN_FILENO);
    close(testpipe[0]);
    close(testpipe[1]);
    execl("./multby", "multby", "3", (char *)NULL);
    fprintf(stderr, "failed to execute ./multby\n");
    exit(EXIT_FAILURE);
}

Следующий блок кода для анализа:

dup2(testpipe[1], 1);
close(testpipe[0]);
printf("5");
fclose(stdout);
close(testpipe[1]);

Теоретически вы должны использовать STDOUT_FILENO вместо 1, но я весьма симпатизирую использованию 1 (не в последнюю очередь потому, что когда я впервые изучал C, такой символической константы не было). На самом деле вы закрываете оба конца канала, но я бы предпочел, чтобы оба закрывались сразу после вызова dup2(), в соответствии с эмпирическим правилом. printf() без новой строки ничего не отправляет по каналу; он помещает 5 в буфер ввода-вывода.

Как указал Крис Додд в своем ответьте, вызов fclose(stdout) доставляет много хлопот. Вероятно, вам следует просто заменить его на fflush(stdout).

Двигаемся дальше:

char initialval[100];
read(testpipe[0], initialval, 100);
fprintf(stderr, "initial value: %s\n", initialval);
wait(NULL);
dup2(stdin_copy, 0);
dup2(stdout_copy, 1);
printf("done");

Вы не проверили, работает ли read(). Это не так; это не удалось с EBADF, потому что чуть выше вы используете close(testpipe[0]);. То, что вы печатаете на stderr, есть неинициализированная строка — это нехорошо. На практике, если вы хотите надежно считывать информацию от дочернего элемента, вам нужны два канала: один для связи родитель-потомок, а другой — для связи потомок-родитель. В противном случае нет никакой гарантии, что родитель не прочитает написанное. Если бы вы ждали, пока ребенок умрет, прежде чем читать, у вас были бы неплохие шансы на то, что это сработает, но вы не всегда можете полагаться на то, что сможете это сделать.

Первый из двух вызовов dup2() бессмыслен; вы не изменили перенаправление для стандартного ввода (поэтому на самом деле stdin_copy не нужен). Второй изменяет назначение дескриптора стандартного выходного файла, но вы уже закрыли stdout, поэтому нет простого способа открыть его заново, чтобы printf() заработал. Сообщение должно заканчиваться новой строкой — большинство строк формата printf() должны заканчиваться новой строкой, если только она не используется намеренно для построения одной строки вывода по частям. Однако, если вы обратите внимание на возвращаемое значение, вы обнаружите, что оно не удалось (-1), и велики шансы, что вы снова найдете errno == EBADF.

Исправление кода — хрупкое решение

Учитывая этот код для multby.c:

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

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage; %s number", argv[0]);
        return 1;
    }
    int multiplier = atoi(argv[1]);
    int number;
    if (scanf("%d", &number) == 1)
        printf("%d\n", number * multiplier);
    else
        fprintf(stderr, "%s: failed to read a number from standard input\n", argv[0]);
    return 0;
}

и этот код (как pipe23.c, скомпилированный в pipe23):

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

int main(void)
{
    int stdout_copy = dup(STDOUT_FILENO);
    int testpipe[2];
    pipe(testpipe);
    int PID = fork();
    if (PID == 0)
    {
        dup2(testpipe[0], 0);
        dup2(testpipe[1], 1);
        close(testpipe[1]);
        close(testpipe[0]);
        execl("./multby", "multby", "3", NULL);// Just multiplies argument 3 with the number in testpipe.
        fprintf(stderr, "failed to execute ./multby (%d: %s)\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }
    else
    {
        dup2(testpipe[1], 1);
        close(testpipe[1]);
        printf("5\n");
        fflush(stdout);
        close(1);
        wait(NULL);
        char initialval[100];
        read(testpipe[0], initialval, 100);
        close(testpipe[0]);
        fprintf(stderr, "initial value: [%s]\n", initialval);
        dup2(stdout_copy, 1);
        printf("done\n");
    }
}

комбинация едва работает — это неустойчивое решение. Например, я добавил новую строку после 5. Ребенок ждет еще один символ после 5, чтобы определить, что он закончил чтение числа. Он не получает EOF, потому что у него открыт конец канала для записи для отправки ответа родителю, даже если он зависает при чтении из канала, поэтому он никогда не будет писать в него. Но поскольку он пытается прочитать только одно число, все в порядке.

Результат:

initial value: [15
]
done

Исправление кода — надежное решение

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

Вот модифицированный multby.c, который зацикливается при чтении:

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

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage; %s number", argv[0]);
        return 1;
    }
    int multiplier = atoi(argv[1]);
    int number;
    while (scanf("%d", &number) == 1)
        printf("%d\n", number * multiplier);
    return 0;
}

а вот модифицированный pipe23.c, который использует два конвейера и записывает 3 числа дочернему элементу и возвращает три результата. Обратите внимание, что в этой организации нет необходимости ставить новую строку после третьего числа (хотя не было бы никакого вреда, если бы она включала новую строку). Кроме того, если вы хитры, второй пробел в списке чисел тоже не нужен; - не является частью второго числа, поэтому сканирование останавливается после 0.

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

int main(void)
{
    int stdout_copy = dup(STDOUT_FILENO);
    int p_to_c[2];
    int c_to_p[2];
    if (pipe(p_to_c) != 0 || pipe(c_to_p) != 0)
    {
        fprintf(stderr, "failed to open a pipe (%d: %s)\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }
    int PID = fork();
    if (PID == 0)
    {
        dup2(p_to_c[0], 0);
        dup2(c_to_p[1], 1);
        close(c_to_p[0]);
        close(c_to_p[1]);
        close(p_to_c[0]);
        close(p_to_c[1]);
        execl("./multby", "multby", "3", NULL);// Just multiplies argument 3 with the number in testpipe.
        fprintf(stderr, "failed to execute ./multby (%d: %s)\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }
    else
    {
        dup2(p_to_c[1], 1);
        close(p_to_c[1]);
        close(p_to_c[0]);
        close(c_to_p[1]);
        printf("5 10 -15");
        fflush(stdout);
        close(1);
        char initialval[100];
        int n = read(c_to_p[0], initialval, 100);
        if (n < 0)
        {
            fprintf(stderr, "failed to read from the child (%d: %s)\n", errno, strerror(errno));
            exit(EXIT_FAILURE);
        }
        close(c_to_p[0]);
        wait(NULL);
        fprintf(stderr, "initial value: [%.*s]\n", n, initialval);
        dup2(stdout_copy, 1);
        printf("done\n");
    }
}

Обратите внимание, что здесь много вызовов close() — по два для каждого из 4 дескрипторов, участвующих в обработке двух каналов. Это нормально. Если не позаботиться о закрытии файловых дескрипторов, это может легко привести к зависанию системы.

Результат запуска этого pipe23 таков, что я и хотел:

initial value: [15
30
-45
]
done
person Jonathan Leffler    schedule 20.02.2020