Независимая демонстрация того, почему закрытие конца чтения канала имеет значение
Вот сценарий, в котором закрытие конца чтения канала имеет значение:
seq 65536 | sed 10q
Если процесс, запускающий seq
, не закрывает конец чтения канала, тогда seq
заполнит буфер канала (он хотел бы записать 382 110 байт, но буфер канала не такой большой), но поскольку существует процесс с открытым концом чтения канала (seq
), он не получит SIGPIPE или ошибку записи, поэтому он никогда не завершится.
Рассмотрим этот код. Программа запускает seq 65536 | sed 10q
, но в зависимости от того, вызывается ли она с какими-либо аргументами или нет, она закрывает или закрывает конец канала чтения для программы seq
. Когда программа запускается без аргументов, программа seq
никогда не получает SIGPIPE или ошибку записи на свой стандартный вывод, потому что существует процесс с открытым концом чтения канала - этот процесс сам по себе seq
.
#include "stderr.h"
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char **argv)
{
err_setarg0(argv[0]);
int fd[2];
int pid1;
int pid2;
if (pipe(fd) != 0)
err_syserr("failed to pipe: ");
if ((pid1 = fork()) < 0)
err_syserr("failed to fork 1: ");
else if (pid1 == 0)
{
char *sed[] = { "sed", "10q", 0 };
if (dup2(fd[0], STDIN_FILENO) < 0)
err_syserr("failed to dup2 read end of pipe to standard input: ");
close(fd[0]);
close(fd[1]);
execvp(sed[0], sed);
err_syserr("failed to exec %s: ", sed[0]);
}
else if ((pid2 = fork()) < 0)
err_syserr("failed to fork 2: ");
else if (pid2 == 0)
{
char *seq[] = { "seq", "65536", 0 };
if (dup2(fd[1], STDOUT_FILENO) < 0)
err_syserr("failed to dup2 write end of pipe to standard output: ");
close(fd[1]);
if (argc > 1)
close(fd[0]);
execvp(seq[0], seq);
err_syserr("failed to exec %s: ", seq[0]);
}
else
{
int corpse;
int status;
close(fd[0]);
close(fd[1]);
printf("read end of pipe is%s closed for seq\n", (argc > 1) ? "" : " not");
printf("shell process is PID %d\n", (int)getpid());
printf("sed launched as PID %d\n", pid1);
printf("seq launched as PID %d\n", pid2);
while ((corpse = wait(&status)) > 0)
printf("%d exited with status 0x%.4X\n", corpse, status);
printf("shell process is exiting\n");
}
}
Код библиотеки доступен в моем репозитории SOQ (Вопросы о переполнении стека) на GitHub в виде файлов stderr.c
и stderr.h
в подкаталоге src / libsoq .
Вот пара примеров прогонов (программа называлась fork29
):
$ fork29
read end of pipe is not closed for seq
shell process is PID 90937
sed launched as PID 90938
seq launched as PID 90939
1
2
3
4
5
6
7
8
9
10
90938 exited with status 0x0000
^C
$ fork29 close
read end of pipe is closed for seq
shell process is PID 90940
sed launched as PID 90941
seq launched as PID 90942
1
2
3
4
5
6
7
8
9
10
90941 exited with status 0x0000
90942 exited with status 0x000D
shell process is exiting
$
Обратите внимание, что состояние выхода seq
во втором примере указывает, что он умер от сигнала 13, SIGPIPE.
Вопрос о решении выше
(1) Как мы можем быть уверены, что здесь seq
выполняется до sed
? Как там нет расы?
Две программы (seq
и sed
) выполняются одновременно. sed
не может читать что-либо, пока seq
не произведет это. seq
может заполнить канал до того, как sed
прочитает что-либо, или он может заполнить его только после того, как sed
завершит работу.
(2) Почему мы закрываем fd[0]
и fd[1]
в sed
? Почему не только fd[1]
? Аналогично для seq
.
Правило большого пальца : Если вы
dup2()
один конец трубы к стандартному входу или стандартному выходу, закройте оба
оригинальные дескрипторы файлов, возвращаемые
pipe()
так скоро, как возможно.
В частности, вы должны закрыть их перед использованием любого из
exec*()
семейство функций.
Правило также применяется, если вы дублируете дескрипторы либо
dup()
или же
fcntl()
с F_DUPFD
Код sed
соответствует правилу большого пальца. Код для seq
делает это только условно, поэтому вы можете видеть, что происходит, если вы не следуете правилу большого пальца.
Независимая демонстрация того, почему закрытие конца записи канала имеет значение
Вот сценарий, в котором закрытие конца записи канала имеет значение:
ls -l | sort
Если процесс, запускающий sort
, не закрывает конец записи канала, тогда sort
может выполнить запись в канал, поэтому он никогда не увидит EOF в канале, поэтому он никогда не завершится.
Рассмотрим этот код. Программа запускает ls -l | sort
, но в зависимости от того, вызывается она с какими-либо аргументами или нет, она закрывает конец записи канала для программы sort
или нет. Когда он запускается без аргументов, программа sort
никогда не видит EOF на своем стандартном входе, потому что есть процесс с открытым концом записи канала - этот процесс сам по себе sort
.
#include "stderr.h"
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char **argv)
{
err_setarg0(argv[0]);
int fd[2];
int pid1;
int pid2;
if (pipe(fd) != 0)
err_syserr("failed to pipe: ");
if ((pid1 = fork()) < 0)
err_syserr("failed to fork 1: ");
else if (pid1 == 0)
{
char *sort[] = { "sort", 0 };
if (dup2(fd[0], STDIN_FILENO) < 0)
err_syserr("failed to dup2 read end of pipe to standard input: ");
close(fd[0]);
if (argc > 1)
close(fd[1]);
execvp(sort[0], sort);
err_syserr("failed to exec %s: ", sort[0]);
}
else if ((pid2 = fork()) < 0)
err_syserr("failed to fork 2: ");
else if (pid2 == 0)
{
char *ls[] = { "ls", "-l", 0 };
if (dup2(fd[1], STDOUT_FILENO) < 0)
err_syserr("failed to dup2 write end of pipe to standard output: ");
close(fd[1]);
close(fd[0]);
execvp(ls[0], ls);
err_syserr("failed to exec %s: ", ls[0]);
}
else
{
int corpse;
int status;
close(fd[0]);
close(fd[1]);
printf("write end of pipe is%s closed for sort\n", (argc > 1) ? "" : " not");
printf("shell process is PID %d\n", (int)getpid());
printf("sort launched as PID %d\n", pid1);
printf("ls launched as PID %d\n", pid2);
while ((corpse = wait(&status)) > 0)
printf("%d exited with status 0x%.4X\n", corpse, status);
printf("shell process is exiting\n");
}
}
Вот пара примеров прогонов (программа называлась fork13
):
$ fork13
write end of pipe is not closed for sort
shell process is PID 90737
sort launched as PID 90738
ls launched as PID 90739
90739 exited with status 0x0000
^C
$ fork13 close
write end of pipe is closed for sort
shell process is PID 90741
sort launched as PID 90742
ls launched as PID 90743
90743 exited with status 0x0000
-rw-r--r-- 1 jleffler staff 1583 Jun 23 14:20 fork13.c
-rwxr-xr-x 1 jleffler staff 22216 Jun 23 14:20 fork13
drwxr-xr-x 3 jleffler staff 96 Jun 23 14:06 fork13.dSYM
total 56
90742 exited with status 0x0000
shell process is exiting
$
(3) Почему нам нужно закрыть как fd[0]
, так и fd[1]
в их родителе?
Родительский процесс не активно использует созданный им канал. Он должен закрыть его полностью, иначе другие программы не закончатся. Попробуйте - я сделал (непреднамеренно), и программы не ведут себя так, как я планировал (ожидал). Мне потребовалось несколько секунд, чтобы понять, что я не сделал!
Адаптация кода из ответа по ОП
snr опубликовал 'ответ' , пытаясь продемонстрировать обработку сигналов и что происходит с закрытием (или нет) конца чтения дескрипторов файла канала.Вот адаптация этого кода в программу, которой можно управлять с помощью параметров командной строки, где перестановки параметров могут давать разные и полезные результаты.Опции -b
и -a
позволяют вам закрыть конец чтения канала до или после разветвления (или вообще не закрывать его).-h
и -i
позволяют вам обрабатывать SIGPIPE с помощью обработчика сигнала или игнорировать его (или использовать обработку по умолчанию - завершение).А опция -d
позволяет вам задержать родителя на 1 секунду, прежде чем он попытается записать.
#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "stderr.h"
#define BUFSIZE 100
static char const *errMsgPipe = "signal handled SIGPIPE\n";
static int errMsgPipeLen;
static void handler(int x)
{
if (x == SIGPIPE)
write(2, errMsgPipe, errMsgPipeLen);
}
static inline void print_bool(const char *tag, bool value)
{
printf(" %5s: %s\n", (value) ? "true" : "false", tag);
}
int main(int argc, char **argv)
{
err_setarg0(argv[0]);
bool sig_ignore = false;
bool sig_handle = false;
bool after_fork = false;
bool before_fork = false;
bool parent_doze = false;
static const char usestr[] = "[-abdhi]";
int opt;
while ((opt = getopt(argc, argv, "abdhi")) != -1)
{
switch (opt)
{
case 'a':
after_fork = true;
break;
case 'b':
before_fork = true;
break;
case 'd':
parent_doze = true;
break;
case 'h':
sig_handle = true;
break;
case 'i':
sig_ignore = true;
break;
default:
err_usage(usestr);
}
}
if (optind != argc)
err_usage(usestr);
/* Both these happen naturally - but should be explicit when printing configuration */
if (sig_handle && sig_ignore)
sig_ignore = false;
if (before_fork && after_fork)
after_fork = false;
printf("Configuration:\n");
print_bool("Close read fd before fork", before_fork);
print_bool("Close read fd after fork", after_fork);
print_bool("SIGPIPE handled", sig_handle);
print_bool("SIGPIPE ignored", sig_ignore);
print_bool("Parent doze", parent_doze);
err_setlogopts(ERR_PID);
errMsgPipeLen = strlen(errMsgPipe);
char bufin[BUFSIZE] = "empty";
char bufout[] = "hello soner";
int bytesin;
pid_t childpid;
int fd[2];
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_flags = 0;
sigfillset(&sa.sa_mask);
sa.sa_handler = SIG_DFL;
if (sig_ignore)
sa.sa_handler = SIG_IGN;
if (sig_handle)
sa.sa_handler = handler;
if (sigaction(SIGPIPE, &sa, 0) != 0)
err_syserr("sigaction(SIGPIPE) failed: ");
printf("Parent: %d\n", (int)getpid());
if (pipe(fd) == -1)
err_syserr("pipe failed: ");
if (before_fork)
close(fd[0]);
int val = -999;
bytesin = strlen(bufin);
childpid = fork();
if (childpid == -1)
err_syserr("fork failed: ");
if (after_fork)
close(fd[0]);
if (childpid)
{
if (parent_doze)
sleep(1);
val = write(fd[1], bufout, strlen(bufout) + 1);
if (val < 0)
err_syserr("write to pipe failed: ");
err_remark("Parent wrote %d bytes to pipe\n", val);
}
else
{
bytesin = read(fd[0], bufin, BUFSIZE);
if (bytesin < 0)
err_syserr("read from pipe failed: ");
err_remark("Child read %d bytes from pipe\n", bytesin);
}
fprintf(stderr, "[%ld]:my bufin is {%.*s}, my bufout is {%s}\n",
(long)getpid(), bytesin, bufin, bufout);
return 0;
}
Может быть трудно (во всяком случае, неочевидно) отследить, что происходит с родительским процессом.Bash генерирует состояние выхода из 128 + номер сигнала, когда ребенок умирает от сигнала.На этой машине SIGPIPE равен 13, поэтому состояние выхода 141 указывает на смерть от SIGPIPE.
Пример запускается:
$ pipe71; echo $?
Configuration:
false: Close read fd before fork
false: Close read fd after fork
false: SIGPIPE handled
false: SIGPIPE ignored
false: Parent doze
Parent: 97984
pipe71: pid=97984: Parent wrote 12 bytes to pipe
[97984]:my bufin is {empty}, my bufout is {hello soner}
pipe71: pid=97985: Child read 12 bytes from pipe
[97985]:my bufin is {hello soner}, my bufout is {hello soner}
0
$ pipe71 -b; echo $?
Configuration:
true: Close read fd before fork
false: Close read fd after fork
false: SIGPIPE handled
false: SIGPIPE ignored
false: Parent doze
Parent: 97987
pipe71: pid=97988: read from pipe failed: error (9) Bad file descriptor
141
$ pipe71 -a; echo $?
Configuration:
false: Close read fd before fork
true: Close read fd after fork
false: SIGPIPE handled
false: SIGPIPE ignored
false: Parent doze
Parent: 98000
pipe71: pid=98000: Parent wrote 12 bytes to pipe
[98000]:my bufin is {empty}, my bufout is {hello soner}
0
pipe71: pid=98001: read from pipe failed: error (9) Bad file descriptor
$ pipe71 -a -d; echo $?
Configuration:
false: Close read fd before fork
true: Close read fd after fork
false: SIGPIPE handled
false: SIGPIPE ignored
true: Parent doze
Parent: 98004
pipe71: pid=98005: read from pipe failed: error (9) Bad file descriptor
141
$ pipe71 -h -a -d; echo $?
Configuration:
false: Close read fd before fork
true: Close read fd after fork
true: SIGPIPE handled
false: SIGPIPE ignored
true: Parent doze
Parent: 98007
pipe71: pid=98008: read from pipe failed: error (9) Bad file descriptor
signal handled SIGPIPE
pipe71: pid=98007: write to pipe failed: error (32) Broken pipe
1
$ pipe71 -h -a; echo $?
Configuration:
false: Close read fd before fork
true: Close read fd after fork
true: SIGPIPE handled
false: SIGPIPE ignored
false: Parent doze
Parent: 98009
pipe71: pid=98009: Parent wrote 12 bytes to pipe
[98009]:my bufin is {empty}, my bufout is {hello soner}
pipe71: pid=98010: read from pipe failed: error (9) Bad file descriptor
0
$ pipe71 -i -a; echo $?
Configuration:
false: Close read fd before fork
true: Close read fd after fork
false: SIGPIPE handled
true: SIGPIPE ignored
false: Parent doze
Parent: 98013
pipe71: pid=98013: Parent wrote 12 bytes to pipe
[98013]:my bufin is {empty}, my bufout is {hello soner}
0
pipe71: pid=98014: read from pipe failed: error (9) Bad file descriptor
$ pipe71 -d -i -a; echo $?
Configuration:
false: Close read fd before fork
true: Close read fd after fork
false: SIGPIPE handled
true: SIGPIPE ignored
true: Parent doze
Parent: 98015
pipe71: pid=98016: read from pipe failed: error (9) Bad file descriptor
pipe71: pid=98015: write to pipe failed: error (32) Broken pipe
1
$ pipe71 -i -a; echo $?
Configuration:
false: Close read fd before fork
true: Close read fd after fork
false: SIGPIPE handled
true: SIGPIPE ignored
false: Parent doze
Parent: 98020
pipe71: pid=98020: Parent wrote 12 bytes to pipe
[98020]:my bufin is {empty}, my bufout is {hello soner}
0
pipe71: pid=98021: read from pipe failed: error (9) Bad file descriptor
$
На моей машине (MacBook Pro с MacOS High Sierra 10.13).5, с GCC 8.1.0), если я не откладываю родителя, родитель последовательно записывает в канал, прежде чем ребенок добирается до закрытия дескриптора файла.Это, однако, не гарантированное поведение.Можно было бы добавить другую опцию (например, -n
для child_nap
), чтобы заставить ребенка спать на секунду.
Код доступен на GitHub
Код для показанных программвыше (fork29.c
, fork13.c
, pipe71.c
) доступны в моем репозитории SOQ (вопросы о переполнении стека) на GitHub в виде файлов fork13.c
, fork29.c
, pipe71.c
в * 1146Подкаталог * src / so-5100-4470 .