进程间通信的目的:
1、数据传输:一个进程需要将它的数据发售那个给另外一个进程。
2、资源共享:多个进程之间需要共享同样的资源。
3、通知事件:一个进程需要向另外一个或者一组进程发送消息,通知它们发生了某种事件(比如:进程终止时要通知父进程)。
4、进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时告知它的状态改变。
进程间通信的本质:
进程间通信的本质其实是:让不同的进程看到了同一份资源。
通过进程的学习我们知道进程之间是具有独立性的,各自进程的数据对方是看不到的,就算是父子进程随意他们的数据是共享的但一旦方式写入。就会发生写时拷贝,数据各自私有一份,所以了进程间进行通信是很困难的。 这也就意味着进程之间要想进行通信,一定要借助第三方资源。这个第三方资源不属于这些进行通信进程中的任何一个。有了这个第三方资源这些进程可以向这个资源里面写入数据或者读取数据,进而实现进程间通信。而这个第三方资源通常是OS提供的内存区域。因此我们可以得出结论:进程间通信的本质是让不同进程看到同一份资源。
一、前提条件(实现进程间通信)
1.进程是具有独立性的--无疑增加了通信的成本。
2.要让两个不同的进程,进行通信,前提条件是要看到同一份“资源。
3.任何进程的通信手段
a.想办法,先让不同的进程,看到同一份资源
b.让一方写入,一方读取,完成通信过程,至于,通信目的与后续工作,要结合具体场景。
二、匿名管道
1、管道的概念
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。 例如:我们使用统计一个文件中代码的行数。
cat proc.cc | wc -l
其中cat指令和wc指令是两个程序当他们运行起来时就变成进程了cat通过标准输出将数据放到管道中wc再从管道中拿数据,至此就完成了进程间通信。
2、匿名管道的概念
匿名管道多用于具有血缘关系的进程进行通信,多用于父子进程。
我们在上面说进程间通信的本质是让不同的进程看到同一份资源,那么使用匿名管道也一定要让父子进程看到同一份资源。匿名管道其实是让父子进程看到了同一份被打开的文件资源,本质其实是一段内核缓冲区。然后了父子进程就可以对这个资源进行读写操作了,进而实现了进程间通信。
3、pipe函数
pipe函数是用于创建匿名管道,pipe函数原型如下:
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,数组pipefd是用来获取读端和写端的文件描述符。
pipe函数调用成功返回0失败返回-1。
4、匿名管道实现通信的原理
管道也是文件,站在管道文件的角度来理解管道实现通信的原理。
管道是一个文件,当一个进程以读和写的方式打开一个管道。再创建一个子进程,子进程会以父进程为模板,拷贝父进程的部分内容。此时file_strcut里的数组(文件描述符与文件的映射关系)会是父进程的拷贝。此时,父子进程都指向了管道文件(同一块空间),并且子进程也是以读写方式打开的该文件(因为子进程会继承父进程代码,父进程再创建子进程之前以读写方式打开的文件),如果将一个进程对文件进行写,一个进程对文件进行读,由于来给你进程指向同一空间,所以读进程拿到的数据就是写进程写进去的数据。此时就完成了对文件的通信。
5、匿名管道的使用代码
实现父进程去读取,子进程去写入的操作。
#include<iostream> #include<cerrno> #include<assert.h> #include<string.h> #include<sys/types.h> #include<unistd.h> 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,我的ID:%d\n",namestr.c_str(),cnt++,getpid()); write(pipefd[1],buffer,strlen(buffer)); //不断写 sleep(1); } close(pipefd[1]); exit(0); } //父进程 //3.关闭不需要的fd,让父进程去读取,子进程去写入 close(pipefd[1]); //4.开始通信 char buffer[1024]; while(true) { sleep(10); int n=read(pipefd[0],buffer,sizeof(buffer)-1); if(n>0) { buffer[n]='\0'; std::cout<<"我是父进程,子进程给我信息:"<<buffer<<std::endl; } } close(pipefd[0]); return 0; }
6、匿名管道的四种情况
6.1写进程比较慢时,读进程会进入阻塞状态,读进程只能等待
-
当读进程进行读操作时,当读条件不满足,读进程进入阻塞状态。
读条件不满足:管道里没有数据或者说写端没有往管道写数据。
读进程进入阻塞状态:PCB的状态设置为S,该进程从运行队列进入等待队列,等待管道中有数据。
由于写进程比读进程慢,读进程在读时,大部分时间,管道是空的,此时读进程会进入阻塞状态,等待管道中有数据。
6.2当写进程进行写操作时,管道慢了,写进程就不能再写了
当写进程进行读操作时,当写条件不满足,写进程进入阻塞状态。写条件不满足:管道满了的时候
写进程进入阻塞状态:PCB的状态设置为S,该进程从运行队列进入等待队列,等待管道中可以写入数据。
改变上面代码:子进程写数据没有时间限制,父进程读数据延时5s。
进程间同步的概念:一个进程快导致另一个进程也快,一个进程慢导致另一个进程也慢。一个进程受到控制,另外一个进程也受到控制,着就叫进程间同步。
6.3如果关闭写端文件描述符,读进程会一直读到文件结尾
这里说明,如果将写文件描述符关闭,读进程最终一定会读到管道的结尾。因为已经没有进程往文件中写入数据了。
6.4如果关闭读进程文件,写端一直写,写进程可能会被进程直接杀死,进程异常退出
读进程退出读文件描述符,系统传13号信号杀死写进程。并不是将写进程变成僵尸状态,上面是因为写进程是子进程,但是父进程没有退出。
所以一般先将写进程关闭写文件描述符,再关闭读进程的读文件描述符。
7、匿名管道的特征
三、命名管道
其实原理和匿名管道差不多,只是需要先创建一个命名管道。再一个进程以读或者写的方式来打开该管道文件,再另外一个进程不需要创建管道,只需要以写或者读的方式来打开管道文件。再调用读写系统调用来往文件写或者读,来进行进程间通信。
两进程分别对同一管道文件分别用读或写的方式打开,两进程看到同一文件(资源)。不需要创建子进程,可以是两个不相关的进程。
1、创建命名管道
1.1、使用命令行mkfifo创建管道
1.2、使用函数创建管道
//头文件 #include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname,mode_t mode) //返回值:成功返回0,失败返回-1 //参数:filename 路径+文件名 // mode:权限
创建命名管道代码:
#include<stdio.h> #include<stdlib.h> #include<sys/types.h> #include<sys/stat.h> int main(){ umask(0);//设置掩码为0 int res=mkfifo("./fifo",0644); if(res==-1){ perror("mkfifo error"); exit(2); } return 0; }
2、使用命名管道实现两个进程间的通信代码实例
com.hpp: 相当于头文件
#pragma once #include<iostream> #include<string> #define NUM 1024 using namespace std; const string fifoname="./fifo"; uint32_t mode=0666;
server.cc: 服务端
#include<iostream> #include<cerrno> #include<cstring> #include<unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "com.hpp" using namespace std; int main(){ //创建管道文件,我们只需要创建一次 umask(0); //这个设置并不影响系统的默认位置,只影响当前进程 int n=mkfifo(fifoname.c_str(),mode); if(n!=0) { cout<<errno<<":"<<strerror(errno)<<endl; return 1; } cout<<"创建管道文件成功"<<endl; //让服务端直接开启管道文件 int rfd = open(fifoname.c_str(),O_RDONLY); if(rfd<0) { cout<<errno<<":"<<strerror(errno)<<endl; return 2; } cout<<"成功打开管道文件,正常通信"<<endl; //3.正常通信 char buffer[NUM]; while(true) { buffer[0]=0; ssize_t n=read(rfd,buffer,sizeof(buffer)-1); if(n>0){ buffer[n]=0; //cout<<"客户端的信息# "<<endl; cout<<"客户端的信息# "<<buffer<<endl; //printf("%c",buffer[0]); fflush(stdout); } else if(n==0) { cout<<"客户端退出,服务端也应该退出"<<endl; break; } //关闭不要的fd close(rfd); unlink(fifoname.c_str()); }
client.cc: 用户端
#include<iostream> #include<cerrno> #include<cstring> #include<cassert> #include<unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "com.hpp" int main(){ int wfd=open(fifoname.c_str(),O_WRONLY); if(wfd<0){ cerr<<errno<<":"<<strerror(errno)<<endl; return 2; } //可以进行常规通信了 char buffer[NUM]; while(true){ cout<<"请输入你的消息#"; char *msg=fgets(buffer,sizeof(buffer),stdin); assert(msg); (void)msg; buffer[strlen(buffer)-1]=0; ssize_t n=write(wfd,buffer,strlen(buffer)); assert(n>=0); (void)n; } close(wfd); return 0; }
即可以实现在用户端输入数据,在服务端接收。