目录
1、共享内存原理
2、申请共享内存
2.1 ftok
2.2 测试shmget、ftok
2.3 查看系统下的共享内存
3、关联共享内存
3.1 测试shmat
4、释放共享内存
4.1 测试shmctl
5、实现共享内存通信
6、共享内存的特性
结语
前言:
在Linux下,有一种进程间通信方式(IPC)名为共享内存,他是IPC中通信最快的方式(通信方式为全双工),因为他直接在物理内存上创建一块区域并且映射在进程的地址空间中,使得进程使用共享内存就如同直接使用动态申请的空间,因此通信过程少了内核的系统调用步骤,以至于相比于其他IPC模式速度更快,不过也正是因为在通信时不受内核管辖,导致共享内存不具备同步互斥机制,因此需要用户手动处理同步互斥问题。
但是需要注意的是共享内存虽然使用起来如同动态空间,但是他的底层和动态空间不一样,动态空间具有独立性,只限于单个进程内部的访问,而共享内存允许多个无亲缘进程进行通信,因此他和动态空间是有区别的。
1、共享内存原理
共享内存的目的就是为了进程间通信,而进程间通信的核心观念是让不同的进程看到同一份资源,所以共享内存必须在物理内存上开辟一块空间,并且映射到进程地址空间中的共享区,具体示意图如下:
但是共享内存的申请和malloc申请是不一样的,因为共享内存要面向所有进程,要做到这一点就必须调用系统接口,所以要进行共享内存通信必须调用系统接口。
2、申请共享内存
在物理内存上申请共享内存的接口是shmget,该接口介绍如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
//key是用户给这段共享内存设置的名字,一个key对应一个共享内存
//size表示申请共享内存的大小
//shmflg表示权限设置,常用的有IPC_CREAT和IPC_EXCL
//调用成功返回非负整数表示共享内存的标识码(给系统看的),失败返回-1
着重介绍shmflg:
1、当传递的是IPC_CREAT|IPC_EXCL,表示若以当前key值申请的共享内存不存在,则创建一个并返回新共享内存的标识码。若以当前key值申请的共享内存存在则返回-1,表示申请失败。
2、当传递的是IPC_CREAT,表示若以当前key值申请的共享内存不存在,则创建一个并返回新共享内存的标识码。若以当前key值申请的共享内存存在则返回该共享内存的标识码。
所以使用IPC_CREAT|IPC_EXCL可以判断一个key对应的共享内存是否存在,即key值是否被用过,当我们想用一段新的共享内存则可以使用IPC_CREAT|IPC_EXCL。
key值的作用是判断两个进程的共享内存是否为同一个,两个进程所用的key一样说明他们共用同一个共享内存,反之则否,因此可以理解为key值是用户给一段共享内存起的名字,而shmget返回值是系统给这段共享内存起的名字。
2.1 ftok
shmget需要用到key值,key的类型虽然是key_t,但是也可以传一个int类型的值给到key,只不过这么做会导致潜在的重名风险,并且key的值需要程序员自己维护,于是系统提供了一个接口ftok,他像是一个算法,可以计算并返回一个key_t类型的值,该接口介绍如下:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
//接收一个路径和一个整形
//成功返回一个key_t类型的值,失败返回-1
所以两个进程调用ftok时传参是一样的,那么这两个进程就会获得相同的key值,这样两个进程就能看到同一份资源了,也就完成了通信的前提。
2.2 测试shmget、ftok
先用代码测试上述接口,测试代码如下:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <iostream>
using namespace std;
int main()
{
const char *pathname = "/home/zh";
int proj_id = 12;
int size = 4096;
key_t key = ftok(pathname, proj_id);
if (key < 0)
{
perror("ftok");
return -1;
}
cout << "key值被成功创建,key:" << key << endl;
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL);
if (shmid < 0)
{
perror("shmget");
return -1;
}
cout<<"共享内存标识码被成功创建,shmid:"<<shmid<<endl;
return 0;
}
运行结果:
2.3 查看系统下的共享内存
共享内存不同于动态申请空间,动态空间的生命周期随进程。但是对于共享内存而言,若用户不主动释放共享内存,则共享内存会一直存活在系统中,他的生命周期随内核,即内核重启才会清理这些共享内存,在Linux下用指令ipcs -m查看当下系统的共享内存,测试如下:
并且可以通过指令ipcrm -m shmid删除对应的shmid,测试如下:
3、关联共享内存
上述接口shmget可以申请一块共享内存,但是申请到了不意味着就可以直接使用共享内存进行通信,要进行通信还要关联共享内存,关联共享内存的接口介绍如下:
#include <sys/types.h>
#include <sys/shm.h>
//关联共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
//shmid表示要关联的共享内存标识码
//shmaddr若不为NULL且shmflg不为SHM_RND,表示将共享内存的地址附加到shmaddr处
//shmaddr若为NULL,则该函数的返回值作为共享内存的地址(通常都设为NULL)
//shmflg表示权限设置,通常设为0表示对共享内存可读可写
//调用成功返回指向共享内存的指针,失败返回值(void*)-1
//去关联
int shmdt(const void *shmaddr);
//让调用该函数的进程不再关联该共享内存
//shmaddr表示共享内存的地址
总的来说,调用shmat关联共享内存后,会拿到一个执行该共享内存的指针,通过该指针就可以对共享内存进行读写操作。
3.1 测试shmat
因为申请共享内存的代码后续会被重复使用,为了后续更好的测试,所以对申请共享内存的接口进行再一层封装,封装成sharemem.hpp文件,代码如下:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <iostream>
#include <unistd.h>
using namespace std;
const char *pathname = "/home/zh";
int proj_id = 12;
int size = 4096;
key_t getkey()//封装ftok
{
key_t key = ftok(pathname, proj_id);
if (key < 0)
{
perror("ftok");
exit(-1);
}
cout << "key值被成功创建,key:" << key << endl;
return key;
}
int getshm(int shmflg)//封装shmget
{
key_t key = getkey();
int shmid = shmget(key, size, shmflg);
if (shmid < 0)
{
perror("shmget");
exit(-1);
}
cout << "共享内存标识码被成功创建,shmid:" << shmid << endl;
return shmid;
}
int creatnewshm()//只想用最新的共享内存来进程通信
{
return getshm(IPC_CREAT|IPC_EXCL|0666);//为了能够观察到变化,所以要保证共享内存的权限
}
int getoldshm()//获取一个已经存在的共享内存进行通信
{
return getshm(IPC_CREAT);
}
后续的测试只需要包含该文件即可,测试shmat代码如下:
#include "sharemem.hpp"
int main()
{
int shmid = creatnewshm();
cout<<"申请共享内存成功"<<endl;
sleep(2);//观察nattch的值
char* poi = (char*)shmat(shmid,NULL,0);
cout<<"关联共享内存成功"<<endl;
sleep(2);//观察nattch的值
return 0;
}
运行结果:
其中,右侧nattch表示当前有多少个进程在关联该共享内存,当一个进程关联某个共享内存后,该共享内存的nattch+1,并且当该进程结束后,对应的nattch会-1。当然也可以使用shmdt手动去关联。
4、释放共享内存
手动释放共享内存的接口是shmctl,该接口本质的功能是控制共享内存,只不过也有删除选项,具体介绍如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//shmid表示要释放的共享内存的标识码
//cmd表示该函数执行的具体任务,比如IPC_RMID表示删除任务
//buf表示指向共享内存数据结构的指针,若使用删除任务则该指针置为NULL即可
//调用成功返回0,失败返回-1
4.1 测试shmctl
测试shmctl的代码如下:
#include "sharemem.hpp"
int main()
{
int shmid = creatnewshm();
cout<<"申请共享内存成功"<<endl;
sleep(2);//观察nattch的值
char* poi = (char*)shmat(shmid,NULL,0);
cout<<"关联共享内存成功"<<endl;
sleep(2);//观察nattch的值
shmdt(poi);
cout<<"成功去关联共享内存"<<endl;
sleep(2);//观察nattch的值
shmctl(shmid,IPC_RMID,nullptr);
cout<<"成功删除共享内存"<<endl;
return 0;
}
运行结果:
从结果可以看到,无论是去关联测试还是删除共享内存,在右边的监控中都会显示对应的效果。
5、实现共享内存通信
有了上述的接口以及sharemem.hpp文件,就可以实现两个进程的通信了,所以需要写一个客户端进程和一个服务器进程,其中服务器进程创建共享内存,让他们两都关联该共享内存,并且由客户端向服务器发送消息,服务器代码如下:
#include "sharemem.hpp"
int main()
{
int shmid = creatnewshm();
char *poi = (char *)shmat(shmid, nullptr, 0);
cout << "关联共享内存成功" << endl;
while (true)
{
cout<<"服务器接收:"<<poi<<endl;
sleep(1);
}
shmdt(poi);
shmctl(shmid,IPC_RMID,nullptr);
return 0;
}
客户端代码如下:
#include "sharemem.hpp"
int main()
{
int shmid = getoldshm();
char* poi = (char*)shmat(shmid,nullptr,0);
cout<<"关联共享内存成功"<<endl;
while (true)
{
string message;
cout<<"客户端发送:";
cin>>message;
strcpy(poi,message.c_str());
}
shmdt(poi);
return 0;
}
测试结果:
从结果可以发现,共享内存的通信本质就是对一个空间进行内存式的访问,无需调用read、write这些系统接口,直接用内存函数写入数据至内存对方就能够读取到内存里的数据。
6、共享内存的特性
1、共享内存不需要调用系统接口实现进程间通信,只需要调用内存函数对内存进行读写即可实现进程间通信。
2、共享内存本身没有同步互斥的概念,体现在上面的运行结果中读端会一直读内容(说明没有同步),并不会因为写端还未写而阻塞住。并且读端和写端可以同时访问共享内存(说明没有互斥)。
3、共享内存在读写效率上更为高效,因为少了write和read这些步骤,即少了一层拷贝。
结语
以上就是关于共享内存通信的讲解,共享内存作为IPC的其中一种方式,相对于其他通信方式,他有利有弊,在实际应用里先熟悉他的接口以及使用共享内存的步骤:申请共享内存(包括创建key值)、关联共享内存(shmat)、去关联(shmdt)、释放共享内存(shmctl) ,通过以上步骤可以实现完整的共享内存通信。
最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!