了解dup2和关闭文件描述符

8

我发布我的代码只是为了让问题更加清晰明了。我并不是在寻求你的帮助来修复它,我更想理解dup2系统调用,但我从手册和其他stackoverflow问题中并没有完全掌握它。

    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后立即关闭,但是如果文件已经关闭,任何东西怎么写入文件呢?
提前感谢您。我已经卡在这个问题上好几天了,我真的很想完成这个项目。
编辑:我已经更新了此处的修改代码和示例输出,供有兴趣为我的问题提供具体帮助的人参考。首先,我有一个完整的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 -lgrep "cook"所示,应该在控制台上生成输出。
1个回答

17
父进程对处理管道所需的文件描述符都有一份拷贝,何时关闭这些描述符呢?关闭哪些呢?是所有的吗?还是所有执行命令后未使用的描述符?文件描述符可以通过以下三种方式之一关闭:1. 明确调用close()函数;2. 进程终止,操作系统自动关闭每个尚未关闭的文件描述符;3. 当进程调用七个exec()函数中的一个并且文件描述符具有O_CLOEXEC标志时。因此,大多数情况下,文件描述符将保持打开状态,直到您手动关闭它们。这也是您的代码中发生的事情-因为您没有指定O_CLOEXEC,所以当子进程调用execvp()时,文件描述符不会关闭。在子进程中,它们在子进程终止后关闭。对于父进程也是如此。如果您想在终止之前任何时候发生这种情况,必须手动调用close()。当在子进程中处理管道时,哪些进程留下哪些描述符呢?例如,如果我执行命令:ls -l | grep "[username]",则ls进程应该留下哪些描述符?只留下管道的写端?如果是,那么何时留下?同样的问题也适用于grep命令。
  1. A new file descriptor with the same properties as a is created, and it is given the number b. The old file descriptor that was previously associated with b is closed.
  2. b was already associated with a file descriptor that was created by a previous call to open(), pipe(), or dup(), and that file descriptor is replaced with a copy of a.

After dup2(a, b), you do not need to manually close the original file descriptor, because it has been replaced by a copy of a. Any subsequent writes to b will actually be done on the same underlying file as a.

  • 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,而不是stdout。你打开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不是STDOUT,且infile不是STDIN,则会关闭读和写通道(更糟糕的是,在关闭读通道后,您尝试复制它)。毫无疑问,这永远不会起作用。
  2. 父进程在等待子进程终止之前关闭了每个管道。这会引发竞争条件。

我建议重新设计您管理管道的方式。您可以查看此答案中使用管道的基本 Shell 工作示例:https://dev59.com/uIvda4cB1Zd3GeqPWDBE#30415995


感谢您对dup2系统调用的详细回答。但是,尽管我关闭了我认为合适的文件描述符,但我仍然无法使dup2正常工作。我本来想独自完成这个项目,但现在我真的迷失了方向。我已经更新了原始帖子,并提供了样本输出和更新后的代码,如果您可以查看并给出更具针对性的建议,我将不胜感激。再次感谢您。我对函数调用有了更好的理解,但似乎仍然无法将其应用到上下文中。 - MichaelCook
Shell也必须关闭其拥有的管道的写入端吗? - user129393192

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接