进程通信是什么
进程通信用我们的自己的话来说就是两个或者多个进程在我们的数据层面上需要进行交互,我们将这种交互行为叫做进程间的通信,我们知道进程是拥有独立性的,所以我们的进行通信一定是会有成本的。
为什么需要进程通信
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
如何做到进程通信
进程通信的本质是让我们的两个进程能够看到同一份"资源",这个资源指定就是我们操作系统为我们的开辟的一段内存空间(通信代价),我们的进程间进行我们的进程通信的时候,需要访问这个共同的空间,所以也就是访问操作系统,这也就意味着我们需要进行系统调用接口来实现我们进程通信。而我们设计系统调用的接口时一般都需要有一个标准,目前常见的两种通信标准分别是system V和posix。
system V通信方式有三种:消息队列,信号量,共享内存
posix通信方式有六种: 消息队列 ,共享内存 ,信号量 ,互斥量 ,条件变量 ,读写锁
管道(文件级别通信方式)
什么是管道
管道是Unix中最古老的进程间通信的形式,他的本质就是一个文件
我们把从一个进程连接到另一个进程的一个数据流称为一个"管道",它不将我们的数据刷新在磁盘上,而是使用内存级文件。分为匿名管道和命名管道
管道原理
我们知道对进程进行通信需要我们共享一份"资源",在我们学过的知识中,我们知道,利用fork创建子进程的时候,子进程会拷贝我们代码和数据,同时也会生成自己的PCB和file_struct等进程信息,但是不会拷贝我们的struct_file 对象,父进程和子进程的file_struct会指向我们相同的文件,而文件内部也有引用计数变量来标记有多少进程使用我这个文件,当我们创建子进程的时候,我们不就是共享了我们的文件资源了吗?所以我们很容易想到,进程间通信与我们的父子进程相关。
那我们子进程的子进程(孙子进程)可以利用共享一个"资源进行通信吗",是可以的,而我们的孙子进程和我们原来的父进程共享的也是同一个"资源",如果一个毫不相干的进程和我们的父进程之间是没有共享的资源的,所以我们只有拥有"血缘关系"的进程间才可以进行通信
管道之所以叫做管道,是因为在我们设计的时候,我们只想让我们的进程间进行单向通信,如果不是单向的,就会导致管道里面的数据混乱,就比如:运输材料,如果两边都向管道里面运输不同的液体,就会导致液体混合,不纯洁。所以叫做管道通信
管道本质是文件,,和文件类似,分为读和写,但是没有设计读写同时的方式,所以我们在创建管道的时候,就需要用两种方式打开文件,一个是写方式,一个是读方式,他们拥有通信inode和缓冲区(如下图所示),子进程再拷贝的时候同样也会拷贝一个写和一个读。所以我们明确了我们谁读谁写之后,我们就需要在父进程和子进程中关闭不需要的端口,避免错误操作。

管道的系统调用接口
知道了管道的原理之后我们就来看看创建我们管道的系统接口

形参:是一个输出型参数,第一个参数又来存储以读方式打开的文件描述符(fd),第二个参数用来存储以写方式打开的文件描述符(fd)
返回值:如果成功打开则返回0,失败则返回-1;
编码实验
实验代码
cpp
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib> //stdlib.h
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
#define N 2
void Writer(int wfd) //写入管道
{
char buff[1024]; //自定义缓冲区
string s = "hello ,I am child";
pid_t self = getpid();
int number = 0;
while (true)
{
buff[0] = 0;
snprintf(buff,sizeof(buff),"%s-%d-%d",s.c_str(),self,number++);//将字符串写入缓冲区
write(wfd,buff,strlen(buff)); //写入管道
sleep(2);
}
}
void Reader(int rfd)//从管道内读
{
char buff[1024];//自定以缓冲区
while(true)
{
buff[0]=0;
ssize_t n = read(rfd,buff,sizeof(buff));//将数据管道读到缓冲区
if(n>0)//读到数据
{
buff[n] = 0;
cout<<"fatehr get message:"<<buff<<endl;
}
else if(n==0)//表示读到文件结尾
{
cout<<"father read file end"<<endl;
}
else break;
}
}
int main()
{
//创建管道
int pipefd[N];
int n = pipe(pipefd);
if(n<0) return 2;
//创建子进程
pid_t id = fork();
if(id<0) return 1;
else if(id==0) {
//child
close(pipefd[0]);
// IPC code
Writer(pipefd[1]);
exit(0);
}
//father
close(pipefd[1]);
//IPC code
Reader(pipefd[0]);
pid_t ret = waitpid(id,nullptr,0);
if(ret < 0 ) return 3;
return 0;
}
我们将上述代码编译运行一下:我们看见我们父进程成功读到了文件,但是我们要注意的是,我们在写的时候是sleep了2秒,这段代码执行时间花费了10秒钟,也就是说,在我们读数据的时候,如果我们没有写数据,他是不会读的,由此我们可知,当我们的读写端都正常时,管道中数据为空,那么读端就要阻塞

