Закрытие неиспользуемых дескрипторов файла канала - PullRequest
0 голосов
/ 23 июня 2018

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

Когда процесс пытается записать в канал, для которого ни один процесс не имеет открытый дескриптор чтения, ядро ​​посылает сигнал SIGPIPE процесс написания. По умолчанию этот сигнал убивает процесс.

Source, Интерфейс программирования Linux, Майкл Керриск

write(), при ошибке возвращается -1, и errno устанавливается соответствующим образом. EPIPE fd подключен к трубе или сокету, конец чтения которого закрыто. Когда это произойдет, процесс записи также получит SIGPIPE сигнал. (Таким образом, возвращаемое значение записи видно, только если программа ловит, блокирует или игнорирует этот сигнал.)

Источник, справочные страницы.

Для этого я закрываю уже прочитанный дескриптор до fork(). Тем не менее, ни я не могу поймать SIGPIPE, ни ошибку печати write() на perror().

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/param.h>
#include <signal.h>
#define BUFSIZE 100

char const * errMsgPipe = "signal handled SIGPIPE\n";
int errMsgPipeLen;

void handler(int x) {
    write(2, errMsgPipe, errMsgPipeLen);
}

int main(void) {
    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 = handler;
    sigaction(SIGPIPE, &sa, 0);

    if (pipe(fd) == -1) {
        perror("Failed to create the pipe");
        return 1;
    }
    bytesin = strlen(bufin);
    childpid = fork();
    if (childpid == -1) {
        perror("Failed to fork");
        return 1;
    }

    close(fd[0]);

    if (childpid) {
        if (write(fd[1], bufout, strlen(bufout)+1) < 0) {
            perror("write");
        }
    }
    else
        bytesin = read(fd[0], bufin, BUFSIZE);
    fprintf(stderr, "[%ld]:my bufin is {%.*s}, my bufout is {%s}\n",
            (long)getpid(), bytesin, bufin, bufout);
    return 0;
}

Выход:

[22686]:my bufin is {empty}, my bufout is {hello soner}
[22687]:my bufin is {empty}, my bufout is {hello soner}

Ожидаемый результат:

[22686]:my bufin is {empty}, my bufout is {hello soner}
signal handled SIGPIPE or similar stuff

Ответы [ 2 ]

0 голосов
/ 24 июня 2018

Моя проблема связана с местом close(fd[0]); Я закомментирую ее причину в коде.Теперь я получаю ожидаемую ошибку.

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/param.h>
#include <signal.h>
#include <errno.h>
#define BUFSIZE 100

char const * errMsgPipe = "signal handled SIGPIPE\n";
int errMsgPipeLen;

void handler(int x) {
    write(2, errMsgPipe, errMsgPipeLen);
}

int main(void) {
    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 = handler;
    sigaction(SIGPIPE, &sa, 0);

    if (pipe(fd) == -1) {
        perror("Failed to create the pipe");
        return 1;
    }

    close(fd[0]); // <-- it's in order for no process has an open read descriptor


    int val = -999;
    bytesin = strlen(bufin);
    childpid = fork();
    if (childpid == -1) {
        perror("Failed to fork");
        return 1;
    }


/*
 * close(fd[0]); <---- if it were here, we wouldn't get expected error and signal
 *                      since, parent can be reached to write(fd[1], .... ) call
 *                      before the child close(fd[0]); call defined here it. 
 *          It means there is by child open read descriptor seen by parent.
 */

// sleep(1);     <---- we can prove my saying by calling sleep() here



    if (childpid) {

       val = write(fd[1], bufout, strlen(bufout)+1);
       if (val < 0) {
           perror("writing process error");
       }

    }
    else {
        bytesin = read(fd[0], bufin, BUFSIZE);
    }
    fprintf(stderr, "[%ld]:my bufin is {%.*s}, my bufout is {%s}\n",
            (long)getpid(), bytesin, bufin, bufout);
    return 0;
}

Вывод:

signal handled SIGPIPE
writing process error: Broken pipe
[27289]:my bufin is {empty}, my bufout is {hello soner}
[27290]:my bufin is {empty}, my bufout is {hello soner}

При этом говорят, что если операция записи родителя не удалась, дочерний элемент bufin содержит empty проверено.

0 голосов
/ 24 июня 2018

Независимая демонстрация того, почему закрытие конца чтения канала имеет значение

Вот сценарий, в котором закрытие конца чтения канала имеет значение:

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 .

...