目录
一、什么是进程通信
二、为什么要进行进程通信
三、如何进行通信
四、管道
1、什么是管道
2、管道的原理
3、接口
4、编码实现
5、管道的特征
6、管道的4种情况
一、什么是进程通信
进程通信是两个或多个进程实现数据层面的交互。
因为进程具有独立性,导致进程通信的成本比较高,因为要打破进程的独立性。
二、为什么要进行进程通信
- 在两个或多个进程可能会有以下
- 发送基本数据
- 发送命令
- 实现某种协同
- 进行通知
三、如何进行通信
进程间通信的本质:必须让不同的进程看到同一份“资源”,也能叫做同一份内存空间。这个资源是特定形式的内存空间。
那么这份空间将由谁来提供呢?
如果存放在进程里,要进行进程通信的话,另一个进程要来访问该进程,对该进程的这份空间可能做增删查改,那这样就破坏了进程的独立性。
所以这份空间由操作系统提供,防止破坏两进程的独立性,开辟第三空间。
我们进程访问这个空间,进行通信,本质上就是访问操作系统。
我们可以把进程理解成用户,内存的创建、使用、释放都由系统调用接口,从底层设计,接口设计,都要由操作系统独立设计。因为操作系统不相信任何用户
一般的操作系统,会有一个独立的通信模块——隶属于文件系统——IPC通信模块
进程间通信是有标准的——system V(消息队列,共享内存,信号量)&&posix
这是历史发展得出两个标准,后面我们会给大家详细介绍system V。
我们接下来给大家讲解基于文件级别的通信方式——管道。
四、管道
1、什么是管道
把一个进程连接到另一个进程的一个数据流称作为管道。
我们来看看这张图理解
如果一个文件被可以多个进程打开,那么这个文件就可以作为公共资源,一个读,一个写即可
所谓的管道就是类似的基于文件级别的通信方式
管道是Unix中最古老的进程间通信的形式。
2、管道的原理
我们启动一个进程的时候,PCB里面有一个文件描述符表,键盘显示器文件对应0、1、2描述符,这里显示器只有一个文件,方便理解我这里画了两个,当我们新打开一个文件的时候,这个文件的地址会放入到文件描述符表中未存放最小的描述符中,这个新打开文件中inode用于寻找文件属性,有file_operators用于实现一切皆文件,还有一个缓冲区。
在磁盘中,也会在特定的分区特定的分组有对应的数据块和属性块,供我们读写,如果写的时候,一旦对应的位置为脏,那么就会往回刷新过去,从而进行写入。
(即打开文件的时候,因为struct file本身就会创建一个struct inode,这个inode里面就是文件的属性,供我们查看,这个inode里面的会直接从磁盘当中的inode里面加载。缓冲区则是磁盘当中的数据要先加载到这个缓冲区中,当我们利用file_operators的方法去修改缓冲区以后,就是数据为脏了,然后就会将这个数据刷新回磁盘)
当我们创建的文件不在磁盘上刷新,并且有对应的inode、file_operators、缓冲区,那么这个文件为内存级文件。
当我们创建子进程的时候,会拷贝父进程的PCB,文件描述符也会拷贝一份,这两份文件描述符表的内容是一模一样的,左边的都是属于进程的,而右边的是属于文件的,不需要拷贝也不会拷贝。
但子进程的文件描述符表要指向父进程文件描述符表指向的内容。
而我们前面所说的进程间通信,本质前提是需要先让不同的进程,看到同一份资源!!
这样我们就可以利用这个文件实现进程间通信了!
这就是管道的一个比较朴素的原理
所以管道其实就是文件
而且在这里会由于有引用计数,即便父进程关闭了这个文件,也不会消失的。
在这里如果我们父进程只有只读方式打开,那么这个文件描述符表继承下来的时候也是只读方式,这就没法通信了。所以其实父进程在打开文件的时候,会把文件以读写方式都打开一遍。这样的话就是下面的原理了!
上图即是父进程关闭了读端,子进程关闭了写端,这样就可以构成父进程往里面写入数据,子进程从中读取数据。
如下图所示,当我们的task_struct要将同一个文件分别以读写的方式打开的时候,会分别创建对应的struct file,只不过他们里面的inode,文件缓冲区,等等都是一样的,只是权限不同。然后我们继续创建子进程
像上面的这种,我们只想用来实现单向通信的
假设现在,我们想让子进程进行写入,父进程进行读取
当我们想要子进程写入,父进程读取的时候,只需要关闭对应的读写端即可
此时,两个struct file的引用计数都会变为1,也不可能会再次产生影响
这就是管道的原理
正式因为这个只能进行单向通信,所以才将它称作管道
那么如果要双向通信呢?
我们可以创建多个管道,比如两个管道就可以了
那么这两个进程如果没有任何关系,可以用我们上面的原理进行通信吗?
不能。必须是父子关系,兄弟关系,爷孙关系…
总之必须是具有血缘关系的进程,只不过常用于父子
那么我们这个文件有名字,路径…吗?即下面这部分
答案是没有的,它根本不需要名字,更不需要怎么标定它,因为它是通过继承父进程的资源来得到的。
所以我们把这种管道的名字叫做匿名管道
当然至此我们还没有通信,我们前面所做的工作都是建立通信信道,那么为什么这么费劲呢?这是因为进程具有独立性,通信是有成本的
3、接口
我们先来认识一下系统调用接口,它的作用是创建一个管道。
如果成功返回0,失败返回-1,并且错误码errno被设置
那这个参数pipefd[2]是什么意思?
调用这个pipe以后,父进程就会以读写方式打开一个内存级文件了。
打开以后,它的工作就完了。
所以这个参数的意思就是,创建好内存级文件以后,就会把对应的两个文件描述符给带出来,供用户使用!!!
其中,一般pipefd[0]是读下标,pipefd[1]是写下标。
4、编码实现
我们先测试一下我们上面得出来的结论
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
int main()
{int pipefd[2]={0};int n=pipe(pipefd);if(n<0) return 1;std::cout<<"pipefd[1]:"<<pipefd[0]<<" "<<"pipefd[2]:"<<pipefd[1]<<std::endl;return 0;
}
结论如下:
我们来模拟实现一下管道
我们这次让子进程写入,父进程读取。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>#define N 2
#define NUM 1024using namespace std;void Writer(int wfd)
{string s = "hello I am child";pid_t self = getpid();int number = 0;char buffer[NUM];while(1){//构建发送字符串buffer[0] = 0;snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);//cout << buffer;//发送给父进程write(wfd, buffer, strlen(buffer));sleep(1);}
}
void Reader(int rfd)
{char buffer[NUM];while(1){buffer[0] = 0;ssize_t n = read(rfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;cout << "father get a message : ["<<buffer << "]" << endl; }}
}int main()
{int pipefd[N] = {0};int n = pipe(pipefd);if(n < 0) return 1;// std::cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << std::endl;pid_t id = fork();if(id < 0){return 2;}else if(id == 0){//childclose(pipefd[0]);Writer(pipefd[1]);close(pipefd[1]);exit(0);}//fatherclose(pipefd[1]);Reader(pipefd[0]);pid_t rid = waitpid(id, NULL, 0);if(rid < 0) return 3;close(pipefd[0]);return 0;
}
运行结果如下:
5、管道的特征
- 管道之间是单向通信的。
- 具有血缘关系的进程进行进程通信
- 父子进程是会进程协同的,同步互斥的。
我们把写入休眠3秒。
我们能看到是子进程输入的时候休眠3秒,父进程在读取的时候,在等待子进程输入。再读
这是为了防止父进程在每读完的时候,子进程又写入,把数据覆盖了,父进程前面的读原来的数据,后面又是新的数据,导致错误。
4.管道是面向字节流的。
5.管道是基于文件的,而文件的生命周期是随进程的,进程关闭,管道也关闭,操作系统会回收未关闭的文件描述符对应的文件。
6、管道的4种情况
-
读写端正常,管道如果为空,读端就要阻塞
这就是我们刚刚介绍的子进程输入休眠3秒,父进程读取的时候等待子进程输入。
2.读写端正常,管道如果被写满,写端就要被阻塞
在父进程读之前休眠50秒。
这里我们会有一个疑问了,那管道的大小是多少呢?
我们可以先用下面这个命令观察一下,这个命令的功能是查看一些数据的最大限制
ulimit -a、
这里显示pipe size是512*8个字节 也就是4kb
我们写一段代码看看是不是4kb呢?
每次往里面写1个字节,直到写满。
我们能看到总共写入了65535次,也就是65535个字节=65535/1024=64kb
那我们前面的4KB是什么呢?
其实管道的大小在不同的内核中是不同的。
当前我们系统的管道大小是64KB
现在回答前面的4KB究竟是什么,这是因为我们写端在写入数据以后,读端要读数据,但是不能写了一半就读走了,这样可能导致数据出现问题。所以要么就不读,要么一次全读完, 也就是PIPE_BUF就是要保证是一个原子性的最大长度。不能被打断的,而这个PIPE_BUF就是4KB,也就是前面查到的4KB。
3.读端正常,写端关闭,读端就会读到0,读到文件结尾,并不会阻塞
结果如下,会不断往显示器上输出0
所以我们应该更改代码
结果如下:
4.写端正常写入,读端关闭了,操作系统会杀死正在写入的进程
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>#define N 2
#define NUM 1024using namespace std;void Writer(int wfd)
{string s = "hello I am child";pid_t self = getpid();int number = 0;char buffer[NUM];while(1){//构建发送字符串buffer[0] = 0;snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);write(wfd,buffer,strlen(buffer)); //cout << buffer;sleep(1);//发送给父进程// char c='c';// write(wfd, &c, 1);// cout <<number++<<endl;}
}
void Reader(int rfd)
{char buffer[NUM];int cnt=5;while(cnt--){buffer[0] = 0;ssize_t n = read(rfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;cout << "father get a message : ["<<buffer << "]" << endl; }else if(n==0){cout<<"father read done!"<<endl;break;}else break;}
}int main()
{int pipefd[N] = {0};int n = pipe(pipefd);if(n < 0) return 1;// std::cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << std::endl;pid_t id = fork();if(id < 0){return 2;}else if(id == 0){//childclose(pipefd[0]);Writer(pipefd[1]);close(pipefd[1]);exit(0);}//fatherclose(pipefd[1]);Reader(pipefd[0]);close(pipefd[0]);cout << "father close read fd" << pipefd[0] <<endl;sleep(5); // 为了维持一段时间的僵尸int status=0;pid_t rid = waitpid(id, &status, 0);if(rid < 0) return 3;cout << "wait child success: " << rid << "exit code: " << ((status>>8)&0xFF) << "exit signal: " << (status&0x7F) <<endl;sleep(3);close(pipefd[0]);return 0;
}
在还没执行到等待的时候,子进程一直是僵尸的
我们最后看到终止信号是13。