Linux(15)——进程间通信
目录
一、进程间通信的介绍
✍️进程间通信的目的
✍️进程间通信的本质
进程间通信的分类
✍️管道
✍️System V IPC
✍️POSIX IPC
二、管道
🧠什么是管道
✍️匿名管道
📝匿名管道的原理
📝pipe函数
📝匿名管道的使用步骤
编辑📝管道读写的规则
📝管道的特点
一、进程间通信的介绍
✍️进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
✍️进程间通信的本质
进程间通信的本质就是让不同的进程看到同一份资源
这里要解释一下了,由于进程具有独立性,所以想要直接实现不同进程间的相互通信还是非常困难的。
这里我们就引入了第三方资源来作为不同进程之间通信的桥梁,也就是说这些进程可以通过向这么个第三方资源进行写入和读取来进行通信,示意图如下:
通过上面的方式,我们就实现的不同进程看到了同一份资源,所以说进程间通信的本质就是让不同的进程看到了同一份资源。
进程间通信的分类
✍️管道
- 匿名管道
- 命名管道
✍️System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
✍️POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
二、管道
🧠什么是管道
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
我们之前在谈Linux的命令的时候也提及了管道这一概念:
例如,这个查看当前服务器有多少用户登录的命令
我们发现这个命令是由who命令和wc命令组成的,这个命令运行起来之后就变成了两个进程了,who进程把执行得到的结果通过标准输出写入到了管道之中,wc进程通过标准输入从管道中读取数据,处理完之后再把结果通过标准输出给到用户。
注意:who显示的是相关信息,wc -l命令是用来统计行数的。
✍️匿名管道
📝匿名管道的原理
匿名管道用于进程间的通信,且仅限于父子进程之间的通信。
我们之前也说了,进程间通信的本质就是让不同的进程看到了同一份资源,使用匿名管道实现通信的原理就是让父子进程看了同一份被打开的文件资源,然后父子进程就可以对同一份资源进行读写操作,从而实现了进程间的通信。
敲黑板:
这里需要注意的是这里打开的文件是由操作系统来进行管理的,父子进程进行读写时并不会进行写时拷贝。
📝pipe函数
我们可以使用pipe函数来创建匿名管道
int pipe(int pipefd[2]);
参数说明:输入的参数实际上是输出型的参数,数组中的两个元素分别指向读端和写端。‘
- pipefd[0]:管道读端的文件描述符
- pipefd[1]:管道写端的文件描述符
如果创建失败则返回-1,成功创建则返回0。
📝匿名管道的使用步骤
我们在使用管道通信的时候,既要使用fork函数来创建子进程,也要使用pipe函数来创建管道。
第一步:父进程创建管道
第二步:父进程创建子进程
第三步:父端关闭写,子端关闭读
敲黑板:
这里的通信只能是单向的,也就是我们说的半双工,在代码中的反应就是管道只开一个读端和写端。
我们这里可以将上面的三个步骤再细化一下:
第一步:父进程创建管道
第二步:父进程创建子进程
第三步:父进程关闭写端,子进程关闭读端
我们来写个代码演示一下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main() {int fd[2] = {0}; // 作为pipe函数的参数来进行传递if(pipe(fd) < 0) {perror("pipe");exit(1);}pid_t id = fork();if(id == 0) // 子进程{close(fd[0]); // 关闭了读端const char* message = "Hello my father, I am child...";int count = 10;while(count--) {write(fd[1], message, strlen(message)); // 字节流的写入数据sleep(1);}close(fd[1]);exit(0);}// 父进程close(fd[1]); // 关闭了写端char buffer[64];while(1) {ssize_t s = read(fd[0], buffer, sizeof(buffer)); // 由于休眠时间的存在,就会读完整if(s > 0) {buffer[s] = '\0';printf("child send to father: %s\n", buffer);}else if (s == 0) {printf("the end\n");break;}else {printf("read error\n");break;}}close(fd[0]);waitpid(id, NULL, 0);return 0;
}
📝管道读写的规则
pipe2函数的操作和pipe函数类似,具体的函数原型如下:
int pipe2(int pipefd[2], int flags);
pipe函数的第二个参数用来设置选项可以设置为O_NONBLOCK ,也就是非阻塞:
- 默认不传参数时,read在没有数据会一直等待数据的到来,write在数据数据写满的时候会等待数据被读走。
- 在加了O_NONBLOCK 参数的时候,read在没有数据的时候会返回-1,并把erron的值设置为EAGIN,write在数据写满的时候也会返回-1,并把erron的值设置为EAGIN。
- 如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
- 当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。
📝管道的特点
第一点:管道内自带了同步和互斥机制
首先我们要知道什么是同步和互斥:
同步:两个或者是两个以上的进程在运行时要按照之前约定好的顺序依次执行,可以类比到做菜的场景,我们要先切菜洗菜才能去炒,要有一个先后的顺序才行。
互斥:防止多个进程同时操作了同一个资源,可以类比到打印的场景,A和B都要使用打印机打印,但是我们不能同时发送内容到打印机,否则就是打印出来的结果出错。
我们仔细思考会发现两者其实是类似的,同步是一种更加复杂的互斥,而互斥则是一种特殊的同步,对于我们的管道而言,互斥就是两个进程不可以同时对管道进行操作,他们会相互排斥,而同步也说的是不能对管道进行操作,但这里的要求是必须要是按照一定的次序来对管道进行操作的。
第二点:管道的生命周期随进程
管道的本质就是通过文件来进行通信的,也就是说管道是依赖于文件系统的,那么当所有的文件都退出了,文件也就被释放掉了,所一说管道的生命周期是随进程的。
第三点:管道提供的是流式服务
对于进程A写入管道的数据,进程B每次都可以从管道中任意读取数据,这种方式被称之为流式服务,与之对应的就是数据报服务,也就是后面我们会讲的字节流和数据包。
- 流式服务:数据没有明确的分割,也就是会粘连在一起。
- 数据报服务:数据有了明确的分割了,数据是一份一份的。
第四点:管道是半双工通信
在数据通信中,数据在线路上的传送方式有以下这几种:
1、单工通信:数据的传输是单向的。通信双方中,一方是发送端,一方是接受数据端。
2、半双工通信:半双工是指既可以从从两个反向上面通信,但是不可以同时通信。
3、全双工通信:全双工就是两个反向上可以同时通信,也就是加强版的半双工,双倍的单双工。