程序卡住了,管道文件描述符何时打开?
我正在创建一个可以读取命令的小外壳。当我运行我的程序并键入:"cat file.txt > file2.txt"
它创建文件,然后它卡在行:if(execvp(structVariables->argv[0], argv) < 0).
(等待输入/输出??)。如果我用ctrl + d结束程序,我可以在我的文件夹中看到该文件已创建,但没有写入任何内容。 (dupPipe是用来处理更多的指令,还没有使用,因为上述问题)程序卡住了,管道文件描述符何时打开?
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
redirect(structVariables->outfile, flags, STDOUT_FILENO);
}
if(structVariables->infile != NULL)
{
flags = 2; // Read
redirect(structVariables->infile, flags, STDIN_FILENO);
}
if(execvp(structVariables->argv[0], argv) < 0)
{
perror("execvp error");
exit(EXIT_FAILURE);
}
}
两个功能我在程序中使用是这样的: dupPipe和重定向
int dupPipe(int pip[2], int end, int destinfd)
{
if(end == READ_END)
{
dup2(pip[0], destinfd);
close(pip[0]);
}
else if(end == WRITE_END)
{
dup2(pip[1], destinfd);
close(pip[1]);
}
return destinfd;
}
int redirect(char *filename, int flags, int destinfd)
{
int newfd;
if(flags == 1)
{
if(access(filename, F_OK) != -1) // If file already exists
{
errno = EEXIST;
printf("Error: %s\n", strerror(errno));
return -1;
}
newfd = open(filename, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
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;
}
看起来你正在试图编写一个shell来运行从输入读取的命令(如果不是这种情况,请编辑你的问题,因为它不清楚)。
我不知道你为什么认为管道被用在像cat file.txt > file2.txt
这样的命令中,但是无论如何它们都不是。让我们来看看引擎盖下会发生什么,当你在一个壳状的bash键入cat file.txt > file2.txt
:创建
- 孩子的过程,其中
cat(1)
运行。 - 孩子的过程打开
file2.txt
写作(稍后更多)。 - 如果
open(2)
成功,则子进程将新打开的文件描述符复制到stdout
(因此stdout
将实际指向与file2.txt
相同的文件表项)。 -
cat(1)
通过调用七个exec()
函数之一来执行。参数file.txt
传递给cat(1)
,因此cat(1)
将打开file.txt
并读取所有内容,将其内容复制到stdout
(重定向到file2.txt
)。 -
cat(1)
完成执行并终止,这会导致任何打开的文件描述符被关闭和刷新。当cat(1)
终止时,file2.txt
是file.txt
的副本。 - 同时,在打印下一个提示符并等待更多命令之前,父shell进程会等待子进程终止。
如您所见,管道不用于I/O重定向。管道是一种进程间通信机制,用于将进程的输出提供给另一进程的输入。你只有一个进程在这里运行(cat
),那么为什么你甚至需要管道?
这意味着你应该调用redirect()
STDOUT_FILENO
作为destinfd
(而不是管道通道)输出重定向。同样,输入重定向应该调用redirect()
和STDIN_FILENO
。这些常量在unistd.h
中定义,因此请确保包含该标题。
你也可能想要退出的孩子,如果exec()
失败,否则你将运行2个shell进程的副本。
最后但并非最不重要的是,您不应该使输入或输出重定向排他。可能是用户想要输入和输出重定向的情况。所以,而不是else if
做I/O重定向时,我只会使用2个独立的ifs。
考虑到这一点,主要的代码你贴应该是这个样子:
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
和destinfd
碰巧是相同的呢?那么你会错误地关闭文件,因为如果你传入两个相同的文件描述符,dup2(2)
本质上是一个无操作。即使您认为这种情况永远不会发生,在关闭原件之前先检查重复的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)
以上S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
更换0666
(确保包括sys/stat.h
和fcntl.h
)。您可能需要使用#define
以使其更清洁,但如果您这样做,则仍然认为它更好,更具描述性,而不是硬编码一些幻数(但这是主观的)。
我不会对dupPipe()
发表评论,因为在这个问题中不需要/使用它。 I/O重定向是您所需要的。如果您想将讨论扩展到管道,请随时编辑问题或创建另一个问题。
UPDATE
好了,现在我有一个看看完整的源代码,我有一对夫妇更多的言论。
原因cat(1)
悬挂是因为这样:
if (execvp(structVariables->argv[0], argv) < 0)
到execvp(2)
的第二个参数应该是structVariables->argv
,不argv
,因为argv
是壳程序的参数数组,这是(通常是)空着。将一个空的参数列表传递给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
命令)。 壳牌分叉并呼叫儿童上的七个
exec()
功能之一运行ls
。现在请记住,管道意味着程序的输出是下一个的输入,因此在exec()
之前,外壳必须创建管道。将要运行ls(1)
的子进程在exec()
之前调用dup2(2)
将管道的写入通道复制到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创建另一个管道,以将grep(1)
的输出连接到sort(1)
的输入。然后,它重复相同的步骤:在子进程中,管道的写入通道被复制到stdout
上。在父项中,管道的读取通道复制到stdin
上。再一次,真正理解这里发生的事情非常重要:即将执行的进程grep(1)
已经从连接到ls(1)
的管道读取其输入,现在它的输出已连接到将输入sort(1)
的管道。所以grep(1)
基本上是从管道读取并写入管道。 OTOH,母壳程序将最后一个管道的读取通道复制到stdin
,从读取ls(1)
的输出(因为grep(1)
将处理它)有效地“放弃”,而是更新输入流以从grep(1)
读取结果。最后,shell看到
sort(1)
是最后一个命令,所以它只是分叉+执行sort(1)
。结果被写入stdout
,因为我们从来没有在外壳工艺改变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;
/* 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()
最后的一些言论:
- 两个
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;
}
玩你的壳!
这个想法是,我应该能够输入ex:cat file.txt |尾部-5> file2.txt。但是,即使我编写了cat file.txt> file2.txt,我发现它不起作用,所以这就是为什么不需要管道(还)。无论如何感谢回答!我根据你输入的内容更新了我的代码,并且发现我做错了,但仍然无效......程序在执行execvp时仍然卡住。然后我使用ctrl + d并查找该文件,并且它实际上已经创建。没有什么。你能找到其他错误的东西吗? – Fjodor
@Fjodor,但现在可以使用'cat file.txt> file2.txt'吗?在我们讨论管道之前,我需要知道这个问题是否已经解决。 –
对不起,也许我不清楚。问题仍然存在,但我明白为什么我不应该涉及管道。所以不,cat file.txt> file2.txt只是把程序“搁置”。但正如我所提到的,我可以在我的文件夹中找到该文件,但我必须用ctrl + d退出该程序。 – Fjodor
execvp做除非有错误才能返回。
因此,原始程序将(通常)不执行超出调用代码execvp()
代码的正常顺序是:
1) fork()
2) if child then call execvp();
3) if parent ....
我不明白为什么在孩子之后或之前安置父母会帮助我解决问题。 – Fjodor
你在redirect()
如果使用不当open()
flags == 1
:
if(flags == 1)
{
if(access(filename, F_OK) != -1) // If file already exists
{
errno = EEXIST;
printf("Error: %s\n", strerror(errno));
return -1;
}
newfd = open(filename, O_CREAT, O_WRONLY);
if(newfd == -1)
{
perror("Open for write failed");
return -1;
}
}
在newfd = open(filename, O_CREAT, O_WRONLY);
,O_WRONLY
是(不正确LY)代替mode
说法用来open()
,而不是在flags
或运算的结果:
if(flags == 1)
{
if(access(filename, F_OK) != -1) // If file already exists
{
errno = EEXIST;
printf("Error: %s\n", strerror(errno));
return -1;
}
newfd = open(filename, O_CREAT | O_WRONLY, mode); //whatever mode you want, but remember umask.
if(newfd == -1)
{
perror("Open for write failed");
return -1;
}
}
此外,对于文件的以前存在的检查是活泼的,另一个程序可以创建后的文件access()
之前和open()
。使用open(filename, O_CREAT | O_EXCL, mode)
自动创建并打开一个文件。
目前尚不清楚你想要做什么。请发布完整的代码,它是如何调用的样本,以及您期望发生的事情。 – dbush