1. 进程间通信
两个进程之间,能够"直接"进行数据的传递吗?
答案是不能的!因为进程具有独立性!我们知道,就连父进程也不能直接读取子进程的信息,他们有各自独立的内存空间,更遑论其他进程之间。
1.1 为什么要有进程间通信?
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。(如我们上节课学的动态库被多个进程同时使用)
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。比如我们在vs里对程序进行调试,此时的调试程序也是一个进程。
很多任务是需要多个进程协同完成的!
1.2 进程间通信是什么?
通俗来讲,一个进程把数据传递给另一个进程就叫做进程间的通信。
1.3 如何做到进程间通信?
要解决这个问题,我们要明晰为什么不能做到进程间通信?是因为进程具有独立性!
那么很简单,我们给出一块公共的空地,让两个进程都可以访问,切记这里的空地不属于任何一个进程!看过电视吧,两个帮派的交易场所是不会在任何一个帮派的领地的,会在大佬的见证下于公共区域进行交易。同理 ,这里的进程间通信也是如此,那么大佬是谁呢?别忘了,进程可是受OS管辖的,那么这里的大佬自然就是OS了。
因此进程间通信的方式如下:
由OS提供一块用于信息交互的空间,让参与通信的进程进行访问(读写)。
OS提供的空间多种多样,根据这一空间的不同,就有了不同的通信方式。
1.4 进程间通信的方式们
管道
匿名管道pipe
命名管道
System V IPC
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
上面这么多的通信方式,都是进程间通信发展以来的产物,其中有很多都已经不再使用了。
2. 管道
2.1 什么是管道?
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
上图就是who进程将信息写入管道,wc-l接收并输出给用户的过程!
2.2 管道的原理
这里我们自己创建一个文件,而后充当管道也是可以的。管道是单向的,也叫做半双工通信。
注意:两个进程对管道的操作权限最好一个读一个写,这是为了避免发生读写冲突。如果你能沟很好的管理这一问题,也可以两个都读和写。
2.3 匿名管道
2.3.1 匿名管道的概念
匿名管道是什么? 就是没有名字的管道!
我们来看一下pipe接口。
也就是说,调用pipe接口,向其内传入一个输出型参数pipefd[],用来返回两个文件描述符,pipefd[0]用来读,pipefd[1]用来写。
代码验证
我们来写一段代码进行验证吧!
本次代码是c/c++混编,实现子进程向管道内写入数据,而后父进程读取并输出在显示器的过程。
#include<iostream>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
void Writer(int wfd)//向管道内写入的函数
{
char buffer[128]="我已向管道写入,请接收!";
int cnt=0;
while(1)
{
snprintf(buffer,sizeof(buffer),"我是子进程,我的pid是: %d ,cnt: %d\n",getpid(),cnt);//将格式化字符串拼接到buffer
write(wfd,buffer,sizeof(buffer));//写入
cnt++;
sleep(1);//睡眠一秒
}
}
void Reader(int rfd)//读取管道内容的函数
{
char buffer[1024];
while(1)
{
read(rfd,buffer,sizeof(buffer));//子进程将读取到的数据存放如buffer
printf("father get message: %s",buffer);
}
}
int main()
{
int pipefd[2]{0};//接收文件描述符的输出型参数
int ret=pipe(pipefd);
if(ret<0)
return -1;
pid_t pid=fork();
if(pid==0)//子进程保留写,并对管道进行写入
{
close(pipefd[0]);//关闭读
Writer(pipefd[1]);
exit(0);
}
//父进程对管道进行读取,关闭写
close(pipefd[1]);
Reader(pipefd[0]);
wait(0);
return 0;
}
下面是我们的运行结果。
2.3.2 匿名管道的读写规则与特点
2.3.2.1 读写规则
1. 当管道内没有数据可读时,读端会陷入阻塞,等待管道内有数据再读。
2. 当写端关闭时,读端会将管道内数据读完,最后读到返回值为0,表示读结束,类似于读到文件末尾。
3. 当管道写满的时候,write陷入阻塞。
4. 当读端关闭而写端仍在向该管道写入,OS会强制关闭写端,并释放信号杀死该写端进程。
验证
我们对代码稍作修改,让父进程读取五秒后关闭读端,此时取子进程的退出码信号为13,说明他被信号13杀死。
2.3.2.2 特性
1. 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个匿名管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
2. 进程退出,管道就释放,所以管道的生命周期随进程
3. 内核会对管道操作进行同步与互斥//即管道自带同步机制
4. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道5. 管道是面向字节流的。
因此,匿名管道的实现机制是基于子进程会继承父进程打开的文件与文件描述符的。
图中命令行的管道就是匿名管道,用以链接这几个进程。
2.4 命名管道
匿名管道只能在有亲缘关系的进程之间支持通信,那如果我们要在不相关的进程之间进行通信该怎么做呢?
可以用FIFO文件来做这件事,它也被称为命名管道。fifo文件是一种特殊的文件,有多么特殊呢?
对普通文件的读写都是需要冲刷进入磁盘的,而fifo文件的特殊之处在于进程对fifo文件(管道)进行读写并不会向磁盘内冲刷,只用以进程之间的数据传输。
那么问题来了,我们也只会普通文件的创建啊。不必担心,这件事已经有人帮我们做了,我们只需要站在巨人的肩膀上。
注意:FIFO文件仅仅是一个标识,匿名管道与命名管道本质上都是通过内核中的一块缓冲区进行通信,该文件的作用仅是用来让进程找到这块缓冲区,因而进程间通信时并不会影响该文件。同时,如果两个进程已经开始通信,此时删除该文件,进程通信不受影响,因为我们已经不再需要通过该文件寻找内核缓冲区。
命令中有mkfifo创建,在程序中同样可以借助mkfifo函数进行创建。
一个文件可以同时被多个进程打开,打开并不会二次加载文件,进程们共用一份。下图就是命名管道的原理图。
我们来写一份代码看看吧!
代码验证 用命名管道实现sever/client单向通信
comm.hpp
实现对fifo的管理
#pragma once
#include<iostream>
#include<unistd.h>
#include<cstring>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
using namespace std;
#define PATH "./fifo.txt"
#define MODE 0666
class FIFO//管理fifo的创建与销毁
{
public:
FIFO(string path)
:_path(path)
{
int n=mkfifo(_path.c_str(),MODE);//创建,成功返回0
if(n==0)
{
cout<<"name_pipe creat success..."<<endl;
}
else
{
cerr<<"name_pipe creat fail... errno: "<<errno<<",errstring: "<<strerror(errno)<<endl;
}
}
~FIFO()
{
int n=unlink(_path.c_str());//销毁,成功返回0
if(n==0)
{
cout<<"name_pipe unlink success..."<<endl;
}
else
{
cerr<<"name_pipe unlink fail... errno: "<<errno<<",errstring: "<<strerror(errno)<<endl;
//标准错误输出
}
}
private:
string _path;
};
client
管道的写端
#include"comm.hpp"
int main()
{
int wfd=open(PATH,O_WRONLY);//打开,返回文件描述符
if(wfd<0)
{
cerr<<"open fifo fail... errno: "<<errno<<",errstring: "<<strerror(errno)<<endl;
return -1;
}
//下面这句只有
cout<<"open success..."<<endl;
cout<<"Please Enter Your Message!"<<endl;
string buffer;
while(1)
{
getline(cin,buffer);//cin遇到“ ”会终止
if(buffer=="quit")
break;
int n=write(wfd,buffer.c_str(),buffer.size());
if(n<0)
{
cerr<<"write fifo fail... errno: "<<errno<<",errstring: "<<strerror(errno)<<endl;
break;
}
}
close(wfd);
return 0;
}
sever
管道的读端
#include"comm.hpp"
//服务器端读
int main()
{
FIFO fifo(PATH);
int rfd=open(PATH,O_RDONLY);
if(rfd<0)
{
cerr<<"open fifo fail... errno: "<<errno<<",errstring: "<<strerror(errno)<<endl;
return -1;
}
//下面这句当客户端连接上才会打印出来
//当写端未打开,先读打开,open会阻塞,直到写端打开
cout<<"open success..."<<endl;
char buffer[1024];
while(1)
{
int n=read(rfd,buffer,sizeof(buffer));//读取内容
if(n<0)
{
cerr<<"read fifo fail... errno: "<<errno<<",errstring: "<<strerror(errno)<<endl;
break;
}
if(n==0)
{
cout<<"client already quit...,me too!"<<endl;
break;
}
else
{
buffer[n]=0;
cout<<"client say: "<<buffer<<endl;
}
}
close(rfd);
return 0;
}
那么命名管道是如何确保两个进程打开的是同一个管道呢?
很简单,根据路径,由用户传入路径,别忘了路径是唯一的标识符。