封闭管道的不同端部

封闭管道的不同端部

我使用以下命令为 IPC 编写了以下代码pipe()

#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
    
    
int main(void) {
    char message_buffer[15] = "Hello World \n";  
    char read_buffer[15];
    int fd[2]; 
   
    int return_value = pipe(fd);
   
    if (return_value < 0) {
        printf("Error creating the pipe");
    }
    
    int rc = fork();
    if (rc  < 0) {
        printf("Error forking a child");
    }
    
    if (rc > 0) {
        close(fd[0]);
        write(fd[1], message_buffer, 15);
        close(fd[1]);
        wait(NULL);
    } else {
        close(fd[1]);
        read(fd[0], read_buffer, 15);
        close(fd[0]);
        printf("The Message: %s", read_buffer);
    }

    return 0;
}

我是管道新手,我有以下问题:

  1. 我不明白为什么父级需要在写入之前关闭读取端,并且在写入之后需要关闭写入端?
  2. 对于孩子来说也是如此,为什么它需要在读取之前关闭写入端,然后为什么需要在读取后关闭读取端?
  3. 由于父进程和子进程同时运行,如果子进程在父进程写入消息时读取消息,会发生什么情况?
  4. 由于父级和子级同时运行,如果子级读取而父级尚未在管道中写入任何内容,会发生什么情况?

我的问题似乎很愚蠢,但请帮忙回答,因为我正在为课程考试学习普通管道。

答案1

答案是问题1和2在里面pipe 手册页(“示例”部分):

分叉后,每个进程都会关闭管道不需要的文件描述符(请参阅管道(7))。

由于管道是单向的,因此它有指定的末端 -结束和结尾。如果父级将使用此管道向子级写入数据,则父级没有必要保持读取端打开。相反,如果子进程要从管道读取数据,则不需要打开写入端。

但一个更重要的原因用于关闭管道不需要的末端描述由用户@ilkkachu。请查看链接的答案。

编辑:

您还问为什么父级在写入后需要关闭写入端,为什么子级在读取后需要关闭读取端。

他们不必这样做。如果两个程序要继续运行并使用管道交换数据,则它们必须保持管道打开。在一个简短的示例程序中,该程序仅演示管道的使用,并且在传输一条消息后终止,父进程和子进程可能会为了在程序终止之前正确清理资源而关闭管道文件描述符。

问题 #3 和 #4 的答案在pipe(7) 手册页

你的问题#3:

由于父进程和子进程可以同时运行,如果子进程在父进程写入消息时读取消息,会发生什么情况?

子级将能够读取管道中已由父级写入的任何可用数据。根据手册页:

POSIX.1 规定小于 PIPE_BUF 字节的写入必须是原子的:输出数据作为连续序列写入管道。超过 PIPE_BUF 字节的写入可能是非原子的:内核可能会将数据与其他进程写入的数据交错。 POSIX.1 要求 PIPE_BUF 至少为 512 字节。 (在 Linux 上,PIPE_BUF 为 4096 字节。)

你的问题#4:

由于父级和子级可以同时运行,如果子级读取而父级尚未在管道中写入任何内容,会发生什么情况?

手册页说:

如果进程尝试从空管道读取数据,则 read(2) 将阻塞,直到数据可用。如果进程尝试写入完整管道(见下文),则 write(2) 会阻塞,直到从管道读取足够的数据以允许写入完成。

回答评论中的问题:

对于问题1和2,这意味着如果我没有关闭不需要的末端,这不会以任何方式影响程序?

它不应阻止管道工作,但会在程序使用的资源上产生一些占用空间。通过关闭管道不需要的端部,这些资源就不会被保留。

对于问题 3,这意味着孩子将阅读父母正在写的内容,孩子如何知道父母已经完成了需要写的内容?

手册页说:

管道提供的通信通道是字节流:没有消息边界的概念。

这意味着管道不关心你传输的数据。它不知道“消息”意味着什么,也不知道父级是否已完成写入或想要写入更多数据。

您将需要实现自己的技术来确定什么是“完整消息”。例如,父级可以通过发送特殊字符(例如,\0或者实际上在使用管道的特定上下文中有意义的任何其他字符)来向子级表示已写入完整消息。

答案2

请参阅pipe(7)手册页

在“管道和 FIFO 上的 I/O”下,它说:

如果引用管道写入端的所有文件描述符都已关闭,则尝试从管道读取(2)将看到文件结尾(读取(2)将返回0)。

如果引用管道读取端的所有文件描述符都已关闭,则 write(2) 将导致为调用进程生成 SIGPIPE 信号。

让子进程关闭其写入端的副本(它不会使用它),使子进程可以检测到何时家长这样做。如果子进程保持写入端打开,它永远不会在管道上看到 EOF,因为它本质上是在等待自己。 (2)

