【问题标题】:Race condition between piped command execution in a shell implementationshell 实现中管道命令执行之间的竞争条件
【发布时间】:2021-08-26 14:03:29
【问题描述】:

(我不确定我是否在标题中选择了正确的措辞,如果听起来不对,请纠正我,标签也是如此)

我正在编写一个必须能够执行管道命令的 shell 实现。这是一段执行ls | wc -l的简化代码(wc可以替换为sleepdate):

#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 %d\n", 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, &status, 0);
        printf("%d TERMINATED; WEXITSTATUS:%d, WTERMSIG:%d, STATUS:%d\n", 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 %d\n", 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, &status, 0);
        printf("%d TERMINATED; WEXITSTATUS:%d, WTERMSIG:%d, STATUS:%d\n", 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() 的联机帮助页,在寡妇管道上写入会通过接收 SIGPIPE(其值由 WTERMSIG() 宏正确提取)来强制写入过程终止,并且 @987654334 @ 退出较早,ls 尝试写入它但没有看到阅读器并捕获 SIGPIPE。但为什么在某些情况下这不是真的?为什么我的执行程序在应该退出时没有以 0 退出?是因为ls吗?

【问题讨论】:

  • 由于 printenv 不期望任何输入,它可能会在 ls 完成之前退出。或者反过来。
  • 我尝试了datesleep,它们似乎也没有等待输入,但由于 SIGPIPE,它仍然成功或失败
  • 您可能希望在关闭管道的读取端之前等待第一个进程退出(假设第二个进程之前已经退出)。
  • 很抱歉,这不是一个选项 -- cat /dev/urandom | head -c 100 会陷入僵局
  • 那你必须先杀掉之前的第一个进程,你期望什么?您已经知道错误行为的原因:您关闭管道太快了。考虑正确的时机,并做出相应的反应。没有灵丹妙药。

标签: c bash exec wait race-condition


【解决方案1】:

我认为这是正常行为。 shell管道的退出状态通常是管道最右边命令的退出状态。

GNU Bash shell 有一个可以启用的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 信号终止。

【讨论】:

    猜你喜欢
    • 2012-03-21
    • 2016-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-02
    • 2013-11-14
    • 1970-01-01
    • 2020-04-18
    相关资源
    最近更新 更多