当前位置: 首页 > news >正文

Linux进程间通信

目录

一、进程通信介绍

1.进程通信的目的

2.进程通信的方式

二、匿名管道通信

1.匿名管道通信的基本原理

2.匿名管道通信原理的细节补充

3.匿名管道通信代码

4.匿名管道通信中的4种情况

5.匿名管道通信的特征

三、进程池

四、命名管道通信

1.命名管道原理

 2.命名管道通信代码

3.命名管道文件的进程同步机制

4.命名管道中的情况

五、System V 共享内存 

1.共享内存的原理

2.共享内存通信代码

3.共享内存通信优缺点

4.为共享内存添加保护机制

5.最终代码


一、进程通信介绍

1.进程通信的目的

数据传输:一个进程需要将它的数据发送给另一个进程

资源共享:多个进程之间共享同样的资源

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

2.进程通信的方式

进程通信的前提是能让不同的进程看到操作系统中的同一份资源,但是为了保证进程之间的独立性(各进程不能访问其他进程的地址空间),由操作系统创建一份共享资源。

进程需要通信,就需要让操作系统创建共享资源,所以操作系统必须提供给进程不同的调用接口,用于创建不同的共享资源,实现不同种类的进程通信。

进程通信有两种通信标准:System V标准和POSIX标准(都是本地通信)

System V标准中有三种通信方式:消息队列、共享内存、信号量

但是System V标准需要重新构建操作系统代码来实现进程通信,比较繁琐。在System V标准出现之前,还有一种通信方式是管道通信,管道通信是直接复用现有操作系统的代码。

(现在本地通信已经被网络通信取代,所以本文只重点介绍管道通信和共享内存通信)

二、匿名管道通信

1.匿名管道通信的基本原理

父进程打开文件时,其内核数据结构task_struct中有一个files指针指向文件描述符表files_struct,文件描述符表中的指针数组fd_array存放着被打开文件的结构体指针file*,每个被打开的文件在内核中都有一个描述其信息的file对象,存放了文件的inode号、操作方法集合、内核级文件缓冲区等。子进程继承父进程的文件时,会额外创建文件描述符表,但是并不会额外创建被打开文件的file对象,而是子进程文件描述符表中file*指针直接指向父进程打开文件的file对象。

当父进程创建子进程时,子进程会继承父进程的文件描述符表,即继承了父进程打开的文件,所以子进程能看到父进程打开的文件,父子进程也是在同一个终端文件中输入输出。

所有进程都是bash进程的子进程,所以只要bash进程打开三个标准输入输出0、1、2,所有的子进程都会默认打开。如果子进程关闭了某个文件,并不会影响父进程,因为每个被打开的文件有一个内存级的引用计数file->ref_count,表示有多少个进程指向该文件,只有当引用计数归0时,文件才会被关闭,释放文件资源。

进程可以通过读/写的方式打开同一个文件,操作系统会创建两个不同的文件对象file,但是文件对象file中的内核级缓冲区、操作方法集合等并不会额外创建,而是一个文件的文件对象的内核级缓冲区、操作方法集合等通过指针直接指向另一个文件的内核级缓冲区、操作方法集合等。这样以读方式打开的文件和以写方式打开的文件共用一个内核级缓冲区。

进程通信的前提是不同进程看到同一份共享资源,所以根据上述原理,父子进程可以看到同一份共享资源:被打开文件的内核级缓冲区。父进程向被打开文件的内核级缓冲区写入,子进程从被打开文件的内核级缓冲区读取,这样就实现了进程通信!这里也将被打开文件的内核级缓冲区称为管道文件。

此外,管道通信只支持单向通信,即只允许父进程传输数据给子进程,或者子进程传输数据给父进程。当父进程要传输数据给子进程时,就可以只使用以写方式打开的文件的管道文件,关闭以读方式打开的文件,同样的,子进程只是用以读方式打开的文件的管道文件,关闭掉以写方式打开的文件。父进程向以写方式打开的文件的管道文件写入,子进程再从以读方式打开的文件的管道文件读取,从而实现管道通信。如果是要子进程向父进程传输数据,同理即可。

