一、什么是共享内存
共享内存就是在物理内存中开辟一片空间,让不同进程通过某种方式能够直接访问该内存块,实现进程间的数据通信
1.1 共享内存原理
谈到共享内存空间,那我们就简述一下进程虚拟地址空间与物理地址空间的关系。
我们都知道进程的虚拟地址空间共有4G大小,那么这4G是不是真实存在于物理地址空间的呢,显然不是的,虚拟地址空间是操作系统为进程抽象出的一个地址空间,虚拟地址并不是一个真实的物理地址。那么又是怎么通过使用虚拟地址来使用物理地址的呢,那就是通过页表映射。
(非原创图片)
共享内存作为存取最快的一种IPC通信方式,原因就是通过映射,进程可以直接访问共享内存,进行数据的读写,不需要在内核空间中转。
让我们来分析一下IPC的几种通信方式:
管道、FIFO或消息队列:完成数据的读写共需要四步
对于写进程:
(1)进程buf -> 内核空间(也就是调用read/write写入缓存区)
(2)内核空间 -> 管道、FIFO或消息队列
对于读进程:
(1)管道、FIFO或消息队列 -> 内核空间
(2)内核空间 -> 进程buf
共享内存:完成数据的读写仅需要两步
对于写进程:
(1)进程 -> 共享内存buf(进程直接对共享内存中的数据进行读写,而不需要在进程间复制)
对于读进程:
(2)共享内存buf -> 进程
总结:
因为共享内存不需要执行read/write等函数进行内核系统调用,减少了数据的拷贝次数,节省了数据传输时间
二、相关函数
2.1 共享内存创建
int shmget(key_t key,size_t size,int shmflg);
通过调用shmget函数在内存中创建一个共享内存块,并生成一个标识符指向这个内存块
(1)第一个参数key:用于标识共享内存的键值,一般使用ftok函数生成。不同进程间使用相同键值来关联同一个共享内存空间。使用特殊常量IPC_PRIVATE作为键值可以保证系统建立一个全新的共享内存块。
(2)第二个参数size:创建共享内存块的大小,因为共享内存是用以页(4kb)为单位,所以实际创建的内存大小会扩大调整为页的整数倍,我们在设置的时候就应该设为页的整数倍;若只获取已存在的共享内存设为0
(3)第三个参数shmflg:权限标志位;设置为IPC_CREAT时,不存在则创建一个共享内存,存在则返回该共享内存标识符;设置为IPC_CREAT | IPC_EXCL时,不存在则创建一个新的共享内存,存在则报错;设置为0,不存在则报错,存在则返回该共享内存标识符
(4)返回值:成功返回一个标识符(为正);失败返回 -1 ,出错结果保存在error中
2.2 绑定共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
绑定共享内存,获得共享内存的首地址
(1)第一个参数:shmget返回的共享内存标识符
(2)第二个参数:指定共享内存存放在进程的哪一地址;指定为NULL,则由内核在进程地址中找一合适位置
(3)第三个参数:对共享内存的操作权限位;SHM_RDONLY,是只读权限;0,表示为可读可写
返回值:成功返回共享内存空间的首地址,用指针指向;失败返回 -1 ,出错结果保存在error中
2.3 解除与共享内存的关联
void *shmdt( const void *shmaddr);
结束进程与共享内存的关联,如果没有调用该函数,进程退出时会自动断开与共享内存的关联,此时共享内存依然存在于内存空间中。
(1)参数:共享内存的起始地址
返回值:成功返回 0;失败返回-1,出错结果保存在error中
2.4 控制共享内存操作
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
(1)第一个参数:共享内存的标识符
(2)第二个参数:对共享内存要采取的操作;IPC_SET,设置共享内存shmid_ds结构体数据;IPC_STAT,获取共享内存shmid_ds结构体数据;IPC_RMID,删除共享内存块
(3)第三个参数:共享内存shmid_ds结构体
三、代码示例
3.1 共享内存的创建
int shmaphore_init(char **shmptr) //这里我将共享内存的创建抽象成一个函数
{
int shmid;
key_t key;
key=ftok(PATH_NAME,PROJ_ID);
if(key < 0)
{
printf("ftok failure:%s\n",strerror(errno));
return -1;
}
printf("key:%d\n",key);
shmid=shmget(key,4096,IPC_CREAT|0660); //如果不存在,则创建一个4kb的共享内存块;如果已存在,则关联
if(shmid < 0)
{
printf("shmget failure:%s\n",strerror(errno));
return -2;
}
printf("shmid:%d\n",shmid);
*shmptr=(char *)shmat(shmid,NULL,0); //不指定共享内存地址,由内核分配,返回共享内存的起始地址
if(*shmptr == (char *)-1)
{
printf("shmat failure:%s\n",strerror(errno));
return -3;
}
memset(*shmptr,0,4096);
return shmid;
}
(1)不同进程通过相同的key值可以关联同一个共享内存,在不同进程间调用ftok函数,只要给定的路径跟ID相同就可以生成相同的key值
(2)注意:对于自定义的shmaphore_init函数,传入的参数为一个初级指针的地址,这里使用到二级指针,注意不要出错
3.2 共享内存解除关联与删除
void shmaphore_exit(int shmid,char *shmptr)
{
shmdt(shmptr); //进程解除对共享内存的关联
if(shmctl(shmid,IPC_RMID,NULL) < 0) //删除共享内存块,释放内存空间
{
printf("shmctl failure:%s\n",strerror(errno));
}
}
注意:进程不再使用共享内存后,虽然进程退出会自动解除与共享内存间的关联,但为了代码的严谨性,我们应主动断开与共享内存的关联。同时,最后一个与之相关联的进程在退出前,一定要将共享内存删除。