程序卡住了,什么时候不应该打开管道文件描述符?

Program stuck, pipe file descriptor open when shouldn't?

我正在创建一个可以读取命令的小型 shell。当我 运行 我的程序并键入:"cat file.txt > file2.txt" 时,它会创建文件,然后卡在行:if(execvp(structVariables->argv[0], argv) < 0).(等待 input/output??)。如果我用 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;
}

execvp 不会 return 除非出现错误。

因此,原始程序(通常)不会执行调用 execvp() 之外的代码

正常的代码顺序是:

1) fork()
2) if child then call execvp(); 
3) if parent ....

您在 redirect() 中使用 open() 不正确 if 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 被(错误地)用于代替 open()mode 参数,而不是 [=19= 中的 or'd ]:

    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) 自动创建和打开文件。

您似乎正在尝试对从输入读取的 shell 到 运行 命令进行编码(如果不是这种情况;请编辑您的问题,因为它不清楚)。

我不确定为什么您认为管道会用在像 cat file.txt > file2.txt 这样的命令中,但无论如何,它们不是。让我们看看当您在 shell 中键入 cat file.txt > file2.txt 时会发生什么,例如 bash:

  1. 创建了一个 child 进程,其中 cat(1) 将 运行。
  2. child 进程打开 file2.txt 进行写入(稍后会详细介绍)。
  3. 如果open(2)成功,child进程将新打开的文件描述符复制到stdout(因此stdout将有效地指向同一个文件table条目为file2.txt).
  4. cat(1) 通过调用七个 exec() 函数之一来执行。参数 file.txt 被传递给 cat(1),因此 cat(1) 将打开 file.txt 并读取所有内容,将其内容复制到 stdout(重定向到 file2.txt).
  5. cat(1) 完成执行并终止,这会导致关闭和刷新所有打开的文件描述符。到 cat(1) 终止时,file2.txtfile.txt.
  6. 的副本
  7. 同时,parent shell 进程等待 child 终止,然后打印下一个提示并等待更多命令。

如您所见,I/O 重定向中未使用管道。管道是一种进程间通信机制,用于将一个进程的输出提供给另一个进程的输入。您在这里只有一个进程 运行ning (cat),那么您为什么还需要管道?

这意味着您应该使用 STDOUT_FILENO 调用 redirect() 作为 destinfd(而不是管道通道)以进行输出重定向。同样,输入重定向应该调用 redirect()STDIN_FILENO。这些常量在 unistd.h 中定义,因此请确保包含 header.

如果 exec() 失败,您还 可能 想在 child 上退出,否则您将 运行ning 2 份shell 进程。

最后但同样重要的是,您不应将输入或输出重定向设置为独占。用户可能需要输入和输出重定向。因此,在执行 I/O 重定向时,我不会使用 else if,而是使用 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() 函数容易出现竞争条件,因为在文件存在检查和实际文件创建之间有一个 window 的时间,另一个进程可以创建文件(这称为 TOCTTOU 错误:检查时间到使用时间)。您应该使用 O_CREAT | O_EXCL 自动测试是否存在并创建文件。

另一个问题是你总是关闭newfd。如果由于某种原因 newfddestinfd 碰巧相同怎么办?然后你会错误地关闭文件,因为如果你传入两个相等的文件描述符,dup2(2) 本质上是一个 no-op。即使您认为这永远不会发生,在关闭原始 fd 之前先检查复制的 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) 中的 0666 替换为 S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH(确保包括 sys/stat.hfcntl.h)。您可能想使用 #define 来使其更清晰,但我仍然认为,如果您这样做而不是硬编码一些幻数(尽管这是主观的),它会更好、更具描述性。

我不会对 dupPipe() 发表评论,因为在这个问题中不需要/使用它。 I/O 重定向就是您所需要的。如果您想将讨论扩展到管道,请随时编辑问题或创建另一个问题。

更新

好的,既然我已经看过完整的源代码,我还有几点要说的。

cat(1)挂起的原因是:

if (execvp(structVariables->argv[0], argv) < 0)

execvp(2)的第二个参数应该是structVariables->argv不是argv,因为argv是参数数组shell 程序,它(通常)是空的。将空参数列表传递给 cat(1) 会使其从 stdin 而不是从文件中读取,因此这就是它似乎挂起的原因 - 它正在等待您提供输入。因此,继续并将该行替换为:

if (execvp(structVariables->argv[0], structVariables->argv) < 0)

这解决了您的一个问题:像 cat < file.txt > file2.txt 这样的东西现在可以工作了(我测试过)。

关于管道重定向