2.匿名管道通信原理的细节补充

(1)父子进程既然要关闭不需要的文件fd,那为什么还要打开呢?

因为需要让子进程继承父进程的读方式和写方式打开的文件。例如父进程只有读方式打开的文件,那么子进程也只能继承到读方式打开的文件,无法实现进程通信。(为什么不让父进程使用读写方式打开文件呢?因为子进程继承后也可以对文件进行读写,例如父进程向子进程通信,父进程应当向管道文件写入,子进程应当从管道文件读取,但是子进程也可以写入,容易发生误写)

(2)父子进程可以不关闭不需要的文件fd吗?

可以不关闭!但是建议关闭!一方面因为一个进程可以打开的文件是有限的,打开一个不使用的文件浪费资源;另一方面还是容易产生误写:父进程向子进程通信,子进程只需要使用读方式打开的文件,但是子进程保留着写方式打开的文件,就容易误写。

(3)父子进程通信时,向内核级缓冲区(管道文件)写入时,操作系统会自动刷新缓冲区到磁盘文件,如何解决?

管道通信单独设计了一个系统调用接口pipe,其底层就是open,但是打开文件时不需要指定文件名和路径(所以也叫匿名管道),返回值是一个输出型参数数组,数组中的两个元素分别是fd[0]以读方式、fd[1]写方式打开的文件的文件描述符。用这种方式打开文件,不会刷新文件的内核级缓冲区到磁盘中。

int pipe(int pipefd[2])

(4)为什么管道只支持单向通信?

因为管道是复用操作系统内核代码,初心是为了简单,复用的操作系统内核代码仅支持单向通信。

3.匿名管道通信代码

匿名管道通信(此处为子进程向父进程单向通信)就是使用pipe系统调用接口打开文件并创建子进程,子进程向管道文件中写入,父进程从管道文件中读取。

#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
using namespace std;
const int size=1024;
string GetOtherMessage()
{static int cnt=0;//计数器string messageid=to_string(cnt);++cnt;pid_t self_id=getpid();string stringpid=to_string(self_id);string msg="messageid: ";msg+=messageid;msg+="my pid is : ";msg+=stringpid;return msg;
}
//子进程向管道文件写入
void SonProcessWrite(int wfd)
{string msg="father,i am your son process!";string info=msg+GetOtherMessage();write(wfd,info.c_str(),info.size());
}
//父进程从管道文件读取
void FatherProcessRead(int rfd)
{char inbuffer[size];ssize_t n=read(rfd,inbuffer,sizeof(inbuffer)-1);if(n>0)//读取成功{inbuffer[n]=0;cout<<"father get message is: "<<inbuffer<<endl;}else if(n==0)//写入端文件已关闭,读取到管道文件末尾{cout<<"client quit, father get return val: "<<n<<"father quit too!"<<endl;}else if(n<0)//读取失败{cerr<<"read error"<<endl;}
}
int main()
{//创建管道int pipefd[2];//输出型参数rfd、wfdint n=pipe(pipefd);if(n!=0){cout<<"errno: "<<errno<<": "<<"errstring :"<<strerror(errno)<<endl;return 1;}cout<<"pipefd[0]: "<<pipefd[0]<<", pipefd[1]: "<<pipefd[1]<<endl; //创建子进程pid_t id=fork();if(id==0){//子进程关闭掉读方式打开的文件close(pipefd[0]);//子进程向管道文件写入SonProcessWrite(pipefd[1]);close(pipefd[1]);exit(0);}//父进程关闭写方式打开的文件close(pipefd[1]);//父进程从管道文件读取FatherProcessRead(pipefd[0]);close(0);return 0;
}
4.匿名管道通信中的4种情况