当我们修改Write和Read函数的代码
cpp
void Writer(int wfd)
{
char buff[1024];
string s = "hello ,I am child";
pid_t self = getpid();
int number = 0;
while (true)
{
char c = 'c';
write(wfd,&c,1);
number++;
cout<<"number:"<<number<<endl;
}
}
void Reader(int rfd)
{
char buff[1024];
while(true)
{
buff[0]=0;
sleep(50); //Read休眠
ssize_t n = read(rfd,buff,sizeof(buff));
if(n>0)
{
buff[n] = 0;
cout<<"fatehr get message:"<<buff<<endl;
}
else if(n==0)
{
cout<<"father read file end"<<endl;
}
else break;
}
}
运行结果如下:我们发下,当我们的管道写满的时候,我们如果不读的话,我们的写就会阻塞,也就说明,当读写两端正常时,如果管道被写满,那么写端将会阻塞

再次修改代码:
cpp
void Writer(int wfd)
{
sleep(1);
char buff[1024];
string s = "hello ,I am child";
pid_t self = getpid();
int number = 0;
while (true)
{
char c = 'c';
write(wfd,&c,1);
number++;
cout<<"number:"<<number<<endl;
if(number>5) break;
}
}
void Reader(int rfd)
{
char buff[1024];
while(true)
{
buff[0]=0;
ssize_t n = read(rfd,buff,sizeof(buff));
if(n>0)
{
buff[n] = 0;
cout<<"fatehr get message:"<<buff<<endl;
}
else if(n==0)
{
cout<<"father read file end"<<endl;
break;
}
else break;
}
}
运行如下:发现当我们读端口正常,关闭了写端口,我们的读端口就会返回0,表示读到了文件的末尾。

修改代码:我们让我们的读端口提前关闭。
cpp
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib> //stdlib.h
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
#define N 2
void Writer(int wfd)
{
sleep(1);
char buff[1024];
string s = "hello ,I am child";
pid_t self = getpid();
int number = 0;
while (true)
{
sleep(1);
buff[0] = 0;
snprintf(buff,sizeof(buff),"%s-%d-%d",s.c_str(),self,number++);
write(wfd,buff,strlen(buff));
}
}
void Reader(int rfd)
{
char buff[1024];
int cnt = 0;
while(true)
{
buff[0]=0;
ssize_t n = read(rfd,buff,sizeof(buff));
if(n>0)
{
buff[n] = 0;
cout<<"fatehr get message:"<<buff<<endl;
}
else if(n==0)
{
cout<<"father read file end"<<endl;
break;
}
else break;
cnt++;
if(cnt>5) break;
}
}
int main()
{
//创建管道
int pipefd[N];
int n = pipe(pipefd);
if(n<0) return 2;
//创建子进程
pid_t id = fork();
if(id<0) return 1;
else if(id==0) {
//child
close(pipefd[0]);
// IPC code
Writer(pipefd[1]);
exit(0);
}
//father
close(pipefd[1]);
//IPC code
Reader(pipefd[0]);
close(pipefd[0]);
cout<<" father close read fd:"<<pipefd[0]<<endl;
sleep(5);
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret < 0 ) return 3;
cout<<"wait child sucess :"<<ret<<"exit code : " << WIFEXITED(status)<<" exit signal:"<< WTERMSIG(status) << endl;
return 0;
}
运行结果如下:我们发现当我们的读端口关闭后,我们的写端口就自动关闭了,并且不是正常推出,退出信号为13,SIGPIPE信号


总结:
管道的特征:
1、具有血缘关系的进程可以进行进程间的通信
2、管道只能单向通信
3、父子进程是会进程协同的,同步与互斥的--保护管道文件的数据安全
4、管道是面向字节流的
5、管道是基于文件的,而文件的生命周期是随进程的
管道中的四种情况:
1、读写端正常,管道如果为空,读端就会阻塞
2、读写端正常,管道如果被写满,写端就会阻塞
3、读端正常,写端关闭,读端就会读到0,表面读到了文件的结尾,不会被阻塞
4、写端正常写入,读端关闭了,操作系统就会杀掉正在写入的进程。