了解dup2并关闭文件描述符

Understanding dup2 and closing file descriptors

本文关键字:文件 描述 dup2 了解      更新时间:2023-10-16

我发布代码只是为了了解问题的上下文。我并不是明确希望你能帮助解决这个问题,我更希望了解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"应该生成到控制台的输出。

使用我的用于处理管道的文件描述符数组拥有所有这些描述符的副本。描述符何时由家长关闭了吗?更重要的是,哪些描述符?都是吗他们执行命令留下的所有未使用的?

文件描述符可以通过以下三种方式之一关闭:

  1. 您在其上显式调用close()
  2. 进程终止,操作系统自动关闭所有仍然打开的文件描述符
  3. 当进程调用七个exec()函数中的一个并且文件描述符具有O_CLOEXEC标志时

正如您所看到的,在大多数情况下,文件描述符将保持打开状态,直到您手动关闭它们。这也是代码中发生的情况——因为您没有指定O_CLOEXEC,所以当子进程调用execvp()时,文件描述符不会关闭。在子级中,它们在子级终止后关闭。父母也是如此。如果您想在终止之前的任何时候发生这种情况,您必须手动调用close()

在处理子级中的管道时,保留了哪些描述符通过哪些流程打开?假设我执行命令:ls-l|grep"[username]",哪些描述符应保留为ls打开过程只是管道的写入端?如果是,什么时候?还是一样这个问题适用于grep命令。

以下是当您键入ls -l | grep "username":时shell的(大致)功能

  1. shell调用pipe()来创建一个新管道。管道文件描述符将由子级在下一步中继承
  2. shell分叉两次,让我们将这些进程称为c1c2。假设c1将运行lsc2将运行grep
  3. c1中,管道的读取通道用close()关闭,然后用管道写入通道和STDOUT_FILENO调用dup2(),从而使对stdout的写入等同于对管道的写入。然后,调用七个exec()函数中的一个来开始执行lsls写入stdout,但由于我们将stdout复制到管道的写入通道,ls将写入管道
  4. c2中,情况正好相反:管道的写入通道关闭,然后调用dup2(),使stdin指向管道的读取通道。然后,调用七个exec()函数中的一个来开始执行grepgrepstdin中读取,但由于dup2()将标准输入到管道的读取通道,因此grep将从管道中读取

当我处理IO重定向到文件时,必须打开一个新文件并被欺骗到STDOUT(我不支持输入重定向)。什么时候这个描述符被关闭了?我在例子中看到它是关闭的在调用dup2之后立即执行,但之后会发生什么情况如果文件已关闭,是否写入文件?

因此,当您调用dup2(a, b)时,以下任一项都为真:

  • a == b。在这种情况下,什么都不发生,dup2()提前返回。没有关闭任何文件描述符
  • a != b。在这种情况下,必要时关闭b,然后使b引用与a相同的文件表条目。文件表条目是一个包含当前文件偏移量和文件状态标志的结构;多个文件描述符可以指向同一个文件表条目,这正是复制文件描述符时发生的情况。因此,dup2(a, b)具有使ab共享同一文件表条目的效果。因此,写入ab将最终写入同一文件。因此,关闭的文件是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的分配位置和方式,以及您创建的管道数量,但请注意:

  1. 进程将永远无法从一个管道读取并写入另一个管道。例如,如果outfile不是STDOUTinfile不是STDIN,则最终会关闭读通道和写通道(更糟糕的是,关闭读通道后,您会尝试复制它)。这是不可能奏效的
  2. 父进程在等待子进程终止之前关闭每个管道。这会引发种族状况

我建议重新设计管道管理方式。您可以在下面的答案中看到一个使用管道的裸骨骼外壳的示例:https://stackoverflow.com/a/30415995/2793118