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

Fjo*_*dor 5 c pipe file-descriptor filehandle

我正在创建一个可以读取命令的小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);
        }
}
Run Code Online (Sandbox Code Playgroud)

我在程序中使用的两个函数如下所示:dupPipe和redirect

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;
}
Run Code Online (Sandbox Code Playgroud)

Fil*_*ves 7

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

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

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

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

这意味着您应该redirect()使用STDOUT_FILENOas destinfd(而不是管道通道)进行输出重定向.同样,输入重定向应该调用redirect()STDIN_FILENO.这些常量是定义的,unistd.h所以请确保包含该标题.

如果失败,您也可能想要退出子项exec(),否则您将运行2个shell进程副本.

最后但并非最不重要的是,您不应该使输入或输出重定向独占.可能是用户想要输入和输出重定向的情况.因此,else if在进行I/O重定向时,我只使用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);
        }
}
Run Code Online (Sandbox Code Playgroud)

正如另一个答案所提到的,你的redirect()函数很容易出现竞争条件,因为文件存在检查和实际文件创建之间有一个时间窗口,其他进程可以创建文件(这称为TOCTTOU错误:检查时间到使用时间).您应该使用O_CREAT | O_EXCL原子测试存在并创建文件.

另一个问题是你总是关闭newfd.出于某种原因,如果newfd并且destinfd碰巧是相同的?那么你将错误地关闭文件,因为dup2(2)如果传入两个相同的文件描述符,本质上是一个无操作.即使您认为这种情况永远不会发生,在关闭原始文件之前,首先检查复制的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;
}
Run Code Online (Sandbox Code Playgroud)

考虑0666open(2)上面替换S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH(确保包括sys/stat.hfcntl.h).您可能希望使用a #define来使其更清晰,但我仍然认为它更好,更具描述性,如果您这样做而不是硬编码一些幻数(虽然这是主观的).

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

UPDATE

好的,既然我已经看了完整的源代码,我还有几个评论.

cat(1)悬挂的原因是因为:

if (execvp(structVariables->argv[0], argv) < 0)
Run Code Online (Sandbox Code Playgroud)

第二个参数execvp(2)应该是structVariables->argv,而不是 argv因为argv是shell程序的参数数组,它通常是空的.传递一个空的参数列表cat(1)使其stdin从文件读取而不是从文件中读取,这就是为什么它似乎挂起 - 它等待你输入输入.所以,继续用以下内容替换该行:

if (execvp(structVariables->argv[0], structVariables->argv) < 0)
Run Code Online (Sandbox Code Playgroud)

这解决了你的一个问题:cat < file.txt > file2.txt现在可以使用的东西(我测试了它).

关于管道重定向

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

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

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

  4. 最后,shell看到这sort(1)是最后一个命令,所以它只是forks + execs sort(1).结果被写入stdout,因为我们从来没有改变过stdout的外壳工艺,但输入是从连接管道中读取grep(1)sort(1),因为我们的行动步骤3中.

那么这是如何实现的呢?

简单:只要有多个命令要处理,我们就会创建一个管道和分支.在子节点上,我们关闭管道的读取通道,将管道的写入通道复制到stdout,并调用七个exec()函数之一.在父级上,我们关闭管道的写入通道,并将管道的读取通道复制到stdin.

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

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

此外,shell进程需要在管道中wait(2)最后一个进程上.如果你考虑它,它是有道理的:管道固有地同步管道中的每个命令; 只有当最后一个命令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");
}
Run Code Online (Sandbox Code Playgroud)

最后一些评论dupPipe():

  • 双方dup2(2)close(2)可能会返回一个错误; 你应该检查一下并采取相应的行动(即通过返回-1将错误传递给调用堆栈).
  • 同样,在复制后不应盲目地关闭描述符,因为源和目标描述符可能是相同的.
  • 您应该验证end是否为READ_END或者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;
}
Run Code Online (Sandbox Code Playgroud)

玩得开心!