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

时间:2015-07-20 14:42:35

标签: 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);
        }
}

我在程序中使用的两个函数如下所示: 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;
}

3 个答案:

答案 0 :(得分:7)

您似乎正在尝试编写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. 通过调用七个cat(1)函数之一来执行
  5. exec()。参数file.txt传递给cat(1),因此cat(1)将打开file.txt并阅读所有内容,将其内容复制到stdout(重定向到{{1} }})。
  6. file2.txt完成执行并终止,这会导致任何打开的文件描述符被关闭和刷新。在cat(1)终止时,cat(1)file2.txt的副本。
  7. 同时,父shell进程在打印下一个提示并等待更多命令之前等待子进程终止。
  8. 如您所见,I / O重定向中未使用管道。管道是一种进程间通信机制,用于将进程的输出提供给另一个进程的输入。你只有一个进程在这里运行(file.txt),所以为什么你甚至需要管道?

    这意味着您应该使用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时在幕后发生的事情。了解这些步骤非常重要,这样您就可以建立一个准确的系统工作心理模型;没有这样的愿景,你就不会真正理解这个实现:

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

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

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

    5. 那么这是如何实现的呢?

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

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

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

      此外,shell进程需要在管道中的 last 进程上man 2 fcntl。如果你考虑它,它是有道理的:管道固有地同步管道中的每个命令;只有当最后一个命令从管道读取wait(2)时,才假定命令集结束(也就是说,我们知道只有当所有数据都流过整个管道时才会完成)。如果shell没有等待最后一个进程,而是等待管道中间(或开头)的其他进程,它会过早地返回命令提示符并让其他命令仍在运行在后台 - 不是智能移动,因为用户期望shell在等待更多之前完成当前作业的执行。

      所以...这是很多信息,但你理解它是非常重要的。所以修改后的主要代码在这里:

      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)都可能返回错误;你应该检查一下并采取相应的行动(即通过返回-1将错误传递给调用堆栈)。
      • 同样,在复制后不应该盲目地关闭描述符,因为源和目标描述符可能是相同的。
      • 您应该验证close(2)end还是READ_END,如果不是,则返回错误(而不是返回WRITE_END,无论如何,给调用者代码一个错误的成功感)

      以下是我如何改进它:

      destinfd

      玩弄你的贝壳!

答案 1 :(得分:2)

除非出现错误,否则execvp不会返回。

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

正常的代码序列是:

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

答案 2 :(得分:0)

open()如果redirect(),您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中,mode(错误地)用于代替open()的{​​{1}}参数,而不是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)之前创建文件。使用 public QueryPage<Merchant> getMerchants(Integer supermerchantId, Geometry point, Double maxDistance, String keyword, List<Integer> businessTypes, Integer numPage, Integer sizePage) { QMerchant merchant = QMerchant.merchant; QMerchantBusinessType merchantBusinessType= QMerchantBusinessType.merchantBusinessType; JPAQuery query = new JPAQuery(entityManager); query = query.from(merchant) .innerJoin(merchant.merchantBusinessTypes, merchantBusinessType); // Master merchant quickFix query = query.where(merchant.idSupermerchant.isNotNull()); if (supermerchantId!=null){ query = query.where(merchant.idSupermerchant.mercId.eq(supermerchantId)); } if(point != null && maxDistance != null){ query = query.where(merchant.point.distance(point).lt(maxDistance)); } if(keyword!=null){ query = query.where(merchant.mercName.containsIgnoreCase(keyword)); } if(businessTypes!=null){ query = query.where(merchantBusinessType.businessType.id.in(businessTypes).and(merchantBusinessType.merchant.eq(merchant))); } QueryPage<Merchant> res = new QueryPage<>(); JPAQuery queryCount = query.clone(entityManager); res.setTotalElements(queryCount.distinct().count()); if(point != null) { LOGGER.debug(point.toString()); query = query.orderBy(merchant.point.distance(point).asc()); } else { query = query.orderBy(merchant.mercName.asc()); } Long offset = new Long((numPage) * sizePage); query = query.distinct(); query = query.offset(offset).limit(sizePage); // distinct() LOGGER.debug("=== QUERY:"); LOGGER.debug(query.toString()); List<Merchant> merchants = query.list(merchant); res.setContent(merchants); res.setPageNumber(numPage); res.setPageSize(sizePage); return res; } 以原子方式创建和打开文件。