Linux进程间通信----管道
进程间通信的概念
两个进程之间可以直接进行数据的传递吗?答案是不能,因为进程具有独立性,每个进程有独立的地址空间和资源,防止一个进程直接访问或干扰另一个进程的数据和状态,所以不能直接通信。
进程间通信是什么?
进程间通信最朴素的概念就是一个进程把自己的数据能够交给另一个进程,让进程之间能够进行数据交互
为什么进程间通信?
- 数据传输:一个进程需要将自己的数据发送给另一个进程
- 资源共享:多个进程共享同一份资源
- 通知事件:一个进程需要向另一个进程发送消息,通知他们发送了某种事件(如子进程终止要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
怎么做进程间通信?
由于进程具有独立性,所以进程间的通信需要花费成本。
所以在不破坏进程独立性的前提条件下,为了完成进程间通信我们需要借助第三方资源。
也就是说需要让不同进程看见同一份资源(一般由OS提供)。
一般规律:
1.首先要有进程交换数据的空间
2.这块空间不能由进程双方任意一方提供
具体做法:
OS提供的空间有不同样式,这就决定了不同的通信方式。
1.管道(匿名和命名管道)
2.共享内存
3.消息队列
4.信号量
下面我们针对管道进行讲解。
管道
认识管道:
什么是管道?作为进程间通信的一种方法,管道肯定也为不同进程之间提供了一个共享资源。
这个资源就是管道文件。
我们看到下面这个场景:一个文件能被同一个进程打开多次吗?答案是可以,比如显示器文件就有两个默认的stdout和stderr两个文件流。当父进程打开同一个文件两次,那么就会在父进程的文件描述符表中有两个指向同一个文件的文件描述符,我们假设是3和4,此时父进程再创建子进程,父子进程资源共享,子进程也获得了两个指向同一个文件的文件描述符3和4
此时我们的两个父子进程就相当于看见了同一份资源(文件),父进程先用w方式打开文件形成fd=3,再用r方式打开形成fd=4,子进程也继承这些,于是父子进程就可以向文件里进行数据的交换了。但是为了不会出现同时读或者同时写的情况,我们规定父进程写,子进程读,在这种情况下进行进程间通信。
关闭掉父进程的读端和子进程的写端:
匿名管道:
匿名管道之所以匿名是因为匿名管道创建的文件不会出现在磁盘,而是一个内存级文件 (只创建struct file结构对象而不在磁盘中创建文件,这个对象正常和fd数组连接,只能通过fd找到,没有文件名所以匿名。
匿名管道如何让不同进程看到同一份资源的呢?
创建子进程,子进程会继承父进程相关信息
匿名管道:只能继承具有血缘关系的进程,进行进程间通信,常见于父子进程。
下面通过代码来验证匿名管道:
管道的接口
int pipe(int pipefd[2])
其中的参数pipefd[2]是一个输出型参数,什么是输出型参数-----通过函数将数据放到参数中。
通过pipe函数我们得到管道的读端和写端和fd存入pipefd[2]中
pipefd[0]代表读端
pipefd[1]代表写端
函数返回值:
成功返回0,失败返回-1,错误码被设置。
管道的编码实现
#include <iostream>
#include <unistd.h>
#include<string.h>
#include <sys/wait.h>
using namespace std;int main()
{int pipefd[2];int n = pipe(pipefd);char buff[1024];if (n < 0){cerr << "pipe error:" << errno << endl;return 1;}pid_t id = fork();if (id == 0){close(pipefd[1]); // 关闭子进程写ssize_t byte_read=read(pipefd[0],&buff,sizeof(buff));if(byte_read== -1){cerr<<"read error"<<endl;}cout<<"child read:' "<<buff<<" 'from father."<<endl;close(pipefd[0]);//子进程完成任务关闭读端}else{close(pipefd[0]); // 关闭父进程读const char* msg="Hello child I am father";ssize_t byte_write=write(pipefd[1],msg,strlen(msg)+1);if (byte_write == -1) {cerr<<"write"<<endl;return 1;}close(pipefd[1]);//父进程完成任务关闭写端wait(nullptr);}return 0;
}
输出结果:
代码说明
- 管道创建: 使用
pipe()
系统调用创建管道,返回两个文件描述符,pipefd[0]
用于读,pipefd[1]
用于写。 - 进程创建: 使用
fork()
创建子进程,父子进程会继承管道的文件描述符。 - 关闭未使用的端: 子进程关闭写端,父进程关闭读端,避免资源泄漏。
- 通信过程:
- 父进程通过
write()
向管道写入数据。 - 子进程通过
read()
从管道读取数据并打印。
- 父进程通过
- 资源清理: 使用完毕后关闭所有文件描述符,父进程等待子进程结束。
操作系统是否允许进程直接访问管道?
因为管道是内存级文件,内存级文件本质是内核的资源,所以操作系统会允许进程直接访问管道吗?
答案是不会,因为操作系统不相信任何人,所以进程在读写文件的时候必须使用系统调用read和write。父进程通过write把数据写入管道,内核将数据复制到内核空间的环形缓冲区中,子进程接收进程通过read()
从缓冲区提取数据。
管道的特性
1、管道内部自带同步与互斥机制。
一次只能被一个进程使用的资源被称为临界资源,管道在同一时间只允许一个进程对其进行读或写操作,所以管道也是一种临界资源。
临界资源需要被保护,如果不保护临界资源,就可能出现一个资源同时被多个进程使用,比如管道同时被同时读写,会导致数据不一致不完整等问题。
为了避免这些问题,内核会对管道操作进行同步和互斥:
- 同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
- 互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
2、管道的生命周期随进程
管道本身是依赖产生的内存级文件的,当所有打开该文件的进程退出,文件也随之被释放掉,所以说管道生命周期随进程。
3、管道提供的是流式服务。
对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:
- 流式服务: 数据没有明确的分割,不分一定的报文段。
- 数据报服务: 数据有明确的分割,拿数据按报文段拿。
4、管道是半双工通信的。
在数据通信中,数据在线路上的传送方式可以分为以下三种:
- 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
- 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
- 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。
管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。
管道的四种特殊情况
1.管道内没有数据,同时写端没有关闭。
这种情况下读端就会一直阻塞等待直到管道内有数据。
2.管道内部被写满,读写端都正常不关闭
这种情况下写端就会阻塞。
3.写端写完内容并且关闭写端fd。
这种情况下读端会将管道内数据读完,最后读到返回值为0,表示读结束,类似读到文件结尾EOF。
4.读端关闭不读了,写端还在写。
这种情况下OS会直接终止写端进程,通过信号13)SIGPIPE杀掉进程。
前面两种情况就能很好说明管道是同步互斥的,不会出现读取空管道或者向写满的管道写数据。
第三种情况也很好理解,对于没有写端的管道,将内容读取完后读端就会继续执行自己的其他操作,不会被挂起。
第四种情况,管道内的数据都没人读了所以写入也没有意义,操作系统就直接杀掉进程,写端进程还没执行完代码就结束属于异常退出所以会收到信号。
接下来通过这段代码来验证第四种情况下写端进程接收到的信号:
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe创建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork创建子进程if (id == 0){//childclose(fd[0]); //子进程关闭读端//子进程向管道写入数据const char* msg = "hello father, I am child...";int count = 10;while (count--){write(fd[1], msg, strlen(msg));sleep(1);}close(fd[1]); //子进程写入完毕,关闭文件exit(0);}
通过kill -l命令查看13号信号是什么:
管道的大小
方法一:通过man手册我们可以查到管道的大小
方法二:使用ulimit命令
512bytes * 8 =4kb
方法三:代码测试
根据前面的情况,读端一直不读,写端一直写,管道被写满就会将写端挂起,我们可以靠这个得到管道的大小。
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe创建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork创建子进程if (id == 0){//child close(fd[0]); //子进程关闭读端char c = 'a';int count = 0;//子进程一直进行写入,一次写入一个字节while (1){write(fd[1], &c, 1);count++;printf("%d\n", count); //打印当前写入的字节数}close(fd[1]);exit(0);}//fatherclose(fd[1]); //父进程关闭写端//父进程不进行读取waitpid(id, NULL, 0);close(fd[0]);return 0;
}
进程阻塞在65536,管道最大字节是65536.