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

进程间通信详解(一):管道机制与实现原理

文章目录

  • 引言
  • 一、进程间通信介绍
    • 1.1 进程间通信目的
    • 1.2 进程间通信发展
    • 1.3 进程间通信分类
  • 二、管道
  • 三、匿名管道
    • 3.1 实例代码
    • 3.2 用 fork 来共享管道
    • 3.3 管道通信的内核实现原理
      • 3.3.1 核心组件与关联关系
      • 3.3.2 通信流程拆解
      • 3.3.3 设计本质:OS 如何实现 “共享资源”?
      • 3.3.4 对比理解:为何管道是 “OS 介导的共享”?
    • 3.4 管道读写规则
    • 3.5 管道特点
    • 3.6 匿名管道适用场景
    • 3.7 管道通信的四种情况
      • 3.7.1 管道有数据 → 读端读取
      • 3.7.2 管道满 → 写端写入
      • 3.7.3 写端关闭 → 读端读取
      • 3.7.4 读端关闭 → 写端写入
  • 四、命名管道
    • 4.1 创建命名管道
    • 4.2 结合 open 使用:控制阻塞行为
    • 4.3 示例1:父子进程通过命名管道通信
    • 4.4 示例2:利用命名管道简单模拟用户端与服务端的通信
      • 4.4.1 server.c
      • 4.4.2 client.c

引言

在操作系统中,不同进程之间往往需要交换数据、同步行为,这就涉及到进程间通信(Inter-Process Communication,IPC)。本篇博客将聚焦于 IPC 中最基础也是最常用的一种机制——管道(Pipe)通信。我们不仅会讲解匿名管道与命名管道的使用方法,还会深入内核实现原理、分析管道的读写规则,并通过多个典型示例理解它在实际开发中的应用场景。无论你是在学习操作系统课程,还是在编写多进程程序,这篇文章都将为你打下扎实的基础。

一、进程间通信介绍

1.1 进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

怎么通信?

进程间通信的本质:是让不同的进程看到同一份资源(如“内存”),从而具备通信的条件。
资源由任何一个进程提供?? 不是!!
是由OS提供 -> 系统调用 -> OS的接口 -> 设计统一的通信接口

1.2 进程间通信发展

  • 管道
  • System V进程间通信
  • POSIX 进程间通信

1.3 进程间通信分类

  • 管道:
    • 匿名管道pipe
    • 命名管道
  • System V IPC
    • System V 消息队列
    • System V 共享队列
    • System V 信号量
  • POSIX IPC
    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

二、管道

什么是管道

  • 管道是 Unix 中最古老的进程间通信的形式
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个 “管道”
    在这里插入图片描述

三、匿名管道

3.1 实例代码

在这里插入图片描述

#include <unistd.h>
int pipe(int fd[2]);

功能
创建一匿名管道

参数
fd:文件描述符数组,其中 fd[0] 表示读端,fd[1] 表示写端

返回值
成功返回 0,失败返回错误代码

在这里插入图片描述

  1 #include <stdio.h>2 #include <stdlib.h>3 #include <string.h>4 #include <unistd.h>5 6 int main()7 {8     int fds[2];9     char buf[128];10     int len;11 12     if(pipe(fds) == -1)13     {14         perror("pipe");15         exit(EXIT_FAILURE);16     }17 18     while(fgets(buf, 100, stdin)) // 从键盘中读取数据19     {20         len = strlen(buf);21         if(write(fds[1], buf, len) != len) // 写入管道22         {23             perror("write");24             exit(EXIT_FAILURE);25         }26 27         memset(buf, 0x00, sizeof(buf)); 28         if((len = read(fds[0], buf, 100)) == -1) // 读取管道29         {30             perror("read");31             exit(EXIT_FAILURE);32         }33 34         if(write(1, buf, len) != len) // 输出到屏幕35         {36             perror("write");37             exit(EXIT_FAILURE);38         }39     }40 41     return 0;42 }                                   

演示:
在这里插入图片描述
可以看到,从键盘中输入什么,在屏幕中会再输出一遍

3.2 用 fork 来共享管道

我之前的文章里讲过,父进程在创建子进程的时候,子进程会继承父进程的文件描述符表,这样,子进程就能获取到父进程的资源。我们可以利用这个原理来实现从子进程向父进程的单向通信。

父进程先创建管道(获得读、写端 fd[0]/fd[1]),再通过 fork 创建子进程。子进程会 复制父进程的文件描述符表(属于 PCB 的一部分),因此继承管道的读、写端。此时,父进程关闭写端 fd[1],子进程关闭读端 fd[0],就能严格实现 父进程写 → 子进程读 的单向通信。这既利用了 fork 的资源继承特性,也依赖管道的 半双工 传输规则。

