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

管道与进程间通信

目录

匿名管道

命名管道

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:功能相对较为基础,主要专注于简单的数据传输,一般不支持像套接字那样丰富的选项设置。其数据传输行为相对固定,主要围绕基本的读写操作。
http://www.xdnf.cn/news/12801.html

相关文章:

  • Riverpod与GetX的优缺点对比
  • KTO: Model Alignment as Prospect Theoretic Optimization
  • 【基础算法】差分算法详解
  • 机器学习的数学基础:神经网络
  • Ajax Systems公司的核心产品有哪些?
  • 华为云Flexus+DeepSeek征文|Dify - LLM 云服务单机部署大语言模型攻略指南
  • 基于Java+VUE+MariaDB实现(Web)仿小米商城
  • 机器学习-经典分类模型
  • 不要调用 TOARRAY() 从 LARAVEL COLLECTION 中获取所有项目
  • DeepSeek-R1-0528:开源推理模型的革新与突破
  • 深入理解 Vue.observable:轻量级响应式状态管理利器
  • UOS 20 Pro为国际版WPS设置中文菜单
  • C++:用 libcurl 发送一封带有附件的邮件
  • Go 并发编程深度指南
  • cmake编译LASzip和LAStools
  • # 主流大语言模型安全性测试(二):英文越狱提示词下的表现与分析
  • Oracle业务用户的存储过程个数及行数统计
  • Linux中MySQL的逻辑备份与恢复
  • 协程的常用阻塞函数
  • 用Ai学习wxWidgets笔记——在 VS Code 中使用 CMake 搭建 wxWidgets 开发工程
  • SQLMesh实战:用虚拟数据环境和自动化测试重新定义数据工程
  • 虚拟电厂发展三大趋势:市场化、技术主导、车网互联
  • Opencv查找图形形状的重要API讲解
  • springboot的test模块使用Autowired注入失败
  • 【storage】
  • 从认识AI开始-----AutoEncoder:生成模型的起点
  • axure制作数据列表并实现单选和多选以及鼠标滑动行hover
  • Vue3+Element Plus表单验证实战:从零实现用户管理
  • 音频剪辑软件少之又少好用
  • 在Vue或React项目中使用Tailwind CSS实现暗黑模式切换:从系统适配到手动控制