了解dup2并关闭文件描述符
Understanding dup2 and closing file descriptors
我发布代码只是为了了解问题的上下文。我并不是明确希望你能帮助解决这个问题,我更希望了解dup2系统调用,我只是没有从手册页和其他许多stackerflow问题中了解到。
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"名称"和IO描述符(我想我可能会去掉作为字段的文件描述符)。
我最难理解的是何时以及关闭哪个文件描述符。我想我会问一些问题来提高我对这个概念的理解。
1) 使用我的用于处理管道的文件描述符数组,父级拥有所有这些描述符的副本。父项持有的描述符何时关闭?更重要的是,哪些描述符?都是他们吗?执行命令留下的所有未使用的?
2) 在处理子级中的管道时,哪些描述符由哪些进程打开?假设我执行命令:ls-l|grep"[username]",哪些描述符应该为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"
应该生成到控制台的输出。
使用我的用于处理管道的文件描述符数组拥有所有这些描述符的副本。描述符何时由家长关闭了吗?更重要的是,哪些描述符?都是吗他们执行命令留下的所有未使用的?
文件描述符可以通过以下三种方式之一关闭:
- 您在其上显式调用
close()
- 进程终止,操作系统自动关闭所有仍然打开的文件描述符
- 当进程调用七个
exec()
函数中的一个并且文件描述符具有O_CLOEXEC
标志时
正如您所看到的,在大多数情况下,文件描述符将保持打开状态,直到您手动关闭它们。这也是代码中发生的情况——因为您没有指定O_CLOEXEC
,所以当子进程调用execvp()
时,文件描述符不会关闭。在子级中,它们在子级终止后关闭。父母也是如此。如果您想在终止之前的任何时候发生这种情况,您必须手动调用close()
。
在处理子级中的管道时,保留了哪些描述符通过哪些流程打开?假设我执行命令:ls-l|grep"[username]",哪些描述符应保留为ls打开过程只是管道的写入端?如果是,什么时候?还是一样这个问题适用于grep命令。
以下是当您键入ls -l | grep "username"
:时shell的(大致)功能
- shell调用
pipe()
来创建一个新管道。管道文件描述符将由子级在下一步中继承 - shell分叉两次,让我们将这些进程称为
c1
和c2
。假设c1
将运行ls
,c2
将运行grep
- 在
c1
中,管道的读取通道用close()
关闭,然后用管道写入通道和STDOUT_FILENO
调用dup2()
,从而使对stdout
的写入等同于对管道的写入。然后,调用七个exec()
函数中的一个来开始执行ls
。ls
写入stdout
,但由于我们将stdout
复制到管道的写入通道,ls
将写入管道 - 在
c2
中,情况正好相反:管道的写入通道关闭,然后调用dup2()
,使stdin
指向管道的读取通道。然后,调用七个exec()
函数中的一个来开始执行grep
。grep
从stdin
中读取,但由于dup2()
将标准输入到管道的读取通道,因此grep
将从管道中读取
当我处理IO重定向到文件时,必须打开一个新文件并被欺骗到STDOUT(我不支持输入重定向)。什么时候这个描述符被关闭了?我在例子中看到它是关闭的在调用dup2之后立即执行,但之后会发生什么情况如果文件已关闭,是否写入文件?
因此,当您调用dup2(a, b)
时,以下任一项都为真:
a == b
。在这种情况下,什么都不发生,dup2()
提前返回。没有关闭任何文件描述符a != b
。在这种情况下,必要时关闭b
,然后使b
引用与a
相同的文件表条目。文件表条目是一个包含当前文件偏移量和文件状态标志的结构;多个文件描述符可以指向同一个文件表条目,这正是复制文件描述符时发生的情况。因此,dup2(a, b)
具有使a
和b
共享同一文件表条目的效果。因此,写入a
或b
将最终写入同一文件。因此,关闭的文件是b
,而不是a
。如果使用dup2(a, STDOUT_FILENO)
,则关闭stdout
,并使stdout
的文件描述符指向与a
相同的文件表条目。任何写入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
,则最终会关闭读通道和写通道(更糟糕的是,关闭读通道后,您会尝试复制它)。这是不可能奏效的 - 父进程在等待子进程终止之前关闭每个管道。这会引发种族状况
我建议重新设计管道管理方式。您可以在下面的答案中看到一个使用管道的裸骨骼外壳的示例:https://stackoverflow.com/a/30415995/2793118
- 使用VerQueryValue检索应用程序的文件描述
- 如何在C/C++中用FD_set Unix设置套接字文件描述符
- I2C 文件描述符上的 I2C 总线可写/可读标志
- 许多文件描述符在调用sys_clone时
- AMQP-CPP >处理程序中的错误文件描述符
- 如何使用 WINAPI 和 C++ 提取可执行文件的文件描述?
- 正在等待在非阻塞文件描述符上长时间运行ioctl
- 有没有适用于Windows.lib文件的GNU二进制文件描述符(BFD)
- 如何强制文件描述符缓冲我的输出
- 如何从 boost::asio::ssl::stream<boost::asio::ip::tcp::socket> 获取本机套接字文件描述器?
- 哪个更适合从C++写入敏感的日志文件,在文件描述符上写()或文件上的fprintf()?
- 将 select() 与非基于文件描述符的输入一起使用
- accept(..) 似乎正在修改我给它的文件描述符参数
- 使用 Select 多路复用未命名的管道和其他文件描述符
- 提升 ASIO 绑定:错误的文件描述符
- 使用文件描述符移动对象
- 无法从零MQ ZMQ_SERVER套接字中获取文件描述符
- read() 上的不同行为取决于写入不可写内存时表示文件、匿名管道或套接字的文件描述符
- 我可以在不使用 FIOCANCEL 的情况下关闭 VxWorks 中的文件描述符吗?
- 将 Boost Asio 与 ZeroMQ 集成,文件描述符错误?