看起来你正在尝试编写一个shell来运行从输入读取的命令(如果不是这样,请编辑你的问题,因为它不够清晰)。我不确定为什么你认为像
cat file.txt > file2.txt
这样的命令中使用管道,但无论如何,都是不需要的。让我们看看在bash等shell中键入
cat file.txt > file2.txt
时发生了什么:
1. 创建一个子进程,其中
cat(1)
将运行。
2. 子进程打开
file2.txt
进行写操作(稍后详细说明)。
3. 如果
open(2)
成功,则子进程将新打开的文件描述符复制到
stdout
(因此
stdout
将有效地指向与
file2.txt
相同的文件表项)。
4. 通过调用七个
exec()
函数之一来执行
cat(1)
。参数
file.txt
传递给
cat(1)
,因此
cat(1)
将打开
file.txt
并读取所有内容,将其内容复制到
stdout
(它被重定向到
file2.txt
)。
5.
cat(1)
完成执行并终止,这将导致关闭和刷新所有打开的文件描述符。在
cat(1)
终止时,
file2.txt
是
file.txt
的副本。
6. 同时,父shell进程等待子进程终止,然后打印下一个提示并等待更多命令。
如你所见,在I/O重定向中不使用管道。管道是一种用于将一个进程的输出馈送到另一个进程的输入的进程间通信机制。在这里只有一个进程运行(
cat
),那么为什么你需要管道呢?
这意味着你应该使用
STDOUT_FILENO
作为
destinfd
(而不是管道通道)来调用
redirect()
进行输出重定向。类似地,输入重定向应该使用
STDIN_FILENO
调用
redirect()
。这些常量在
unistd.h
中定义,因此请确保包含该头文件。
最后但并非最不重要的是,你不应该使输入或输出重定向排他性。用户可能同时需要输入和输出重定向。因此,在进行I/O重定向时,我会使用2个独立的ifs而不是
else if
。
考虑到这一点,你发布的主代码应该类似于:
if((pid = fork()) < 0)
{
perror("fork error");
}
else if(pid > 0)
{
if(waitpid(pid,NULL,0) < 0)
{
perror("waitpid error");
}
}
else
{
int flags = 0;
if(structVariables->outfile != NULL)
{
flags = 1;
redirect(structVariables->outfile, flags, STDOUT_FILENO);
}
if(structVariables->infile != NULL)
{
flags = 2;
redirect(structVariables->infile, flags, STDIN_FILENO);
}
if(execvp(structVariables->argv[0], structVariables->argv) < 0)
{
perror("execvp error");
exit(EXIT_FAILURE);
}
}
如另一个答案所述,您的
redirect()
函数容易发生竞态条件,因为在文件存在检查和实际文件创建之间存在一段时间窗口,在此期间另一个进程可能会创建该文件(这称为TOCTTOU错误:检查时间到使用时间)。您应该使用
O_CREAT | O_EXCL
来原子地测试文件是否存在并创建文件。
另一个问题是您总是关闭
newfd
。如果由于某种原因
newfd
和
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;
}
考虑将
open(2)
中的
0666
替换为
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
(确保包含
sys / stat.h
和
fcntl.h
)。您可能希望使用
#define
使其更清晰,但我仍然认为如果您这样做而不是硬编码某些魔法数字,则更好且更具描述性(尽管这是主观的)。
我不会评论
dupPipe()
,因为在此问题中不需要/使用I / O重定向就足够了。如果你想扩展讨论到管道,可以随意编辑问题或创建另一个问题。
更新:
好的,现在我已经查看了完整的源代码,我有几个更多的备注。
cat(1)
挂起的原因是:
if (execvp(structVariables->argv[0], argv) < 0)
execvp(2)
的第二个参数应该是
structVariables->argv
,而
不是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会fork并在子进程上调用七个exec()
函数之一来运行ls
。现在,请记住,管道意味着程序的输出是下一个程序的输入,因此在exec()
之前,shell必须创建一个管道。即将运行ls(1)
的子进程在exec()
之前调用dup2(2)
来将管道的写通道复制到stdout
上。同样,父进程调用dup2(2)
将管道的读通道复制到stdin
上。理解这一步非常重要:因为父进程将管道的读端复制到stdin
上,所以无论我们接下来做什么(例如再次fork以执行更多命令),都将始终从管道读取输入。因此,在此时,我们有ls(1)
写入stdout
,而stdout
被重定向到由shell的父进程读取的管道。
Shell现在将执行grep(1)
。同样,它fork一个新进程来执行grep(1)
。请记住,文件描述符跨fork继承,并且父shell进程的stdin
与连接到ls(1)
的管道的读端相连,因此即将执行grep(1)
的新子进程将“自动”从该管道中读取!但是,请等一下!Shell知道管道中还有另一个进程(sort命令),因此在执行grep之前(并在fork之前),shell创建了另一个管道,以将grep(1)
的输出连接到sort(1)
的输入。然后,它重复相同的步骤:在子进程上,将管道的写通道复制到stdout
上。在父进程中,将管道的读通道复制到stdin
上。再次强调,真正理解这里发生的事情非常重要:即将执行grep(1)
的进程已经从连接到ls(1)
的管道中读取其输入,现在它的输出已连接到将提供sort(1)
的管道。因此,grep(1)
基本上是从管道中读取并写入管道。另一方面,父shell进程将最后一个管道的读通道复制到stdin
上,有效地“放弃”从ls(1)
的输出中读取(因为grep(1)
将处理它),而是更新输入流以从grep(1)
读取结果。
最后,shell看到sort(1)
是最后一个命令,因此它只fork + execs sort(1)
。结果被写入stdout
,因为我们没有在shell进程中更改stdout
,但是由于第3步中的操作,输入从连接grep(1)
到sort(1)
的管道中读
那么这是如何实现的呢?
很简单:只要还有多个命令需要处理,我们就创建一个管道并进行分叉。在子进程中,我们关闭管道的读通道,将管道的写通道复制到stdout上,并调用七个exec()函数之一。在父进程中,我们关闭管道的写通道,并将管道的读通道复制到stdin上。
当只剩下一个命令需要处理时,我们只需fork + exec,而不需要创建管道。
只有最后一个细节需要澄清:在开始pipe(2)重定向之前,我们需要存储对原始shell标准输入的引用,因为我们可能会多次更改它。如果我们没有保存它,我们可能会失去对原始stdin文件的引用,然后我们将无法再读取用户输入!在代码中,我通常使用fcntl(2)和F_DUPFD_CLOEXEC(请参见man 2 fcntl)来执行此操作,以确保在子进程中执行命令时关闭描述符(通常不良的做法是在使用时保留打开的文件描述符)。
此外,shell进程需要在管道中的最后一个进程上等待wait(2)。如果你想一想,这是有道理的:管道本质上同步了管道中的每个命令;假设命令集仅在最后一个命令从管道中读取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;
for (i = 0; i < n-1; i++) {
int pipefds[2];
if (pipe(pipefds) < 0) {
perror("pipe(2) error");
break;
}
if ((pid = fork()) < 0) {
perror("fork(2) error");
break;
}
if (pid > 0) {
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;
if (redirect(structVariables[i].outfile, flags, STDOUT_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (structVariables[i].infile != NULL)
{
flags = 2;
if (redirect(structVariables[i].infile, flags, STDIN_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
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) {
break;
}
if ((pid = fork()) < 0) {
perror("fork(2) error on last command");
}
if (pid > 0) {
if (waitpid(pid, NULL, 0) < 0) {
perror("waitpid(2) error");
}
} else {
int flags = 0;
if (structVariables[i].outfile != NULL)
{
flags = 1;
if (redirect(structVariables[i].outfile, flags, STDOUT_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (structVariables[i].infile != NULL)
{
flags = 2;
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);
}
}
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()
的一些最终说明:
dup2(2)
和 close(2)
都可能返回错误;你应该检查这一点,并相应地采取行动(即通过返回 -1 将错误传递给调用堆栈)。
- 同样,不应该盲目关闭复制后的描述符,因为源描述符和目标描述符可能是相同的。
- 你应该验证
end
是否为 READ_END
或 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;
}
享受您的shell吧!