一、共享内存原理:
通过在物理内存申请一段共享内存空间,这个空间可以被任何进程看见。然后将内存空间映射到想要以此进行通信的进程的进程地址空间中的共享区(堆栈之间),用户就可以直接使用这个虚拟起始地址进行数据传输和接收了。
二、共享内存内核数据结构:
当创建完共享内存后,OS为了管理这些共享内存,会创建内核数据结构来管理这些共享内存,每个结构体里面存放的是共享内存段的属性信息,类似于文件系统中的inode。
共享内存=共享内存内核数据结构(伪代码:struct shm)+真正开辟的内存空间
三、函数方法
1.shmget
//头文件
#include <sys/ipc.h>
#include <sys/shm.h>
//方法
static int shmHelper(key_t key,int size,int flag)
//static修饰的全局变量或函数,作用域被限定在当前文件内;
{
int shmid=shmget(key,size,flag);
if(shmid==-1)
{
cout<<"errno :"<<errno<<"| error : "<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
//创建共享内存
int createSharememory(key_t key,int size)
{
return shmHelper(key,size,IPC_CREAT|IPC_EXCL|0666);//注意在创建的时候把读写权限0666带上,否则后面如果要读取shmid_ds数据结构的属性就读不到了。
}
//获取共享内存
int getSharememory(key_t key,int size)
{
return shmHelper(key,size,IPC_CREAT);
}
描述:
翻译:
shmget() 返回与参数键的key值关联的系统 V 共享内存段的标识符。 一个新的共享内存段,大小等于PAGE_SIZE的整数倍。
如果 shmflg 同时指定了 IPC_CREAT 和 IPC_EXCL,并且密钥的共享内存段已经存在,则 shmget() 失败,errno 设置为 EEXIST。 如果shmflg只指定了IPC_CREAT,如果已经存在这段共享内存,那么就返回已存在的标识符,否则就创建一个共享内存段。
参数解析:
<1>
key_t key就是一个整型数据,相当于一个钥匙,它是进程创建共享内存的钥匙,也是其它想与之通信的进程的钥匙。A进程设置好key具体是什么,并且使用shmget(key,size,IPC_CREAT | IPC_EXCL)创建完成共享内存之后,B进程就可以使用shmget(key,size,IPC_CREAT)获取共享内存标识符。
key其实是可以随意设置的,但是我们一般使用ftok函数。使用路径名加上项目id可以创建出一个冲突概率非常低的key值。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
#define PATHNAME "."//当前路径
#define PROJID 0x666//随意proj_id
//封装好的getKey函数
key_t getKey()
{
key_t key=ftok(PATHNAME,PROJID);
if(key==-1)
{
//On success, the generated key_t value is returned. On failure -1 is returned, with errno indicating the error as for the stat(2) system call.
cout<<"errno :"<<errno<<"| error : "<<strerror(errno)<<endl;
exit(1);
}
return key;
}
共享内存的内核数据结构里面包含有key,所以其他进程才能通过key找到相应的共享内存,并得到共享内存的shmid;
<2>
size_t size就是想要申请的共享内存块的大小,单位是字节。但实际上操作系统会根据用户想要申请的字节数判断这段共享内存给多大。因为对于操作系统来说单位是PAGE_SIZE,也就是4KB,4096字节,一个数据块的大小。所以共享内存大小就是PAGE_SIZE的整数倍。但是如果你使用共享内存的时候,超过你所申请的字节数,操作系统会报错。
<3>
shmflg就是权限标志。如果 shmflg 同时指定了 IPC_CREAT 和 IPC_EXCL,并且密钥的共享内存段已经存在,则 shmget() 失败,errno 设置为 EEXIST。 如果shmflg只指定了IPC_CREAT,如果已经存在这段共享内存,那么就返回已存在的标识符,否则就创建一个共享内存段。
返回值:
如果成功返回一个共享内存标识符,失败就返回-1;
2.shmat
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
该函数意为把申请好的共享内存和进程挂接起来,就是把共享内存的物理地址通过页表映射到进程地址空间中的共享区,可以让进程直接访问到共享内存,最后返回虚拟地址。这里类似于malloc函数,只不过malloc函数是在真实使用的时候才分配物理内存,申请的时候只是给了虚拟地址罢了,等到使用的时候通过页表将物理内存和虚拟地址映射起来。而共享内存的挂接是之前就已经有真实的物理内存了。
参数解析:
const void* shamaddr是让我们设置虚拟地址,这里我们交给操作系统就好,设为nullptr即可。
shmflg设为0就行,就是读写权限。
返回值:
RETURN VALUE
On success shmat() returns the address of the attached shared memory segment; on error (void *) -1 is returned, and errno is set to indicate the cause of the error.
dest情况解释:
当创建共享内存的A进程把共享内存删除了,而之前关联共享内存的B进程还在的话,status就会呈现下图中dest这样的情况。我们要尽量避免这样的情况(虽然等所有关联该共享内存的进程自动退出也可以),在共享内存被删除之前,我们就要取消所有进程的关联--->shmdt函数。
所以,共享内存只有在当前映射连接数为0时才会被删除释放。
代码:
//把返回的虚拟地址由void*强转成char*,方便我们后续使用,来IOcahr类型的数据;
char* start=(char*)shmat(shmid,nullptr,0);
3.shmdt(取消关联)
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数解析:
shmaddr是共享内存映射在进程地址空间的虚拟地址,只需要用这个虚拟地址,就可以取消页表对共享内存空间的映射,从而达到该进程取消关联共享内存的目的。
返回值:
On success shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.
4.shmctl
<1>命令:
ipcrm -m shmid
<2>系统调用
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数解析:
<1>
shmid就是所创建的共享内存标识符。
<2>
cmd:图中红色标记意为如果要使用IPC_STAT,拷贝属性信息到自己定义的输出型参数strcut shmid_ds结构体中,必须要有对共享内存的读权限。那么只要在创建的时候设置读权限就好了
经过如下代码:
struct shmid_ds ds;//在这里相当于一个输出型参数
int n=shmctl(shmid,IPC_STAT,&ds);
if(n!=-1)
{
cout<<"创建进程的pid : "<<ds.shm_cpid<<endl;
cout<<"共享内存的大小 : "<<ds.shm_segsz<<endl;
cout<<"本进程的pid : "<<getpid()<<endl;
}
static int shmHelper(key_t key,int size,int flag)
{
int shmid=shmget(key,size,flag);
if(shmid==-1)
{
cout<<"errno :"<<errno<<"| error : "<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
//创建共享内存
int createSharememory(key_t key,int size)
{
umask(0);
//创建共享内存时设置666权限;
return shmHelper(key,size,IPC_CREAT|IPC_EXCL|0666);
}
结果就变成下图所示了:
下图就是man手册中的cmd选项:
<3>
struct shmid_ds*buf是指向内核数据结构的指针,这个内核数据结构里面存放的是一些共享内存的属性,比如大小,创建进程的pid等等。
(IPC_RMID)删除结果:
//删除共享内存
void delshm(int shmid)
{
int n=shmctl(shmid,IPC_RMID,nullptr);
assert(n!=-1);
(void)n;
}
那么这里有一个问题,只有起始地址,操作系统怎么知道删除多大空间呢?
解释:因为物理内存里面还有关于共享内存的内核数据结构,里面存有共享内存所有的属性,相当于额外申请出来的空间,既然有了属性,那么不就知道该删除多大了吗。
这就对应了前面所说的共享内存=共享内存内核数据结构+真正开辟的内存空间。
进程退出共享内存并不会被销毁。管道随进程生命周期的结束而结束,但是共享内存却不会,需要我们手动销毁。
再次使用同样的key创建共享内存会出错:因为该共享内存已经存在
我们使用shmid删除以后:
四、训练代码:
comm.hpp:
#ifndef _COMM_HPP_
#define _COMM_HPP_
#include<iostream>
#include<cerrno>
#include<cstring>
#include<cassert>
#include<string>
#include<sys/shm.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<cstdio>
#include<unistd.h>
#include<sys/stat.h>
using namespace std;
#define PATHNAME "."//当前路径
#define PROJID 0x666//随便,不影响
#define M_SIZE 4096//共享内存大小
//枚举
typedef enum Type
{
Client,
Server
}Type;
class shmInit
{
public:
//构造函数
shmInit(Type type)
:_type(type)
{
//获取key
int key=getKey();
if(_type==Server)
{
_shmid=createSharememory(key,M_SIZE);
}
else
{
_shmid=getSharememory(key,M_SIZE);
}
//关联
_start=attachshm(_shmid);
}
//析构函数
~shmInit()
{
//取消关联
detachshm(_start);
if(_type==Server)
{
//删除共享内存
delshm(_shmid);
}
}
char* get_start()
{
return _start;
}
int get_shmid()
{
return _shmid;
}
private:
//static修饰的全能变量或函数,作用域被限定在当前文件内;
static int shmHelper(key_t key,int size,int flag)
{
int shmid=shmget(key,size,flag);
if(shmid==-1)
{
cout<<"errno :"<<errno<<"| error : "<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
//得到key
key_t getKey()
{
key_t key=ftok(PATHNAME,PROJID);
if(key==-1)
{
//On success, the generated key_t value is returned. On failure -1 is returned, with errno indicating the error as for the stat(2) system call.
cout<<"errno :"<<errno<<"| error : "<<strerror(errno)<<endl;
exit(1);
}
return key;
}
//创建共享内存
int createSharememory(key_t key,int size)
{
return shmHelper(key,size,IPC_CREAT|IPC_EXCL|0666);
}
//获取共享内存
int getSharememory(key_t key,int size)
{
return shmHelper(key,size,IPC_CREAT);
}
//关联
char* attachshm(int shmid)
{
char* start=(char*)shmat(shmid,nullptr,0);//把返回的虚拟地址由void*强转成char*,方便我们后续使用,来IOcahr类型的数据;
return start;
}
//去关联
void detachshm(char* start)
{
int n=shmdt(start);//只需要共享内存起始虚拟地址作为参数;
assert(n!=-1);
(void)n;
}
//删除共享内存
void delshm(int shmid)
{
int n=shmctl(shmid,IPC_RMID,nullptr);
assert(n!=-1);
(void)n;
}
private:
char* _start;
Type _type;
int _shmid;
};
#endif
shmclient.cc
#include "comm.hpp"
int main()
{
Type type=Client;
shmInit shm(type);
char *start=shm.get_start();
//通信
int n=0;
while(n<26)
{
start[n]='A'+n;
sleep(1);
n++;
start[n]='\0';
}
return 0;
}
shmserver.cc
#include "comm.hpp"
int main()
{
Type type=Server;
shmInit shm(type);
char *start=shm.get_start();
//通信
int n=26;
while(n>=0)
{
//标准输出
cout<<"Client->Server# "<<start<<endl;
sleep(1);
n--;
}
// struct shmid_ds ds;//在这里相当于一个输出型参数
// int n=shmctl(shmid,IPC_STAT,&ds);
// if(n!=-1)
// {
// cout<<"创建进程的pid : "<<ds.shm_cpid<<endl;
// cout<<"共享内存的大小 : "<<ds.shm_segsz<<endl;
// cout<<"本进程的pid : "<<getpid()<<endl;
// }
return 0;
}
makefile
.PHONY:all
all:shmclient shmserver
shmclient:shmclient.cc
g++ $^ -o $@ -std=c++11
shmserver:shmserver.cc
g++ $^ -o $@ -std=c++11
.PHONY:clean
clean:
rm -rf shmclient shmserver
五、结论
1.共享内存与管道有所不同,如果管道里面没有数据,进程如果想读取管道里面的数据,就会陷入阻塞或直接返回-1(取决于打开文件时候的阻塞和非阻塞)。但当A进程并未输入数据到共享内存里面,而B进程却读取共享内存里面的数据,可能会出现乱码或读取不到任何数据的情况。
2.共享内存效率较高,相比较于管道减少了拷贝次数,原因是共享内存并无缓冲区。
3.共享内存直接通信,管道通过系统调用接口通信。一旦共享内存映射到进程的地址空间,那么该空间直接就被所有进程看到了。
4.共享内存没有保护机制(同步互斥)。