看来您正在尝试编写一个 shell 来运行从输入读取的命令(如果不是这种情况;请编辑您的问题,因为它不清楚)。
我不知道为什么你认为管道在像这样的命令中使用cat file.txt > file2.txt
,但无论如何,它们都不是。让我们看看当您键入时会发生什么cat file.txt > file2.txt
在像 bash 这样的 shell 中:
- 子进程创建于何处
cat(1)
会跑。
- 子进程打开
file2.txt
用于写作(稍后会详细介绍)。
- If
open(2)
成功后,子进程将新打开的文件描述符复制到stdout
(so stdout
将有效地指向相同的文件表条目file2.txt
).
-
cat(1)
通过调用七个之一来执行exec()
功能。论据file.txt
被传递给cat(1)
, so cat(1)
将打开file.txt
并阅读所有内容,将其内容复制到stdout
(被重定向到file2.txt
).
-
cat(1)
完成执行并终止,这会导致所有打开的文件描述符被关闭并刷新。到......的时候cat(1)
终止,file2.txt
是一个副本file.txt
.
- 同时,父 shell 进程等待子进程终止,然后再打印下一个提示并等待更多命令。
正如您所看到的,I/O 重定向中不使用管道。管道是一种进程间通信机制,用于将一个进程的输出提供给另一个进程的输入。你这里只有一个进程在运行(cat
),那为什么还需要管道呢?
这意味着您应该致电redirect()
with STDOUT_FILENO
as destinfd
(而不是管道通道)用于输出重定向。同样,输入重定向应该调用redirect()
with STDIN_FILENO
。这些常量定义在unistd.h
所以请确保包含该标头。
你也probably如果想要退出孩子exec()
失败,否则您将运行 shell 进程的 2 个副本。
最后但并非最不重要的一点是,您不应将输入或输出重定向设置为独占。用户可能需要输入和输出重定向。所以而不是else if
当进行 I/O 重定向时,我只会使用 2 个独立的 if。
考虑到这一点,您发布的主要代码应该类似于:
if((pid = fork()) < 0)
{
perror("fork error");
}
else if(pid > 0) // Parent
{
if(waitpid(pid,NULL,0) < 0)
{
perror("waitpid error");
}
}
else // Child
{
int flags = 0;
if(structVariables->outfile != NULL)
{
flags = 1; // Write
// We need STDOUT_FILENO here
redirect(structVariables->outfile, flags, STDOUT_FILENO);
}
if(structVariables->infile != NULL)
{
flags = 2; // Read
// Similarly, we need STDIN_FILENO here
redirect(structVariables->infile, flags, STDIN_FILENO);
}
// This line changed; see updated answer below
if(execvp(structVariables->argv[0], structVariables->argv) < 0)
{
perror("execvp error");
// Terminate
exit(EXIT_FAILURE);
}
}
正如另一个答案中提到的,你的redirect()
函数很容易出现竞争条件,因为在文件存在检查和实际文件创建之间存在一个时间窗口,其中另一个进程可以创建该文件(这称为 TOCTTOU 错误:检查时间到使用时间)。你应该使用O_CREAT | O_EXCL
以原子方式测试文件是否存在并创建文件。
另一个问题是你总是关闭newfd
。如果什么newfd
and destinfd
出于某种原因,碰巧是一样的?那么你会错误地关闭文件,因为dup2(2)
如果您传入两个相同的文件描述符,则本质上是无操作。即使您认为这种情况永远不会发生,在关闭原始 fd 之前先检查复制的 fd 是否与原始 fd 不同始终是一个好的做法。
这是解决这些问题的代码:
int redirect(char *filename, int flags, int destinfd)
{
int newfd;
if(flags == 1)
{
newfd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
if(newfd == -1)
{
perror("Open for write failed");
return -1;
}
}
else if(flags == 2)
{
newfd = open(filename, O_RDONLY);
if(newfd == -1)
{
perror("Open for read failed");
return -1;
}
}
else
return -1;
if(dup2(newfd, destinfd) == -1)
{
perror("dup2 failed");
close(newfd);
return -1;
}
if (newfd != destinfd)
close(newfd);
return destinfd;
}
考虑更换0666
in open(2)
上面有S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
(确保包括sys/stat.h
and fcntl.h
)。您可能想使用#define
为了使其更清晰,但我仍然认为,如果你这样做,而不是硬编码一些神奇的数字,它会更好,更具描述性(尽管这是主观的)。
我不会评论dupPipe()
因为这个问题不需要/使用它。 I/O 重定向就是您所需要的。如果您想将讨论扩展到管道,请随意编辑问题或创建另一个问题。
UPDATE
好的,现在我已经查看了完整的源代码,我还有一些评论。
原因cat(1)
挂起是因为这个:
if (execvp(structVariables->argv[0], argv) < 0)
第二个参数为execvp(2)
应该structVariables->argv
, not argv
, 因为argv
是 shell 程序的参数数组,(通常)为空。将空参数列表传递给cat(1)
使其读取自stdin
而不是来自文件,所以这就是它似乎挂起的原因 - 它正在等待您提供输入。因此,继续将该行替换为:
if (execvp(structVariables->argv[0], structVariables->argv) < 0)
这解决了您的问题之一:诸如此类的事情cat < file.txt > file2.txt
现在可以工作了(我测试过)。
关于管道重定向
所以现在我们需要进行管道重定向。每次我们看到管道重定向都会发生|
在命令行上。让我们通过一个例子来了解当我们输入时幕后发生了什么ls | grep "file.txt" | sort
。了解这些步骤非常重要,这样您就可以建立系统如何工作的准确心理模型;没有这样的愿景,你将无法真正理解实现:
- shell(通常)首先通过管道符号分割命令。这也是你的代码的作用。这意味着在解析之后,shell 已经收集了足够的信息,并且命令行被分成 3 个实体(
ls
命令,该grep
命令和sort
命令)。
shell 分叉并调用七个中的一个exec()
子函数运行ls
。现在,请记住,管道意味着程序的输出是下一个程序的输入,所以在之前exec()
在中,外壳必须创建一个管道。即将运行的子进程ls(1)
calls dup2(2)
before exec()
将管道的写入通道复制到stdout
。同样,父进程调用dup2(2)
将管道的读取通道复制到stdin
。了解此步骤非常重要:因为父级将管道的读取端复制到stdin
,那么无论我们接下来做什么(例如再次分叉以执行更多命令)都将始终从管道读取输入。所以,此时,我们有ls(1)
写信给stdout
,它被重定向到由 shell 的父进程读取的管道。
shell 现在将执行grep(1)
。再次,它派生了一个新进程来执行grep(1)
。请记住,文件描述符是通过 fork 继承的,并且父 shell 的进程具有stdin
连接到管道的读取端ls(1)
,所以即将执行的新子进程grep(1)
将“自动”从该管道中读取!但是等等,还有更多! shell 知道管道中还有另一个进程(sort
命令),因此在执行 grep 之前(以及分叉之前),shell 创建another连接输出的管道grep(1)
的输入sort(1)
。然后,它重复相同的步骤:在子进程上,管道的写入通道被复制到stdout
。在父级中,管道的读取通道被复制到stdin
。再次强调,真正理解这里发生的事情很重要:即将执行的流程grep(1)
已经从连接到的管道读取其输入ls(1)
,现在它的输出连接到将馈送的管道sort(1)
. So grep(1)
本质上是从管道中读取数据并写入管道。 OTOH,父 shell 进程将最后一个管道的读取通道复制到stdin
,有效地“放弃”读取输出ls(1)
(因为grep(1)
无论如何都会处理它),而是更新输入流以读取结果grep(1)
.
最后,shell 看到了sort(1)
是最后一个命令,所以它只是 forks + execssort(1)
。结果写入stdout
,因为我们从未改变stdout
在 shell 进程中,但输入是从连接的管道读取的grep(1)
to sort(1)
因为我们在第 3 步中的操作。
那么这是如何实现的呢?
很简单:只要还有多个命令需要处理,我们就创建一个管道和叉子。在子进程上,我们关闭管道的读取通道,将管道的写入通道复制到stdout
,并呼叫七个中的一个exec()
功能。在父级上,我们关闭管道的写入通道,并将管道的读取通道复制到stdin
.
当只剩下一个命令需要处理时,我们只需 fork + exec,而不创建管道。
只有最后一个细节需要澄清:在开始之前pipe(2)
重定向方,我们需要存储对原始 shell 标准输入的引用,因为我们(可能)会在整个过程中多次更改它。如果我们不保存它,我们可能会丢失对原始内容的引用stdin
文件,然后我们将无法再读取用户输入!在代码中,我通常这样做fcntl(2)
与F_DUPFD_CLOEXEC
(see man 2 fcntl
),以确保在子进程中执行命令时关闭描述符(在使用打开的文件描述符时保留它们通常是不好的做法)。
另外,shell 进程需要wait(2)
on the last管道中的过程。如果你仔细想想,这是有道理的:管道本质上同步管道中的每个命令;仅当最后一个命令读取时,才假定命令集结束EOF
来自管道(也就是说,只有当所有数据流经整个管道时我们才知道我们完成了)。如果 shell 没有等待最后一个进程,而是等待管道中间(或开头)的某个其他进程,那么它会过早返回到命令提示符,并使其他命令仍在管道上运行。后台 - 这不是明智之举,因为用户希望 shell 在等待更多任务之前完成当前作业的执行。
所以......这是很多信息,但理解它确实很重要。所以修改后的主要代码在这里:
int saved_stdin = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, 0);
if (saved_stdin < 0) {
perror("Couldn't store stdin reference");
break;
}
pid_t pid;
int i;
/* As long as there are at least two commands to process... */
for (i = 0; i < n-1; i++) {
/* We create a pipe to connect this command to the next command */
int pipefds[2];
if (pipe(pipefds) < 0) {
perror("pipe(2) error");
break;
}
/* Prepare execution on child process and make the parent read the
* results from the pipe
*/
if ((pid = fork()) < 0) {
perror("fork(2) error");
break;
}
if (pid > 0) {
/* Parent needs to close the pipe's write channel to make sure
* we don't hang. Parent reads from the pipe's read channel.
*/
if (close(pipefds[1]) < 0) {
perror("close(2) error");
break;
}
if (dupPipe(pipefds, READ_END, STDIN_FILENO) < 0) {
perror("dupPipe() error");
break;
}
} else {
int flags = 0;
if (structVariables[i].outfile != NULL)
{
flags = 1; // Write
if (redirect(structVariables[i].outfile, flags, STDOUT_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (structVariables[i].infile != NULL)
{
flags = 2; // Read
if (redirect(structVariables[i].infile, flags, STDIN_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
/* Child writes to the pipe (that is read by the parent); the read
* channel doesn't have to be closed, but we close it for good practice
*/
if (close(pipefds[0]) < 0) {
perror("close(2) error");
break;
}
if (dupPipe(pipefds, WRITE_END, STDOUT_FILENO) < 0) {
perror("dupPipe() error");
break;
}
if (execvp(structVariables[i].argv[0], structVariables[i].argv) < 0) {
perror("execvp(3) error");
exit(EXIT_FAILURE);
}
}
}
if (i != n-1) {
/* Some error caused an early loop exit */
break;
}
/* We don't need a pipe for the last command */
if ((pid = fork()) < 0) {
perror("fork(2) error on last command");
}
if (pid > 0) {
/* Parent waits for the last command to execute */
if (waitpid(pid, NULL, 0) < 0) {
perror("waitpid(2) error");
}
} else {
int flags = 0;
/* Execute last command. This will read from the last pipe we set up */
if (structVariables[i].outfile != NULL)
{
flags = 1; // Write
if (redirect(structVariables[i].outfile, flags, STDOUT_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (structVariables[i].infile != NULL)
{
flags = 2; // Read
if (redirect(structVariables[i].infile, flags, STDIN_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (execvp(structVariables[i].argv[0], structVariables[i].argv) < 0) {
perror("execvp(3) error on last command");
exit(EXIT_FAILURE);
}
}
/* Finally, we need to restore the original stdin descriptor */
if (dup2(saved_stdin, STDIN_FILENO) < 0) {
perror("dup2(2) error when attempting to restore stdin");
exit(EXIT_FAILURE);
}
if (close(saved_stdin) < 0) {
perror("close(2) failed on saved_stdin");
}
最后的一些评论dupPipe()
:
- Both
dup2(2)
and close(2)
可能会返回错误;您可能应该检查这一点并采取相应的行动(即通过返回 -1 将错误传递到调用堆栈)。
- 再次强调,复制描述符后不应该盲目关闭它,因为可能会出现源描述符和目标描述符相同的情况。
- 你应该验证这一点
end
或者是READ_END
or WRITE_END
,如果不成立则返回错误(而不是返回destinfd
无论如何,这可能会给调用者代码带来成功的错误感觉)
以下是我将如何改进它:
int dupPipe(int pip[2], int end, int destinfd)
{
if (end != READ_END && end != WRITE_END)
return -1;
if(end == READ_END)
{
if (dup2(pip[0], destinfd) < 0)
return -1;
if (pip[0] != destinfd && close(pip[0]) < 0)
return -1;
}
else if(end == WRITE_END)
{
if (dup2(pip[1], destinfd) < 0)
return -1;
if (pip[1] != destinfd && close(pip[1]) < 0)
return -1;
}
return destinfd;
}
祝你的贝壳玩得开心!