[英]Issue implementing command pipes in a simple shell program
我正在用 C 編寫一個非常簡單的類似 bash 的 shell,目前正在命令之間實現管道(即 command1 | command2,它應該同時運行兩個命令,第一個命令的標准輸出通過管道連接,標准輸入為第二個)。
我已經到了這樣的地步
shell> echo test | cat | cat
正確地將“test”打印到字符串,但任何比這更復雜的東西都不會成功。 例如:
shell> ls -1 / | sort | rev
它(據我所知)在管道方面等同於前一個,但這個失敗了,另一個成功了。
我完全不知道為什么會這樣,因為我已經詳盡地調試了主進程和子進程,並驗證了這些進程在工作命令和非工作命令中都是通過正確的連接啟動的。
這是代碼的簡化版本:
// Uncomment to use hardcoded input
// #define USE_HARDCODED_INPUT
#include <stdlib.h>
#include <string.h>
#include <stddef.h> // NULL
#include <errno.h> // ENOENT
#include <stdio.h> // setbuf, printf
#include <unistd.h> // exec, fork
#include <fcntl.h> // open
#include <sys/types.h> // wait
#include <sys/wait.h>
void set_process_FDs(int input, int output, int error)
{
if (input)
{
dup2(input, STDIN_FILENO);
close(input);
}
if (output)
{
dup2(output, STDOUT_FILENO);
close(output);
}
if (error)
{
dup2(error, STDERR_FILENO);
close(error);
}
}
void child_setup(char **argv, int input, int output, int error)
{
if (input || output || error)
set_process_FDs(input, output, error);
execvp(argv[0], argv);
perror("exec()");
exit(1);
}
int launch_process(char **argv, int is_last,
int input, int output, int error)
{
int status;
pid_t pid = fork();
switch(pid)
{
case -1:
perror("fork()");
return 0;
case 0:
child_setup(argv, input, output, error);
return 0;
default:
break;
}
if (is_last)
wait(&status);
return 1;
}
int run_commands(char ***argvv)
{
int no_commands_ran = 0;
int argc;
char **argv = argvv[0];
int in_pipe[2];
int out_pipe[2];
for (int i=0; (argv = argvv[i]); ++i)
{
pipe(out_pipe);
if (i == 0)
in_pipe[0] = 0;
if (!argvv[i+1])
{
close(out_pipe[0]);
close(out_pipe[1]);
out_pipe[1] = 0;
}
for (argc=0; argv[argc]; ++argc);
if (!launch_process(argv, !argvv[i+1],
in_pipe[0], out_pipe[1], 0))
break;
if (i != 0)
{
close(in_pipe[0]);
close(in_pipe[1]);
}
in_pipe[0] = out_pipe[0];
in_pipe[1] = out_pipe[1];
no_commands_ran = i + 1;
}
return no_commands_ran;
}
extern int obtain_order(); // Obtains an order from stdin
int main(void)
{
char ***argvv = NULL;
int argvc;
char *filev[3] = {NULL, NULL, NULL};
int bg;
int ret;
setbuf(stdout, NULL); // Unbuffered
setbuf(stdin, NULL);
while (1)
{
#ifndef USE_HARDCODED_INPUT
printf("%s", "shell> "); // Prompt
ret = obtain_order(&argvv, filev, &bg);
if (ret == 0) // EOF
{
fprintf(stderr, "EOF\n");
break;
}
if (ret == -1)
continue; // Syntax error
argvc = ret - 1; // Line
if (argvc == 0)
continue; // Empty line
if (!run_commands(argvv))
continue; // Error executing command
#else
argvc = 3;
char ***argvv1 = calloc(4, sizeof(char*));
argvv1[0] = calloc(3, sizeof(char*));
argvv1[0][0] = strdup("echo");
argvv1[0][1] = strdup("test");
argvv1[1] = calloc(2, sizeof(char*));
argvv1[1][0] = strdup("cat");
argvv1[2] = calloc(2, sizeof(char*));
argvv1[2][0] = strdup("cat");
char ***argvv2 = calloc(4, sizeof(char*));
argvv2[0] = calloc(4, sizeof(char*));
argvv2[0][0] = strdup("ls");
argvv2[0][1] = strdup("-1");
argvv2[0][2] = strdup("/");
argvv2[1] = calloc(4, sizeof(char*));
argvv2[1][0] = strdup("sort");
argvv2[2] = calloc(4, sizeof(char*));
argvv2[2][0] = strdup("rev");
printf("%s", "shell> echo test | cat | cat\n");
if (!run_commands(argvv1))
continue; // Error executing command
usleep(500);
printf("%s", "shell> ls -1 / | sort | rev\n");
if (!run_commands(argvv2))
continue; // Error executing command
printf("%s", "\nNo more hardcoded commands to run\n");
break;
#endif
}
return 0;
}
obtain_order() 是一個位於解析器中的函數,它是一個簡單的 Yacc 解析器。 它只是用 shell 中輸入的任何內容填充稱為 argvv 的 argvs 向量。 如果有人想嘗試代碼並查看問題,只需取消注釋開頭的 #define 即可查看手動鍵入有問題的命令后的行為。
首先,您的父進程不會等待其所有子進程完成執行。
這個對wait
的調用確實發生在最后一個子進程被派生之后
if (is_last)
wait(&status);
但它不一定等待最后一個子進程。 也就是說,它會在任何一個子進程執行完畢(或發生錯誤)時返回。
正確等待所有子進程完成,在run_commands
結束時,
/* ... */
/* reap children */
pid_t pid;
int status;
while ((pid = wait(&status)) > 0)
if (WIFEXITED(status))
fprintf(stderr, "LOG: Child<%ld> process exited with status<%d>\n",
(long) pid,
WEXITSTATUS(status));
return no_commands_ran;
暴露了第一個孩子掛起的事實,因為wait
阻止了父程序的執行。
(放置一些fprintf
語句后。這里的 █ 表示程序正在阻塞。)
shell> echo test | cat | cat
LOG: Child<30607> (echo)
LOG: Child<30608> (cat)
LOG: Child<30609> (cat)
LOG: Child<30607> process exited with status <0>
█
無需等待所有子進程,您正在創建孤兒進程。
至於為什么這些進程無法終止,這是因為某些文件描述符沒有被關閉。
調用launch_process
launch_process(argv, !argvv[i+1], in_pipe[0], out_pipe[1], 0)
確保in_pipe[0]
和out_pipe[1]
在子進程中關閉,但泄漏任何有效的文件描述符in_pipe[1]
或out_pipe[0]
。 由於那些泄漏的文件描述符在子進程中仍然打開,關聯的管道仍然有效,因此進程在等待更多數據到達時將繼續阻塞。
最快的解決方法是更改launch_process
以接受兩個管道
int launch_process(char **argv, int is_last,
int input[2], int output[2], int error);
通過兩個管道
if (!launch_process(argv, !argvv[i+1], in_pipe, out_pipe, 0))
關閉多余的文件描述符
case 0:
close(input[1]);
close(output[0]);
child_setup(argv, input[0], output[1], error);
return 0;
去掉
if (is_last)
wait(&status);
並將之前顯示的wait
循環添加到run_commands
的末尾。
這是一個粗略的、帶注釋的示例,用於建立一系列管道和流程。 它的工作方式與您的示例類似,並且可能有助於進一步展示必須打開、復制和關閉文件描述符的順序。
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wait.h>
int valid(int fd)
{
return fd >= 0;
}
/* these safe_* functions are a non-operation when passed negative values */
void safe_close(int fd)
{
if (valid(fd) && !valid(close(fd)))
perror("close");
}
void safe_dup2(int old, int new)
{
if (valid(old) && valid(new) && !valid(dup2(old, new)))
perror("dup2");
}
void execute(char *args[][8], size_t length)
{
int channel[2] = { -1, -1 };
for (size_t i = 0; i < length; i++) {
/* get previous reader in parent */
int from = channel[0];
/* close previous writer in parent */
safe_close(channel[1]);
/* create current-writer-to-next-reader pipe */
if (!valid(pipe(channel)))
perror("pipe");
int to = (i < length - 1) ? channel[1] : -1;
if (0 == fork()) {
/* duplicate previous reader to stdin in child */
safe_dup2(from, fileno(stdin));
/* close previous reader in child */
safe_close(from);
/* close next reader in current child */
safe_close(channel[0]);
/* duplicate current writer to stdout in child */
safe_dup2(to, fileno(stdout));
/* close current writer in child */
safe_close(to);
execvp(args[i][0], args[i]);
perror("exec");
exit(EXIT_FAILURE);
}
/* close previous reader in parent */
safe_close(from);
}
/* close final pipe in parent */
safe_close(channel[0]);
safe_close(channel[1]);
/* reap children */
pid_t pid;
int status;
while ((pid = wait(&status)) > 0)
if (WIFEXITED(status))
fprintf(stderr, "LOG: Child<%ld> process exited with status<%d>\n",
(long) pid,
WEXITSTATUS(status));
}
int main(void)
{
char *argv[][8] = {
{ "echo", "test" },
{ "cat" },
{ "cat", "-n" }
};
execute(argv, 3);
char *argv2[][8] = {
{ "ls", "-1", "/" },
{ "sort" },
{ "rev" }
};
execute(argv2, 3);
}
旁白:作為邊緣情況, 0
是有效的文件描述符。 set_process_FDs
的缺陷在於,如果關閉STDIN_FILENO
並獲取新的文件描述符,它可能為零。 if (output)
或if (error)
可能不會按預期運行。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.