1.管道内部是空的但是wfd文件并没有关闭,读条件不具备,读进程会被阻塞

2.管道文件大小是有限的,管道被写满,写条件不具备,写进程会被阻塞

3.wfd文件关闭没有写入,此时如果rfd文件一直在读取管道文件,rfd文件会已知读取到管道文件末尾,返回值为0

4.rfd文件被关闭,但是写端wfd仍然在写入,写端进程会被操作系统直接使用13号信号杀掉。

5.匿名管道通信的特征

1.匿名管道仅支持具有“血缘关系”的进程通信,例如父子进程、爷孙进程

2.管道内部自带进程同步机制:例如子进程在向管道文件中写入时,父进程会等待子进程全部写完再进行读取。而不是像普通的父子进程一样各自执行自己的代码。

3.管道文件的生命周期是跟随进程的

4.管道文件通信时是面向字节流的,即向管道写入数据的次数和从管道读取数据的次数不是一一匹配的

5.管道的通信模式是一种特殊的半双工模式(同一时间只允许一端写数据或者另一端读数据)

三、进程池

父进程创建多个子进程,并为每个子进程创建一个管道文件,父进程为写端,子进程为读端。父进程给子进程通过管道传输任务,这就是进程池。

如果父进程没有给子进程传输任务,即管道文件中没有数据,根据进程通信情况1,读端即子进程会阻塞等待父进程传输任务。

此外父进程还要给子进程平衡任务,不能让某个进程特别繁忙,其他进程没有任务可做。这就是负载均衡

四、命名管道通信

1.命名管道原理

匿名管道用于解决具有血缘关系的进程通信,而命名管道用于解决没有血缘关系的进程通信

已知匿名管道原理:父进程打开的文件被子进程继承,父子进程可以看到同一份资源,即文件内核缓冲区(管道文件)

没有血缘关系的两个进程打开同一个文件,操作系统会为文件创建两个文件对象file,并将其添加到两个进程各自的文件描述符表中。同样的,对于打开的同一份文件,其文件对象中的文件内核缓冲区都是相同的,操作系统没有必要创建重复且浪费资源的事情,因此两个进程打开的同一份文件的两个文件对象file共用一个文件内核缓冲区。

但问题是如何确保两个毫不相关的进程打开同一份文件呢?对于父子进程,子进程通过继承父进程打开的文件来确保打开的是同一份文件。对于两个毫不相关的进程可以通过文件的唯一性路径来确保打开的是同一份文件,由于带上了文件路径,所以就叫有名管道。

Linux系统中,使用mkfifo命令创建有名管道文件,再使用两个进程打开即可

mkfifo myfifo

Linux系统编程中使用mkfifo函数创建一个管道文件,再让两个不相关的进程打开: 

mkfifo函数原型:pathname 是管道文件的路径名,mode 是设置管道的权限。函数成功时返回0,失败时返回-1,并设置errno以指示错误原因。

int mkfifo(const char *pathname, mode_t mode);

unlink函数用于删除文件系统中的一个文件

unlink函数原型:pathname 是管道文件的路径名。函数成功时返回0,失败时返回-1,并设置errno以指示错误原因。

int unlink(const char *pathname);
 2.命名管道通信代码

一共有三个文件:namedPipe.hpp、client.cc、server.cc

namedPipe.hpp文件中是命名管道类的实现

client.cc文件是客户端,用作写端进程

server.cc文件是服务端,用作读端进程

namedPipe.hpp:

