【问题标题】:Why does read() block and wait forever in parent process despite the writing end of pipe being closed?尽管管道的写入端已关闭,为什么 read() 会在父进程中阻塞并永远等待?
【发布时间】:2023-09-19 22:35:01
【问题描述】:

我正在编写一个包含两个通过管道进行通信的进程的程序。子进程从父进程中读取一些参数,用它们执行一个shell脚本,并将结果逐行返回给父进程。

我的代码运行良好,直到我在父进程结束时编写了while(read()) 部分。孩子将执行 shell 脚本,从popen() 读取其回显并将它们打印到标准输出。

现在我也尝试将结果写入管道并在父端的while() 循环中读取它们,但它会阻塞并且子进程也不会将结果打印到标准输出。显然它在从父发送的管道中读取数据后甚至都达不到这一点。

如果我在父进程注释掉while(),子进程打印结果返回,程序顺利结束。

为什么即使我在父进程和子进程中都关闭了管道的写入端,while(read()) 也会阻塞?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int read_from_file(char **directory, int *octal) {
        FILE *file = fopen("input", "r");
        if (file == NULL) {
                perror("error opening file");
                exit(1);
        }
        fscanf(file, "%s %d", *directory, octal);
}

int main(int argc, char *argv[]) {

        char *directory = malloc(256);
        int *octal = malloc(sizeof *octal);

        pid_t pid;
        int pfd[2];
        char res[256];
        if (pipe(pfd) < 0) {
                perror("Error opening pipe");
                return 1;
        }

        if ((pid = fork()) < 0)
                perror("Error forking");

        if (pid == 0) {
                printf("client here\n");
                if (read(pfd[0], directory, 256) < 0)
                        perror("error reading from pipe");
                if (read(pfd[0], octal, sizeof(int)) < 0)
                        perror("error reading from pipe");
// This won't get printed:
                printf("client just read from pipe\n");
//              close(pfd[0]);

                char command[256] = "./asd.sh ";
                strcat(command, directory);
                char octal_c[5];
                sprintf(octal_c, " %d", *octal);
                strcat(command, octal_c);

                FILE *f = popen(command, "r");
                while (fgets(res, 256, f) != NULL) {
                        printf("%s", res);
                        if (write(pfd[1], res, 256) < 0)
                                perror("Error writing res to pipe");
                }
                fclose(f);
                close(pfd[1]);
                close(pfd[0]);
                fflush(stdout);
                return 1;
        }

        read_from_file(&directory, octal);

        if (write(pfd[1], directory, 256) < 0)
                perror("Error writing dir to pipe");
        if (write(pfd[1], octal, sizeof(int)) < 0)
                perror("error writing octal to pipe");

        int r;
        close(pfd[1]);

        while (r = read(pfd[0], res, 256)) {
                if (r > 0) {
                        printf("%s", res);
                }
        }
        close(pfd[0]);

        while (wait(NULL) != -1 || errno != ECHILD);
}

【问题讨论】:

  • read 在出错时返回 -1,因此您的 while 循环将永远不会终止。您需要检查这种情况。
  • 这似乎不是问题的直接意义,但是“这不会被打印”的评论准确吗?这很容易与问题有关。
  • @JohnBollinger 我收到一条“客户端在这里”消息,它在那里阻塞,没有打印“客户端刚刚从管道读取”日志。我认为这一定与管道没有正确关闭有关,但我看不出我做错了什么,因为如果我在父进程中注释掉 while() (这样它就不会尝试从pipe),一切运行顺利并被打印出来,然后程序结束。
  • @Thomas 会不会是因为父进程试图读取它打印到管道的消息,而不是让子进程先读取它?

标签: c pipe fork blocking


【解决方案1】:

因为孩子明显达到...

                printf("client here\n");

...但似乎达不到...

                printf("client just read from pipe\n");

...我们可以假设它在两个read() 之间的调用之一上无限期地阻塞。有了正确的时机,这就解释了为什么父级会自行阻止 read() 来自管道。但是这种阻塞是如何以及为什么会发生的呢?

您的程序中至少存在三个严重的语义错误:

  1. 管道不适用于双向通信。例如,一个进程有可能读回它自己写入并打算用于不同进程的字节。如果要双向通信,请使用两个管道。在您的情况下,我认为这可以避免明显的死锁,尽管它本身不会使程序正常工作。

  2. writeread 不一定传输请求的全部字节数,短读和写不被认为是错误的。成功时,这些函数返回传输的字节数,如果您想确保传输特定数量的字节,则需要在循环中运行readwrite,使用返回值来跟踪进度通过正在传输的缓冲区。或者改用fread()fwrite()

  3. 管道传送未区分的字节流。也就是说,它们不是面向消息的。假设从管道读取将与对管道的写入配对是不安全的,因此每次读取都会准确接收一次写入写入的字节。然而,您的代码依赖于这种情况发生。

这里有一个可能解释你观察到的失败场景:

父母:

  1. fork()s 孩子。
  2. 一段时间后对管道执行两次写入,一次来自变量directory,另一次来自变量octal。至少其中第一个是短文。
  3. 关闭管道写入端的副本。
  4. 尝试从管道读取的块。

孩子:

  1. 读取所有通过其第一次读取写入的字节(到其directory的副本中)。
  2. 在第二个read() 上阻塞。尽管父级关闭了其写入端的副本,它仍可以执行此操作,因为管道的写入端在子级中仍处于打开状态。

然后你就会陷入僵局。管道的两端至少在一个进程中是打开的,管道是空的,并且两个进程都被阻塞,试图读取永远无法到达的字节。

还有其他可能性也基本相同,其中一些不依赖于简短的写入。

【讨论】:

    【解决方案2】:

    父进程试图在子进程读取管道并将结果写入管道之前从管道中读取。使用两个不同的管道进行双向通信解决了这个问题。

    【讨论】: