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

5
我正在创建一个小型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和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;
}

不太清楚您想要做什么。请发布完整的代码,以及它是如何被调用的样本,以及您期望发生的确切情况。 - dbush
3个回答

8
看起来你正在尝试编写一个shell来运行从输入读取的命令(如果不是这样,请编辑你的问题,因为它不够清晰)。我不确定为什么你认为像cat file.txt > file2.txt这样的命令中使用管道,但无论如何,都是不需要的。让我们看看在bash等shell中键入cat file.txt > file2.txt时发生了什么:
1. 创建一个子进程,其中cat(1)将运行。 2. 子进程打开file2.txt进行写操作(稍后详细说明)。 3. 如果open(2)成功,则子进程将新打开的文件描述符复制到stdout(因此stdout将有效地指向与file2.txt相同的文件表项)。 4. 通过调用七个exec()函数之一来执行cat(1)。参数file.txt传递给cat(1),因此cat(1)将打开file.txt并读取所有内容,将其内容复制到stdout(它被重定向到file2.txt)。 5. cat(1)完成执行并终止,这将导致关闭和刷新所有打开的文件描述符。在cat(1)终止时,file2.txtfile.txt的副本。 6. 同时,父shell进程等待子进程终止,然后打印下一个提示并等待更多命令。
如你所见,在I/O重定向中不使用管道。管道是一种用于将一个进程的输出馈送到另一个进程的输入的进程间通信机制。在这里只有一个进程运行(cat),那么为什么你需要管道呢?
这意味着你应该使用STDOUT_FILENO作为destinfd(而不是管道通道)来调用redirect()进行输出重定向。类似地,输入重定向应该使用STDIN_FILENO调用redirect()。这些常量在unistd.h中定义,因此请确保包含该头文件。
最后但并非最不重要的是,你不应该使输入或输出重定向排他性。用户可能同时需要输入和输出重定向。因此,在进行I/O重定向时,我会使用2个独立的ifs而不是else 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。如果由于某种原因newfddestinfd是相同的呢?那么您将错误地关闭文件,因为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;
}

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

  3. Shell现在将执行grep(1)。同样,它fork一个新进程来执行grep(1)。请记住,文件描述符跨fork继承,并且父shell进程的stdin与连接到ls(1)的管道的读端相连,因此即将执行grep(1)的新子进程将“自动”从该管道中读取!但是,请等一下!Shell知道管道中还有另一个进程(sort命令),因此在执行grep之前(并在fork之前),shell创建了另一个管道,以将grep(1)的输出连接到sort(1)的输入。然后,它重复相同的步骤:在子进程上,将管道的写通道复制到stdout上。在父进程中,将管道的读通道复制到stdin上。再次强调,真正理解这里发生的事情非常重要:即将执行grep(1)的进程已经从连接到ls(1)的管道中读取其输入,现在它的输出已连接到将提供sort(1)的管道。因此,grep(1)基本上是从管道中读取并写入管道。另一方面,父shell进程将最后一个管道的读通道复制到stdin上,有效地“放弃”从ls(1)的输出中读取(因为grep(1)将处理它),而是更新输入流以从grep(1)读取结果。

  4. 最后,shell看到sort(1)是最后一个命令,因此它只fork + execs sort(1)。结果被写入stdout,因为我们没有在shell进程中更改stdout,但是由于第3步中的操作,输入从连接grep(1)sort(1)的管道中读

    那么这是如何实现的呢?
    很简单:只要还有多个命令需要处理,我们就创建一个管道并进行分叉。在子进程中,我们关闭管道的读通道,将管道的写通道复制到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");
    }
    

    有关 dupPipe() 的一些最终说明:

    • dup2(2)close(2) 都可能返回错误;你应该检查这一点,并相应地采取行动(即通过返回 -1 将错误传递给调用堆栈)。
    • 同样,不应该盲目关闭复制后的描述符,因为源描述符和目标描述符可能是相同的。
    • 你应该验证 end 是否为 READ_ENDWRITE_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;
    }
    

    享受您的shell吧!


这个想法是我应该能够输入例如:cat file.txt | tail -5 > file2.txt。但我发现即使我写了cat file.txt > file2.txt,它也不起作用,所以管道(pipe)还不需要(尚未实现)。无论如何,感谢你的回答!我根据你的输入更新了我的代码,我看到了我的错误,但它仍然不起作用……程序在执行execvp时仍然会卡住。然后我使用ctrl + d查找文件,实际上已经创建了文件,但里面没有任何内容。你能找出其他问题吗? - Fjodor
@Fjodor 这很奇怪。你能否更新你的问题并添加所有代码,以便我可以在这里编译它?structVariables中可能有问题,或者你解析命令的方式有问题。请展示整个代码给我看。 - Filipe Gonçalves
这是我正在处理的一个任务,我得到了免费的解析代码,所以我可以保证没有任何问题。如果您想编译代码,我可以通过电子邮件或其他方式将代码发送给您,这样我就不必花费很多时间将其放在这里并淹没Q。但正如我所说,我相当确定解析不是问题所在。 - Fjodor
@Fjodor 嗯,我并不是说这是解析代码的问题,我的观点是你认为没问题的代码中可能存在一些其他部分的错误,其影响在程序后期才会显现。请随时通过电子邮件将代码发送给我,我很乐意为您解决此问题(并更新我的答案)。请查看我的个人资料以获取我的地址。 - Filipe Gonçalves
好的,我现在已经发送了文件。感谢您抽出时间来处理这个事情!我很感激。 - Fjodor
显示剩余3条评论

2

如果没有错误,execvp函数将不会返回。

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

代码的正常执行顺序为:

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

我不明白将父级放在子级之前或之后会如何帮助我解决我的问题。 - Fjodor

0

如果flags == 1,在redirect()中你正在错误地使用open()

    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 参数,而不是与 flags 进行或运算:
    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) 来原子性地创建和打开一个文件。

我已经创建了一个模式,但我不确定如何使用umask,我之后只需要调用它吗? - Fjodor
如果您想更改进程的文件创建掩码,您需要在open()/creat()之前调用umask()。如果您不知道该怎么做,应该阅读umask()的手册,但如果您很着急,可以直接调用umask(0),这样open()mode参数将不会被修改。 - EOF
好的,我通过在open(3)中输入0666来解决了问题。现在,在创建文件后打开文件时它可以正常工作。但是文件里面没有任何内容...这表明管道出了问题?我找不到解决方案... - Fjodor
是的,一个新创建的文件是空的。你需要更具体地描述你的问题。你希望代码做什么,它实际上做了什么,相关的代码是什么? - EOF
我的意思是,我正在使用命令“cat myFile.c”(已经存在代码的文件),但它不会打印任何内容(当我在我的shell中时)。程序在execvp行处挂起...根据互联网上的其他线程,问题似乎出现在文件描述符没有正确打开和关闭。所以我想知道是否你能看出有什么问题,因为我已经看了一段时间了。相关代码是除了“redirect”函数中的内容之外的所有内容,因为如果我只输入“cat myFile.c”,它永远不会进入那里。 - Fjodor
显示剩余2条评论

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