//namedPipe.hpp
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <cerrno>
#include <unistd.h>
#include <fcntl.h>
#define Creater 1//进程身份为1就可以创建管道文件
#define User 2//进程身份为2只能使用管道文件
#define DefaultFd -1//文件描述符初始值
#define Read O_RDONLY//读方式打开文件
#define Write O_WRONLY//写方式打开文件
#define BaseSize 4096//通信数据大小
using namespace std;
const string comm_path="./myfifo";//设置全局路径为当前路径,使得两个进程都能看见
//有名管道文件类
class NamePiped
{
public:NamePiped(const string& path, int who)//构造函数:_fifo_path(path),_id(who),_fd(DefaultFd){if(_id==Creater)//只有管道文件管理者才可以创建管道文件{int res=mkfifo(_fifo_path.c_str(),0666);//创建管道文件if(res==-1)//创建失败perror("mkfifo");cout<<"Creater create named pipe"<<endl;}}bool OpenForRead()//读方式打开文件{return OpenNamedPipe(Read);}bool OpenForWrite()//写方式打开文件{return OpenNamedPipe(Write);}//输入型参数:const &://输出型参数:*//输入输出型参数:&int ReadNamedPipe(string *out)//读取管道文件{   char buffer[BaseSize];int n=read(_fd,buffer,sizeof(buffer));//将数据读取到buffer中if(n>0){buffer[n]=0;//读取成功返回的n就是读取到的字节数,将末尾设置为'\0'*out=buffer;//将读取到的数据传给输出型参数out}return n;//读取失败返回-1,读取成功返回读取到的字节数,所以可以通过是否大于0判定是否读取成功}int WriteNamePipe(const string& in)//写入管道文件{return write(_fd,in.c_str(),in.size());//将数据in写入管道文件中}~NamePiped()//析构函数{if(_id==Creater)//只有管道文件管理者才可以删除文件{int res=unlink(_fifo_path.c_str());//删除管道文件if(res==-1)//删除失败perror("unlink");cout<<"Creater free named pipe"<<endl;}if(_fd!=DefaultFd) close(_fd);    }
private:bool OpenNamedPipe(int mode)//打开管道文件,但是不能让进程手动传入打开方式,所以要再次封装{_fd=open(_fifo_path.c_str(),mode);if(_fd<0) return false;//打开管道文件失败return true;}
private:const string _fifo_path;//管道文件路径int _id;//当前使用管道文件的进程int _fd;//文件描述符
};

 client.cc:

//client.cc
#include "namedPipe.hpp"
// client:write
int main()
{NamePiped fifo(comm_path, User);if (fifo.OpenForWrite()) // 写入端,写入成功{while (true)//一直写入{cout << "Please Enter> ";string message;getline(cin, message);       // 将输入的数据传到message中fifo.WriteNamePipe(message); // 将message数据写入管道文件中}}return 0;
}

 server.cc:

//server.cc
#include "namedPipe.hpp"
// server:read且管理管道文件的整个生命周期(创建和删除)
int main()
{NamePiped fifo(comm_path, Creater);if (fifo.OpenForRead()) // 读取端,读取成功{while (true)//一直读取{string message;int n = fifo.ReadNamedPipe(&message);if (n > 0) // 读取成功{cout << "Client say: " << message << endl;}else if(n==0)//如果写端关闭,读端就会读到文件末尾返回0{cout<<"Client quit! Server quit too!"<<endl;break;}else//读取错误{cout<<"fifo.ReadNamedPipe Error"<<endl;break;//读取错误即退出}}}return 0;
}

运行效果: 客户端向服务端传输数据,当客户端(写端)退出时,服务端(读端)也要推出

3.命名管道文件的进程同步机制

注意:在有名管道通信中,如果读端打开文件,但是写端还没打开文件,读端进程会阻塞在open调用中,直到写端进程打开文件。这是一种变相的进程同步机制。

验证代码: 在server进程打开管道文件前加上输出语句"server open named pipe done",在client进程打开管道文件前加上输出语句"client open named pipe done"。在写端进程client运行前,读端进程并不会输出语句,因为其被阻塞在open中了;此时如果写端进程运行,读端进程才会输出语句。

