进程间通信(IPC)与匿名管道
- IT业界
- 2025-09-18 13:18:01

目录
一、进程间通信(IPC)概述
1. 核心概念
2. 核心目的
3. IPC分类
二、匿名管道
1. 什么是管道
示例:Shell中的管道
2. 匿名管道的原理
3. 匿名管道的实现
3.1 创建管道:pipe()函数
3.2 使用 fork 共享管道
3.3 站在文件描述符角度理解管道
3.4 示例代码
4. 匿名管道的核心特性与规则
4.1 读写规则
4.2 四种特殊情况
🌵情况 1:写端不写,读端一直读
🌵情况 2:读端不读,写端一直写
🌵情况 3:写端关闭后
🌵情况 4:读端关闭后
4.3 五种特征
🌴特征1:血缘关系限制
🌴特征2:流式服务
🌴特征3:生命周期随进程
🌴特征4:内核同步与互斥
🌴特征5:半双工通信
一、进程间通信(IPC)概述 1. 核心概念
进程间通信(Interprocess Communication, IPC)是操作系统提供的一种机制,用于不同进程之间的数据交换与协作。由于每个进程拥有独立的虚拟地址空间,直接访问对方内存不可行,因此需要通过操作系统提供的共享资源实现通信。其本质是让不同进程看到同一份资源(如内存缓冲区、文件或内核数据结构)。
2. 核心目的数据传输:一个进程需要将它的数据发送给另一个进程,如客户端与服务器间的请求响应。
资源共享:多个进程共享同一文件或内存区域。
事件通知:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如子进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
3. IPC分类 类型具体实现管道匿名管道、命名管道(FIFO)System V IPCSystem V 消息队列、System V 共享内存、System V 信号量POSIX IPC消息队列、共享内存、信号量、互斥量、条件变量、读写锁其他机制信号(Signal)、套接字(Socket)、内存映射文件(Memory-Mapped Files)二、匿名管道 1. 什么是管道
管道是 UNIX 中最古老的进程间通信方式。它是一种半双工的通信机制,本质是内核维护的环形缓冲区,数据只能单向流动。管道通过将一个进程的标准输出连接到另一个进程的标准输入,实现数据的传递。
示例:Shell中的管道 who | wc -l # 统计登录用户数who命令的输出通过管道传递给 wc处理,实现进程间协作。
2. 匿名管道的原理
匿名管道用于本地父子进程之间的通信。其原理是让父子进程共享同一文件资源,通过操作系统维护的文件描述符实现通信。匿名管道的数据存储在内存中,不会写入磁盘。
父进程和子进程的 task_struct:
每个进程都有一个 task_struct,用于描述进程的状态和资源。
父进程和子进程各自有自己的 task_struct,但它们可以通过共享的文件描述符表进行通信。
文件描述符表 files_struct 和 fd_array:
files_struct 是与进程相关的文件描述符表,管理进程打开的文件或管道。
fd_array 是一个数组,存储了文件描述符(file descriptor),每个文件描述符对应一个打开的文件或管道。
父进程和子进程的 fd_array 都指向了同一个 struct file,这表明它们共享了同一份文件资源(例如管道)。
文件结构 struct file 和 inode:
struct file 是 Linux 内核中表示打开文件的结构体。
inode 是文件系统中用于存储文件元数据的数据结构,描述了文件在文件系统中的位置和属性。
内核级文件缓冲区:
内核中有一个文件缓冲区,用于缓存文件数据,提高文件读写效率。
当进程通过文件描述符读写文件时,数据会先从磁盘加载到文件缓冲区,再由进程从缓冲区中读取或写入。
磁盘文件 file.txt:
磁盘上的文件 file.txt 是实际存储数据的地方。
当进程通过文件描述符读写文件时,数据会先从磁盘加载到文件缓冲区,再由进程从缓冲区中读取或写入。
父子进程共享资源:
父进程和子进程通过共享的文件描述符和文件系统结构(如 struct file 和 inode)访问同一份数据。
这种机制是管道通信的基础:父进程和子进程通过共享的管道文件描述符进行数据交换。
内核管理:
内核负责管理管道的缓冲区和同步机制,确保数据的正确传递。
数据在内核缓冲区中传输,不需要刷新到磁盘,因此效率较高。
3. 匿名管道的实现 3.1 创建管道:pipe()函数pipe 函数是用于创建匿名管道的系统调用,其函数原型如下:
#include <unistd.h> int pipe(int pipefd[2]); // pipefd[0]为读端,pipefd[1]为写端🌻参数说明:
pipefd:这是一个指向整数数组的指针,数组必须能够容纳至少两个元素。pipe 函数会填充这个数组,使其包含两个文件描述符:
pipefd[0]:管道的读端文件描述符(用于从管道中读取数据)。
pipefd[1]:管道的写端文件描述符(用于向管道中写入数据)。
🌻返回值:
成功:返回 0,并且 pipefd 数组被填充为两个有效的文件描述符。
失败:返回 -1,表示创建管道失败。此时,pipefd 数组的内容是未定义的。可以通过 errno 获取具体的错误原因。
🌻注意事项:
文件描述符的使用:
pipefd[0] 通常用于读取数据,应该在读端进程中使用。
pipefd[1] 通常用于写入数据,应该在写端进程中使用。
关闭不需要的文件描述符:
在父子进程通信时,父进程和子进程需要分别关闭不需要的文件描述符。例如,父进程关闭写端文件描述符(pipefd[1]),子进程关闭读端文件描述符(pipefd[0])。
文件描述符的继承:
当父进程调用 fork() 创建子进程时,子进程会继承父进程的文件描述符表,包括管道的两个文件描述符。
3.2 使用 fork 共享管道1. 父进程调用pipe()创建管道。
2. 调用fork()创建子进程。
3. 父子进程分别关闭未使用的端(父关写端,子关读端)。
3.3 站在文件描述符角度理解管道1. 父进程创建管道
2. 父进程 fork 出子进程
3. 父进程关闭 fd[0],子进程关闭 fd[1]
3.4 示例代码 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> int main() { int pipefd[2]; // 用于存储管道的读端和写端文件描述符 pid_t pid; // 创建匿名管道 if (pipe(pipefd) == -1) { perror("pipe"); exit(1); } // 创建子进程 pid = fork(); if (pid == -1) { perror("fork"); exit(1); } if (pid == 0) { // 子进程 close(pipefd[0]); // 关闭读端 const char *msg = "Hello from child!"; int count = 5; while (count--) { write(pipefd[1], msg, strlen(msg) + 1); // 向管道写入数据 } close(pipefd[1]); // 关闭写端 exit(0); } else { // 父进程 close(pipefd[1]); // 关闭写端 char buffer[128]; ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1); // 从管道读取数据 for (int i = 0; i < 5; i++) { if (n > 0) { buffer[n] = '\0'; // 确保字符串以 '\0' 结尾 printf("Received from child: %s\n", buffer); } else if (n == 0) { printf("No data received.\n"); } else { perror("read"); } } close(pipefd[0]); // 关闭读端 wait(NULL); // 等待子进程结束 } return 0; }代码解释:
🌵创建匿名管道:
if (pipe(pipefd) == -1) { perror("pipe"); exit(1); }pipe(pipefd) 创建一个匿名管道,pipefd[0] 是读端文件描述符,pipefd[1] 是写端文件描述符。
🌵创建子进程:
pid = fork(); if (pid == -1) { perror("fork"); exit(1); }fork() 创建一个子进程。pid 为 0 表示子进程,pid 为正数表示父进程。
🌵子进程写入数据:
if (pid == 0) { close(pipefd[0]); // 关闭读端 const char *msg = "Hello from child!"; write(pipefd[1], msg, strlen(msg) + 1); // 向管道写入数据 close(pipefd[1]); // 关闭写端 exit(0); }子进程关闭读端文件描述符 pipefd[0],只保留写端文件描述符 pipefd[1]。
子进程通过 write() 函数将数据写入管道。
写入完成后,子进程关闭写端文件描述符并退出。
🌵父进程读取数据:
else { close(pipefd[1]); // 关闭写端 char buffer[128]; read(pipefd[0], buffer, sizeof(buffer)); // 从管道读取数据 printf("Received from child: %s\n", buffer); close(pipefd[0]); // 关闭读端 wait(NULL); // 等待子进程结束 }父进程关闭写端文件描述符 pipefd[1],只保留读端文件描述符 pipefd[0]。
父进程通过 read() 函数从管道中读取数据。
读取完成后,父进程打印从子进程接收到的数据,并关闭读端文件描述符。
父进程调用 wait(NULL) 等待子进程结束,确保子进程的资源被正确回收。
运行结果:
4. 匿名管道的核心特性与规则 4.1 读写规则
1. 当没有数据可读时
O_NONBLOCK 禁用(默认阻塞模式):
如果管道中没有数据,read() 调用会阻塞,即进程会暂停执行,直到有数据可读为止。
O_NONBLOCK 启用(非阻塞模式):
如果管道中没有数据,read() 调用会立即返回 -1,并设置 errno 为 EAGAIN,表示没有数据可读。
2. 当管道满的时候
O_NONBLOCK 禁用(默认阻塞模式):
如果管道已满,write() 调用会阻塞,即进程会暂停执行,直到有空间可写为止。
O_NONBLOCK 启用(非阻塞模式):
如果管道已满,write() 调用会立即返回 -1,并设置 errno 为 EAGAIN,表示没有空间可写。
3. 如果所有管道写端对应的文件描述符被关闭
当所有写端文件描述符都被关闭时,read() 调用会返回 0,表示管道已经关闭,没有更多数据可读。4. 如果所有管道读端对应的文件描述符被关闭
当所有读端文件描述符都被关闭时,write() 操作会产生 SIGPIPE 信号,这通常会导致写进程退出。5. 写入数据的原子性
当写入的数据量不大于 PIPE_BUF 时:
Linux 会保证写入操作的原子性,即写入的数据要么全部写入,要么都不写入,不会被其他进程的读写操作打断。
当写入的数据量大于 PIPE_BUF 时:
Linux 不再保证写入操作的原子性,数据可能会被拆分成多个部分写入,可能会被其他进程的读写操作打断。
🌴总结:
阻塞模式:默认情况下,read() 和 write() 会阻塞,直到有数据可读或有空间可写。
非阻塞模式:启用 O_NONBLOCK 后,read() 和 write() 会立即返回,不会阻塞。
文件描述符关闭:关闭写端文件描述符后,读端会返回 0;关闭读端文件描述符后,写端会产生 SIGPIPE 信号。
原子性:写入数据量不大于 PIPE_BUF 时,写入操作是原子的;大于 PIPE_BUF 时,写入操作可能不原子。
4.2 四种特殊情况写端不写,读端一直读:读端进程挂起,直到有数据可读。
读端不读,写端一直写:写端进程挂起,直到有空间可写。
写端关闭后:读端读取完数据后继续执行,不会挂起。
读端关闭后:写端进程收到 SIGPIPE 信号,被操作系统终止。
🌵情况 1:写端不写,读端一直读
描述:
如果写端不写数据,而读端一直尝试读取数据,读端进程会挂起,直到有数据可读。
代码验证:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> int main() { int pipefd[2]; pid_t pid; if (pipe(pipefd) == -1) { perror("pipe"); exit(1); } pid = fork(); if (pid == -1) { perror("fork"); exit(1); } if (pid == 0) { // 子进程:写端不写数据 close(pipefd[0]); // 关闭读端 close(pipefd[1]); // 关闭写端(不写数据) exit(0); } else { // 父进程:读端一直读 close(pipefd[1]); // 关闭写端 char buffer[128]; printf("父进程尝试读取数据...\n"); ssize_t n = read(pipefd[0], buffer, sizeof(buffer)); // 读取数据 if (n > 0) { buffer[n] = '\0'; printf("Received: %s\n", buffer); } else if (n == 0) { printf("没有数据可读,子进程关闭了写端。\n"); } else { perror("read"); } close(pipefd[0]); // 关闭读端 wait(NULL); // 等待子进程结束 } return 0; }运行结果:
解释:
子进程关闭了写端,没有写入数据。父进程尝试读取数据时,由于没有数据可读,read() 会阻塞,直到子进程关闭写端,父进程的 read() 返回 0。🌵情况 2:读端不读,写端一直写
描述:
如果读端不读数据,而写端一直尝试写入数据,写端进程会挂起,直到有空间可写。
代码验证:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> int main() { int pipefd[2]; pid_t pid; if (pipe(pipefd) == -1) { perror("pipe"); exit(1); } pid = fork(); if (pid == -1) { perror("fork"); exit(1); } if (pid == 0) { // 子进程:写端一直写 close(pipefd[0]); // 关闭读端 for (int i = 0; i < 5; i++) { char buffer[128]; sprintf(buffer, "数据 %d\n", i + 1); write(pipefd[1], buffer, strlen(buffer) + 1); // 写入数据 sleep(1); // 模拟写入间隔 } close(pipefd[1]); // 关闭写端 exit(0); } else { // 父进程:读端不读 close(pipefd[1]); // 关闭写端 printf("父进程不读取数据...\n"); sleep(6); // 模拟父进程不读取数据 close(pipefd[0]); // 关闭读端 wait(NULL); // 等待子进程结束 } return 0; }运行结果:
解释:
父进程关闭了读端,没有读取数据。子进程尝试写入数据时,由于管道已满,write() 会阻塞,直到父进程读取数据或关闭读端。🌵情况 3:写端关闭后
描述:
如果写端关闭后,读端读取完数据后会继续执行,不会挂起。
代码验证:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> int main() { int pipefd[2]; pid_t pid; if (pipe(pipefd) == -1) { perror("pipe"); exit(1); } pid = fork(); if (pid == -1) { perror("fork"); exit(1); } if (pid == 0) { // 子进程:写端写入数据后关闭 close(pipefd[0]); // 关闭读端 char buffer[128]; sprintf(buffer, "子进程写入的数据\n"); write(pipefd[1], buffer, strlen(buffer) + 1); // 写入数据 close(pipefd[1]); // 关闭写端 exit(0); } else { // 父进程:读端读取数据 close(pipefd[1]); // 关闭写端 char buffer[128]; ssize_t n = read(pipefd[0], buffer, sizeof(buffer)); // 读取数据 if (n > 0) { buffer[n] = '\0'; printf("父进程读取到数据: %s\n", buffer); } else if (n == 0) { printf("写端已关闭,没有更多数据。\n"); } else { perror("read"); } close(pipefd[0]); // 关闭读端 wait(NULL); // 等待子进程结束 } return 0; }运行结果:
解释:
子进程写入数据后关闭写端。父进程读取数据后,read() 返回 0,表示写端已关闭,没有更多数据。🌵情况 4:读端关闭后
描述:
如果读端关闭后,写端进程会收到 SIGPIPE 信号,导致写进程被操作系统终止。
代码验证:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> void handle_sigpipe(int sig) { printf("收到 SIGPIPE 信号,写端被终止。\n"); exit(1); } int main() { int pipefd[2]; pid_t pid; if (pipe(pipefd) == -1) { perror("pipe"); exit(1); } signal(SIGPIPE, handle_sigpipe); // 捕获 SIGPIPE 信号 pid = fork(); if (pid == -1) { perror("fork"); exit(1); } if (pid == 0) { // 子进程:写端写入数据 close(pipefd[0]); // 关闭读端 char buffer[128]; sprintf(buffer, "子进程写入的数据\n"); write(pipefd[1], buffer, strlen(buffer) + 1); // 写入数据 close(pipefd[1]); // 关闭写端 exit(0); } else { // 父进程:关闭读端 close(pipefd[1]); // 关闭写端 close(pipefd[0]); // 关闭读端 wait(NULL); // 等待子进程结束 } return 0; }运行结果:
解释:
父进程关闭了读端。子进程尝试写入数据时,由于读端已关闭,write() 会触发 SIGPIPE 信号,导致子进程被终止。 4.3 五种特征血缘关系限制:只能用于具有共同祖先的进程。
流式服务:数据无明确分割,按顺序传输。
生命周期随进程:进程退出后,管道释放。
内核同步与互斥:内核保证管道操作的同步与互斥。
半双工通信:数据单向流动,双向通信需两个管道。
🌴特征1:血缘关系限制
含义:
匿名管道只能用于具有共同祖先的进程之间的通信,通常是父子进程。管道的文件描述符在创建时由父进程持有,子进程通过 fork() 继承父进程的文件描述符表,从而共享管道。原因:
匿名管道的文件描述符只在创建它的进程及其子进程中有效。非亲缘进程无法共享匿名管道的文件描述符,因此无法使用匿名管道进行通信。🌴特征2:流式服务
含义:
数据在管道中没有明确的分割,按顺序传输。读取数据时,数据会连续地从管道中流出,直到读取完毕。写入数据时,数据会连续地写入管道,直到写入完毕。特点:
数据传输是连续的,没有固定的报文边界。读取数据时,可能一次读取多个数据块,直到管道中的数据被读取完毕。写入数据时,可能一次写入多个数据块,直到数据全部写入管道。🌴特征3:生命周期随进程
含义:
匿名管道的生命周期与创建它的进程相关。当所有引用管道的进程退出或关闭文件描述符后,管道会被销毁。特点:
管道的文件描述符在所有进程中关闭后,管道会被释放。如果父进程或子进程退出,管道的文件描述符会被关闭,管道会被销毁。🌴特征4:内核同步与互斥
含义:
内核负责管理管道的同步与互斥,确保数据的正确传递。当多个进程访问同一管道时,内核会自动处理同步与互斥问题,防止数据竞争和不一致。特点:
同步:确保数据按顺序传输,读取操作在写入操作之后进行。互斥:确保同一时间只有一个进程可以对管道进行读写操作,防止数据冲突。🌴特征5:半双工通信
含义:
数据在管道中只能单向流动,即从写端到读端。如果需要双向通信,必须创建两个管道,每个管道负责一个方向的数据传输。特点:
单向通信:数据只能从写端流向读端,不能反向传输。半双工:数据可以在两个方向上传输,但不能同时进行。需要两个管道来实现双向通信。进程间通信(IPC)与匿名管道由讯客互联IT业界栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“进程间通信(IPC)与匿名管道”