file2.txt"时,它将创建文件,然后卡在以下一行:if(execvp(structVariables->a-6ren">
gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-01 16:10:59 25 4
gpt4 key购买 nike

我正在创建一个可以读取命令的小外壳。当我运行程序并键入:"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这样的命令中使用管道,但无论如何都不会使用管道。让我们看看在bash之类的shell中键入cat file.txt > file2.txt时会发生什么情况:

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

  • 如您所见,I / O重定向中未使用管道。管道是一种进程间通信机制,用于将一个进程的输出馈送到另一个进程的输入中。您仅在此处运行一个进程( cat),那么为什么还要使用管道呢?

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

    如果 exec()失败,您可能还想退出子进程,否则您将运行2个Shell进程副本。

    最后但并非最不重要的一点是,您不应使输入或输出重定向排他。用户可能同时需要输入和输出重定向。因此,在执行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不同。

    这是解决了这些问题的代码:
    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中的 open(2)替换为 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时,幕后情况。了解这些步骤很重要,这样您就可以建立有关系统工作方式的准确心智模型。没有这样的愿景,您将不会真正理解实现:
  • shell(通常)首先通过管道符号分割命令。这也是您的代码所做的。这意味着在解析之后,shell收集了足够的信息,并将命令行分为3个实体(ls命令,grep命令和sort命令)。
  • Shell会在子进程上派生并调用七个exec()函数之一来运行ls。现在,记住管道意味着程序的输出是下一个的输入,因此在exec()编码之前,外壳程序必须创建一个管道。将要运行ls(1)的子进程在dup2(2)之前调用exec(),以将管道的写通道复制到stdout上。同样,父进程调用dup2(2)将管道的读取通道复制到stdin上。理解这一步非常重要:因为父级将管道的读取端复制到stdin,然后我们接下来要做的任何事情(例如再次执行fork来执行更多命令)将始终从管道中读取输入。因此,在这一点上,我们已经将ls(1)写入stdout,它被重定向到由外壳程序的父进程读取的管道。
  • 现在,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)读取结果。
  • 最后,shell看到sort(1)是最后一个命令,因此它只是派生+ 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时,才假定命令集结束了(也就是说,我们知道只有当所有数据都流过整个管道时我们才完成)。如果外壳程序不等待最后一个进程,而是在管道的中间(或开始)等待其他进程,它将很快返回到命令提示符,而其他命令仍在运行背景-不是明智之举,因为用户希望外壳程序在等待更多任务之前完成当前作业。

    所以……这是很多信息,但了解它确实很重要。因此,修改后的主要代码如下:
    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将错误传递到调用堆栈中)。
  • 同样,复制描述符后也不要盲目关闭描述符,因为源描述符和目标描述符可能是相同的。
  • 您应该验证endREAD_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;
    }

    玩得开心!

    关于c - 程序卡住了,管道文件描述符何时不应该打开?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31519435/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com