⭐前言:在前面的博文中分析了什么的进程间通信和进程间通信的方式之一:管道(匿名管道和命名管道)。接下来分析第二种方式:共享内存。
要实现进程间通信,其前提是让不同进程之间看到同一份资源。所谓共享内存,那就是不同进程之间,可以看到内存中同一块资源,这就是共享内存的概念。
共享内存原理
用户通过操作系统提供的系统调用,让操作系统帮助用户去申请一块空间,跟C语言中malloc函数、C++的new的意思差不多。创建好后,将创建好的内存映射到进程地址空间中,然后返回这个地址的起始地址给用户。最后,当结束通信后,就会取消进程和内存的映射关系去掉,然后释放这段内存空间!
而这段内存,就称为共享内存!进程与内存关联的行为称为挂接。取消进程与内存的映射关系,称为去关联。释放这段内存,叫做释放共享内存。
理解共享内存的开辟
①用户申请开辟共享内存空间的系统接口,是专门为了进程间通信而设计出来的,可以让不同进程同时跟其建立关联。跟malloc,new等等的函数不一样,它们虽然也可以在物理内存上开辟空间,但是只能用于本身进程。
②共享内存是一种通信方式,意味着所有想通信的进程都可以使用它。
③既然共享内存是一种通信方式,因此在OS中,一定存在多个共享内存!
实例代码
共享内存函数
按照上图的步骤:第一步,创建共享内存。以下是创建共享内存的两个函数。
①shmget函数
功能:用来创建共享内存
原型:int shmget(key_t key, size_t size, int shmflg);
头文件:#include<sys/ipc.h> #include<sys/shm.h>
参数:
key : 这个共享内存段名字。
size : 共享内存大小
shmflg : 由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样
其中重要的两个:
IPC_CREAT:如果不存在,创建之。如果存在,获取之。
IPC_EXCL:无法单独使用。需要与IPC_CREAT结合使用,
IPC_CREAT | IPC_EXCL:如果不存在,创建之。如果存在,出错并返回。如果创建成功,那么一定是一个新的共享内存。
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回 - 1
shmget函数中的参数key,它能够标定唯一性!因为需要保证一个进程去申请共享内存,另外的进程去获取这个共享内存,它们的共享内存是同一个共享内存!而获取key是通过ftok函数来获取的。
②ftok函数
功能:将一个路径明和一个项目标识符转化成一个IPC的key
原型:key_t ftok(const char* pathname , int proj_id);
头文件:#include<sys/ipc.h> #include<sys/types.h>
参数:
pathname:传进来的字符串
proj_id:项目标识符
返回值:成功返回key;失败返回-1
只要不同进程在调用ftok的时候,参数一模一样,获取相同的key,再去调用shmget函数,通过同一个key,就能访问同一个共享内存。
补充说明:
共享内存=物理内存块+共享内存的相关属性
上面谈到,OS中一定存在多个共享内存,而OS必须要对这些用户申请开辟的空间进行管理!即先描述再组织,因此,OS会对开辟的共享内存创建一个数据结构,一个共享内存一个数据结构,然后通过链表链接起来,统一管理。于是,在谈到申请开辟一块共享内存,就需要想到:共享内存 = 物理内存块 + 共享内存的相关属性!
key值被包含在了共享内存的属性中。
共享内存的相关属性被包含在共享内存的数据结构中,而其中的key值也包含在了里面。即key值是在shmget函数创建出来后被设置进入共享内存的属性当中,用来表示该共享内存,并表示该共享内存在内核中的唯一性!
shmid和key的关系区分
shmget函数返回值,假设命名为shmid。那么shmid与key的关系就如同在文件IO中的文件描述符fd和inode的关系一样,inode是一个文件一个inode,表示文件的唯一性,key是一个共享内存一个,表示的是共享内存的唯一性,它们都是底层访问目标的工具。但是上层是不用key或inode的,而是使用shmid和fd这样一个特定的整数来访问。一句话来说,一个是用户的,一个是系统的,两个互不干扰,这是它的好处。
查看共享内存指令
ipcs -m
ipc资源的特征
共享内存的生命周期是随操作系统的,不是随进程的,即使进程终止了,但没有去释放这段共享内存,那么它就会一直存在。
删除共享内存
ipcrm -m shmid
按照上图所示:以下是删除共享内存的函数。
③shmctl函数
功能:用于控制共享内存,即删除共享内存,设置共享内存属性等等
原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
头文件:#include<sys/ipc.h> #include<sys/shm.h>
参数:
shmid:由shmget返回的共享内存标识码。
cmd:将要采取的动作(有三个可取值)
动作:
①IPC_STAT:获取共享内存属性
②IPC_SET:设置共享内存属性
③IPC_RMID:删除共享内存
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
按照上图所示,以下是将共享内存映射到进程地址空间的函数。
④shmat函数
功能:将共享内存段连接到进程地址空间
原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
头文件:#include<sys/shm.h> #include<sys/types.h>
参数:
shmid: 共享内存标识,即想和哪个共享内存关联起来
shmaddr:指定连接的地址。就是想把这个共享内存映射到哪个进程地址空间中,给出这个进程地址。
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存;失败返回-1
使用完后,不直接删除共享内存,而是先去关联。以下是去关联的函数。
⑤shmdt函数
功能:将共享内存段与当前进程脱离
原型:int shmdt(const void *shmaddr);
头文件:#include<sys/shm.h> #include<sys/types.h>
参数:shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
示例代码代码如下:
代码思路:创建一段共享内存,创建两个没有亲属关系的进程,client进程负责写入,server进程负责读取。
头文件comm.hpp:
#ifndef _COMM_HPP_ #define _COMM_HPP_ #include <iostream> #include <cerrno> #include <cstring> #include <cstdlib> #include <cstdio> #include <sys/ipc.h> #include <sys/shm.h> #define PATHNAME "." #define PROJ_ID 0X66 //设置共享内存大小:建议为4KB的整数倍 //因为系统分配共享内存是以4KB为单位的! #define MAX_SIZE 4096 //获取key key_t getKey() { //通过ftok函数获取key key_t k = ftok(PATHNAME,PROJ_ID);//获得同一个key if(k < 0) { std::cerr<<errno<<":"<<strerror(errno)<<std::endl; exit(1); } return k; } //创建共享内存 int getShmHelper(key_t k,int flags) { //通过shmget函数创建共享内存。 //第一个参数是key,第二个参数是共享内存的大小。第三个参数是权限标志 int shmid = shmget(k,MAX_SIZE,flags);//创建共享内存 if(shmid<0) { std::cerr<<errno<<":"<<strerror(errno)<<std::endl; exit(2); } return shmid; } //通过封装函数给用户去使用,只需传入key值即可。 //获取共享内存,不一定要新的,因为不用调用它的进程去创建新的 int getShm(key_t k) { return getShmHelper(k,IPC_CREAT); } //创建共享内存,使用IPC_CREAT | IPC_EXCL,确定创建的共享内存一定是新的。需要给权限0600 int createShm(key_t k) { return getShmHelper(k,IPC_CREAT | IPC_EXCL | 0600); } //进程地址空间与共享内存相联 void* attachShm(int shmid) { //通过shmat函数将共享内存段连接到进程地址空间 //传入shmid和指定连接的进程地址的地址,但是这个一般不填,系统会自动去填 //第三个参数是权限标志,是对内存只读还是读写。 //在Linux系统中,一般是64位。我们这里需要将shmat函数返回的指针判断是否关联成功 //强行转化为longlong void *men = shmat(shmid,nullptr,0); if((long long)men==-1L) { std::cerr<<errno<<":"<<strerror(errno)<<std::endl; exit(3); } return men;//返回起始地址 } void detachShm(void* start) { //通过shmdt函数去关联 if(shmdt(start)==-1) { std::cerr<<errno<<":"<<strerror(errno)<<std::endl; } } void delShm(int shmid) { //通过shmctl函数删除共享内存 //第一个参数是函数是需要对哪个共享内存操作,那个共享内存 //第二个参数是需要进行什么样的操作 //第三个参数一般给nullptr if(shmctl(shmid,IPC_RMID,nullptr)==-1) { std::cerr<<errno<<":"<<strerror(errno)<<std::endl; } }
负责写入的进程程序代码client.cc:
#include"comm.hpp" #include<unistd.h> int main() { //第一步:创建key,创建共享内存 key_t k = getKey();//获取key printf("key: 0x%x\n",k);//查看key值 int shmid = getShm(k);//创建共享内存 printf("shmid:%d\n",shmid);//查看shmid //第二步:关联内存和进程地址空间 char* start = (char*)attachShm(shmid); printf("attach success,address start: %p\n",start);//查看起始地址 //开始使用 //写下需要往共享内存段写入的数据 const char* message = "hello server,我是另一个进程,正在和你通信"; pid_t id = getpid(); int cnt = 1; while(true) { sleep(5); //写入到共享内存段,将共享内存段当字符串,不需要额外char buffer[]; snprintf(start,MAX_SIZE,"%s[pid:%d][消息编号:%d]",message,id,cnt++); } //去关联 detachShm(start); //这个工程项目不需要删除共享内存 return 0; }
负责读取的进程的程序代码server.cc
#include"comm.hpp" #include<unistd.h> int main() { key_t k = getKey();//获取key值 printf("key: 0x%x\n",k);//查看key值 int shmid = createShm(k);//创建共享内存,必须是新的 printf("shmid: %d\n",shmid);//查看共享内存 //关联 char* start = (char*)attachShm(shmid); printf("attach success, address start: %p\n", start); //使用 while(true) { //读取共享内存中的数据 printf("client say: %s\n",start); //获取共享内存中的属性数据(部分) struct shmid_ds ds; shmctl(shmid,IPC_STAT,&ds); printf("获取属性: size: %d, pid: %d, myself: %d, key: 0x%x",\ ds.shm_segsz, ds.shm_cpid, getpid(), ds.shm_perm.__key); sleep(1); } //去关联 detachShm(start); //删除共享内存 delShm(shmid); return 0; }
结果如下:在第一个五秒时,共享内存中没有任何数据。第二个五秒,消息编号为1。第三个五秒,消息编号为2......
对于从内核数据结构中获取共享内存的属性,发现没有直接显示key值。但实际上key值是在这个内核数据结构中里面的另外一个结构体里面。
共享内存的优缺点
优点:所有使用共享内存的进程通信,速度是最快的!能大大减少数据拷贝的次数!并且生命周期是随系统的!那么,如果我们考虑到同样一份代码,分别使用管道和共享内存的话,并且考虑键盘输入和显示器输出,那么管道有几次拷贝?共享内存有几次拷贝?
如图,管道的话,需要创建buffer来获取数据,然后通过管道进行通信。而共享内存不需要,因为共享内存可以作为字符串空间,直接写入和读取数据。因此,根据上图所示,管道是6次拷贝,共享内存是4次拷贝。当然,代码不同,拷贝的次数也不会同。
缺点:共享内存没有同步和互斥!