了解 dup2 和关闭文件描述符
Understanding dup2 and closing file descriptors
我发布我的代码只是为了解决我的问题。我并没有明确要求您帮助修复它,我更希望了解我只是没有从手册页和许多其他 Whosebug 问题中获取的 dup2 系统调用。
pid = fork();
if(pid == 0) {
if(strcmp("STDOUT", outfile)) {
if (command->getOutputFD() == REDIRECT) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_TRUNC)) == -1)
return false;
command->setOutputFD(outfd);
if (dup2(command->getOutputFD(), STDOUT_FILENO) == -1)
return false;
pipeIndex++;
}
else if (command->getOutputFD() == REDIRECTAPPEND) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_APPEND)) == -1)
return false;
command->setOutputFD(outfd);
if (dup2(command->getOutputFD(), STDOUT_FILENO) == -1)
return false;
pipeIndex++;
}
else {
if (dup2(pipefd[++pipeIndex], STDOUT_FILENO) == -1)
return false;
command->setOutputFD(pipefd[pipeIndex]);
}
}
if(strcmp("STDIN", infile)) {
if(dup2(pipefd[pipeIndex - 1], STDIN_FILENO) == -1)
return false;
command->setOutputFD(pipefd[pipeIndex - 1]);
pipeIndex++;
}
if (execvp(arguments[0], arguments) == -1) {
std::cerr << "Error!" << std::endl;
_Exit(0);
}
}
else if(pid == -1) {
return false;
}
对于您的上下文,该代码表示基本 linux shell 的执行步骤。命令对象包含命令参数、IO "name" 和 IO 描述符(我想我可能会去掉文件描述符作为字段)。
我最难理解的是何时以及关闭哪些文件描述符。我想我会问一些问题来尝试提高我对这个概念的理解。
1) 我的文件描述符数组用于处理管道,父级拥有所有这些描述符的副本。父级持有的描述符何时关闭?更重要的是,哪些描述符?是全部吗?所有未被执行命令使用的?
2) 在子进程中处理管道时,哪些描述符由哪些进程保持打开状态?假设我执行命令:ls -l | grep
“[用户名]”,哪些描述符应该为 ls 进程保持打开状态?只是管道的写入端?如果是,什么时候?同样的问题也适用于 grep 命令。
3) 当我处理 IO 到文件的重定向时,必须打开一个新文件并将其复制到 STDOUT(我不支持输入重定向)。这个描述符什么时候关闭?我在示例中看到它在调用 dup2 后立即关闭,但是如果文件已关闭,如何将任何内容写入文件?
提前致谢。我已经被这个问题困了好几天了,我真的很想完成这个项目。
EDIT 我已经用修改后的代码和样本输出更新了这个,供有兴趣为我的问题提供具体帮助的任何人使用。首先,我有处理执行的整个 for 循环。它已更新为我关闭各种文件描述符的调用。
while(currCommand != NULL) {
command = currCommand->getData();
infile = command->getInFileName();
outfile = command->getOutFileName();
arguments = command->getArgList();
pid = fork();
if(pid == 0) {
if(strcmp("STDOUT", outfile)) {
if (command->getOutputFD() == REDIRECT) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_TRUNC)) == -1)
return false;
if (dup2(outfd, STDOUT_FILENO) == -1)
return false;
close(STDOUT_FILENO);
}
else if (command->getOutputFD() == REDIRECTAPPEND) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_APPEND)) == -1)
return false;
if (dup2(outfd, STDOUT_FILENO) == -1)
return false;
close(STDOUT_FILENO);
}
else {
if (dup2(pipefd[pipeIndex + 1], STDOUT_FILENO) == -1)
return false;
close(pipefd[pipeIndex]);
}
}
pipeIndex++;
if(strcmp("STDIN", infile)) {
if(dup2(pipefd[pipeIndex - 1], STDIN_FILENO) == -1)
return false;
close(pipefd[pipeIndex]);
pipeIndex++;
}
if (execvp(arguments[0], arguments) == -1) {
std::cerr << "Error!" << std::endl;
_Exit(0);
}
}
else if(pid == -1) {
return false;
}
currCommand = currCommand->getNext();
}
for(int i = 0; i < numPipes * 2; i++)
close(pipefd[i]);
for(int i = 0; i < commands->size();i++) {
if(wait(status) == -1)
return false;
}
执行此代码时,我收到以下输出
ᕕ( ᐛ )ᕗ ls -l
total 68
-rwxrwxrwx 1 cook cook 242 May 31 18:31 CMakeLists.txt
-rwxrwxrwx 1 cook cook 617 Jun 1 22:40 Command.cpp
-rwxrwxrwx 1 cook cook 9430 Jun 8 18:02 ExecuteExternalCommand.cpp
-rwxrwxrwx 1 cook cook 682 May 31 18:35 ExecuteInternalCommand.cpp
drwxrwxrwx 2 cook cook 4096 Jun 8 17:16 headers
drwxrwxrwx 2 cook cook 4096 May 31 18:32 implementation files
-rwxr-xr-x 1 cook cook 25772 Jun 8 18:12 LeShell
-rwxrwxrwx 1 cook cook 243 Jun 5 13:02 Makefile
-rwxrwxrwx 1 cook cook 831 Jun 3 12:10 Shell.cpp
ᕕ( ᐛ )ᕗ ls -l > output.txt
ls: write error: Bad file descriptor
ᕕ( ᐛ )ᕗ ls -l | grep "cook"
ᕕ( ᐛ )ᕗ
ls -l > output.txt
的输出暗示我关闭了错误的描述符,但关闭了其他相关的描述符,虽然没有呈现错误,但没有向文件提供任何输出。如 ls -l
、grep "cook"
所示,应向控制台生成输出。
With my array of file descriptors used for handling pipes, the parent
has a copy of all those descriptors. When are the descriptors held by
the parent closed? And even more so, which descriptors? Is it all of
them? All of the ones left unused by the executing commands?
可以通过以下 3 种方式之一关闭文件描述符:
- 您明确调用了
close()
。
- 进程终止,操作系统自动关闭所有仍然打开的文件描述符。
- 当进程调用七个
exec()
函数之一并且文件描述符具有 O_CLOEXEC
标志时。
如您所见,大多数时候,文件描述符将保持打开状态,直到您手动关闭它们。这也是您的代码中发生的情况 - 因为您没有指定 O_CLOEXEC
,当 child 进程调用 execvp()
时,文件描述符不会关闭。在 child 中,它们在 child 终止后关闭。 parent 也是如此。如果您希望在终止之前的任何时间发生这种情况,您必须手动调用 close()
.
When handling pipes within the children, which descriptors are left
open by which processes? Say if I execute the command: ls -l | grep
"[username]", Which descriptors should be left open for the ls
process? Just the write end of the pipe? And if so when? The same
question applies to the grep command.
下面是当您键入 ls -l | grep "username"
:
时 shell 的一个(粗略)概念
- shell 调用
pipe()
创建新管道。管道文件描述符在下一步中由children继承。
- shell分叉两次,我们称这些进程为
c1
和c2
。假设 c1
将 运行 ls
并且 c2
将 运行 grep
.
- 在
c1
中用close()
关闭pipe的读通道,然后用pipe写通道和STDOUT_FILENO
调用dup2()
,从而使写入 stdout
相当于写入管道。然后,调用七个exec()
函数之一开始执行ls
。 ls
写入 stdout
,但由于我们将 stdout
复制到管道的写入通道,因此 ls
将写入管道。
- 在
c2
中发生相反的情况:关闭管道的写通道,然后调用dup2()
使stdin
指向管道的读通道。然后,调用七个exec()
函数之一开始执行grep
。 grep
从 stdin
读取,但由于我们 dup2()
是管道读取通道的标准输入,因此 grep
将从管道读取。
When I handle redirection of IO to a file, a new file must be opened
and duped to STDOUT (I do not support input redirection). When does
this descriptor get closed? I've seen in examples that it gets closed
immediately after the call to dup2, but then how does anything get
written to the file if the file has been closed?
因此,当您调用 dup2(a, b)
时,其中任何一个为真:
a == b
。在这种情况下,什么也没有发生,并且 dup2()
returns 过早地发生了。没有文件描述符被关闭。
a != b
。在这种情况下,b
会在必要时关闭,然后 b
会引用与 a
相同的文件 table 条目。文件 table 条目是一个包含当前文件偏移量和文件状态标志的结构;多个文件描述符可以指向同一个文件 table 条目,这正是复制文件描述符时发生的情况。因此,dup2(a, b)
具有使 a
和 b
共享同一文件 table 条目的效果。因此,写入 a
或 b
将最终写入同一个文件。所以关闭的文件是b
,而不是a
。如果您 dup2(a, STDOUT_FILENO)
,您关闭 stdout
并使 stdout
的文件描述符指向与 a
相同的文件 table 条目。任何写入 stdout
的程序都会写入文件,因为 stdout
的文件描述符指向您复制的文件。
更新:
因此,针对您的具体问题,在简要浏览代码后,我要说的是:
你不应该在这里调用 close(STDOUT_FILENO)
:
if (command->getOutputFD() == REDIRECT) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_TRUNC)) == -1)
return false;
if (dup2(outfd, STDOUT_FILENO) == -1)
return false;
close(STDOUT_FILENO);
}
如果您关闭 stdout
,您以后尝试写入 stdout
时将会出错。这就是你得到 ls: write error: Bad file descriptor
的原因。毕竟,ls
正在写入 stdout
,但你关闭了它。糟糕!
你在倒退:你想关闭 outfd
。您打开 outfd
以便可以将 STDOUT_FILENO
重定向到 outfd
,一旦重定向完成,您实际上不再需要 outfd
并且可以将其关闭。但是您绝对不想关闭 stdout
,因为我们的想法是让 stdout
写入 outfd
.
引用的文件
那么,继续吧:
if (command->getOutputFD() == REDIRECT) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_TRUNC)) == -1)
return false;
if (dup2(outfd, STDOUT_FILENO) == -1)
return false;
if (outfd != STDOUT_FILENO)
close(outfd);
}
注意最后的 if
是必要的:如果 outfd
碰巧等于 STDOUT_FILENO
,你不想关闭它,原因我刚才提到.
同样适用于else if (command->getOutputFD() == REDIRECTAPPEND)
里面的代码:你想关闭outfd
而不是STDOUT_FILENO
:
else if (command->getOutputFD() == REDIRECTAPPEND) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_APPEND)) == -1)
return false;
if (dup2(outfd, STDOUT_FILENO) == -1)
return false;
if (outfd != STDOUT_FILENO)
close(STDOUT_FILENO);
}
这至少可以让您 ls -l
按预期工作。
关于管道的问题:你的管道管理不是很正确。从您显示的代码中不清楚 pipefd
的分配位置和方式,以及您创建的管道数量,但请注意:
- 进程将永远无法从一个管道读取并写入另一个管道。例如,如果
outfile
不是 STDOUT
并且 infile
不是 STDIN
,您最终会关闭读取和写入通道(更糟糕的是,在关闭读取之后频道,你试图复制它)。这永远行不通。
- parent 进程正在关闭每个管道,然后等待 children 终止。这会引发竞争条件。
我建议重新设计您管理管道的方式。你可以看到考试在这个答案中使用管道的工作 bare-bones shell 的 le:
我发布我的代码只是为了解决我的问题。我并没有明确要求您帮助修复它,我更希望了解我只是没有从手册页和许多其他 Whosebug 问题中获取的 dup2 系统调用。
pid = fork();
if(pid == 0) {
if(strcmp("STDOUT", outfile)) {
if (command->getOutputFD() == REDIRECT) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_TRUNC)) == -1)
return false;
command->setOutputFD(outfd);
if (dup2(command->getOutputFD(), STDOUT_FILENO) == -1)
return false;
pipeIndex++;
}
else if (command->getOutputFD() == REDIRECTAPPEND) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_APPEND)) == -1)
return false;
command->setOutputFD(outfd);
if (dup2(command->getOutputFD(), STDOUT_FILENO) == -1)
return false;
pipeIndex++;
}
else {
if (dup2(pipefd[++pipeIndex], STDOUT_FILENO) == -1)
return false;
command->setOutputFD(pipefd[pipeIndex]);
}
}
if(strcmp("STDIN", infile)) {
if(dup2(pipefd[pipeIndex - 1], STDIN_FILENO) == -1)
return false;
command->setOutputFD(pipefd[pipeIndex - 1]);
pipeIndex++;
}
if (execvp(arguments[0], arguments) == -1) {
std::cerr << "Error!" << std::endl;
_Exit(0);
}
}
else if(pid == -1) {
return false;
}
对于您的上下文,该代码表示基本 linux shell 的执行步骤。命令对象包含命令参数、IO "name" 和 IO 描述符(我想我可能会去掉文件描述符作为字段)。
我最难理解的是何时以及关闭哪些文件描述符。我想我会问一些问题来尝试提高我对这个概念的理解。
1) 我的文件描述符数组用于处理管道,父级拥有所有这些描述符的副本。父级持有的描述符何时关闭?更重要的是,哪些描述符?是全部吗?所有未被执行命令使用的?
2) 在子进程中处理管道时,哪些描述符由哪些进程保持打开状态?假设我执行命令:ls -l | grep “[用户名]”,哪些描述符应该为 ls 进程保持打开状态?只是管道的写入端?如果是,什么时候?同样的问题也适用于 grep 命令。
3) 当我处理 IO 到文件的重定向时,必须打开一个新文件并将其复制到 STDOUT(我不支持输入重定向)。这个描述符什么时候关闭?我在示例中看到它在调用 dup2 后立即关闭,但是如果文件已关闭,如何将任何内容写入文件?
提前致谢。我已经被这个问题困了好几天了,我真的很想完成这个项目。
EDIT 我已经用修改后的代码和样本输出更新了这个,供有兴趣为我的问题提供具体帮助的任何人使用。首先,我有处理执行的整个 for 循环。它已更新为我关闭各种文件描述符的调用。
while(currCommand != NULL) {
command = currCommand->getData();
infile = command->getInFileName();
outfile = command->getOutFileName();
arguments = command->getArgList();
pid = fork();
if(pid == 0) {
if(strcmp("STDOUT", outfile)) {
if (command->getOutputFD() == REDIRECT) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_TRUNC)) == -1)
return false;
if (dup2(outfd, STDOUT_FILENO) == -1)
return false;
close(STDOUT_FILENO);
}
else if (command->getOutputFD() == REDIRECTAPPEND) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_APPEND)) == -1)
return false;
if (dup2(outfd, STDOUT_FILENO) == -1)
return false;
close(STDOUT_FILENO);
}
else {
if (dup2(pipefd[pipeIndex + 1], STDOUT_FILENO) == -1)
return false;
close(pipefd[pipeIndex]);
}
}
pipeIndex++;
if(strcmp("STDIN", infile)) {
if(dup2(pipefd[pipeIndex - 1], STDIN_FILENO) == -1)
return false;
close(pipefd[pipeIndex]);
pipeIndex++;
}
if (execvp(arguments[0], arguments) == -1) {
std::cerr << "Error!" << std::endl;
_Exit(0);
}
}
else if(pid == -1) {
return false;
}
currCommand = currCommand->getNext();
}
for(int i = 0; i < numPipes * 2; i++)
close(pipefd[i]);
for(int i = 0; i < commands->size();i++) {
if(wait(status) == -1)
return false;
}
执行此代码时,我收到以下输出
ᕕ( ᐛ )ᕗ ls -l
total 68
-rwxrwxrwx 1 cook cook 242 May 31 18:31 CMakeLists.txt
-rwxrwxrwx 1 cook cook 617 Jun 1 22:40 Command.cpp
-rwxrwxrwx 1 cook cook 9430 Jun 8 18:02 ExecuteExternalCommand.cpp
-rwxrwxrwx 1 cook cook 682 May 31 18:35 ExecuteInternalCommand.cpp
drwxrwxrwx 2 cook cook 4096 Jun 8 17:16 headers
drwxrwxrwx 2 cook cook 4096 May 31 18:32 implementation files
-rwxr-xr-x 1 cook cook 25772 Jun 8 18:12 LeShell
-rwxrwxrwx 1 cook cook 243 Jun 5 13:02 Makefile
-rwxrwxrwx 1 cook cook 831 Jun 3 12:10 Shell.cpp
ᕕ( ᐛ )ᕗ ls -l > output.txt
ls: write error: Bad file descriptor
ᕕ( ᐛ )ᕗ ls -l | grep "cook"
ᕕ( ᐛ )ᕗ
ls -l > output.txt
的输出暗示我关闭了错误的描述符,但关闭了其他相关的描述符,虽然没有呈现错误,但没有向文件提供任何输出。如 ls -l
、grep "cook"
所示,应向控制台生成输出。
With my array of file descriptors used for handling pipes, the parent has a copy of all those descriptors. When are the descriptors held by the parent closed? And even more so, which descriptors? Is it all of them? All of the ones left unused by the executing commands?
可以通过以下 3 种方式之一关闭文件描述符:
- 您明确调用了
close()
。 - 进程终止,操作系统自动关闭所有仍然打开的文件描述符。
- 当进程调用七个
exec()
函数之一并且文件描述符具有O_CLOEXEC
标志时。
如您所见,大多数时候,文件描述符将保持打开状态,直到您手动关闭它们。这也是您的代码中发生的情况 - 因为您没有指定 O_CLOEXEC
,当 child 进程调用 execvp()
时,文件描述符不会关闭。在 child 中,它们在 child 终止后关闭。 parent 也是如此。如果您希望在终止之前的任何时间发生这种情况,您必须手动调用 close()
.
When handling pipes within the children, which descriptors are left open by which processes? Say if I execute the command: ls -l | grep "[username]", Which descriptors should be left open for the ls process? Just the write end of the pipe? And if so when? The same question applies to the grep command.
下面是当您键入 ls -l | grep "username"
:
- shell 调用
pipe()
创建新管道。管道文件描述符在下一步中由children继承。 - shell分叉两次,我们称这些进程为
c1
和c2
。假设c1
将 运行ls
并且c2
将 运行grep
. - 在
c1
中用close()
关闭pipe的读通道,然后用pipe写通道和STDOUT_FILENO
调用dup2()
,从而使写入stdout
相当于写入管道。然后,调用七个exec()
函数之一开始执行ls
。ls
写入stdout
,但由于我们将stdout
复制到管道的写入通道,因此ls
将写入管道。 - 在
c2
中发生相反的情况:关闭管道的写通道,然后调用dup2()
使stdin
指向管道的读通道。然后,调用七个exec()
函数之一开始执行grep
。grep
从stdin
读取,但由于我们dup2()
是管道读取通道的标准输入,因此grep
将从管道读取。
When I handle redirection of IO to a file, a new file must be opened and duped to STDOUT (I do not support input redirection). When does this descriptor get closed? I've seen in examples that it gets closed immediately after the call to dup2, but then how does anything get written to the file if the file has been closed?
因此,当您调用 dup2(a, b)
时,其中任何一个为真:
a == b
。在这种情况下,什么也没有发生,并且dup2()
returns 过早地发生了。没有文件描述符被关闭。a != b
。在这种情况下,b
会在必要时关闭,然后b
会引用与a
相同的文件 table 条目。文件 table 条目是一个包含当前文件偏移量和文件状态标志的结构;多个文件描述符可以指向同一个文件 table 条目,这正是复制文件描述符时发生的情况。因此,dup2(a, b)
具有使a
和b
共享同一文件 table 条目的效果。因此,写入a
或b
将最终写入同一个文件。所以关闭的文件是b
,而不是a
。如果您dup2(a, STDOUT_FILENO)
,您关闭stdout
并使stdout
的文件描述符指向与a
相同的文件 table 条目。任何写入stdout
的程序都会写入文件,因为stdout
的文件描述符指向您复制的文件。
更新:
因此,针对您的具体问题,在简要浏览代码后,我要说的是:
你不应该在这里调用 close(STDOUT_FILENO)
:
if (command->getOutputFD() == REDIRECT) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_TRUNC)) == -1)
return false;
if (dup2(outfd, STDOUT_FILENO) == -1)
return false;
close(STDOUT_FILENO);
}
如果您关闭 stdout
,您以后尝试写入 stdout
时将会出错。这就是你得到 ls: write error: Bad file descriptor
的原因。毕竟,ls
正在写入 stdout
,但你关闭了它。糟糕!
你在倒退:你想关闭 outfd
。您打开 outfd
以便可以将 STDOUT_FILENO
重定向到 outfd
,一旦重定向完成,您实际上不再需要 outfd
并且可以将其关闭。但是您绝对不想关闭 stdout
,因为我们的想法是让 stdout
写入 outfd
.
那么,继续吧:
if (command->getOutputFD() == REDIRECT) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_TRUNC)) == -1)
return false;
if (dup2(outfd, STDOUT_FILENO) == -1)
return false;
if (outfd != STDOUT_FILENO)
close(outfd);
}
注意最后的 if
是必要的:如果 outfd
碰巧等于 STDOUT_FILENO
,你不想关闭它,原因我刚才提到.
同样适用于else if (command->getOutputFD() == REDIRECTAPPEND)
里面的代码:你想关闭outfd
而不是STDOUT_FILENO
:
else if (command->getOutputFD() == REDIRECTAPPEND) {
if ((outfd = open(outfile, O_CREAT | O_WRONLY | O_APPEND)) == -1)
return false;
if (dup2(outfd, STDOUT_FILENO) == -1)
return false;
if (outfd != STDOUT_FILENO)
close(STDOUT_FILENO);
}
这至少可以让您 ls -l
按预期工作。
关于管道的问题:你的管道管理不是很正确。从您显示的代码中不清楚 pipefd
的分配位置和方式,以及您创建的管道数量,但请注意:
- 进程将永远无法从一个管道读取并写入另一个管道。例如,如果
outfile
不是STDOUT
并且infile
不是STDIN
,您最终会关闭读取和写入通道(更糟糕的是,在关闭读取之后频道,你试图复制它)。这永远行不通。 - parent 进程正在关闭每个管道,然后等待 children 终止。这会引发竞争条件。
我建议重新设计您管理管道的方式。你可以看到考试在这个答案中使用管道的工作 bare-bones shell 的 le: