一 共享内存的概念
1. 1 共享内存的原理
之前我们学过管道通信,分为匿名管道和命名管道,匿名管道通过父子进程的属性继承原理来完成父子进程看到同一份资源的目的,而命名管道则是通过路径与文件名来唯一标识管道文件,来让不同的进程之间进行通信。
而共享内存也是同理, 就是允许两个或多个不相关的进程访问同一段物理内存空间。
1.2 基本概念
不同进程之间共享的内存通常为同一段物理内存。进程可以 将同一段物理内存连接到他们自己的地址空间中 ,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
概念图如下:
二 共享内存的使用
2.1 使用流程
一般而言,共享内存的使用可以分为以下几步:
- 创建共享内存段
- 关联共享内存(建立通信)
- 传输数据
- 关闭共享内存区域
2.1.1 创建共享内存段 shmget
创建共享内存接口:shmget
创建一个新共享内存段或者取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的 共享内存标识符。
接口如下:
int shmget(key_t key, size_t size, int shmflg);- 功能: 创建一个新的共享内存段,或者获取一个既有的共享内存段的标识新创建的内存段中的数据都会被初始化为0- 参数:- key: key_t 类型是一个整型,通过这个找到或者创建一个共享内存一般使用16进制标识,非0值- size: 共享内存的大小,以几页的大小创建(大于1的数值,一般为4的整数)- shmflg: 属性- 访问权限- 附加属性: 创建/判断共享内存是不是存在- 创建: IPC_CREAT- 判断共享内存是否存在: IPC_EXCL,需要和 IPC_CREAT一起使用IPC_CREAT | IPC_EXCL | 0664(权限)- IPC_CREAT 如果内核中对应key值的共享内存不存在,则新建一个共享内存并返回该共享内存的句柄;如果已存在,则直接返回该共享内存的句柄– IPC_CREAT IPC_EXCL 如果不存在对应key值的共享内存则新建一个共享内存并返回该共享内存的句柄;如果已存在,则出错返回- 返回值:失败: -1 并设置错误号成功: >0 返回共享内存的引用的ID (int类型),后面操作共享内存都是通过这个值
2.1.2 key值的获取 ftok
key值的作用:
问:当我们调用系统接口申请了一块共享内存,我们要保证对应的进程能够访问到同一块共享内存,那么如何做到这一点呢?
答案:每一个共享内存被申请的时候都会有一个key值,这个key值用于标识系统中共享内存的唯一性。
ftok函数的作用:通过数学运算将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。
ftok运算出来的key值可能会产生冲突,不过概率很小。如果产生冲突,就对ftok的参数进行修改即可。
当我们想让两个进程使用共享内存的时候,我们就利用key值寻找到这一块共享内存。
key_t ftok(const char* pathname, int proj_id);- 功能: 根据指定的路径名, 和int值, 生成一个共享内存的key- 参数: - pathname: 指定一个存在的路径/home/ubuntu/Linux/a.txt/root- proj_id: int类型的值,但是这个系统调用只会使用其中的1个字节(8bit)范围: 0-255 一般指定一个字符 'a'
2.1.3 共享内存的关联 shmat
我们不是单单把共享内存创建出来就行了,我们还要将需要获取这块共享内存的进程与对应的共享内存关联起来,通信结束后,解除关联,最后进行共享内存的释放。
void* shmat(int shmid, const void* shmaddr, int shmflg);- 功能: 和当前的进程进行关联- 参数: - shmid: 共享内存的标识(ID),由shmget返回值获取- shmaddr: 申请的共享内存的起始地址(虚拟内存的地址,一般不会手动指定,由内核指定,设置为NULL)- shmflg: 对共享内存的操作- 读权限: SHM_RDONLY,必须要有读权限- 读写: 0- 返回值:成功: 返回共享内存的首(起始)地址失败: (void*) -1 设置错误号
2.1.4 解除共享内存的关联 shmdt
接触进程和共享内存的关联。
int shmdt(const void* shmaddr);- 功能: 解除当前进程和共享内存的关联- 参数:- shmaddr: 共享内存的首地址- 返回值:成功: 0失败: -1
2.1.5 删除共享内存函数 shmctl
int shmctl(int shmid, int cmd, struct shmid_ds* buf);- 功能: 对共享内存进行操作。 删除共享内存,共享内存要删除才会消失, 创建共享内存的进程被销毁了对共享内存是没有任何影响的。- 参数:- shmid: 共享内存的ID- cmd: 要做的操作- IPC_STAT: 获取共享内存的当前状态- IPC_SET: 设置共享内存的状态- IPC_RMID: 标记共享内存被销毁- buf: 需要设置或者获取的共享内存的属性信息- IPC_STAT: buf存储数据- IPC_SET: buf中需要初始化数据,设置到内核中- IPC_RMID: 没有用,NULL
注意:一般有几个进程关联 这一块共享内存,就需要解除关联多少次,但是,删除共享内存只需要删除一次就好了(其实可以删除多次,看补充)。
2.1.6 示例
接下来我们进行一段示例,操作内容如下:
要求:使用代码创建一个共享内存, 支持 A.B 两个进程进行通信。
进程A 向共享内存当中写 “i am process A”。
进程B 从共享内存当中读出内容,并且打印到标准输出。
因为两个进程中有大量重复代码,因此,我们封装一个头文件,以此来复用。
shared.h内容如下:
#include<iostream>
#include<cassert>
#include<cstring>
#include<cerrno>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cstdlib>
#include<unistd.h>
using namespace std;#define PATHNAME "./shared2"//ftok的第一个参数,是一个合法路径
#define PROJ_IO 'a'//ftok的第二个参数
#define MAXSIZE 4096//创建的共享内存的大小
//1.获取key值代码
key_t GetKey()
{key_t k = ftok(PATHNAME, PROJ_IO);//如果返回值小于0 则创建失败if(k < 0){std::cerr << errno << ":" << strerror(errno) << std::endl;exit(1);}return k;
}//2.创建共享内存段代码
int GetShmget(key_t k,int flags){int shmid = shmget(k, MAXSIZE, flags);if(shmid < 0){std::cerr << errno << ":" << strerror(errno) << std::endl;exit(2);}return shmid;
}//获取共享内存
int Getshm(key_t k){return GetShmget(k, IPC_CREAT);//没有就创建,有就获取
}//创建共享内存
int Createshm(key_t k){return GetShmget(k, IPC_CREAT | IPC_EXCL | 0600);//没有创建,有就报错,这里创建内存需要给对应的权限
}//3. 关联共享内存,返回共享内存的空间起始位置
void* attachshm(int shmid){void* p = shmat(shmid, nullptr, 0);//因为linux系统是64位,一个地址是8个字节,所以要变成8个字节大小的数据类型做对比if((long long)p == -1L){std::cerr << errno << ":" << strerror(errno) << std::endl;exit(4);}return p;
}//4.解除共享内存的关联
void detachshm(void* p){//如果失败报错if(shmdt(p) == -1){std::cerr << errno << ":" << strerror(errno) << std::endl;}
}//5 shmctl删除共享内存
void delshm(int shmid){if(shmctl(shmid, IPC_RMID, nullptr) == -1){std::cerr << errno << ":" << strerror(shmid) << std::endl;exit(3);}
}
A.cpp:
#include"shared.hpp"int main(){//先生成一个key值key_t k=GetKey();printf("A 获取key值成功: %d\n",k);//创建共享内存,有就获取标识码shmidint shmid=Getshm(k);printf("A 获取共享内存块成功\n");//关联共享内存块char* p = (char*)attachshm(shmid);if (p == (void*)-1) {perror("shmat failed");delshm(shmid);printf("A 删除共享内存成功\n");exit(EXIT_FAILURE);}printf("A 关联共享内存块成功\n");//开始写入:const char* str = "i am process A";snprintf(p, MAXSIZE, "%s", str);//去关联detachshm(p);printf("A 去关联成功\n");return 0;
}
B.cpp
#include"shared.hpp"int main(){//先生成一个key值key_t k=GetKey();printf("B 获取key值成功: %d\n",k);//创建共享内存,有就获取标识码shmidint shmid=Getshm(k);printf("B 获取共享内存块成功\n");//关联共享内存块char* p = (char*)attachshm(shmid);if (p == (void*)-1) {perror("shmat failed");delshm(shmid);printf("B 删除共享内存成功\n");exit(EXIT_FAILURE);}printf("B 关联共享内存块成功\n");//开始读出:sleep(5);printf("attach sucess, address p: %s\n",p);//去关联detachshm(p);printf("B 去关联成功\n");//删除共享内存delshm(shmid);printf("B 删除共享内存成功\n");return 0;
}
代码结果:
2.2 共享内存的命令行操作
2.2.1 共享内存的查看 – ipcs指令
报告进程间通信设施的状态,包括共享内存、消息队列以及信号量等等!
ipcs用法:
ipcs -a //打印当前系统中所有进程间通信方式的信息
ipcs -m //打印出使用共享内存进行进程间通信的信息(**常用**)
ipcs -q //打印出使用消息队列进行进程间通信的信息
ipcd -s //打印出使用信号进行进程间通信的信息
2.2.2 共享内存的删除- ipcrm 指令
ipcrm用法(rm:remove)
ipcrm -M shmkey //移除用shmkey创建的共享内存段
ipcrm -m shmid //移除用shmid标识的共享内存段
ipcrm -Q msgkey //移除用msqkey创建的消息队列
ipcrm -q msqid //移除用msqid标识的消息队列
ipcrm -S semkey //移除用semkey创建的信号
ipcrm -s semid //移除用semid标识的信号
三 补充说明
问题1:操作系统如何知道共享内存被读诵进程关联- 共享内存委会一个结构体 struct shmid_ds 这个结构体中有一个成员 shm_nattach- shm_nattach 记录了关联的进程个数
问题2:可不可以对共享内存进行多次删除 shmctl- 可以的- 因为 shmctl 仅是标记删除共享内存,不是直接删除- 什么时候真正删除呢?当和共享内存关联的进程数为0的时候,就真正被删除- 当共享内存的key为0的时候, 共享内存被标记删除了- 因此,需要合理衡量共享内存的删除,不要在其它进程还在使用时删除