当前位置: 首页 > news >正文

管道与进程间通信

目录

匿名管道

命名管道

socketpair()创建全双工管道

匿名管道

        匿名管道是一种半双工的通信机制,数据只能在一个方向上流动,并且只能在具有亲缘关系(如父子进程)的进程间使用。
        int pipe(int pipefd[2]);
        
pipefd 是一个包含两个文件描述符的数组,pipefd[0] 用于读取管道数据,pipefd[1] 用于写入管道数据。

        工作原理:

  1. 父进程调用 pipe 函数创建管道,得到两个文件描述符 pipefd[0] 和 pipefd[1]
  2. 父进程调用 fork 创建子进程,子进程会继承父进程的文件描述符,因此父子进程都拥有这两个文件描述符。
  3. 父子进程各自关闭不需要的文件描述符(例如父进程关闭 pipefd[0],子进程关闭 pipefd[1],这样就建立了单向通信)。
  4. 父进程通过 pipefd[1] 写入数据,子进程通过 pipefd[0] 读取数据,实现进程间通信。

       示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>#define BUFFER_SIZE 1024int main() {int pipefd[2];pid_t pid;char buffer[BUFFER_SIZE];// 创建管道if (pipe(pipefd) == -1) {perror("pipe");return 1;}// 创建子进程pid = fork();if (pid == -1) {perror("fork");close(pipefd[0]);close(pipefd[1]);return 1;} else if (pid == 0) {// 子进程close(pipefd[1]); // 关闭写入端ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);if (bytes_read == -1) {perror("read");} else {buffer[bytes_read] = '\0';printf("Child received: %s\n", buffer);}close(pipefd[0]); // 关闭读取端} else {// 父进程close(pipefd[0]); // 关闭读取端const char *message = "Hello from parent";if (write(pipefd[1], message, strlen(message)) == -1) {perror("write");}close(pipefd[1]); // 关闭写入端}return 0;
}

        输出结果:

命名管道

        命名管道是一种特殊类型的文件,它在文件系统中有一个对应的文件名,因此可以在不相关的进程间进行通信。
        int mkfifo(const char *pathname, mode_t mode)

  • pathname 是命名管道的路径名。
  • mode 用于指定命名管道的权限,类似于 open 函数中的权限参数。
  • 当 mkfifo 函数成功创建命名管道时,返回值为 0。这表明命名管道已成功在文件系统中创建,后续可以通过相应的文件操作函数(如 openreadwrite 等)对其进行操作,实现进程间通信。如果 mkfifo 函数执行失败,返回值为 -1,并且会设置全局变量 errno 来指示具体的错误原因。(errno的两个常见值:1.EACCES:权限不足。调用进程没有足够的权限在指定路径下创建命名管道。例如,尝试在没有写权限的目录中创建管道时会出现此错误。2.EEXIST:指定的路径名已经存在,并且它不是一个命名管道。在调用 mkfifo 时,如果指定路径下已有同名的普通文件、目录或其他类型的文件,就会返回此错误。不过,通常在代码中可以通过检查 errno 是否为 EEXIST 来决定是否忽略该错误,因为有时可能只是想确保命名管道存在,而不关心它是否已提前创建。)

        工作原理:

  1. 一个进程调用 mkfifo 创建命名管道,在文件系统中创建一个特殊的 FIFO 文件。
  2. 不同的进程可以通过 open 函数打开这个 FIFO 文件,一个进程以写入模式打开(O_WRONLY),另一个进程以读取模式打开(O_RDONLY)。
  3. 写入进程通过 write 函数向 FIFO 文件写入数据,读取进程通过 read 函数从 FIFO 文件读取数据,从而实现进程间通信。

        示例代码如下:

        写端代码(writer.c)

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>#define FIFO_PATH "/tmp/myfifo"
#define BUFFER_SIZE 1024int main() {int fd;char buffer[BUFFER_SIZE];const char *message = "Hello from writer";// 创建命名管道if (mkfifo(FIFO_PATH, 0666) == -1 && errno != EEXIST) {perror("mkfifo");return 1;}// 打开命名管道进行写入fd = open(FIFO_PATH, O_WRONLY);if (fd == -1) {perror("open");return 1;}// 写入数据if (write(fd, message, strlen(message)) == -1) {perror("write");}close(fd);return 0;
}

        读端代码(reader.c)

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>#define FIFO_PATH "/tmp/myfifo"
#define BUFFER_SIZE 1024int main() {int fd;char buffer[BUFFER_SIZE];// 打开命名管道进行读取fd = open(FIFO_PATH, O_RDONLY);if (fd == -1) {perror("open");return 1;}// 读取数据ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);if (bytes_read == -1) {perror("read");} else {buffer[bytes_read] = '\0';printf("Reader received: %s\n", buffer);}close(fd);return 0;
}