//server.cc
#include "namedPipe.hpp"
// server:read且管理管道文件的整个生命周期(创建和删除)
int main()
{NamePiped fifo(comm_path, Creater);if (fifo.OpenForRead()) // 读取端,读取成功{cout<<"server open named pipe done"<<endl;while (true)//一直读取{string message;int n = fifo.ReadNamedPipe(&message);if (n > 0) // 读取成功{cout << "Client say: " << message << endl;}else if(n==0)//如果写端关闭,读端就会读到文件末尾返回0{cout<<"Client quit! Server quit too!"<<endl;break;}else//读取错误{cout<<"fifo.ReadNamedPipe Error"<<endl;break;//读取错误即退出}}}return 0;
}
//client.cc
#include "namedPipe.hpp"
// client:write
int main()
{NamePiped fifo(comm_path, User);if (fifo.OpenForWrite()) // 写入端,写入成功{cout<<"client open named pipe done"<<endl;while (true)//一直写入{cout << "Please Enter> ";string message;getline(cin, message);       // 将输入的数据传到message中fifo.WriteNamePipe(message); // 将message数据写入管道文件中}}return 0;
}

4.命名管道中的情况

 和匿名管道中的3、4点一样

3.wfd文件关闭没有写入,此时如果rfd文件一直在读取管道文件,rfd文件会已知读取到管道文件末尾,返回值为0

4.rfd文件被关闭,但是写端wfd仍然在写入,写端进程会被操作系统直接使用13号信号杀掉。

五、System V 共享内存 

共享内存是System V进程通信中的一种方式,是本地通信

1.共享内存的原理

进程在进行动态库加载时,动态库会通过页表映射到进程地址空间的共享区中。如果有多个进程要加载同一个动态库,动态库加载到内存后会被这些进程共同使用。

所以根据动态库加载的原理,操作系统可以在内存中创建一个共享内存空间,再通过页表映射到两个进程的共享区中,这样两个进程就可以看到同一份资源了。

操作系统可以为进程通信创造通信条件,但是什么时候通信是取决于进程,所以操作系统必须提供共享内存通信相应的系统调用接口。 

操作系统中会有多个进程使用共享内存通信,所以会有多个共享内存空间,这就需要操作系统统一管理这些共享内存空间。因此就有了内核数据结构struct Shm来管理共享内存通信。

2.共享内存通信代码

系统调用接口介绍:

(1) int shmget(key_t key, size_t size, int shmflg)  #include <sys/ipc.h>  #include <sys/shm.h>  

用于创建获取共享内存,key_t key是一个键值,用于唯一标识共享内存段,由用户自己给值确定,通常使用ftok函数随机定值;size_t size是需要分配的共享内存段的大小(以字节为单位);int shmflg是标志位,用于控制共享内存段的创建和访问权限。

常见标志位:IPC_CREAT 如果要获取的共享内存不存在则创建,如果存在则获取并返回(主要用于获取共享内存)

                      IPC_EXCL 单独使用无意义

                      IPC_CREAT|IPC_EXCL 如果获取的共享内存不存在则创建,如果存在则报错(主要用于创建共享内存)

返回值:成功返回共享内存标识符(非负整数);失败返回-1,并设置errno来指示错误

(2) key_t ftok(const char *pathname, int proj_id)  #include<sys/types.h>

用于根据文件的属性(如inode编号)生成一个唯一的键值,const char *pathname是文件路径名,指向系统中的一个现有文件或目录(任意路径,通信进程使用相同的路径即可);int proj_id是项目标识符,通常为一个字符或整数(任意字符或整数)。即相同路径和项目标识符生成的唯一键值是相同的。

返回值:成功返回key唯一键值;失败返回-1,并设置errno来指示错误

(3)系统命令 ipcs -m查看已存在的共享内存

 (4)系统命令 ipcrm -m 共享内存标识符 删除指定的共享内存、

共享内存和文件不同,不会随着进程结束而结束,而是一直在内存中,只能手动删除

区分共享内存唯一键值key和标识符shmid:

