Halo,这里是Ppeua。平时主要更新C语言,C++,数据结构算法…感兴趣就关注我吧!你定不会失望。
本篇导航
- 0. 进程间通信原理
- 1. 匿名管道
- 1.1 通信原理
- 1.2 接口介绍
- 2. 命名管道
- 2.1 接口介绍
- 3. 共享内存
- 3.1 通信原理
- 3.2 接口介绍
0. 进程间通信原理
进程间的是相互独立的。那么想要让两个进程间进行通信,本质是让其看到同一份资源。因为进程具有独立性,所以大多时候让两个或多个进程看到同一份资源是最费力的。
根据看到资源的方式不同,将进程通信划分为以下几种:
- 匿名管道通信
- 命名管道通信
- 共享内存
其中,匿名管道通信与命名管道通信的本质都是让进程看到同一份内存级文件
内存级文件是一个仅存储在内存中的文件.不会刷新到磁盘中
1. 匿名管道
1.1 通信原理
管道实际上是一份内存级文件,其被创建出来,通过文件的方式去访问.
内存模型如下:
其中file_r为写缓冲区,file_w为读缓冲区(缓冲区本质上也为一个内存级文件)
创建管道时,系统会为其分配两个fd.一个为读端,一个为写端.但是 管道只能进行单向通信.
为了方便控制,通常情况下,我们会手动关闭我们不需要的那个fd.(以下为了方便测试,规定由父进程写读,子进程写)
那么在匿名管道通信时如何让多个进程看到同一份内存级文件呢?
子进程会继承父进程的大多数资源,file_struct也在其中.但因为操作系统节省资源的特性,文件并不会被创建多份
所以可以通过创建子进程的方法来让多个进程看到同一份资源
所以 匿名管道的特点之一:仅能在有关系的进程中进行通信(父子进程,兄弟进程)
创建一个子进程时,内存模型如下:
(通信本质是让不同进程看到同一份资源,所以资源的准备需要在进程创建之前!!!)
此时子进程也能够访问这个内存级文件了.这时双方就可以根据fd,按照访问文件的方式去访问这个内存级文件.也就是可以进行通信
1.2 接口介绍
创建匿名管道使用的函数为 int pipe(int pipefd[2])
其中 **int pipefd[2]**为输出型参数 pipefd[0]为读端,pipefd[1]为写端
该接口创建完管道,并为用户分配所需读写端的fd,将其存入该数组后返回给用户.
如果创建成功则返回0,如果创建失败则返回-1,同时设置errno
这是一份简单的管道通代码.创建管道需要在创建子进程前才能被共享到!
#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>
#include<string>
using namespace std;
#define N 2
#define NUM 1024
void Read(int rfd)
{
while (true) {
char buffer[1024];
int n=read(rfd,buffer,sizeof(buffer));
if(n<=0)
{
cout<<"wait write into pipe"<<endl;
}
else {
cout<<buffer;
}
}
}
void Write(int wfd)
{
string example="i am a child,hello linux communitate ";
pid_t self=getpid();
example+=to_string(self);
int flag=example.size();
int cnt=0;
while(true)
{
example.erase(flag);
example+=" "+to_string(cnt++)+"\n";
int n=write(wfd,example.c_str(),example.length());
if(n<=0)
{
cout<<"pipe close"<<endl;
break;
}
sleep(1);
}
}
int main()
{
int pipefd[2];
pipe(pipefd);
if(pipe(pipefd)<0)
{
perror("create pipe failed\n");
}
cout<<"0: "<<pipefd[0]<<endl;
cout<<"1: "<<pipefd[1]<<endl;
pid_t id = fork();
if(id==0) //0 read 1 write
{
//write
close(pipefd[0]);
Write(pipefd[1]);
}
else {
//read
close(pipefd[1]);
Read(pipefd[0]);
}
return 0;
}
完成了匿名管道的通信.
管道通信为单向的.读端会将读取的内容从管道中取走.先写入的数据会被先取走(与队列的原理相似)
父进程会随着子进程发送信息的频率而读取信息.(上文写端进行了休眠,而读端并没有)
所以:
读写端正常.当管道中没有内容时,读端会阻塞等待
如果我们重复的写入一段内容而不读取呢?
void Write(int wfd)
{
string example="i am a child,hello linux communitate ";
pid_t self=getpid();
example+=to_string(self);
int flag=example.size();
int cnt=0;
while(true)
{
example.erase(flag);
example+=" "+to_string(cnt++)+"\n";
int n=write(wfd,example.c_str(),example.length());
if(n<=0)
{
cout<<"pipe close"<<endl;
break;
}
cout<<cnt<<endl;;
}
}
将写端逻辑做出如上更改,当写不进去时,输出 “pipe full”;
void Read(int rfd)
{
while(true){};
while (true) {
char buffer[1024];
int n=read(rfd,buffer,sizeof(buffer));
if(n<=0)
{
cout<<"wait write into pipe"<<endl;
}
else {
cout<<buffer;
}
}
}
将读端做出如上更改.手动阻塞进程
观察到写端阻塞,等待读端读取
所以
读写端正常.当管道写满时,写端会阻塞等待读端读取
将写端设置为一段时间后自动关闭.读端不会被阻塞.read返回0,可以根据这个特性做出行为
所以
写端被关闭,读端读到文件结尾,返回0.但此时不会被阻塞
将读端设置为一段时间后自动关闭.为了节省资源.写端将被操作系统关闭
void Read(int rfd)
{
int cnt=5;
while (cnt>0) {
char buffer[1024];
int n=read(rfd,buffer,sizeof(buffer));
if(n<=0)
{
cout<<"wait write into pipe"<<endl;
}
else {
cout<<buffer;
}
cnt--;
}
cout<<"read close"<<endl;
}
//main 中修改的部分
//read
close(pipefd[1]);
Read(pipefd[0]);
close(pipefd[0]);
int status=0;
waitpid(id,&status,0);
cout<<"receive signal : "<<(status& 0x7f)<<endl;
收到13号信号,进程被终止.13号信号为SIGPIPE
所以
读端被关闭.写端也被关闭
综上,匿名管道通信时有四种情况:
- 读写端正常.当管道中没有内容时,读端会阻塞等待
- 读写端正常.当管道写满时,写端会阻塞等待读端读取
- 写端被关闭,读端读到文件结尾,返回0.但此时不会被阻塞
- 读端被关闭.节省资源.写端也被关闭
所以我们可以得到匿名管道有以下特征:
- 具有血缘关系的进程才可以进行通信
- 管道只能单向通信
- 父子进程是会进程协同的,同步与互斥
- 管道是面向字节流的
- 管道基于内存级文件.其生命周期随进程
2. 命名管道
与匿名管道大同小异.都是基于文件级的通信,但是命名管道在指定路径下创建了一个具有名称的内存级文件.
这使得 没有血缘关系的进程也能够看到同一份资源,所以此时,不同的进程也可以进行通信了
2.1 接口介绍
我们可以使用mkfifo 依照创建文件的方法,在指定目录下创建出内存级文件.
其表示文件属性的权限位,显示其为一个管道文件.
删除这个管道文件我们通常使用unlink
在语言层面上,也为我们封装了该接口
pathname:为指定路径 mode:为权限
#pragma once
#include <sys/stat.h>
#include <unistd.h>
#include"log.hpp"
#define FIFO_PATH "./myfifo"
class InitPipe{
public:
InitPipe()
{
int n=mkfifo(FIFO_PATH,MODE);
if(n!=0)
{
log(FATAL,"create pipe failed");
exit(0);
}
}
~InitPipe()
{
unlink(FIFO_PATH);
}
};
接口使用
为了避免每次退出进程时,还要去手动释放该管道文件.所以利用RAII的方式来存储管道文件
之后使用该管道时,根据读写文件那一套来即可.
sever.cpp:
#include"log.hpp"
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "create_pipe.hpp"
int main(int argc,char * argv[])
{
InitPipe pipe;
Log log(SCREEN|CLASS_FILE);
int fd=open(FIFO_PATH,O_RDONLY);
if (fd < 0)
{
log(FATAL, "error string: %s, error code: %d", errno, errno);
}
while(true)
{
char sz[1024]={0};
int x=read(fd,sz,sizeof(sz));
if(x > 0)
{
cout<<"client say# "<< sz <<endl;
log(INFO,sz);
}
else if(x==0)
{
break;
}
else break;
}
}
client.cpp:
#include <fcntl.h>
#include<unistd.h>
#include<iostream>
#include<string>
using namespace std;
int main()
{
int fd=open("./myfifo",O_WRONLY);
if (fd < 0)
{
cout<<"failed"<<endl;
exit(0);
}
string s1;
while(true)
{
cout<<"client say @";
getline(cin,s1);
write(fd,s1.c_str(),s1.length());
}
}
(上文用到的log可以查看我下一篇博客的日志插件)
3. 共享内存
共享内存相较于前两种通信方式,速度上有明显的优势.
其不会涉及到复制写入/读取的内容.而是直接写入内存.而管道是将内容复制到文件当中,在复制出来
3.1 通信原理
共享内存的本质就是将一段真实的 物理内存,映射到PCB的共享内存当中.当多个进程映射同一个物理内存时,通过对内存数据直接的读写,就可以实现通信.
同样,我们需要使用系统调用接口去申请这段共享内存.使用系统调用接口去释放这段共享内存
共享内存没有像管道一样的同步互斥,需要用户自己去规定.
3.2 接口介绍
申请共享内存:
key:可以理解为申请共享内存的一段密钥,该密钥在系统中是唯一的,就可以申请到唯一的一块共享内存,也是系统内核去校验两个共享是否相同的一个手段
通过ftok去申请:
该函数是一个算法结合两个参数去生成一个唯一的key.所以这两个参数可以根据使用情况去定制.
若申请成功,则返回key,若申请失败则返回-1,并设置errno.
size:为申请的共享内存大小,一般为4096的整数倍.
shmflg具有以下两个值:
- IPC_CREAT (申请一段共享内存,若不存在则创建并返回shmid,若存在则返回shmid(用户级的key))
- IPC_CREAT | IPC_EXEL| 八进制权限信息 (申请一段共享内存,若不存在则创建并返回shmid,若存在则创建失败) 需要带上权限信息
为什么会有第二个选项呢?用来保证你申请的共享内存是一段新的,唯一被您使用的内存
shmid是什么?与上文的key类似,内核使用key去操作控制共享内存,而用户通过shmid完成如上操作
返回值为 成功返回shmid,失败返回-1,并设置errno
获取共享内存地址.
char * address = (char *)shmat(shmid,nullptr,0);
**取消挂接该地址.**若成功则返回0,失败返回-1
对该共享内存进行控制,一般用来删除共享内存
cmd参数填上IPC_RMID 表示删除当前内存
下面是一个简单的示例demo:
config.hpp
#pragma once
#include <cerrno>
#include <cstring>
#include <sys/ipc.h>
#include<sys/types.h>
#include <sys/shm.h>
#include<stdlib.h>
#include<iostream>
using namespace std;
string PATH= "/tmp";
int MODE= 255;
#define SIZE 4096
class Init{
public:
Init(){
_key=ftok(PATH.c_str(), MODE);
if(_key==-1)
{
cout<<"create failed"<<endl;
strerror(errno);
exit(0);
}
}
int CreateShm()
{
//需要加上权限 否则创建失败
int shmid=shmget(_key,SIZE,IPC_CREAT|IPC_EXCL|0666);
if(shmid==-1)
{
cout<<"creatshm failed"<<endl;
strerror(errno);
exit(0);
}
return shmid;
}
int GetShm()
{
int shmid=shmget(_key,SIZE,IPC_CREAT|0666);
if(shmid==-1)
{
cout<<"getshm failed"<<endl;
strerror(errno);
exit(0);
}
return shmid;
}
void destoryShmid(int shmid)
{
if(shmctl(shmid,IPC_RMID, nullptr)!=-1)
cout<<"destory success";
}
private:
key_t _key;
};
processaa.cpp 接受方
#include "config.hpp"
#include <cstddef>
#include <sys/shm.h>
#include<iostream>
using namespace std;
int main()
{
Init it;
int shmid=it.GetShm();
cout<<"get success a"<<endl;
char * address = (char *)shmat(shmid,nullptr,0);
while(true)
cout<<address<<endl;
}
processbb.cpp 发送方
#include "config.hpp"
#include<iostream>
#include <cstddef>
using namespace std;
int main()
{
Init it;
int shmid=it.CreateShm();
cout<<"create success b"<<endl;
char * address = (char *)shmat(shmid,nullptr,0);
cout<<"address success "<<endl;
int cnt=5;
while (cnt-->0) {
fgets(address,4096,stdin);
}
it.destoryShmid(shmid);
}