shell 实现中管道命令执行之间的竞争条件
Race condition between piped command execution in a shell implementation
(我不确定我是否在标题中选择了正确的措辞,所以如果听起来不对请纠正我,标签也是如此)
我正在编写一个 shell 实现,它必须能够执行管道命令。这是执行 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 %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()
宏正确提取)强制写入过程终止),并且 printenv
提前退出,ls
尝试写入它但没有看到 reader 并捕获 SIGPIPE。但为什么在某些情况下这不是真的呢?为什么我的执行程序在应该退出时没有以 0 退出?是因为ls
吗?
我认为这是正常行为。 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
信号终止。
(我不确定我是否在标题中选择了正确的措辞,所以如果听起来不对请纠正我,标签也是如此)
我正在编写一个 shell 实现,它必须能够执行管道命令。这是执行 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 %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()
宏正确提取)强制写入过程终止),并且 printenv
提前退出,ls
尝试写入它但没有看到 reader 并捕获 SIGPIPE。但为什么在某些情况下这不是真的呢?为什么我的执行程序在应该退出时没有以 0 退出?是因为ls
吗?
我认为这是正常行为。 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
信号终止。