这一部分代码使用C++来写:

  1 #include <iostream>2 #include <unistd.h>3 #include <cstring>4 #include <sys/types.h>5 #include <sys/wait.h>6 7 void ChildWrite(int wfd)8 {9     int cnt = 0;10     char buffer[128];11     while(cnt <= 10000)12     {13         printf("child : %d\n", cnt);                                                              14         int len = snprintf(buffer, sizeof(buffer), "cnt: %d\n", cnt++);15         write(wfd, buffer, len);16         usleep(100);17     }18 }19 20 void FatherRead(int rfd)21 {22     char buffer[128];23     while(true)24     {25         ssize_t n = read(rfd, buffer, sizeof(buffer)-1);26         if(n > 0)27         {28             buffer[n] = 0;29             std::cout << "child say: " << buffer << std::endl;30         }31         else if(n == 0)32         {33             std::cout << "n : " << n << std::endl;34             std::cout << "child 退出,我也退出" << std::endl;35             break;36         }37         else38         {39             break;40         }41     }42 }43 44 45 int main()46 {47     // 1. 创建管道48     int fds[2] = {0};49     int n = pipe(fds);50     if(n < 0)51     {52         std::cerr << "pipe error" << std::endl;53         return 1;54     }55     std::cout << "fds[0]: " << fds[0] << std::endl;56     std::cout << "fds[1]: " << fds[1] << std::endl;57 58     // 2. 创建子进程59     pid_t pid = fork();60     if(pid == 0)61     {62         // child63         // 3. 子进程关闭读端64         close(fds[0]);65         ChildWrite(fds[1]);66         close(fds[1]);67         exit(0);68     }69     // 3. 父进程关闭写端70     close(fds[1]);71     FatherRead(fds[0]);72     close(fds[0]);73 74     sleep(1);75 76     int status = 0;77     int ret = waitpid(pid, &status, 0); // 获取子进程退出信息78     if(ret > 0)79     {80         printf("exit code: %d, exit signal: %d\n", (status>>8)&0xFF, status&0x7F);81         sleep(5);82     }83 84     return 0;85 }

在这里插入图片描述

用图片来解释文件描述符继承问题:
在这里插入图片描述

3.3 管道通信的内核实现原理

在这里插入图片描述

3.3.1 核心组件与关联关系

  1. 进程的 file 结构体
    • 每个进程打开文件(包括管道)时,内核会为其创建 file 结构体,记录文件的操作属性(如 f_mode 读写权限、f_pos 读写偏移、f_flags 标志位等)。
    • 图中 “进程 1” 和 “进程 2” 各自有独立的 file 结构体,但它们的 f_inode 字段指向同一个 inode(关键!实现共享的核心)。
  2. inode(索引节点)
    inode 是内核中描述文件元数据的结构(如文件类型、权限、数据存储位置)。
    对于管道,inode 不对应磁盘文件,而是对应 内核中的一块共享内存区域(数据页),作为管道的 “缓冲区”。
  3. 数据页(管道缓冲区)
    • 物理内存中的一块区域,用于存储管道传输的数据。
    • 进程 1 的 write 操作向这里写入数据,进程 2 的 read 操作从这里读取数据。

