前言:
- 接下来我将带大家探索 进程间通信 的方式。本期,要讲的就是管道其中之一“匿名管道”!!
(一)进程间通信介绍
进程间通信(Inter-process communication,IPC)是指操作系统中不同进程之间进行数据交换和通信的机制。
1、进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程;
- 资源共享:多个进程之间共享同样的资源;
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程);
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
在一个计算机系统中,多个进程可能需要在运行时进行相互协作、共享数据或进行消息传递。为实现这些目的,操作系统提供了多种形式的进程间通信机制。
2、进程间通信发展
进程间通信(IPC)的发展一直与计算机领域的进步和需求密切相关。随着计算机技术的不断发展,IPC也经历了许多演进和改进,以满足不断增长的通信需求。以下是进程间通信发展的一些关键方面:
- 管道
- System V进程间通信
- POSIX进程间通信
3、进程间通信分类
下面是几种常见的进程间通信机制:
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
这些进程间通信机制各有优缺点,选择合适的机制取决于应用程序的需求和特点。开发者需要考虑数据传输的速度、数据大小、并发性、可靠性等因素来选择适当的通信机制。
(二)管道
1、什么是管道
管道通信是消息传递的一种特殊方式(见下图):
- 所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间的通信的一个共享文件,又名pipe文件;
- 向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入(写)管道;而接收管道输出的接收进程(即读进程)则从管道中接收(读)数据;
- 为了协调双方的通信,管道机制必须提供以下三方面的协调能力:互斥、同步和确定对方的存在。
2、站在文件描述符角度-深度理解管道
【解释说明】
- 上述描述了一个进程的默认文件符打开表,其中0、1、2是默认打开的,而3、4是分别使用读和写分别打开管道文件(当然反过来也是一样的)。
【解释说明】
- 此时。父进程通过 fork 创建了子进程,只需把进程相关的数据结构调入,大家可以看成共享一片地址空间。
【解释说明】
- 此时,我们关闭管道文件的读写端。就会形成一个单向通信的信道,至此双方就可以使用文件描述符对管道进行一个读一个写的操作,即实现了通信。
(三)管道分类
在Linux中,管道是一种用于进程间通信的特殊机制。根据使用方式和功能,Linux中的管道可以分为不同类型:
- 匿名管道(Anonymous Pipes): 匿名管道是最基本的管道类型,在命令行中使用竖线符号(|)来创建。它只能用于相关进程之间的通信,父进程与子进程之间或者同一管道链中的进程之间。
- 命名管道(Named Pipes): 命名管道(也称为FIFO)是一种有名字的管道,由mkfifo命令创建。它可以在磁盘上持久存在,并允许无关的进程之间进行通信。多个进程可以通过读取和写入相同的命名管道来进行数据交换。
1、匿名管道
匿名管道是一种在进程间进行通信的机制,通常用于父子进程之间或者通过衍生的进程之间传递数据。匿名管道是一种单向通信机制,即数据只能从一个进程流向另一个进程。
💨 接下来介绍匿名管道的一些关键特性:
1️⃣创建匿名管道(大家可以通过man手册进行查询)
- 匿名管道通过系统调用(例如,在Unix/Linux中使用
pipe
函数)创建; - 创建管道时,操作系统会返回两个文件描述符,一个用于读取(读文件描述符),另一个用于写入(写文件描述符)
2️⃣下面是一个简单的C语言示例,演示了如何使用匿名管道在父子进程之间进行通信:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define BUFFER_SIZE 25 int main() { int pipefd[2]; // 用于存放管道两端文件描述符的数组 pid_t pid; char message[BUFFER_SIZE] = "Hello, child process!"; // 创建管道 if (pipe(pipefd) == -1) { perror("pipe"); exit(EXIT_FAILURE); } // 创建子进程 pid = fork(); if (pid < 0) { perror("fork"); exit(EXIT_FAILURE); } if (pid > 0) { // 父进程 close(pipefd[0]); // 关闭用于读的文件描述符 // 向管道写入消息 write(pipefd[1], message, strlen(message) + 1); // 关闭用于写的文件描述符 close(pipefd[1]); printf("Parent process: Message sent to child.\n"); } else { // 子进程 close(pipefd[1]); // 关闭用于写的文件描述符 // 从管道读取消息 char received_message[BUFFER_SIZE]; read(pipefd[0], received_message, sizeof(received_message)); // 关闭用于读的文件描述符 close(pipefd[0]); printf("Child process: Received message - %s\n", received_message); } return 0; }
2、场景分类
💨当读的一方 read 完所有的管道数据,如果写的一方不往管道里面发送数据,此时就只能等待:
int main() { // 任何一种任何一种进程间通信中,一定要 先 保证不同的进程之间看到同一份资源 int pipefd[2] = {0}; //1. 创建管道 int n = pipe(pipefd); if(n < 0) { std::cout << "pipe error, " << errno << ": " << strerror(errno) << std::endl; return 1; } std::cout << "pipefd[0]: " << pipefd[0] << std::endl; // std::cout << "pipefd[1]: " << pipefd[1] << std::endl; // //2. 创建子进程 pid_t id = fork(); assert(id != -1); //正常应该用判断,我这里就断言:意料之外用if,意料之中用assert if(id == 0) // 子进程 { //3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入 close(pipefd[0]); //4. 开始通信 -- 结合某种场景 const std::string namestr = "hello, 我是子进程"; int cnt = 1; char buffer[1024]; while(true) { snprintf(buffer, sizeof buffer, "%s, 计数器: %d, 我的PID: %d",namestr.c_str(), cnt++, getpid()); write(pipefd[1], buffer, strlen(buffer)); sleep(10); } close(pipefd[1]); exit(0); } //父进程 //3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入 close(pipefd[1]); //4. 开始通信 -- 结合某种场景 char buffer[1024]; while(true) { int n = read(pipefd[0], buffer, sizeof(buffer) - 1); if(n > 0) { buffer[n] = '\0'; std::cout << "我是父进程, child give me message: " << buffer << std::endl; } } close(pipefd[0]); return 0; }
💨当写的一方不往管道里面 write 数据,而读的一方此时不读,当管道写满之后就不能在继续写数据(即管道有大小限制):
输出结果:
💨当此时关闭了写数据的一方,读取完毕管道数据。如果在继续读取的话,就会 read 返回0,表明读到文件结尾。
输出结果:
💨当写端一直写,读端关闭,此时就没有意义,OS不会维护无意义、低效率的事件,会通过 13号 信号来杀死一直写入的进程:
输出结果:
3、管道读写规则
🔥 当没有数据可读时:
- 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 将不再保证写入的原子性。
4、管道特点
通过上述,简单总结一下管道有哪些特点:
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道提供流式服务 ;
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
(四)总结
接下来,简单总结一下本期关于“匿名管道”的全部知识!!!
- 使用匿名管道可以方便地在父子进程之间传递数据,但也有一些限制,如只能用于具有亲缘关系的进程间通信,无法用于无关进程之间的通信;
- 此外,管道的容量有限,如果写入速度超过读取速度,可能会导致阻塞;
- 在实际使用中,可以根据需求选择其他更复杂的进程间通信机制,如命名管道、共享内存或消息队列等。
以上便是本期关于匿名管道的全部内容了,感谢大家的观看与支持!!!