程序卡住,管道文件描述符何时不应该打开?

2024-02-27

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

看来您正在尝试编写一个 shell 来运行从输入读取的命令(如果不是这种情况;请编辑您的问题,因为它不清楚)。

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

  1. 子进程创建于何处cat(1)会跑。
  2. 子进程打开file2.txt用于写作(稍后会详细介绍)。
  3. If open(2)成功后,子进程将新打开的文件描述符复制到stdout (so stdout将有效地指向相同的文件表条目file2.txt).
  4. cat(1)通过调用七个之一来执行exec()功能。论据file.txt被传递给cat(1), so cat(1)将打开file.txt并阅读所有内容,将其内容复制到stdout(被重定向到file2.txt).
  5. cat(1)完成执行并终止,这会导致所有打开的文件描述符被关闭并刷新。到......的时候cat(1)终止,file2.txt是一个副本file.txt.
  6. 同时,父 shell 进程等待子进程终止,然后再打印下一个提示并等待更多命令。

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

这意味着您应该致电redirect() with STDOUT_FILENO as destinfd(而不是管道通道)用于输出重定向。同样,输入重定向应该调用redirect() with STDIN_FILENO。这些常量定义在unistd.h所以请确保包含该标头。

你也probably如果想要退出孩子exec()失败,否则您将运行 shell 进程的 2 个副本。

最后但并非最不重要的一点是,您不应将输入或输出重定向设置为独占。用户可能需要输入和输出重定向。所以而不是else if当进行 I/O 重定向时,我只会使用 2 个独立的 if。

考虑到这一点,您发布的主要代码应该类似于:

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()函数很容易出现竞争条件,因为在文件存在检查和实际文件创建之间存在一个时间窗口,其中另一个进程可以创建该文件(这称为 TOCTTOU 错误:检查时间到使用时间)。你应该使用O_CREAT | O_EXCL以原子方式测试文件是否存在并创建文件。

另一个问题是你总是关闭newfd。如果什么newfd and destinfd出于某种原因,碰巧是一样的?那么你会错误地关闭文件,因为dup2(2)如果您传入两个相同的文件描述符,则本质上是无操作。即使您认为这种情况永远不会发生,在关闭原始 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;
}

考虑更换0666 in open(2)上面有S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH(确保包括sys/stat.h and fcntl.h)。您可能想使用#define为了使其更清晰,但我仍然认为,如果你这样做,而不是硬编码一些神奇的数字,它会更好,更具描述性(尽管这是主观的)。

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

UPDATE

好的,现在我已经查看了完整的源代码,我还有一些评论。

原因cat(1)挂起是因为这个:

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

第二个参数为execvp(2)应该structVariables->argv, not 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(通常)首先通过管道符号分割命令。这也是你的代码的作用。这意味着在解析之后,shell 已经收集了足够的信息,并且命令行被分成 3 个实体(ls命令,该grep命令和sort命令)。
  2. shell 分叉并调用七个中的一个exec()子函数运行ls。现在,请记住,管道意味着程序的输出是下一个程序的输入,所以在之前exec()在中,外壳必须创建一个管道。即将运行的子进程ls(1) calls dup2(2) before exec()将管道的写入通道复制到stdout。同样,父进程调用dup2(2)将管道的读取通道复制到stdin。了解此步骤非常重要:因为父级将管道的读取端复制到stdin,那么无论我们接下来做什么(例如再次分叉以执行更多命令)都将始终从管道读取输入。所以,此时,我们有ls(1)写信给stdout,它被重定向到由 shell 的父进程读取的管道。

  3. shell 现在将执行grep(1)。再次,它派生了一个新进程来执行grep(1)。请记住,文件描述符是通过 fork 继承的,并且父 shell 的进程具有stdin连接到管道的读取端ls(1),所以即将执行的新子进程grep(1)将“自动”从该管道中读取!但是等等,还有更多! shell 知道管道中还有另一个进程(sort命令),因此在执行 grep 之前(以及分叉之前),shell 创建another连接输出的管道grep(1)的输入sort(1)。然后,它重复相同的步骤:在子进程上,管道的写入通道被复制到stdout。在父级中,管道的读取通道被复制到stdin。再次强调,真正理解这里发生的事情很重要:即将执行的流程grep(1)已经从连接到的管道读取其输入ls(1),现在它的输出连接到将馈送的管道sort(1). So grep(1)本质上是从管道中读取数据并写入管道。 OTOH,父 shell 进程将最后一个管道的读取通道复制到stdin,有效地“放弃”读取输出ls(1)(因为grep(1)无论如何都会处理它),而是更新输入流以读取结果grep(1).

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

那么这是如何实现的呢?

很简单:只要还有多个命令需要处理,我们就创建一个管道和叉子。在子进程上,我们关闭管道的读取通道,将管道的写入通道复制到stdout,并呼叫七个中的一个exec()功能。在父级上,我们关闭管道的写入通道,并将管道的读取通道复制到stdin.

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

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

另外,shell 进程需要wait(2) on the last管道中的过程。如果你仔细想想,这是有道理的:管道本质上同步管道中的每个命令;仅当最后一个命令读取时,才假定命令集结束EOF来自管道(也就是说,只有当所有数据流经整个管道时我们才知道我们完成了)。如果 shell 没有等待最后一个进程,而是等待管道中间(或开头)的某个其他进程,那么它会过早返回到命令提示符,并使其他命令仍在管道上运行。后台 - 这不是明智之举,因为用户希望 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():

  • Both dup2(2) and close(2)可能会返回错误;您可能应该检查这一点并采取相应的行动(即通过返回 -1 将错误传递到调用堆栈)。
  • 再次强调,复制描述符后不应该盲目关闭它,因为可能会出现源描述符和目标描述符相同的情况。
  • 你应该验证这一点end或者是READ_END or WRITE_END,如果不成立则返回错误(而不是返回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;
}

祝你的贝壳玩得开心!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

程序卡住,管道文件描述符何时不应该打开? 的相关文章

随机推荐