唯一键值key是提供给操作系统用来创建共享内存的,操作系统创建好共享内存后返回的共享内存标识符shmid是用来给用户管理共享内存的

(5)int shmctl(int shmid, int cmd, struct shmid_ds *buf) #include <sys/ipc.h>#include <sys/shm.h> 

删除指定的共享内存,int shmid是指定的共享内存标识符,int cmd是要操作的命令,struct shmid_ds *buf是这是一个指向 shmid_ds 结构的指针,该结构包含了共享内存段的详细信息。根据 cmd 的值,这个参数可以是输入(用于 IPC_SET)或输出(用于 IPC_STAT)的。

删除cmd用到的命令是IPC_RMID

IPC_RMID:立即删除共享内存段。这个操作只能由共享内存的创建者或拥有适当权限的进程执行。

(6)void *shmat(int shmid, const void *shmaddr, int shmflg)

#include <sys/types.h>  #include <sys/shm.h> 

shmat函数在Linux系统中用于将共享内存连接到当前进程的地址空间中。int sgmid是共享内存的标识符;const void *shmaddr是指定共享内存连接到当前进程的地址空间的起始地址,如果这个参数为NULL,系统会自动选择一个合适的地址;int shmflg是指定连接共享内存的权限标志,常用的权限标志有SHM_RDONLY(只读连接),其他情况默认为读写模式(参数传0)。

返回值:成功返回共享内存的起始地址;失败返回-1,并将错误原因存于errno中

(7)int shmdt(const void *shmaddr)  #include <sys/shm.h>

shmdt函数在Linux系统中用于将进程和共享内存断开连接。const void *shmaddr是当前进程地址空间中共享内存段的起始地址。

返回值:成功时返回 0;失败时返回 -1,并设置相应的 errno

Shm.hpp:

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string>
#include <cerrno>
#include <unistd.h>
#include <cstring>
#include <memory.h>
using namespace std;
const string gpathname = "/home/ubuntu/241018"; // 指定路径,用于生成共享内存的唯一键值
const int gproj_id = 0x66;                      // 指定项目标识符,用于生成共享内存的唯一键值
const int gShmSize=4096;//共享内存大小
const int gCreater = 1;                         // 身份标识:创建管理者
const int gUser = 2;                            // 身份标识:使用者
class Shm
{
public:// 构造函数Shm(const string &pathname, int proj, int who): _pathname(pathname), _proj_id(proj),_who(who),_addrshm(nullptr){_key=GetCommKey();if(_who==gCreater) GetShmUseCreate();//服务端创建共享内存else if(_who==gUser) GetShmForUse();//客户端使用共享内存_addrshm=AttachShm();//构造时,即将进程连接共享内存cout<<"shmid: "<<_shmid<<endl;cout<<"key: "<<ToHex(_key)<<endl;}// 析构函数~Shm() {if(_who==gCreater){int res=shmctl(_shmid,IPC_RMID,nullptr);//删除共享内存}cout<<"shm remove done..."<<endl;}// 十进制转十六进制string ToHex(key_t key){char buffer[128];                               // 缓冲区,字符串拼接的内容放到缓冲区中snprintf(buffer, sizeof(buffer), "0x%x", key); // 十进制转十六进制,再和0x拼接return buffer;}//服务端创建共享内存bool GetShmUseCreate(){if(_who==gCreater){_shmid=GetShmHelper(_key,gShmSize,IPC_CREAT|IPC_EXCL|0666);//创建共享内存if(_shmid>=0) return true;cout<<"shm create done"<<endl;}return false;//客户端不允许创建共享内存}//客户端获取共享内存bool GetShmForUse(){if(_who==gUser){_shmid=GetShmHelper(_key,gShmSize,IPC_CREAT|0666);//获取共享内存if(_shmid>=0) return true;cout<<"shm get done"<<endl;}return false;//服务端不允许调用此接口}//共享内存连接进程void* AttachShm(){if(_addrshm!=nullptr) DetachShm(_addrshm);//每次连接之前,进程与共享内存断连void* shmaddr=shmat(_shmid,nullptr,0);//共享内存读写方式连接进程if(shmaddr==nullptr) perror("shmat");//连接失败cout<<"who: "<<RoleToString(_who)<<" attach shm..."<<endl;return shmaddr;//连接成功返回共享内存起始地址}//共享内存与进程断连void DetachShm(void * shmaddr){if(shmaddr==nullptr) return;//共享内存为空即退出shmdt(shmaddr);//断连cout<<"who: "<<RoleToString(_who)<<" detach shm..."<<endl;}//获取共享内存起始地址void* Addr(){return _addrshm;}//清空共享内存中的数据void Zero(){if(_addrshm)memset(_addrshm,0,gShmSize);//全部设置为0}
private:// 随机生成共享内存唯一键值(私有成员,不需要提供接口)key_t GetCommKey(){key_t k = ftok(_pathname.c_str(), _proj_id); // 使用ftok函数随机生成共享内存唯一键值if (k < 0)perror("ftok"); // 生成失败return k;           // 返回生成的共享内存唯一键值}// 创建获取共享内存(私有成员,不需要提供接口)int GetShmHelper(key_t key, int size, int flag){int shmid = shmget(key, size, flag); // 使用shmget函数创建共享内存if (shmid < 0)perror("shmget"); // 创建共享内存失败return shmid;         // 返回共享内存标识符}//角色转换string RoleToString(int who){if(who==gCreater) return "Creater";else if(who==gUser) return "gUser";else return "None";}private:key_t _key;       // 共享内存唯一键值int _shmid;       // 共享内存标识符string _pathname; // 指定路径,用于生成共享内存的唯一键值int _proj_id;     // 指定项目标识符,用于生成共享内存的唯一键值int _who;         // 共享内存身份标识,服务端用于创建并管理共享内存,客户端仅限于使用void* _addrshm;//共享内存的起始地址
};

