共享内存
- 共享内存原理介绍
- 共享内存系统调用接口
- shmget 创建共享内存段
- ftok 生成唯一键 key
- 开始创建共享内存
- 指令 ipcs -m 查看共享内存
- 指令 ipcrm -m 删除共享内存段
- shmctl 控制创建的共享内存
- 通过系统调用来删除共享内存
- 共享内存权限问题
- 关联/去关联共享内存
- 封装处理
共享内存原理介绍
system V 共享内存是两个独立的进程之间完成通信的一个物理内存块。
直接看概念是带蒙的,下面来讲讲其中的缘由:
一个正在运行进程,由下面几个方面构成:进程的 PCB、进程地址空间 和 页表。进程的数据通过页表映射到物理内存特定的位置。每个进程通过页表的映射,在物理内存的位置都会不一样。由此才能保证进程的独立性!
为了保证两个独立的进程能够进行通信,就要让这两个进程看到同一块空间。匿名管道和命名管道都是通过创建内存级的文件,来让两个独立的进程看到同一份资源。
那么共享内存又是如何操作的呢?
共享内存就是通过系统调用接口,在物理内存中开辟一块空间。在这里假设有两个独立的进程分别为:进程A 和 进程B。
前面提到,每个进程都有属于自己的进程地址空间。这个进程地址空间包含有:栈区、堆区、代码区… 在这里主要说一下在栈区 和 堆区之间的 共享区。
为了让 进程A 和 进程B 能够通信,可以将创建的共享内存,通过页表的映射方式分别映射到 进程A 和 进程B 的共享区:
此时,共享内存的地址就通过页表映射到进程地址空间的共享区,用户就可以通过共享区拿到对应的起始地址。这样就可以使 进程A 和进程B 看到同一份资源,进而就可以对两个独立的进程实现通信了。
进程完成通信后,共享内存又是如何释放资源的呢?
对于创建共享内来说,释放空间是比较简单的。只需要找到进程,进程再通过页表映射到物理内存开辟的共享内存的地址进行修改,然后再对这块空间进行释放即可。
因此使用共享内存,通常这几步:创建共享内存、将独立的进程对共享内存进行关联通信、取消进程与共享内存的关联、释放共享内存。
共享内存系统调用接口
shmget 创建共享内存段
创建共享内存需要用到系统调用接口:shmget
使用这个接口需要包含头文件:#include<sys/ipc.h>
、#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数介绍:
- key:用于唯一标识共享内存段(介绍另一个接口时,详细介绍这个参数)
- size:创建共享内存段的大小,以字节为单位
- shmflg:标志位,常用的标志位有两个 IPC_CREAT、IPC_EXCL
- 返回值:成功创建共享内存返回共享内存的 描述符(后续介绍);失败返回 -1
IPC_CREAT
标志位表示:创建一个共享内存段。如果共享内存段不存在,创建一个新的共享内存段;如果共享内存段存在,返回这个共享内存段。
IPC_EXCL
通常要配合 IC_CREAT
一起使用,加上 IPC_EXCL
标志位表示:创建一个新的共享内存段。如果这个共享内存段不存在,创建一个新的共享内存段;如果这个共享内存段存在,直接出错返回。
下面再来介绍,第二个参数唯一键: key
ftok 生成唯一键 key
这个key值一般是通过 ftok
函数来生成的,ftok
函数内部会使用一些算法生成唯一的 key 值。
使用 ftok
函数需要包含头文件:#include <sys/types.h>
、#include <sys/ipc.h>
。
key_t ftok(const char* pathname, int proj_id);
参数介绍:
- pathname:一个已存在的文件路径名。这个文件用于生成键,但其内容并不重要
- proj_id:一个非负整数,作为项目标识符,与 pathname 一起用于生成键
- 返回值:设置成功返回唯一键 key;失败返回 -1
进程A 和 进程B 可以通过共享内存进行通信。在操作系统中,存在这么多进程,是不是都是可以通过共享内存进行通信呢?
答案是可以的。一对对的进程都可以进行共享内存进行通信,这样将会开辟多个共享内存。针对如此多的共享内存,操作系统是需要对其进行做管理的。为了管理如此多的共享内存,操作系统会先对这些共享内存进行描述,创建属于共享内存结构体对象。假设创建:struct shm 结构体,每个结构体内都存储着共享内存的属性(指向共享内存的指针、共享内存大小、创建者、权限是多少、当前有多少进程正在使用这个共享内存等等),最重要的是这些属性中会存在着
struct shm*
指针,指向下一个共享内存的结构体属性。正是有了这些结构体指针,操作系统对共享内存的 删除 和 更改 就变成对 链表 的 增删查改!完成组织的操作。
因此,可以这样说:共享内存 == 共享内存的内核数据结构 + 真正在物理内存中开辟的内存空间
但是接下来就会出现这样的情况:
在物理内存中,存在着多个共享内存,操作系统为了管理这些共享内存会创建对应的 struct shm 结构体,形成链表的方式组织管理起来。面对如此多的共享内存,进程A 为了和 进程B 进行通信,就必须找到同一块资源。也就是要让 进程A 和 进程B 达成共识性的东西。
这也是为什么要使用到 ftok
函数了。 进程A 和 进程B 约定,调用 ftok
函数一起传入一个两个进程都知道的文件路径,然后再传入一个项目标识符,最后生成一个唯一键 key。
在这里假设 进程A 来创建一个共享内存(一般是谁创建谁来设置 唯一键 key),创建好一个共享内存后,进程A 将唯一键 key 设置到共享内存结构体的属性中。由于 进程A 和 进程B 是有约定的,所以在 进程B 使用 ftok 函数的时候,也会生成同一个 key 值。通过这个key值,进程A 和 进程B 就可以在诸多共享内存中找到 进程A 创建的共享内存,通信就可以开始。
因此,进程A 和 进程B 内部都要调用 ftok 函数来生成这样的唯一键,才能找到对应的共享内存。
提了这么多理论上的点,下面来实操演示下:
- 创建三个文件:server.cc、 client.cc 、comm.hpp。
comm.hpp 文件用来实现一些函数,为两个源文件提供使用:
#pragma once#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/ipc.h>//文件路径和项目id可以随意设置,但是要保证通信的进程之间都一样
#define PATHNAME "." //文件路径
#define PROJID 0x6666 //项目id值#define NUM 64key_t getKey()
{key_t n = ftok(PATHNAME, PROJID);if(n == -1){std::cout << errno << ":" << strerror(errno) << std::endl;return errno;}return n;
}std::string toHex(int x)
{char buffer[NUM];snprintf(buffer, NUM - 1, "0x%x", x);//将整除x转换为16进制字符串显示return buffer;
}
- 实现server.cc 和 client.cc 文件,两个文件都包含 comm.hpp文件,分别调用 getKey 函数。检验不同进程之间生成的唯一键是否相同:
//server.cc 文件
#include "comm.hpp"int main()
{ //创建唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;
}
//client.cc 文件
#include "comm.hpp"int main()
{ //创建唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;
}
编译运行,查看结果:
共识达成后,接下来就是创建共享内存的时候了。
接下来我会标注每一段代码都在哪个文件实现,防止老铁们迷路。整体代码会在篇尾全部给出。
开始创建共享内存
- 在 comm.hpp 文件增加两个函数 :createShm 函数用于创建共享内存、 getShm 函数用于获取共享内存
createShm 函数和 getShm 函数的实现逻辑其实都差不多,最大差异就是创建共享内存的时候是要开辟一个全新的共享内存段。获取函数只需要获取已经存在的共享内存即可。为了防止代码冗余,再实现多一个函数 createShmHelper ,用于createShm 函数和 getShm 函数 调用。
const int gsize = 4096;int createShmHelper(key_t key, int size, int flag)
{int shmid = shmget(key, size, flag);//创建共享内存段if(shmid == -1){std::cout << errno << strerror(errno) << std::endl;exit(2);}return shmid;
}int createShm(key_t key, int size)
{return = createShmHelper(key, gsize, IPC_CREAT|IPC_EXCL); //创建一块全新的共享内存段
}int getShm(key_t key, int size)
{return = createShmHelper(key, gsize, IPC_CREAT); //获取创建的共享内存块
}
- server.cc 文件创建共享内存段,client.cc 文件来获取共享内存段。
//servet.cc文件
#include "comm.hpp"int main()
{ //1. 定义唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;//2.创建共享内存段int shmid = createShm(key, gsize);std::cout << "shmid:" << toHex(shmid) << std::endl;return 0;
}
//client.cc文件
#include "comm.hpp"int main()
{ //1.创建唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;//2.获取共享内存块int shmid = getShm(key, gsize);std::cout << "shmid:" << toHex(shmid) << std::endl;return 0;
}
- 编译 server.cc 和 client.cc 文件,运行结果如下:
如果只是创建共享内存,那么还是比较简单的。但是,当我们再次运行 server 文件时会出现下面这种情况:
创建的共享内存还存在!共享内存再次被创建之后,共享内存的生命周期不会随着进程的退出而结束。它会一直被维护在操作系统中,直到操作系统退出。要对原来的共享内存先进行释放,才能继续创建共享内存
下面来介绍两个Linux 指令,分别是 :ipcs
查看共享内存段 、ipcrm
删除共享内存段。
指令 ipcs -m 查看共享内存
ipcs -m
指令通常用来查询共享内存段的信息:
ipcs -m
可以看到,标志位 32768 就是kunkun 这个用户通过 server 进程创建的共享内存段。此时的 server进程已经完全退出了,但是开辟共享内存依旧存在。
那么如何去释放这个空间呢?下面来介绍另一个指令:
指令 ipcrm -m 删除共享内存段
ipcrm -m 共享内存段标志位
- -m 选项:删除用指定 标志位 的共享内存段
注意:-m 选项后面加上的是共享内存断的标志位,不是唯一键 key
下面将 32768 标志位的共享内存段释放:
除了指令的方式删除共享内存段,还可以通过系统调用的方式去删除共享内存段
shmctl 控制创建的共享内存
shmctl 函数是Linux中用于控制共享内存操作的一个系统调用函数,通过这个函数可以对共享内存进行 写、读、删除等操作。
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
参数介绍:
- shmid 参数:共享内存标识符
- cmd 参数:类似于位图,传入控制宏参数,指定shmctl函数对共享内存执行的具体操作
- buf 参数:指向 shmid_ds 结构体的指针,shmid_ds 结构体包含了共享内存的各种信息
- 返回值:成功返回0;失败返回 -1
这里主要来介绍如何对共享内存进行删除操作,其他像是 cmd 参数中诸多宏的用法,有感兴趣的老铁可以自行去使用使用。
通过系统调用来删除共享内存
删除共享内存,需要用到 shmctl 函数的第二个参数 cmd 的 IPC_RMID
宏。
在 comm.hpp 文件增加一个删除的函数,方便 server.cc 文件对共享内存进行删除的操作
//comm.hpp
void delShm(int shmid)
{//删除共享内存int n = shmctl(shmid, IPC_RMID, nullptr);assert(-1);(void)n;std::cout << "共享内存已经被删除\n";
}
对server.cc 文件内容进行修改:
//server.cc 文件
int main()
{ //1. 定义唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;//2.创建共享内存段int shmid = createShm(key, gsize);std::cout << "shmid:" << toHex(shmid) << std::endl;sleep(3);//停顿3秒模拟实现共享内存使用效果//删除共享内存delShm(shmid);return 0;
}
再来编译运行看看:
共享内存权限问题
前面介绍共享内存的操作都是如何创建、查看 和 删除共享内存,没有提到权限的问题。由于接下来要使用这块共享内存,对此要讲一下关于共享内存权限访问问题:
通过先前的方式创建一个共享内存,仔细观察一下这块共享内存的信息:
标红处表示:kunkun这个用户创建的的共享内存是没有读写权限的。尽管这块空间是kunkun用户创建,kunkun本身也没有读写权限,是不能使用这块空间的。但是创建这块共享内存的主人是可以对这块空间进行释放的。
如何解决呢?
只需要在创建共享内存的时候给上对应的权限即可,修改 comm.hpp 文件:
int createShm(key_t key, int size)
{umask(0);//将权限掩码设置为0int shmid = createShmHelper(key, gsize, IPC_CREAT|IPC_EXCL| 0666); //创建一块新的共享内存段,赋予666的权限,0666表示八进制return shmid;
}
共享内存的权限设置犹如文件权限那般,受到权限掩码的影响(要设置的权限 & ~umask 得到最终权限)
编译运行看看结果:
创建的共享内存权限已经被设置为 666
关联/去关联共享内存
共享内存的创建、释放都有了,接下来的问题就是如何关联共享内存。
来介绍两个接口 shmat
和 shmdt
,这个接口是用来 关联和 去关联 一块共享内存空间。使用这两个接口需要包含头文件:#include<sys/types.h>
、#include<sys/shm.h>
先来介绍第一个接口 shmat ,用于关联共享内存:
void *shmat(int shmid, const void *shmaddr, int shmflg);
at 是 attach 单词缩写,表示关联的意思。
参数作用:
- shmid:要关联的共享内存
- shmaddr:将共享内存挂接到进程地址空间的确定位置
- shmflg:设置共享内存读写
- 返回值:挂接成功后,返回共享内存映射在进程地址空间的起始地址(虚拟地址)
一般的,shmaddr 参数我们都不会自己去手动设置,直接传 nullptr 让OS去帮我们分配;shmflg 参数 是通过传标志位的方式来控制这块共享内存读写,例如 :SHM_RDONLY 宏表示只读。没有其他要求,在这里我们传 0 即可。
shmat 的系统接口的返回值 void* 很像C语言的 malloc 函数的返回值,我们可以像 malloc 那般去使用这个返回值。
再来介绍一下 shmdt 接口,将进程 去关联 共享内存:
int shmdt(const void *shmaddr);
dt 是单词 detach 缩写,表示分离的意思。
参数作用:
- shmaddr:表示已经被挂载到进程地址空间的共享内存的起始地址
- 返回值:去关联成功返回0,失败返回-1
下面来编写代码,将两个独立的进程 关联 和 去关联到同一块共享内存:
先在 comm.hpp 文件中去编写两个函数:attachShm
关联共享内存、detachShm
去关联共享内存
//comm.hpp
//关联
char* attachShm(int shmid)
{char* start = (char*)shmat(shmid, nullptr, 0);if(start == nullptr){exit(errno);}return start;
}//去关联
void detachShm(char* start)
{int n = shmdt(start);if(n == -1) exit(errno);
}
通过这两个接口,server进程 和 client进程就可以关联到同一块共享内存进行通信了。下面来编写这两个进程的代码:
//server.cc
int main()
{ //1. 定义唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;//2.创建共享内存段int shmid = createShm(key, gsize);std::cout << "shmid:" << toHex(shmid) << std::endl;//将当前进程与共享内存关联起来char* start = attachShm(shmid);sleep(15);//停顿5秒模拟实现共享内存使用效果//将当前进程与共享内存去关联detachShm(start);//删除共享内存delShm(shmid);return 0;
}
//client.cc
int main()
{ //1.创建唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;//2.获取共享内存块int shmid = getShm(key, gsize);std::cout << "shmid:" << toHex(shmid) << std::endl;//关联共享内存char* start = attachShm(shmid);//使用共享内存sleep(10);//去关联detachShm(start);return 0;
}
先将 server进程 跑起来,创建共享内存,再将 client进程跑起来;由于,server进程运行的时间比 client进程 运行的时间长,client进程会先退出,通过先后顺序来观察进程关联共享内存的现象。
下面将两个进程跑起来:
- server创建共享内存,并且与这个共享内存关联起来:
2. client 进程运行起来关联到这块共享内存,此时的关联数变成2:
3. 经过一段时间后,client 进程会先去关联再退出,关联数会变成1:
4. 再后来就是 server进程去关联,关联数会变成1,最后 server进程释放这块共享内存:
封装处理
创建共享内存、关联共享内存、去关联共享内存、删除共享内存,对于创建与删除 都是由 serve进程在处理,关联与去关联操作server进程与client 进程都是在重复操作。
对此可以将上述操作都封装成一个类,利用构造函数去创建与关联共享内存,析构函数用来去关联与删除共享内存的操作。在这里要注意一下,创建与删除共享内存的操作只需要一个进程负责即可。
在 comm.hpp 文件中,定义两个宏,用于标识谁来创建与删除共享内存;实现 Init 类,去封装上面实现的 创建、删除、关联、去关联 函数接口:
#define SERVER 0
#define CLIENT 1class Init
{
public:Init(int type): _type(type){// 获取唯一键key_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;if (_type == SERVER) // 符合SERVER创建{// 创建共享内存_shmid = createShm(key, gsize);std::cout << "进程创建共享内存成功,shmid:" << toHex(_shmid) << std::endl;}else{// 获取共享内存_shmid = getShm(key, gsize);std::cout << "进程已经获取到共享内存:shmid:" << toHex(_shmid) << std::endl;}// 关联共享内存_start = attachShm(_shmid);}char* getStart() { return _start; } //用于获取共享内存地址~Init(){// 去关联共享内存detachShm(_start);if (_type == SERVER){// 删除共享内存delShm(_shmid);}}
private:int _type; // 用于标识谁来创建、删除共享内存int _shmid; // 共享内存编号char *_start; // 共享内存地址(虚拟地址)
};
对此,我们在 server.cc 和 client.cc 文件中只需要创建 Init 对象就可以实现共享内存的创建、关联操作;Init 对象生命周期结束,调用析构函数完成对应的共享内存 去关联、删除操作。
有兴趣的小伙伴可以尝试利用下面的一个简单示例,来实现两个独立的进程通过共享内存完成一次通信过程:
client进程 向共享内存中逐个写入 1~26 个字符,server 从共享内存中获取client 写入的字符串并输出到终端。完成写的工作,client 进程写完后会先退出,server进程随后退出。
//server.cc
int main()
{Init init(SERVER);char *start = init.getStart();int n = 0;while(n <= 30){cout <<"client -> server# "<< start << endl; //读取字符串sleep(1);n++;}return 0;
}
//client.cc
int main()
{Init init(CLIENT);char *start = init.getStart();char c = 'A';while(c <= 'Z'){start[c - 'A'] = c;c++;start[c - 'A'] = '\0';sleep(1);}return 0;
}