3.3.2 通信流程拆解

  1. 进程 1:写操作(write
    1. 进程 1 调用 write 系统调用,传入自己的文件描述符(关联到自身的 file 结构体)。
    2. 内核通过 file 结构体找到 f_inode,定位到共享的 inode
    3. 内核将数据写入 inode 关联的数据页(管道缓冲区)。
  2. 进程 2:读操作(read
    1. 进程 2 调用 read 系统调用,传入自己的文件描述符(关联到自身的 file 结构体)。
    2. 内核通过 file 结构体找到 f_inode,同样定位到同一个 inode(共享的管道)。
    3. 内核从 inode 关联的数据页对应的物理内存中读取数据,返回给进程 2。

3.3.3 设计本质:OS 如何实现 “共享资源”?

  1. 资源由 OS 提供
    管道的 inode 和数据页由内核创建和管理,而非进程自行分配。进程只能通过系统调用(pipe() 创建管道,write()/read() 操作)访问,确保安全。
  2. “看到同一份资源” 的实现
    两个进程的 file 结构体通过 f_inode 指向同一个内核 inode,间接共享同一块数据页。这种设计让进程无需感知物理内存,只需通过文件接口操作,屏蔽了底层复杂度。
  3. 管道的特性映射
    • 半双工通信:图中虽画了读写,但实际管道是单向的(一个进程写,另一个读),若需双向通信,需创建两个管道。
    • 同步与互斥:内核会自动处理数据的读写同步(如写满时阻塞写进程,读空时阻塞读进程),保证通信有序。

3.3.4 对比理解:为何管道是 “OS 介导的共享”?

  • 如果让 “进程自建资源”(比如进程 1 分配一块内存,告诉进程 2 地址),会有 安全问题(进程 2 的虚拟地址可能无效,或权限不足)。
  • OS 统一管理管道的 inode 和数据页,通过 file 结构体为进程提供 “合法访问接口”,既实现了共享,又保证了系统稳定性(如内存回收、权限检查)。

总结
管道通过内核的 inode 和共享数据页,让两个进程的 file 结构体‘间接共享同一份资源’,从而实现通信。这就好比两个用户(进程)在银行(操作系统)注册了各自的账号(file),但他们都指向同一个保险箱(inode + 数据页),从而实现了受控共享。

3.4 管道读写规则

  • 当没有数据可读时
    • O_NONBLOCK disable:read 调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    • O_NONBLOCK enable:read 调用返回 - 1,errno 值为 EAGAIN
  • 当管道满的时候
    • O_NONBLOCK disable:write 调用阻塞,直到有进程读走数据
    • O_NONBLOCK enable:调用返回 - 1,errno 值为 EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则 read 返回 0
  • 如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生信号 SIGPIPE,进而可能导致 write 进程退出
  • 当要写入的数据量不大于 PIPE_BUF 时,linux 将保证写入的原子性。
  • 当要写入的数据量大于 PIPE_BUF 时,linux 将不再保证写入的原子性。

3.5 管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用 fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务。
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程。
  • 一般而言,内核会对管道操作进行同步与互斥。
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
    在这里插入图片描述

3.6 匿名管道适用场景

匿名管道适用于父子进程或兄弟进程之间进行短生命周期、快速数据交换的通信需求。

例如:

  • Linux Shell 中的 ls | grep xxx
  • 父进程向子进程传递初始化参数

注意:匿名管道无法在两个完全无关的进程之间通信,这时应使用命名管道或其他 IPC 机制。

3.7 管道通信的四种情况

Linux 管道默认 64KB

3.7.1 管道有数据 → 读端读取

  • 阻塞模式(默认,O_NONBLOCK 禁用)
    • 若管道有数据,read 正常读取,返回读取的字节数。
    • 若管道暂时无数据,读进程会阻塞(暂停执行),直到写端写入新数据。
  • 非阻塞模式(O_NONBLOCK 启用)
    • 若管道无数据,read 立即返回 -1,并设置 errno = EAGAIN(表示 “资源暂时不可用”)。

3.7.2 管道满 → 写端写入

管道缓冲区有固定容量(如 Linux 中 PIPE_BUF 通常为 4096 字节),当缓冲区被写满时:

  • 阻塞模式(O_NONBLOCK 禁用)
    • 写进程调用 write 时,会阻塞,直到读端取走数据、腾出缓冲区空间。
  • 非阻塞模式(O_NONBLOCK 启用)
    • write 立即返回 -1,并设置 errno = EAGAIN

3.7.3 写端关闭 → 读端读取

所有写端的文件描述符都被关闭(如父子进程均关闭写端):

  • 若管道中还有剩余数据,读端会正常读取数据。
  • 当管道数据被读完后,后续 read 会返回 0(表示 “文件结束”,类似读到普通文件末尾)。

3.7.4 读端关闭 → 写端写入

所有读端的文件描述符都被关闭(如父子进程均关闭读端):

  • 写端调用 write 时,操作系统会向写进程发送 SIGPIPE 信号(默认行为是终止进程),导致写进程崩溃。
  • 若需避免崩溃,需捕获 SIGPIPE 信号(如通过 signal 注册信号处理函数)。

四、命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

4.1 创建命名管道

  • 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename
  • 命名管道也可以从程序里创建,相关函数有:
#include <sys/types.h>  
#include <sys/stat.h>  int mkfifo(const char *pathname, mode_t mode);  
  • 参数
    • pathname:管道文件路径(如 "/tmp/my_fifo")。
    • mode:权限位(如 0666),实际权限为 mode & ~umask(受进程 umask 影响)。
  • 返回值
    • 成功:0;失败:-1,错误码存于 errno

错误码解析

错误码含义
EEXIST管道文件已存在
EACCESS路径所在目录无写权限
ENOSPC文件系统空间不足
ENOTDIR路径中某组件不是目录

创建命名管道:

int main(int argc, char *argv[])
{mkfifo("p2", 0644);return 0;
}

4.2 结合 open 使用:控制阻塞行为

命名管道的读写行为受 openO_NONBLOCK 标志 影响:

  • 默认(无 O_NONBLOCK
    • 打开读端(O_RDONLY):阻塞,直到有进程打开写端。
    • 打开写端(O_WRONLY):阻塞,直到有进程打开读端。
  • 开启 O_NONBLOCK
    • 打开读端:若无写端,立即返回 -1errno = ENXIO
    • 打开写端:若无读端,立即返回 -1errno = ENXIO

4.3 示例1:父子进程通过命名管道通信

  1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 #include <sys/stat.h>5 #include <fcntl.h>6 #include <string.h>7 #include <stdlib.h>8 #include <sys/wait.h>9 10 int main()11 {12     const char *fifo_path = "/home/zkp/linux/25/6/9/fifo/my_fifo";umask(0); // 设置权限掩码为 0,,以免影响后面的操作13     // 1. 创建命名管道14     if(mkfifo(fifo_path, 0666) == -1)15     {16         perror("mkfifo");17         return 1;18     }19 20     // 2. 创建子进程21     pid_t pid = fork();22     if(pid < 0)23     {24         perror("fork");25         return 1;26     }27     else if(pid == 0)28     {29         // 子进程:读端30         int fd = open(fifo_path, O_RDONLY);31         char buf[100];32         ssize_t n = read(fd, buf, sizeof(buf));          33         buf[n] = '\0';34         printf("子进程读到:%s\n", buf);35         close(fd);36         exit(0);37     }38     else39     {40         // 父进程:写端41         int fd = open(fifo_path, O_WRONLY);42         const char *msg = "Hello, named pipe!";43         write(fd, msg, strlen(msg));44         close(fd);45         wait(NULL); // 等待子进程退出46     }47 48     // 3. 删除管道文件(也可以不删除,长期保留)49     unlink(fifo_path);50 51     return 0;52 }          

运行结果:
在这里插入图片描述

4.4 示例2:利用命名管道简单模拟用户端与服务端的通信

我这里就用C语言简单模拟一下,真要写的话可以将 FileOper(用于支持读写操作)、NameFifo(用于创建管道文件)写到头文件中,再直接在 server.cclient.c 中直接调用接口。

4.4.1 server.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>int main() 
{int fd = open("myfifo", O_RDONLY);if (fd == -1){perror("open");exit(1);}char buf[128];while (read(fd, buf, sizeof(buf)) > 0) {printf("Received: %s", buf);}close(fd);return 0;
}

4.4.2 client.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main() {int fd = open("myfifo", O_WRONLY);if (fd == -1) {perror("open");exit(1);}char buf[128];while (fgets(buf, sizeof(buf), stdin)) {write(fd, buf, strlen(buf));}close(fd);return 0;
}

命令行演示:

mkfifo myfifo
./fifo_read   # 在一个终端运行
./fifo_write  # 在另一个终端运行

相关文章:

  • 规则引擎中复合变量的深度解析:从数据库查询到业务规则落地的全链路攻略
  • JavaSec-其他漏洞
  • SpringBoot 框架第 1 次接口调用慢
  • 使用homeassistant 插件将tasmota 接入到米家
  • Spring Boot 3+:现代Java应用开发的新标杆
  • 【C++特殊工具与技术】优化内存分配(四):定位new表达式、类特定的new、delete表达式
  • 可视化预警系统:如何实现生产风险的实时监控?
  • AlgorithmVisualizer项目改进与部署-网页算法可视化
  • ChatGPT 辅助 PyTorch 开发:从数据预处理到 CNN 图像识别的全流程优化
  • 对比一下blender快捷键:p和alt+p
  • k8s从入门到放弃之Service负载均衡
  • 【C/C++】玩转正则表达式
  • 对象回调初步研究
  • MySQL中【正则表达式】用法
  • Web中间件--tomcat学习
  • Python如何给视频添加音频和字幕
  • ui框架-文件上传组件
  • 在鸿蒙HarmonyOS 5中使用DevEco Studio实现指南针功能
  • 门静脉高压——治疗
  • 智能体革命:企业如何构建自主决策的AI代理?
  • 如何制作一个二维码/seo网站推广排名
  • 中国企业名录黄页/站长工具seo下载
  • 西安网站开发培训多少钱/如何做网站seo
  • 淘宝做网站价格/找文网客服联系方式
  • 建设银行武威分行网站/如何制作app软件
  • 淘宝客 网站无备案/公关服务