server.cc: 

#include "Shm.hpp"
#include "namedPipe.hpp"
int main()
{//1.创建共享内存Shm shm(gpathname,gproj_id,gCreater);//服务端创建共享内存char* shmaddr=(char*)shm.Addr();//获取共享内存起始地址//2.创建管道//NamePiped fifo(comm_path,Creater);//fifo.OpenForRead();//读方式打开管道//进程通信//服务端从共享内存中读数据while(true){string temp;                //fifo.ReadNamedPipe(&temp);//读到什么不重要,关键是如果没读到数据就会阻塞等待。//只有当服务端从管道中读到数据了,说明客户端已经完成写入了,才会开始读取共享内存的数据cout<<"shm memory content: "<<shmaddr<<endl;sleep(1);}return 0;
}

client.cc: 

#include "Shm.hpp"
#include "namedPipe.hpp"
int main()
{//1.创建共享内存Shm shm(gpathname,gproj_id,gUser);shm.Zero();//获取共享内存后,先将其内容清零char* shmaddr=(char*)shm.Addr();//获取共享内存起始地址//2.创建管道//NamePiped fifo(comm_path,User);//fifo.OpenForWrite();//写方式打开管道//进程通信//客户端向共享内存中写数据char ch='A';while(ch<='Z'){shmaddr[ch-'A']=ch;string temp="anything";//fifo.WriteNamePipe(temp);//客户端完成一次写入,向管道中写入数据,告诉服务端自己完成了一次写入++ch;sleep(2);}return 0;
}
3.共享内存通信优缺点

缺点:共享内存没有任何保护机制,客户端向共享内存中写数据时,还没有写完,服务端就会从共享内存中读取数据,导致数据不一致问题。如下所示,服务端总是读出相同的数据,其实是客户端只完成了一次写入,服务端就已经读了两次。