所以现在我们需要处理管道重定向。每次我们在命令行上看到 | 时都会发生管道重定向。让我们通过一个示例来了解当我们键入 ls | grep "file.txt" | sort 时幕后发生的事情。理解这些步骤很重要,这样您就可以建立一个准确的系统工作心智模型;没有这样的愿景,你就不会真正理解实现:

  1. shell(通常)首先通过管道符号拆分命令。日s 也是您的代码所做的。这意味着在解析后,shell 收集了足够的信息,命令行被分成 3 个实体(ls 命令、grep 命令和 sort 命令) .
  2. shell 分叉并调用 child 到 运行 ls 上的七个 exec() 函数之一。现在,记住管道意味着一个程序的输出是下一个程序的输入,所以在 exec()ing 之前,shell 必须创建一个管道。即将 运行 ls(1) 的 child 进程在 exec() 之前调用 dup2(2) 将管道的写入通道复制到 stdout。同样,parent 进程调用 dup2(2) 将管道的读取通道复制到 stdin。理解这一步非常重要:因为 parent 将管道的读取端复制到 stdin,那么无论我们接下来做什么(例如再次 fork 以执行更多命令)都将始终从管道读取输入.所以,在这一点上,我们有 ls(1) 写入 stdout,它被重定向到由 shell 的 parent 进程读取的管道。

  3. shell 现在将执行 grep(1)。同样,它派生一个新进程来执行 grep(1)。请记住,文件描述符是通过 fork 继承的,并且 parent shell 的进程将 stdin 绑定到连接到 ls(1) 的管道的读取端,因此即将执行 grep(1) 的新 child 进程将从该管道中读取 "automagically"!但是等等,还有更多! shell 知道管道中还有另一个进程(sort 命令),因此在执行 grep 之前(以及分叉之前),shell 创建了 另一个 管道将 grep(1) 的输出连接到 sort(1) 的输入。然后,它重复相同的步骤:在 child 进程中,管道的写入通道被复制到 stdout 上。在 parent 中,管道的读取通道被复制到 stdin 中。同样,重要的是要真正理解这里发生的事情:即将执行的进程 grep(1) 已经从连接到 ls(1) 的管道读取了它的输入,现在它的输出连接到将供给 sort(1) 的管道。所以 grep(1) 本质上是从管道读取并写入管道。 OTOH,parent shell 进程将最后一个管道的读取通道复制到 stdin,有效地 "giving up" 从读取 ls(1) 的输出(因为 grep(1)无论如何都会处理它),而是更新输入流以从 grep(1).

  4. 读取结果
  5. 最后,shell 看到 sort(1) 是最后一个命令,所以它只是 forks + execs sort(1)。结果写入stdout,因为我们在shell过程中从未更改过stdout,但输入是从连接grep(1)sort(1)的管道读取的,因为我们在步骤 3 中的操作。

那么这是如何实现的呢?

简单:只要还有多个命令需要处理,我们就创建管道和分支。在 child 上,我们关闭管道的读取通道,将管道的写入通道复制到 stdout 上,然后调用七个 exec() 函数之一。在 parent 上,我们关闭管道的写入通道,并将管道的读取通道复制到 stdin.

当只剩下一个命令要处理时,我们只需 fork + exec,而不创建管道。

只有最后一个细节需要澄清:在开始 pipe(2) 重定向之前,我们需要存储对原始 shell 标准输入的引用,因为我们(可能)整个过程中多次更改它。如果我们不保存它,我们可能会丢失对原始 stdin 文件的引用,然后我们将无法再读取用户输入!在代码中,我通常用fcntl(2)F_DUPFD_CLOEXEC(见man 2 fcntl)这样做,以确保在child进程中执行命令时关闭描述符(在使用时留下打开的文件描述符通常是不好的做法)。

此外,shell 进程需要在管道中的 最后一个 进程上 wait(2)。如果您考虑一下,这是有道理的:管道天生就同步管道中的每个命令;只有当最后一个命令从管道读取 EOF 时,才假定命令集结束(也就是说,我们知道只有当所有数据都流过整个管道时我们才完成)。如果 shell 没有等待最后一个进程,而是等待管道中间(或开始)的其他进程,它会 return 过早返回命令提示符并在后台保留其他命令 运行ning - 这不是明智之举,因为用户希望 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)都可能return出错;您可能应该检查一下并采取相应行动(即通过错误或通过 returning -1).
  • 向上调用堆栈
  • 同样,复制描述符后不要盲目关闭,因为源描述符和目标描述符可能是相同的。
  • 您应该验证 endREAD_END 还是 WRITE_END,如果不正确则 return 是一个错误(而不是 returning 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;
}

玩得开心 shell!