需要注意的是:

  • mkfifo 创建的命名管道在文件系统中表现为一个特殊类型的文件。如同普通文件一样,除非通过特定的文件删除操作,否则它会一直存在于文件系统中。这与匿名管道不同,匿名管道是内核在内存中创建和管理的,当所有使用它的进程关闭相关文件描述符后,匿名管道会自动消失。
  • 例如,你在 /tmp 目录下创建了一个命名管道 /tmp/myfifo,即使创建它的进程已经结束运行,只要没有执行删除操作,/tmp/myfifo 这个命名管道文件就会一直保留在 /tmp 目录中。

socketpair()创建全双工管道

        如果大家尚未拥有Linux网络编程的基础,可以先不着急看这部分内容。
        int socketpair(int domain, int type, int protocol, int sv[2]);

  • domain:指定套接字域。通常使用 AF_UNIX(也称为 AF_LOCAL),表示本地通信,这意味着套接字对在同一主机上的进程间进行通信,不需要网络协议栈的参与。
  • type:指定套接字类型。常见的类型有 SOCK_STREAM(面向连接的字节流套接字,提供可靠的、有序的、无差错的数据传输)和 SOCK_DGRAM(无连接的数据报套接字,数据传输不保证顺序和可靠性)。对于 socketpair,一般使用 SOCK_STREAM 来确保数据传输的可靠性。
  • protocol:通常设置为 0,表示使用默认协议。对于给定的 domain 和 type,系统会选择合适的默认协议。
  • sv:是一个整数数组,长度为 2。函数成功时,sv[0] 和 sv[1] 分别是创建的套接字对的两个文件描述符
  • 如果函数成功,返回 0,并且 sv 数组将包含两个有效的套接字文件描述符。
  • 如果函数失败,返回 -1,并设置 errno 以指示错误原因。常见的错误包括 EAFNOSUPPORT(不支持指定的地址族)、EINVAL(无效的参数)等。

     本质上来讲socketpair()创建的是两个可以相互通信的套接字,大家都知道套接字是可读可写的。所以说逻辑上可以把socketpair()看做创建了一条全双工管道。

        示例代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>#define BUFFER_SIZE 1024void handle_error(const char *msg) {perror(msg);exit(EXIT_FAILURE);
}int main() {int socket_pair[2];pid_t pid;char send_buffer[BUFFER_SIZE];char recv_buffer[BUFFER_SIZE];// 创建 socketpairif (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1) {handle_error("socketpair creation failed");}// 创建子进程pid = fork();if (pid == -1) {handle_error("fork failed");} else if (pid == 0) {// 子进程close(socket_pair[0]); // 父进程关闭socket_pair[0]套接字,只使用socket_pair[1]进行读写// 子进程向父进程发送数据const char *child_message = "Hello from child";if (send(socket_pair[1], child_message, strlen(child_message), 0) == -1) {handle_error("child send failed");}printf("Child sent: %s\n", child_message);// 子进程从父进程接收数据ssize_t child_bytes_received = recv(socket_pair[1], recv_buffer, sizeof(recv_buffer) - 1, 0);if (child_bytes_received == -1) {handle_error("child recv failed");}recv_buffer[child_bytes_received] = '\0';printf("Child received from parent: %s\n", recv_buffer);close(socket_pair[1]);} else {// 父进程close(socket_pair[1]); // 父进程关闭socket_pair[1]套接字,只使用socket_pair[0]进行读写// 父进程从子进程接收数据ssize_t parent_bytes_received = recv(socket_pair[0], recv_buffer, sizeof(recv_buffer) - 1, 0);if (parent_bytes_received == -1) {handle_error("parent recv failed");}recv_buffer[parent_bytes_received] = '\0';printf("Parent received from child: %s\n", recv_buffer);// 父进程向子进程发送数据const char *parent_message = "Hello from parent";if (send(socket_pair[0], parent_message, strlen(parent_message), 0) == -1) {handle_error("parent send failed");}printf("Parent sent: %s\n", parent_message);close(socket_pair[0]);}return 0;
}

        输出结果如下:

        接下来对比一下socketpair()与传统pipe的不同:
  

  • socketpair:创建的是全双工通信通道。意味着两个进程(或线程)通过这对套接字可以同时进行数据的发送和接收操作,数据能够在两个方向上同时传输。例如,在父子进程通过 socketpair 通信时,父进程可以在向子进程发送数据的同时,从子进程接收数据。
  • pipe:建立的是半双工通信管道。数据只能在一个方向上流动,在某一时刻,数据要么从管道的一端流向另一端,不能同时双向传输。比如,父进程向子进程发送数据时,子进程不能同时向父进程发送数据。
  • socketpair:常用于需要双向实时交互的场景,或者在多线程编程中利用套接字的特性(如异步 I/O、信号驱动 I/O 等)实现更灵活的通信。由于它创建的是套接字对,在功能上更接近网络套接字,因此在一些需要类似网络通信特性但又局限于本地进程间通信的场景中较为适用。
  • pipe:适用于简单的单向数据传递场景,比如一个进程产生数据,另一个进程消费数据。常见于父子进程间的简单数据传输,例如父进程将一些计算结果传递给子进程进行后续处理。
  • socketpair:理论上可以用于任何进程间通信,不过在实际应用中,常用于有亲缘关系(如父子进程)的进程间通信,但并不局限于此。在多线程编程中,不同线程也可以使用 socketpair 进行通信。
  • pipe:主要用于具有亲缘关系的进程间通信,通常是父子进程。这是因为管道依赖于文件描述符的继承机制,父进程创建管道后通过 fork 创建子进程,子进程继承父进程的文件描述符从而实现通信。
  • socketpair:创建的套接字对在文件系统中没有对应的实体文件,它们是基于内存的通信机制,不依赖于文件系统。这使得 socketpair 的通信效率较高,因为不需要进行文件系统相关的操作。
  • pipe:匿名管道同样在文件系统中没有对应的实体文件,其生命周期完全依赖于使用它的进程。然而,命名管道(通过 mkfifo 创建,与 pipe 原理相关)在文件系统中有对应的文件,这使得命名管道可以用于不相关进程间的通信,但也引入了文件系统相关的开销和管理。
  • socketpair:由于基于套接字机制,支持一些高级特性,如设置套接字选项(setsockopt)来调整通信行为,包括设置缓冲区大小、启用或禁用某些功能等。例如,可以设置 SO_RCVBUF 来调整接收缓冲区的大小,以优化数据接收性能。
  • pipe:功能相对较为基础,主要专注于简单的数据传输,一般不支持像套接字那样丰富的选项设置。其数据传输行为相对固定,主要围绕基本的读写操作。