优点:访问共享内存时没有任何系统调用,因为共享内存被映射到了进程地址空间的共享区,其是用户级别的,不需要将数据拷贝到管道中再拷贝回进程地址空间中,大大减少了拷贝次数,所以共享内存通信速度最快。

4.为共享内存添加保护机制

原理:两个进程通信时,同时为其创建共享内存和管道,利用管道中的进程同步机制来保护共享内存。服务端通过判断是否能从管道中读取到数据,确定客户端是否写入完成。客户端通过在写入完成后向管道中写入数据,来告知服务端自己完成了一次写入。

添加保护机制后如下,不会出现客户端还没有完成一次写入,服务端就开始读数据

server.cc:

#include "Shm.hpp"
#include "namedPipe.hpp"
int main()
{//1.创建共享内存Shm shm(gpathname,gproj_id,gCreater);//服务端创建共享内存char* shmaddr=(char*)shm.Addr();//获取共享内存起始地址//2.创建管道NamePiped fifo(comm_path,Creater);fifo.OpenForRead();//读方式打开管道//进程通信//服务端从共享内存中读数据while(true){string temp;                fifo.ReadNamedPipe(&temp);//读到什么不重要,关键是如果没读到数据就会阻塞等待。//只有当服务端从管道中读到数据了,说明客户端已经完成写入了,才会开始读取共享内存的数据cout<<"shm memory content: "<<shmaddr<<endl;sleep(1);}return 0;
}

client.cc:

#include "Shm.hpp"
#include "namedPipe.hpp"
int main()
{//1.创建共享内存Shm shm(gpathname,gproj_id,gUser);shm.Zero();//获取共享内存后,先将其内容清零char* shmaddr=(char*)shm.Addr();//获取共享内存起始地址//2.创建管道NamePiped fifo(comm_path,User);fifo.OpenForWrite();//写方式打开管道//进程通信//客户端向共享内存中写数据char ch='A';while(ch<='Z'){shmaddr[ch-'A']=ch;string temp="anything";fifo.WriteNamePipe(temp);//客户端完成一次写入,向管道中写入数据,告诉服务端自己完成了一次写入++ch;sleep(2);}return 0;
}
5.最终代码

最终代码即为添加了保护机制的共享内存通信


http://www.mrgr.cn/news/55326.html

相关文章:

  • C++面向对象编程学习
  • Python 自编码器(Autoencoder)算法详解与应用案例
  • 数据治理(1)-数据规划
  • Ubuntu下解决python程序首次连接mysql拒绝访问之总结
  • 1.2电子商务安全内涵
  • 如何提高外贸网站在谷歌的收录速度?
  • repo 命令大全详解(第二十二篇 repo upload)
  • Adobe的反击,传统大厂全面AI化,正面激战OpenAI!
  • TF卡长期不用会丢失数据吗?TF卡数据恢复容易吗?
  • Stable Diffusion模型资源合集(附整合包)
  • 【小白学机器学习19】什么是量化分析(草稿)
  • CRM系统有哪些功能
  • 皮具发霉怎么处理 发霉的原因分析及防霉方案
  • ThisisaTestforBehavior
  • 浅谈高标准农田智慧设备之土壤墒情监测站
  • 【中草药识别系统】Python+卷积神经网络算法+深度学习项目+人工智能项目+TensorFlow+图像识别+Django网页界面+CNN算法
  • 【多类别分类中的准确率召回率平均策略】
  • 论文研读 | End-to-End Object Detection with Transformers
  • UE(其他)
  • 基于STM32的宿舍防火防盗系统设计
  • 双十一购物节有哪些好物值得入手?2024双十一好物清单合集分享
  • ‌‌现货白银价格怎么算出来的?
  • AI Weekly2:过去一周重要的AI资讯汇总
  • MySql数据库left join中添加子查询
  • JVM参数
  • 终于把Vision Transformer(ViT)搞懂了!