共享内存
共享内存是SystemV标准的一种进程间通信方式,基于SystemV标准的通信方式有共享内存、消息队列和信号量。
基本原理
共享内存的建立
共享内存指的是操作系统在物理内存中申请一块空间,让不同进程的页表都能映射到这一块内存区域,这样2个进程就看到了同一份资源。
共享内存映射的区域是进程地址空间堆栈之间的镂空区域,共享内存和动态库所映射的区域都是进程地址空间堆栈之间的区域。
共享内存的释放
撤销共享内存与进程地址空间的映射,释放在物理内存中申请的共享内存的资源,同时回收管理共享内存的内核数据结构。
共享内存的基本认识
-
共享内存是操作系统单独设计的一个内核模块,是操作系统专门为了进程间通信而设计的一种手段,共享内存的作用就是专门用来进行进程间通信。
-
操作系统申请了共享内存,需要对共享内存进行管理,Linux系统需要知道如下问题:
- 共享内存的大小是多少?
- 有多少个进程的进程地址空间映射到了这一段共享内存?
- 这一段共享内存的其它属性?
- ……
因此,Linux系统需要管理共享内存,管理共享内存的方法是先描述,在组织,只要申请了共享内存,在内核中一定要有管理这一段共享内存的内核数据结构。
-
共享内存不单是一段物理内存空间,还有管理该空间的内核数据结构,共享内存=共享内存块+管理该共享内存块的内核数据结构
申请共享内存
申请共享内存使用系统接口shmget
(shared memory get).
man 2 shmget
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
返回值
:shmget函数申请共享内存成功会返回有效的共享内存用户层标识符,调用失败返回-1,并且errno被设置。
参数:
size_t size:想要申请的共享内存的大小,一般这个数值应该是4096Bytes的整数倍。如果申请4097Bytes,那么操作系统实际上会向内存申请4096*2Bytes,但是只给用户使用4097Bytes,剩下的空间虽然操作系统申请了,但是不会给用户使用,属于内存浪费。
shmflg:调用shmget函数时需要传入的选项,有
IPC_CREAT和IPC_EXCL
如果只传
IPC_CREAT
:若在内核中存在共享内存,它的key值等于在shmget函数中传入的参数key,那么shmget函数返回这个共享内存的用户层标识符;若在内核中没有key值等于传入的key值的共享内存,那么Linux系统会在底层创建一个共享内存,让它的key值=传入的key值,然后返回这个共享内存的用户层标识符。如果只传入
IPC_EXCL
:那么调用shmget函数是没有意义的如果传入
IPC_CREAT|IPC_EXCL
:若在底层有一个共享内存的key值等于传入的key值,那么shmget函数返回-1,否则Linux系统在底层创建一个共享内存,这个共享内存的key值设置为传入的key值,返回这个共享内存的用户层标识符。使用IPC_CREAT|IPC_EXCL
,只要shmget函数正常返回了,说明底层一定是创建了一个全新的共享内存。
shmget函数的第一个参数key:
共享内存=共享内存的内存块+管理共享内存的内核数据结构。第一个参数key是被写入到共享内存的内核数据结构中的,在内核中通过key值来区分不同的共享内存。
客户端在调用shmget时,使用
IPC_CREAT
选项,并且生成一个和服务端相同的key值传入shmget函数,客户端就能得到一个和服务端相同的用户层标识符,与服务端访问到同一段共享内存,实现进程间通信。生成key值
生成key值使用ftok函数。
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);
ftok函数的内部有特定的算法会生成一个随机的key值。
- ftok函数会根据传入的第一个参数对应的文件(目录)的inode编号和传入的project_id,使用指定的算法生成一个特定的key值,只要参数一样,生成的key值就是一样的
- 若ftok函数生成的key值与内核中某一个共享内存的key值一样的话,shmget(ftok(),…)会返回-1(选项是
IPC_CREAT|IPC_EXCL
)或返回这个共享内存的用户层标识符(IPC_CREAT
)- ftok函数的第一个参数传入的路径应该是系统中存在的路径,而且最好要有访问权限,因为ftok是在文件系统中拿到传入的目录(文件)的inode编号,根据inode编号和project_id综合得出key值。
- ftok函数的返回值:调用成功返回0,失败返回-1,errno被设置
#include <sys/types.h> #include <sys/ipc.h> #include <stdio.h> #include <stdlib.h> int main() { key_t key = ftok("/home", 4); if (key == -1) { printf("ftok调用失败,可能是路径错误或没有对该路径的访问权限\n"); exit(0); } printf("ftok(\"/hmoe\",4)产生的key值是%d\n", key); return 0; }
key值与shmget函数的返回值
shmget函数的参数key只有在创建共享内存的时候才会使用到,这个key值是内核层面区分不同共享内存的标志,key值可以标定共享内存在系统层面的唯一性。
shmget函数的返回值是用户层标识符,用户层是通过shmget函数的返回值shmid来使用共享内存的,一般情况下对于共享内存的使用都是通过shmid,不会用到key.
共享内存的生命周期
共享内存的生命周期是随内核的,若通过shmget函数创建共享内存却不关闭的话,那么创建的这一个共享内存会一直存在于内核,占用系统资源,除非用户手动删除或重启设备。
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<stdio.h>
#define PATH "/home/slowstep"
#define PROJECT_ID 4
#define SHM_SIZE 4096
int main(){
int key=ftok(PATH,PROJECT_ID);//使用ftok产生一个随机的key
if(key==-1){
perror("ftok函数生成key发生错误");
exit(1);
}
//ftok函数生成key成功,创建共享内存
int shmid=shmget(key,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);//IPC_CREAT|IPC_EXCL,要么创建新的共享内存,要么失败,不会使用已经存在的共享内存。0666是创建的共享内存的权限,共享内存也有权限
if(shmid==-1){
perror("创建共享内存失败");
exit(2);
}
printf("创建共享内存成功,shmid=%d\n",shmid);
return 0;
}
使用ipcs -m
命令查看系统中共享内存的属性
[slowstep@localhost shm]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 4 slowstep 777 16384 1 dest
0x00000000 5 slowstep 777 2129920 2 dest
0x00000000 13 slowstep 600 524288 2 dest
0x00000000 14 slowstep 600 524288 2 dest
0x00000000 15 slowstep 777 2129920 2 dest
0x00000000 16 slowstep 600 524288 2 dest
0x04030040 17 slowstep 666 4096 0 #通过shmget创建的共享内存
- key,共享内存在内核中的标识符
- shmid,共享内存在用户层的标识符
- owner,这个共享内存是谁通过shmget函数创建的
- perms,共享内存的权限
- bytes,共享内存的内存块大小,共享内存=共享内存的内存块+管理该内存块的内核数据结构,4096Bytes指的是前者,管理共享内存内存块的内核数据结构的大小需要另外计算。
- nattch,有多少个进程与当前这个共享内存有关联,nattch=0表示没有进程与key值为0x04030040的共享内存有关联。
删除共享内存
由于共享内存的生命周期是随内核的,因此,若创建一个共享内存且使用完毕,应该删除它,否则会造成内存泄漏(存在无效的内存占用)。
删除共享内存使用命令ipcrm -m +shmid
,shmid是共享内存的用户层标识符,用户层任何对于共享内存的操作都是通过shmid来进行的,除了创建共享内存(创建共享内存需要传入key)。
[slowstep@localhost shm]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 4 slowstep 777 16384 1 dest
0x00000000 5 slowstep 777 2129920 2 dest
0x00000000 13 slowstep 600 524288 2 dest
0x00000000 14 slowstep 600 524288 2 dest
0x00000000 15 slowstep 777 2129920 2 dest
0x00000000 16 slowstep 600 524288 2 dest
0x04030040 17 slowstep 666 4096 0
[slowstep@localhost shm]$ ipcrm -m 17
[slowstep@localhost shm]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 4 slowstep 777 16384 1 dest
0x00000000 5 slowstep 777 2129920 2 dest
0x00000000 13 slowstep 600 524288 2 dest
0x00000000 14 slowstep 600 524288 2 dest
0x00000000 15 slowstep 777 2129920 2 dest
0x00000000 16 slowstep 600 524288 2 dest
控制共享内存
shmctl
接口可以控制共享内存#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回值:调用shmctl成功返回0,失败返回-1
参数:
shmid
,要控制的共享内存在用户层的标识符
cmd
:控制共享内存的方式。有IPC_STAT:获取共享内存的属性;IPC_SET:设置共享内存的属性;IPC_RMID:删除共享内存;IPC_INF:查看共享内存
buf
:若只是删除共享内存的话,buf设置为NULL即可。
通过shmctl删除共享内存
#include<sys/ipc.h>
#include<sys/shm.h>
#include<iostream>
int main(){
int key=ftok("/",4);
if(key==-1){
perror("ftok fail");
exit(0);
}
int shmid=shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);
if(shmid<0){
perror("shmget fail");
exit(0);
}
std::cout<<"Creat shared memory success"<<std::endl;
int ret=shmctl(shmid,IPC_RMID,nullptr);//删除共享内存
if(ret<0){
perror("shmctl(delete) fail");
}
printf("delete shared memory success\n");
return 0;
}
通过shmctl删除共享内存有一个强势之处:无论是否有进程与该共享内存挂接,都会直接删除掉该共享内存。
共享内存的挂接
共享内存是有权限的,挂接共享内存需要有对共享内存的权限,共享内存的挂接过程就是把共享内存与进程地址空间建立映射的过程。
挂接共享内存使用
shmat
:shared memory attach.#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- shmid,要挂接的共享内存的用户层标识符
- shmaddr,指定挂载到进程地址空间的虚拟地址,一般该参数可以设置为nullptr,表示让系统自动挂载
- shmflg:挂载的方式,一般shmflg设置为0,表示默认以读写的方式进行挂载
返回值
如果挂接成功,返回挂接成功的起始地址(这个地址是进程地址空间的某一个虚拟地址),如果挂接失败返回(void*)-1
通过shmat函数返回的起始地址+偏移量就可以实现对共享内存的访问,当然这个访问是通过页表映射来访问的,不是通过虚拟地址直接访问的,偏移量指的就是共享内存的内存块大小
使用shmat完成挂接以后,共享内存的nattch属性会+1,表示多了一个进程与该共享内存产生关联。
取消挂接
将进程与指定的共享内存去除关联使用shmdt
:shared memory detach
int shmdt(const void* shmaddr);
shmdt的参数是shmat函数返回的地址,shmdt调用成功返回0,失败返回-1,errno被设置。
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<sys/shm.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
int key=ftok("/home",2);
if(key<0){
perror("ftok fail");
exit(0);
}
printf("ftok success,key=%d\n",key);
sleep(5);
int shmid=shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);
if(shmid<0){
perror("shmget fail");
exit(0);
}
printf("shmget success ,shmid=%d\n",shmid);
sleep(5);
char* shmaddr=(char*)shmat(shmid,NULL,0);
if(shmaddr==(char*)-1){
perror("shmat fail");
exit(0);
}
printf("shmat success,the return address is %p\n",shmaddr);
sleep(5);
int x=shmdt(shmaddr);
if(x<0){
perror("shmdt fail");
exit(0);
}
printf("shmdt success\n");
sleep(5);
int y=shmctl(shmid,IPC_RMID,NULL);
if(y<0){
perror("shmctl(delete) fail");
exit(0);
}
printf("delete success\n");
sleep(5);
return 0;
}
将2个进程挂接到同一个共享内存
consistant.hpp
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#define PATH "/home"
#define PROJECT_ID 5
#define SHM_SIZE 4096
using namespace std;
服务端Serve
/*
服务端
服务端创建共享内存
*/
#include "consistant.hpp"
int main(){
try{
int key=ftok(PATH,PROJECT_ID);
if(key<0){
throw "ftok fail";
}
printf("ftok success,key=%d\n",key);
sleep(5);
int shmid=shmget(key,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
if(shmid<0){
throw "shmget fail";
}
printf("shmget success,shmid=%d\n",shmid);
sleep(5);
//将进程地址空间与共享内存进行挂载
char* shmaddr=(char*)shmat(shmid,NULL,0);//0,默认以读写方式进行挂载
if(shmaddr==(char*)-1){
throw "shmat fail";
}
printf("shmat success,shmaddr= %p\n",shmaddr);
sleep(5);
/*
挂载完成可进行写入
*/
//将进程与共享内存去除关联
int ret=shmdt(shmaddr);
if(ret<0){
throw "shmdt fail";
}
printf("shmdt success\n");
sleep(5);
//使用shmctl删除共享内存
int x=shmctl(shmid,IPC_RMID,nullptr);
if(x<0){
throw "shmctl(delete) fail";
}
printf("shmctl(delete) success\n");
sleep(5);
}
catch(const char* errmessage){
cout<<errmessage<<endl;
}
catch(...){
cout<<"unknown exception"<<endl;
}
return 0;
}
客户端Client
/*
客户端
客户端通过接口连接共享内存
*/
#include "consistant.hpp"
int main(){
try{
int key=ftok(PATH,PROJECT_ID);
if(key<0){
throw "ftok fail";
}
printf("ftok success,key=%d\n",key);
sleep(5);
int shmid=shmget(key,SHM_SIZE,IPC_CREAT);//单独使用IPC_CREAT,如果key对应的共享内存存在,直接返回该共享内存在用户层的标识符
if(shmid<0){
throw "shmget fail";
}
printf("shmget success,shmid= %d\n",shmid);
sleep(5);
char* shmaddr=(char*)shmat(shmid,nullptr,0);
if(shmaddr==(char*)-1){
throw "shmat fail";
}
printf("shmat success,shmaddress= %p\n",shmaddr);
sleep(5);
/*
挂接完成,可对共享内存进行读写操作
*/
int ret=shmdt(shmaddr);
if(ret<0){
throw "shmdt dail";
}
printf("shmdt success\n");
sleep(5);
//删除共享内存由服务端完成
}
catch(const char* errmessage){
cout<<errmessage<<endl;
}
catch(...){
cout<<"unknown exception"<<endl;
}
return 0;
}
使用共享内存
共享内存可以直接使用,不用通过系统调用,类似于通过malloc申请了一块堆区空间。
进程地址空间的[0GB,3GB]属于用户空间,[3GB,4GB]属于内核空间,共享内存的挂接是把共享内存通过页表映射到进程地址空间的用户空间,映射到的区域是用户空间的堆栈之间。
使用共享内存:
consistant.hpp
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#define PATH "/home"
#define PROJECT_ID 5
#define SHM_SIZE 4096
using namespace std;
shmServe.cpp
,服务端
/*
服务端
服务端创建共享内存
*/
#include "consistant.hpp"
#include<cstring>
int main(){
int key=ftok(PATH,PROJECT_ID);
if(key<0){
perror("ftok fail");
exit(0);
}
printf("ftok success,key=%d\n",key);
int shmid=shmget(key,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
if(shmid<0){
perror("shmget fail");
exit(0);
}
printf("shmget success,shmid=%d\n");
char* shmaddr=(char*)shmat(shmid,nullptr,0);
if(shmaddr==(char*)-1){
perror("shmat fail");
exit(0);
}
printf("shmat success,shmaddress=%p\n",shmaddr);
//服务端从共享内存中读取数据
while(true){
if(strcmp("quit",shmaddr)==0){
break;
}
printf("%s\n",shmaddr);
}
int ret=shmdt(shmaddr);
if(ret<0){
perror("shmdt fail");
exit(0);
}
printf("shmdt success\n");
int x=shmctl(shmid,IPC_RMID,nullptr);
if(x<0){
perror("delete fail");
exit(0);
}
printf("delete success\n");
return 0;
}
shmClient.cpp
,客户端
/*
客户端挂接共享内存,并向共享内存中写入内容
*/
#include"consistant.hpp"
#include<cstring>
int main(){
int key=ftok(PATH,PROJECT_ID);
if(key<0){
perror("ftok fail");
exit(0);
}
printf("ftok success,key=%d\n",key);
int shmid=shmget(key,SHM_SIZE,IPC_CREAT);
if(shmid<0){
perror("shmget fail");
exit(0);
}
printf("shmget success,shmid=%d\n",shmid);
char* shmaddr=(char*)shmat(shmid,nullptr,0);
if(shmaddr==(char*)-1){
perror("shmat fail");
exit(0);
}
printf("shmat success,shmaddress=%p\n",shmaddr);
//客户端向共享内存中写入内容
while(true){
ssize_t s=read(0,shmaddr,SHM_SIZE);
shmaddr[s-1]='\0';
if(strcmp(shmaddr,"quit")==0){
break;
}
}
int ret=shmdt(shmaddr);
if(ret<0){
perror("shmdt fail");
exit(0);
}
printf("shmdt success\n");
//删除共享内存由服务端完成
return 0;
}
共享内存的特点
-
共享内存在被创建以后会全部被初始化为0
-
共享内存是所有进程间通信最快的方式,因为共享内存的使用不用经过系统调用接口,可以直接使用,这就有效的减少了拷贝的次数。
如果使用管道通信需要发生4次拷贝,因为使用管道完成通信需要借助于系统调用接口read和write
而使用共享内存2个进程通过进程地址空间会映射到同一段共享内存,访问共享内存不需要经过系统调用,只会在读取数据和写入数据时发生2次数据的拷贝,没有中间过程会导致数据发生拷贝
-
共享内存没有同步和互斥,缺乏访问控制,会带来并发的问题。使用共享内存的时候,写端只负责写,读端只负责读,读写双方没有一定的顺序性。
可以使用共享内存+管道,通过管道的方式实现访问控制。
consistant.hpp
#pragma once #include<iostream> #include<sys/types.h> #include<sys/stat.h> #include<sys/ipc.h> #include<sys/shm.h> #include<unistd.h> #include<fcntl.h> #define PATH "/home" #define PROJECT_ID 5 #define SHM_SIZE 4096 #define PIPENAME "./mypipe" using namespace std; class Pipe{ public: Pipe(){ int ret=mkfifo(PIPENAME,0666); if(ret==-1){ perror("Creat pipe fail"); exit(0); } printf("Creat pipe success\n"); } ~Pipe(){ int ret=unlink(PIPENAME); if(ret==-1){ perror("delete pipe file fail"); exit(0); } printf("delete pipe file success\n"); } };
shmServe.cpp
/* 服务端 服务端创建共享内存 */ #include "consistant.hpp" #include<cstring> int main(){ try{ int key=ftok(PATH,PROJECT_ID); if(key<0){ throw "ftok fail"; } printf("ftok success,key=%d\n",key); // sleep(5); int shmid=shmget(key,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666); if(shmid<0){ throw "shmget fail"; } printf("shmget success,shmid=%d\n",shmid); // sleep(5); //将进程地址空间与共享内存进行挂载 char* shmaddr=(char*)shmat(shmid,NULL,0);//0,默认以读写方式进行挂载 if(shmaddr==(char*)-1){ throw "shmat fail"; } printf("shmat success,shmaddr= %p\n",shmaddr); // sleep(5); /* 服务端读取从客户端发送过来的消息 */ Pipe tmp;//创建管道,在析构函数中会自动删除命名管道 int fd=open(PIPENAME,O_RDONLY);//服务端以读的方式打开管道文件 while(true){ char tmp[256]={0}; read(fd,tmp,256);//如果读不到内容就会被阻塞,相当于使用管道间接实现了对于共享内存的访问控制 if(strcmp("quit",shmaddr)==0){ break; } printf("%s\n",shmaddr); } close(fd); //将地址空间与共享内存去除关联 int ret=shmdt(shmaddr); if(ret<0){ throw "shmdt fail"; } printf("shmdt success\n"); // sleep(5); //使用shmctl删除共享内存 int x=shmctl(shmid,IPC_RMID,nullptr); if(x<0){ throw "shmctl(delete) fail"; } printf("shmctl(delete) success\n"); // sleep(5); } catch(const char* errmessage){ cout<<errmessage<<endl; } catch(...){ cout<<"unknown exception"<<endl; } return 0; }
shmClient.cpp
/* 客户端 客户端通过接口连接共享内存 */ #include "consistant.hpp" #include<cstring> int main(){ try{ int key=ftok(PATH,PROJECT_ID); if(key<0){ throw "ftok fail"; } printf("ftok success,key=%d\n",key); // sleep(5); int shmid=shmget(key,SHM_SIZE,IPC_CREAT);//单独使用IPC_CREAT,如果key对应的共享内存存在,直接返回该共享内存在用户层的标识符 if(shmid<0){ throw "shmget fail"; } printf("shmget success,shmid= %d\n",shmid); // sleep(5); char* shmaddr=(char*)shmat(shmid,nullptr,0); if(shmaddr==(char*)-1){ throw "shmat fail"; } printf("shmat success,shmaddress= %p\n",shmaddr); // sleep(5); /* 挂接完成,可对共享内存进行读写操作 客户端发送消息 */ //客户端以写的方式打开管道文件,随便向管道文件中写一些内容 //服务端会读取管道中的内容,只要客户端不写入,服务端就会被阻塞 int fd=open(PIPENAME,O_WRONLY); while(true){ ssize_t s=read(0,shmaddr,SHM_SIZE); if(s){ shmaddr[s-1]='\0'; write(fd,"1111",256); if(strcmp("quit",shmaddr)==0){ break; } } } close(fd); int ret=shmdt(shmaddr); if(ret<0){ throw "shmdt dail"; } printf("shmdt success\n"); // sleep(5); //删除共享内存由服务端完成 } catch(const char* errmessage){ cout<<errmessage<<endl; } catch(...){ cout<<"unknown exception"<<endl; } return 0; }