Я удивлен, что есть проблема, но, похоже, это проблема в Linux (я тестировал Ubuntu 16.04 LTS, работающую в VMWare Fusion VM на моем Mac) - но это не было проблемой на моем Mac macOS 10.13.4 (High Sierra), и я не ожидаю, что это станет проблемой и для других вариантов Unix.
Как я отмечал в комментарии :
За каждым потоком стоит описание открытого файла и дескриптор открытого файла. Когда процесс разветвляется, дочерний элемент имеет свой собственный набор дескрипторов открытых файлов (и файловых потоков), но каждый дескриптор файла в дочернем элементе разделяет описание открытого файла с родителем. IF (и это большое «если»), дочерний процесс, закрывающий файловые дескрипторы, сначала сделал эквивалент lseek(fd, 0, SEEK_SET)
, затем это также поместило бы файловый дескриптор для родительского процесса. и это может привести к бесконечному циклу. Однако я никогда не слышал о библиотеке, которая занимается этим; нет причин делать это.
См. POSIX open()
и fork()
для получения дополнительной информации об дескрипторах открытых файлов и описаниях открытых файлов.
дескрипторы открытых файлов являются частными для процесса; описания открытого файла являются общими для всех копий файлового дескриптора, созданного начальной операцией «открыть файл» Одним из ключевых свойств описания открытого файла является текущая позиция поиска. Это означает, что дочерний процесс может изменить текущую позицию поиска для родителя - потому что он находится в описании общего открытого файла.
neof97.c
Я использовал следующий код - слегка адаптированную версию оригинала, которая аккуратно компилируется с строгими параметрами компиляции:
#include "posixver.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
enum { MAX = 100 };
int main(void)
{
if (freopen("input.txt", "r", stdin) == 0)
return 1;
char s[MAX];
for (int i = 0; i < 30 && fgets(s, MAX, stdin) != NULL; i++)
{
// Commenting out this region fixes the issue
int status;
pid_t pid = fork();
if (pid == 0)
{
exit(0);
}
else
{
waitpid(pid, &status, 0);
}
// End region
printf("%s", s);
}
return 0;
}
Одна из модификаций ограничивает количество циклов (дочерних) только 30.
Я использовал файл данных с 4 строками из 20 случайных букв плюс символ новой строки (всего 84 байта):
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
Я запустил команду под strace
в Ubuntu:
$ strace -ff -o st-out -- neof97
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
…
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
$
Было 31 файл с именами вида st-out.808##
, где хэши были двузначными числами. Основной файл процесса был довольно большим; остальные были небольшими, с одним из размеров 66, 110, 111 или 137:
$ cat st-out.80833
lseek(0, -63, SEEK_CUR) = 21
exit_group(0) = ?
+++ exited with 0 +++
$ cat st-out.80834
lseek(0, -42, SEEK_CUR) = -1 EINVAL (Invalid argument)
exit_group(0) = ?
+++ exited with 0 +++
$ cat st-out.80835
lseek(0, -21, SEEK_CUR) = 0
exit_group(0) = ?
+++ exited with 0 +++
$ cat st-out.80836
exit_group(0) = ?
+++ exited with 0 +++
$
Так уж вышло, что первые 4 ребенка демонстрировали одно из четырех поведений - и каждый следующий набор из 4 детей демонстрировал один и тот же паттерн.
Это показывает, что трое из четырех детей действительно делали lseek()
на стандартном вводе перед выходом. Очевидно, я теперь видел библиотеку, делающую это. Я понятия не имею, почему это считается хорошей идеей, но опытным путем именно это и происходит.
neof67.c
Эта версия кода, использующая отдельный файловый поток (и дескриптор файла) и fopen()
вместо freopen()
, также сталкивается с проблемой.
#include "posixver.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
enum { MAX = 100 };
int main(void)
{
FILE *fp = fopen("input.txt", "r");
if (fp == 0)
return 1;
char s[MAX];
for (int i = 0; i < 30 && fgets(s, MAX, fp) != NULL; i++)
{
// Commenting out this region fixes the issue
int status;
pid_t pid = fork();
if (pid == 0)
{
exit(0);
}
else
{
waitpid(pid, &status, 0);
}
// End region
printf("%s", s);
}
return 0;
}
Это также демонстрирует то же поведение, за исключением того, что дескриптор файла, по которому происходит поиск, равен 3
вместо 0
. Итак, две мои гипотезы опровергнуты - они связаны с freopen()
и stdin
; оба кода неверно показаны во втором коде теста.
Предварительный диагноз
ИМО, это ошибка. Вы не должны быть в состоянии столкнуться с этой проблемой.
Скорее всего, это ошибка в библиотеке Linux (GNU C), а не в ядре. Это вызвано lseek()
в дочерних процессах. Непонятно (потому что я не пошел смотреть исходный код), что делает библиотека и почему.
Ошибка GLIBC 23151
GLIBC Ошибка 23151 - Разветвленный процесс с незамкнутым файлом выполняет lseek перед выходом и может вызвать бесконечный цикл в родительском вводе / выводе.
Ошибка была создана 2019-05-08 в США / Тихоокеанском регионе и закрыта как НЕДЕЙСТВИТЕЛЬНАЯ к 2018-05-09. Причина была:
Пожалуйста, прочитайте
http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_05_01,
особенно этот пункт:
Обратите внимание, что после fork()
две ручки существуют там, где одна существовала раньше. [...]
1085 * POSIX *
СоУпомянутый раздел POSIX (помимо словоблудия, отмечающий, что это не охватывается стандартом C), является следующим:
2.5.1 Взаимодействие файловых дескрипторов и стандартных потоков ввода / вывода
Открыть описание файла можно через дескриптор файла, который создается с помощью таких функций, как open()
или pipe()
, или через созданный поток используя такие функции, как fopen()
или popen()
. Дескриптор файла или поток называется «дескриптором» описания открытого файла, к которому он относится; описание открытого файла может иметь несколько дескрипторов.
Дескрипторы могут быть созданы или уничтожены явным действием пользователя, не влияя на основное описание открытого файла. Некоторые из способов их создания включают fcntl()
, dup()
, fdopen()
, fileno()
и fork()
. Они могут быть уничтожены по крайней мере fclose()
, close()
и exec
функциями.
Дескриптор файла, который никогда не используется в операции, которая может повлиять на смещение файла (например, read()
, write()
или lseek()
) не считается ручкой для этого обсуждения, но может привести к одному (например, вследствие fdopen()
, dup()
или fork()
). Это исключение не включает дескриптор файла, лежащий в основе потока, независимо от того, был ли он создан с помощью fopen()
или fdopen()
, если он не используется приложением напрямую для воздействия на файл смещение. Функции read()
и write()
неявно влияют на смещение файла; lseek()
явно влияет на него.
Результат вызовов функций, включающих какой-либо один дескриптор («активный дескриптор»), определен в другом месте этого тома POSIX.1-2017, но если используются два или более дескрипторов, и любой из них является потоком, Приложение должно гарантировать, что их действия скоординированы, как описано ниже. Если этого не сделать, результат не определен.
Дескриптор, который является потоком, считается закрытым, когда fclose()
или freopen()
с неполным (1) имя файла, выполняется на нем (для freopen()
с нулевым именем файла, это определяется реализацией, создается ли новый дескриптор или повторно используется существующий), или когда процесс, владеющий этим потоком, завершается с помощью exit()
, abort()
или по сигналу. Дескриптор файла закрывается с помощью функций close()
, _exit()
или exec()
, если для этого файлового дескриптора установлено значение FD_CLOEXEC.
(1) [sic] Использование 'non-full', вероятно, является опечаткой для 'non-null'.
Чтобы дескриптор стал активным дескриптором, приложение должно обеспечить выполнение следующих действий между последним использованием дескриптора (текущий активный дескриптор) и первым использованием второго дескриптора (будущий активный дескриптор). Затем вторая ручка становится активной. Все действия приложения, влияющие на смещение файла на первом дескрипторе, должны быть приостановлены до тех пор, пока он снова не станет активным дескриптором файла. (Если потоковая функция имеет в качестве базовой функции функцию, которая влияет на смещение файла, считается, что функция потока влияет на смещение файла.)
Дескрипторы не должны быть в одном и том же процессе для применения этих правил. Обратите внимание, что после fork()
существуют две ручки, где одна существовала раньше. Приложение должно гарантировать, что, если оба дескриптора будут доступны, они оба находятся в состоянии, когда другой может стать активным дескриптором первым. Заявка должна быть подготовлена для fork()
точно так же, как если бы это была смена активной ручки. (Если единственным действием, выполняемым одним из процессов, является одна из функций exec()
или _exit()
(не exit()
), дескриптор никогда не доступны в этом процессе.)
Для первой ручки применяется первое условие, указанное ниже. После выполнения действий, требуемых ниже, если дескриптор все еще открыт, приложение может его закрыть.
Если это дескриптор файла, никаких действий не требуется.
Если единственное дальнейшее действие, которое нужно выполнить с любым дескриптором этого дескриптора открытого файла, - это закрыть его, никаких действий предпринимать не нужно.
Если это поток без буферизации, никаких действий предпринимать не нужно.
Если это поток с буферизацией строки, и последний байт, записанный в поток, был <newline>
(то есть, как если бы:
putc('\n')
была самой последней операцией в этом потоке), никаких действий предпринимать не нужно.
Если это поток, который открыт для записи или добавления (но не открыт для чтения), приложение должно выполнить fflush()
, или поток должен быть закрыт.
Если поток открыт для чтения и находится в конце файла (feof()
- true), никаких действий предпринимать не нужно.
Если поток открыт с помощью режима, который позволяет читать, и основное описание открытого файла относится к устройству, которое способно искать, приложение должно либо выполнить fflush()
, либо поток должен быть закрыт.
Для второй ручки:
Если какой-либо предыдущий активный дескриптор использовался функцией, которая явно изменила смещение файла, кроме случаев, указанных выше для первого дескриптора, приложение должно выполнить lseek()
или fseek()
(в зависимости от типа ручки) в соответствующее место.
Если активный дескриптор перестает быть доступным до того, как будут выполнены требования к первому дескриптору, описанному выше, состояние описания открытого файла становится неопределенным. Это может произойти во время таких функций, как fork()
или _exit()
.
Функции exec()
делают недоступными все потоки, открытые во время их вызова, независимо от того, какие потоки или файловые дескрипторы могут быть доступны для нового образа процесса.
При соблюдении этих правил, независимо от последовательности используемых дескрипторов, реализации должны гарантировать, что приложение, даже если оно состоит из нескольких процессов, должно давать правильные результаты: никакие данные не должны быть потеряны или дублированы при записи, а все данные должны быть написано в порядке, кроме как запрашивает ищет. Это определяется реализацией, если и при каких условиях все входные данные видны ровно один раз.
Говорят, что каждая функция, работающая с потоком, имеет ноль или более "базовых функций". Это означает, что функция потока разделяет определенные черты с базовыми функциями, но не требует наличия какой-либо связи между реализациями функции потока и ее базовыми функциями.
Толкование
Это трудно читать! Если вам неясно различие между дескриптором открытого файла и описанием открытого файла, прочитайте спецификации open()
и fork()
(и dup()
или dup2()
). Определения для дескриптора файла и открытого описания файла также актуальны, если кратко.
В контексте кода в этом вопросе (а также для нежелательных дочерних процессов, создаваемых при чтении файла ), у нас есть дескриптор потока файлов, открытый только для чтения, который еще не встречал EOF (поэтому feof()
не вернет true, даже если позиция чтения находится в конце файла).
Одной из важных частей спецификации является: Заявка должна подготовиться к fork()
точно так же, как если бы это было изменение активной ручки.
Это означает, что шаги, описанные для «первого дескриптора файла», актуальны, и, шагая через них, первое применимое условие является последним:
- Если поток открыт с помощью режима, который позволяет читать, и основное описание открытого файла относится к устройству, которое способно искать, приложение должно выполнить
fflush()
, или поток должен быть закрыт.
Если вы посмотрите на определение fflush()
, вы найдете:
Если stream указывает на выходной поток или поток обновления, в который не была введена самая последняя операция, fflush()
должен привести к тому, что любые неписанные данные для этого потока будут записаны в файл, [CX ] ⌦ и отметки времени последнего изменения данных и последнего изменения состояния файла базового файла должны быть помечены для обновления.
Для потока, открытого для чтения с базовым описанием файла, если файл еще не находится в EOF, и файл способен искать, смещение файла базового описания открытого файла должно быть установлено в позицию файла: поток и любые символы, выдвинутые обратно в поток с помощью ungetc()
или ungetwc()
, которые впоследствии не были прочитаны из потока, должны быть отброшены (без дальнейшего изменения смещения файла ). ⌫
Не совсем ясно, что произойдет, если вы примените fflush()
к входному потоку, связанному с файлом без возможности поиска, но это не наша непосредственная задача. Однако, если вы пишете универсальный код библиотеки, вам может потребоваться узнать, доступен ли для поиска дескриптор файла, прежде чем выполнить fflush()
в потоке. В качестве альтернативы, используйте fflush(NULL)
, чтобы система делала все необходимое для всех потоков ввода / вывода, отмечая, что при этом будут потеряны любые символы возврата (через ungetc()
и т. Д.).
Операции lseek()
, показанные в выводе strace
, по-видимому, реализуют семантику fflush()
, связывающую смещение файла описания открытого файла с позицией файла потока.
Итак, для кода в этом вопросе кажется, что fflush(stdin)
необходим перед fork()
для обеспечения согласованности. Невыполнение этого приводит к неопределенному поведению («если это не сделано, результат не определен») - например, к бесконечному циклу.