类似地,让父进程关闭其读取端的副本也可以让父进程检测子进程是否消失。 (1)

并不是说您那里的代码曾经检查read()write()或尝试读取/写入可变数量的数据的返回值,因此除了父级获取 SIGPIPE 信号之外,这基本上没有任何意义。

在写入后关闭父级中的写入端,在子级中读取后关闭读取端只是常见的内务处理。如果进程在之后立即退出,则显式关闭不会产生任何影响。

我不确定你的问题3和4是否相同,但是如果读者在没有什么可读的时候阅读,系统调用将会阻塞:

如果进程尝试从空管道读取数据,则 read(2) 将阻塞,直到数据可用。

如果写入器在读取器正在执行其他操作时进行写入,则数据将被复制到操作系统中的缓冲区(至少在有足够空间的情况下)。如果没有,那么作者将阻止:

如果进程尝试写入完整管道(见下文),则 write(2) 会阻塞,直到从管道读取足够的数据以允许写入完成。

“下面”是“管道容量”部分。

如果他们同时执行此操作,操作系统将仅复制数据。

答案3

其他人提供了很好的答案。这是 ilkkachu 对上面答案的评论的延伸,旨在为您提供实验性的帮助。考虑以下程序,它是您最初发布的程序的稍加修改的版本:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

struct message {
    char content[15];
};

int main(void) {
    int fds[2];

    if (pipe(fds) < 0) {
        printf("Error creating the pipe\n");
        return 1;
    }

    srand(time(NULL));

    const pid_t pid = fork();
    if (pid < 0) {
        printf("Error forking a child");
        return 1;
    }

    if (pid > 0) {
        const int message_count = (rand() % 9) + 1;
        const struct message message_buffer = {
            .content = "Hello, World\n",
        };

        /* 1 */ close(fds[0]);

        for (int i = 0; i < message_count; ++i) {
            write(fds[1], &message_buffer, sizeof(message_buffer));
        }

        /* 2 */ close(fds[1]);

        wait(NULL);
    } else {
        struct message read_buffer;

        /* 3 */ close(fds[1]);

        while (read(fds[0], &read_buffer, sizeof(read_buffer)) > 0) {
            printf("The Message: %s", read_buffer.content);
        }

        /* 4 */ close(fds[0]);
    }

    return 0;
}

这个版本不发送A消息,它会发送 1 到 10 之间的随机数量的消息。这样,子进程就不会提前知道要读取多少条消息——当它从管道中读取所有内容时,它就会停止并且没有其他内容可以写入管道(即,当所有写端都关闭并read返回负值时)。以下是几个示例运行:

$ ./a.out
The Message: Hello, World
$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
$

close()请注意,我在与管道关联的每个调用之前都添加了注释。如果仅注释掉第 (1) 行,则程序的行为不会发生明显变化:

$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
$ ./a.out
The Message: Hello, World
The Message: Hello, World
$

如果只注释掉第(2)行,则程序在运行时会死锁,例如:

$ ./a.out
The Message: Hello, World
The Message: Hello, World
(program hangs here)

为什么?如果父进程不关闭管道的写入端,那么子进程将永远阻塞在调用中,等待read更多数据。(read如果有任何打开与管道写入端关联的文件描述符。)然后父级将永远阻塞在对 的调用中wait。父级和子级都永远等待对方。

如果只注释掉第(3)行,则程序在运行时会死锁,例如:

$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
(program hangs here)

为什么?同样,read只要存在,对的调用就会阻塞任何打开与管道写入端关联的文件描述符,并且子进程有一个。结果,子级再次阻塞在调用中read,父级阻塞在调用中wait,并且没有任何进一步的进展。

最后,如果您仅注释掉第 (4) 行,则程序的行为不会发生明显变化:

$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
$ ./a.out
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
The Message: Hello, World
$

通过确保所有进程关闭它们不需要的文件描述符,您可以确保没有进程因等待从永远不会到来的管道中读取数据而被阻塞。

答案4

在某些情况下,未正确关闭未使用的管道描述符会产生重要影响:

  1. 为什么作者应该关闭读描述符

尝试写入没有读取器的管道会生成该SIGPIPE信号。这应该在读者离开时发生(例如进程已退出)。

但是,当写入者保持管道的读取描述符打开时,不会SIGPIPE发生任何情况,因为从技术上讲,管道没有损坏,写入者本身就是可能的读取者。

  1. 为什么读者应该关闭写描述符

关闭管道会生成 EOF 条件。如果读取器不关闭其写入描述符的副本,则写入器无法发送 EOF,因为读取器仍可以将更多数据写入管道。

相关内容