Условие гонки между выполнением команд по конвейеру в реализации оболочки

#c #bash #exec #wait #race-condition

Вопрос:

(Я не уверен, что выбрал правильную формулировку в заголовке, поэтому поправьте меня, если это звучит неправильно, то же самое касается тегов)

Я пишу реализацию оболочки, которая должна быть способна выполнять переданные по конвейеру команды. Вот упрощенный фрагмент кода, который выполняется ls | wc -l ( wc может быть заменен на sleep или date ):

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

pid_t   family[2];
int     pipefd[2];
extern char **environ;

void    ft_exec(int i)
{
    char **ls = malloc(sizeof(char *) * 2);
    ls[0] = "/bin/ls";
    ls[1] = NULL;

    char **wc = malloc(sizeof(char *) * 3);
    wc[0] = "/usr/bin/wc";
    wc[1] = "-l";
    wc[2] = NULL;

    printf("ENTERED BY %dn", getpid());
    if (i == 0)
    {
        dup2(pipefd[1], 1);
        close(pipefd[0]);
        close(pipefd[1]);
        execve(ls[0], ls, environ);
    }
    if (i == 1)
    {
        dup2(pipefd[0], 0);
        close(pipefd[0]);
        close(pipefd[1]);
        execve(wc[0], wc, environ);
    }
}

void    ft_block_main_process(void)
{
    int     i;
    pid_t   terminated;
    int     status;

    i = 0;
    while (i < 2)
    {
        terminated = waitpid(-1, amp;status, 0);
        printf("%d TERMINATED; WEXITSTATUS:%d, WTERMSIG:%d, STATUS:%dn", terminated, WEXITSTATUS(status), WTERMSIG(status), status);
        if (terminated == family[0])
            close(pipefd[1]);
        else if (terminated == family[1])
            close(pipefd[0]);
        i  ;
    }
}

void    ft_interpret(void)
{
    int i;

    i = 0;
    while (i < 2)
    {
        family[i] = fork();
        if (family[i] == 0)
            ft_exec(i);
        i  ;
    }
    ft_block_main_process();
}

int main(int argc, char **argv)
{
    (void)argc;
    (void)argv;

    pipe(pipefd);
    ft_interpret();
}
 

Результат выглядит следующим образом:

 ENTERED BY 1511
ENTERED BY 1512
1511 TERMINATED; WEXITSTATUS:0, WTERMSIG:0, STATUS:0
       3
1512 TERMINATED; WEXITSTATUS:0, WTERMSIG:0, STATUS:0
 

и это правильно.

Тем не менее, если я попытаюсь изменить вторую команду, скажем, printenv на , это взорвется:

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

pid_t   family[2];
int     pipefd[2];
extern char **environ;

void    ft_exec(int i)
{
    char **ls = malloc(sizeof(char *) * 2);
    ls[0] = "/bin/ls";
    ls[1] = NULL;

    char **printenv = malloc(sizeof(char *) * 2);
    printenv[0] = "/usr/bin/printenv";
    printenv[1] = NULL;

    printf("ENTERED BY %dn", getpid());
    if (i == 0)
    {
        dup2(pipefd[1], 1);
        close(pipefd[0]);
        close(pipefd[1]);
        execve(ls[0], ls, environ);
    }
    if (i == 1)
    {
        dup2(pipefd[0], 0);
        close(pipefd[0]);
        close(pipefd[1]);
        execve(printenv[0], printenv, environ);
    }
}

void    ft_block_main_process(void)
{
    int     i;
    pid_t   terminated;
    int     status;

    i = 0;
    while (i < 2)
    {
        terminated = waitpid(-1, amp;status, 0);
        printf("%d TERMINATED; WEXITSTATUS:%d, WTERMSIG:%d, STATUS:%dn", terminated, WEXITSTATUS(status), WTERMSIG(status), status);
        if (terminated == family[0])
            close(pipefd[1]);
        else if (terminated == family[1])
            close(pipefd[0]);
        i  ;
    }
}

void    ft_interpret(void)
{
    int i;

    i = 0;
    while (i < 2)
    {
        family[i] = fork();
        if (family[i] == 0)
            ft_exec(i);
        i  ;
    }
    ft_block_main_process();
}

int main(int argc, char **argv)
{
    (void)argc;
    (void)argv;

    pipe(pipefd);
    ft_interpret();
}
 

На выходе получается:

 ENTERED BY 1937
ENTERED BY 1938
TMPDIR=/var/folders/zz/zyxvpxvq6csfxvn_n000ccj800334_/T/
<...> // environ stuff
_=/Users/aisraely/testing/./a.out
1938 TERMINATED; WEXITSTATUS:0, WTERMSIG:0, STATUS:0
1937 TERMINATED; WEXITSTATUS:0, WTERMSIG:13, STATUS:13
 

Теперь , если то же самое выполняется bash , оно завершается с 0:

 bash-3.2$ ls | printenv
TERM_PROGRAM=iTerm.app
<...> // environ stuff
_=/usr/bin/printenv
bash-3.2$ echo $?
0
 

Я понимаю, что , согласно справочной pipe() странице, запись на овдовевшем канале заставляет процесс записи завершаться получением СИГПАЙПА (значение которого правильно извлекается WTERMSIG() макросом), и он printenv завершается раньше, ls пытается записать в него, но не видит считывателя и ловит СИГПАЙП. Но почему в некоторых случаях это не так? Почему моя исполняемая программа не завершается с 0, когда должна? Это из ls -за чего ?

Комментарии:

1. Поскольку printenv не ожидает никаких входных данных, он может завершиться до завершения ls. Или наоборот.

2. Я попробовал с date and sleep , который также, похоже, не ждет ввода, но он все равно либо завершается успешно, либо завершается неудачно из-за SIGPIPE

3. Возможно, вам захочется дождаться завершения первого процесса, прежде чем закрывать конец канала для чтения (учитывая, что второй процесс завершался раньше).

4. извините, но это не вариант- cat /dev/urandom | head -c 100 тогда вы зайдете в тупик

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

Ответ №1:

Я думаю, что это нормальное поведение. Статус выхода из конвейера оболочки обычно является статусом выхода самой правой команды в конвейере.

Оболочка GNU Bash имеет pipefail опцию, которую можно включить. Если включено, статус выхода конвейера-это статус выхода самой правой команды, которая завершила работу с ненулевым статусом выхода.

Например:

 bash$ set -o pipefail  # turn pipefail option on
bash$ seq 0 1000000 | date
Thu Aug 26 14:49:51 UTC 2021
bash$ echo $?
141
bash$ set  o pipefail  # turn pipefail option off
bash$ seq 0 1000000 | date
Thu Aug 26 14:50:10 UTC 2021
bash$ echo $?
0
bash$ 
 

Состояние выхода 141 связано с seq тем, что команда завершается SIGPIPE сигналом.