主页 > IT业界  > 

进程间通信(IPC)与匿名管道

进程间通信(IPC)与匿名管道

目录

一、进程间通信(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)与匿名管道