相关文章:

  • FreeRTOS事件组-笔记
  • 抖音怎么下载视频?抖音怎么无水印下载别人的视频
  • LeetCode 08.06 面试题 汉诺塔 (Java)
  • springBoot 通过模板导出Excel文档的实现
  • 第一章 计算机系统构成及硬件基础知识
  • 基于Java的离散数学题库系统设计与实现:附完整源码与论文
  • Web前端基础:JavaScript
  • 混合云数据库连接问题:本地与云实例的兼容性挑战
  • AI推理服务的高可用架构设计
  • 如何区分 “通信网络安全防护” 与 “信息安全” 的考核重点?
  • 【JavaWeb】Docker项目部署
  • VirtualBox启动失败@Ubuntu22.04 说是配置文件有问题
  • 数组复制--System.arraycopy
  • Redis:现代应用开发的高效内存数据存储利器
  • 【HTTP三个基础问题】
  • 文件(保存)通讯录
  • win11无法打开.bat文件、打开.bat文件闪退解决方案,星露谷smapi mod安装时,.bat安装文件一闪而
  • 如何从浏览器中导出网站证书
  • bat批量去掉本文件夹中的文件扩展名
  • Windows 系统安装 Redis 详细教程
  • 大连网站代运营的公司有哪些/宁德seo公司
  • 直播网站开发步骤/云南优化公司
  • 石家庄网站制作哪家好/快手seo
  • 科技网站制作公司/广告牌
  • 有没有一个网站做黄油视频/指数分布的期望和方差
  • 免费制作一个自己的网站/长沙营销型网站建设