文章目录
- 什么是共享内存?共享内存的原理
- 共享内存的知识点构建
- 创建共享内存的前提-key值
- 共享内存的创建
- ipcs命令
- 共享内存的释放
- 共享内存的关联与解除
- 代码演示
- 共享内存的大小
- 共享内存的特点
什么是共享内存?共享内存的原理
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与对应的进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
- 使用步骤:先创建共享内存,再关联对应的进程,最后取消共享内存和进程的关联,释放共享内存块。
- 共享内存不是只要在内存中开辟空间,系统还要为了管理shm共享内存,构建对应的描述共享内存的结构体对象。
- 开辟物理内存,建立映射等操作都是通过我们调用系统函数接口完成的,也就是说这些操作都是由操作系统完成。
- 对于虚拟地址空间中栈和堆之间的共享区,这里不仅可用于之前说到的存储动态库,共享内存,还可以用于加载网络映像,共享数据以及存储某些不需要保存在磁盘上的临时数据。
共享内存的结构
在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中。
共享内存的知识点构建
创建共享内存的前提-key值
问:当我们调用系统接口申请了一块共享内存,我们要保证对应的进程能够访问到同一块共享内存,那么如何做到这一点呢?
答案:每一个共享内存被申请的时候都会有一个key值,这个key值用于标识系统中共享内存的唯一性。
传入shmget函数(shmget函数用于创建共享内存)的第一个参数key,需要我们使用ftok函数进行获取。ftok函数的函数原型如下:
key_t ftok(const char *pathname, int proj_id);
ftok函数的作用:通过数学运算将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。
ftok运算出来的key值可能会产生冲突,不过概率很小。如果产生冲突,就对ftok的参数进行修改即可。
需要访问同一块共享内存的进程(进程间通信),在使用ftok函数获取key值时,都要用同样的路径字符串和整数标识符,这样才能生成同一key值,然后不同的进程之间才能找到同一块共享内存,进而开始通信。
以下代码模拟了两个不同的进程,如何获取同一共享内存(key值)的过程:
运行结果:发现俩进程获取的是一样的key值!!!
共享内存的创建
创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:
int shmget(key_t key, size_t size, int shmflg);
参数解读:
- key:共享内存段的名字(就是上面我们通过ftok函数获取的key值)
- size:共享内存的大小
- shmflg:创建共享内存的方式
返回值:成功返回一个非负整数。即该共享内存段的标识码;
失败返回-1。
关于共享内存,一定是一个进程创建,另一个进程获取,并且最好是创建新的共享内存。关于shmget函数的第一个参数key前面已经解析过了,关于第二个参数,我们暂时将共享内存的大小设置为4096个字节,接下来我们开始讲解shmget函数的第三个参数,并教大家如何创建共享内存,那就开始吧!!!
第三个参数的组合方式:
组合方式 | 作用 |
---|---|
IPC_CREAT | 如果内核中对应key值的共享内存不存在,则新建一个共享内存并返回该共享内存的句柄;如果已存在,则直接返回该共享内存的句柄 |
– | – |
IPC_CREAT IPC_EXCL | 如果不存在对应key值的共享内存则新建一个共享内存并返回该共享内存的句柄;如果已存在,则出错返回 |
注意:
- IPC_CREAT:一定可以获得一个共享内存,但是无法不一定是新建的共享内存。
- IPC_CREAT | IPC_EXCL:只有shmget调用成功才会返回共享内存,并且该共享内存一定是新的。
创建共享内存shm,示例代码:server进程代表服务端,client代表客户端,server进程使用 IPC_CREAT | IPC_EXCL组合创建新的共享内存,client直接用 IPC_CREAT获取对应key值的共享内存即可。
//makefile
1 .PHONY:all
2 all:client server
3
4 client:client.cc
5 g++ -o $@ $^ -std=c++11
6 server:server.cc
7 g++ -o $@ $^ -std=c++11
8
9 .PHONY:clean
10 clean:
11 rm -f client server
//comm.hpp
1 #ifndef __COMM_HPP__
2 #define __COMM_HPP__
3
4 #include <iostream>
5 #include <cerrno>
6 #include <cstdio>
7 #include <cstring>
8 #include <cassert>
9 #include <string>
10 #include <sys/ipc.h>
11 #include <sys/shm.h>
12 #include <sys/types.h>
13 #include <sys/stat.h>
14
15
16
17 using namespace std;
18
19 #define PATHNAME "."
20 #define PROJID 0x6666
21
22 const int gsize = 4096;//共享内存的大小
23
24 //获取key值
25 key_t getKey()
26 {
27 key_t k = ftok(PATHNAME,PROJID);
28 if(k==-1)
29 {
30 cerr << errno << ":" << strerror(errno) << endl;
31 exit(1);
32 }
33 return k;
34 }
35 //将key值以十六进制的形式输出
36 string toHex(int x)
37 {
38 char buffer[64];
39 snprintf(buffer,sizeof(buffer),"0x%x",x);
40 return buffer;
41 }
42
43 static int createShmHelper(key_t k, int size, int flag)
44 {
45 int shmid = shmget(k, gsize, flag);
46 if(shmid == -1)
47 {
48 cerr << "error: "<<errno<<":"<<strerror(errno)<<endl;
49 exit(2);
50 }
51 return shmid;
52 }
53 //一个进程创建共享内存,一个进程获取
54 //这里server服务端创建,客户端获取
55 int createShm(key_t k,int size)
56 {
57 umask(0);
58 return createShmHelper(k,size,IPC_CREAT | IPC_EXCL | 0666);
59 }
60 int getShm(key_t k,int size)
61 {
62 return createShmHelper(k,size,IPC_CREAT);
63
64 }
65
66 #endif
//server.cc服务端
1 #include"comm.hpp"
2 #include<unistd.h>
3
4 int main()
5 {
6 //1.创建key
7 key_t k = getKey();
8 cout << "server: " << toHex(k) << endl;
9
10 //2.创建共享内存
11 int shmid = createShm(k,gsize);
12 cout << "server shmid: "<< shmid << endl;
13
14
15
16 return 0;
17 }
//client.cc
1 #include"comm.hpp"
2 #include<unistd.h>
3 int main()
4 {
5 //1.获取key值
6 key_t k = getKey();
7 cout << "client: "<<toHex(k) << endl;
8
9 int shmid = getShm(k,gsize);
10 cout << "client shmid: " << shmid << endl;
11
12 return 0;
13 }
运行结果:客户端和服务端获取到的key值和shmid都是一样的
注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于文件inode和文件描述符fd之间的的关系。
ipcs命令
使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
- -q:列出消息队列相关信息。
- -m:列出共享内存相关信息。
- -s:列出信号量相关信息。
共享内存的释放
当我们在某一个程序中使用shmget()接口创建了共享内存之后,当该进程退出,此时共享内存的生命周期并不会像匿名管道和命名管道一样,随着进程的结束而释放。
我们再次运行这两个进程,由上图可以看出,server进程显示该共享内存已存在,因此得出结论:共享内存的生命周期不随进程,而是随OS操作系统,进程需要主动删除所创建的共享内存,同时也说明了IPC资源是由内核提供并维护的。
那么接下来介绍两种释放共享内存的方法:
1.使用命令释放共享内存资源
使用ipcrm
命令释放共享资源:
ipcrm -m shmid
2.使用程序函数释放对应的共享内存资源
控制共享内存我们需要用shmctl函数,shmctl函数的函数原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
解读3个函数参数:
- shmid:是由shmget(创建共享内存函数)返回的共享内存标识码-句柄
- cmd:表示控制动作
- buf:指向一个保存着(共享内存的模式状态和访问权限-即属性)的数据结构
返回值:成功返回0,失败返回-1
第2个参数的有关选项如下:
命令 | 说明 |
---|---|
IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值 |
IPC_RMID | 删除共享内存段 |
在comm.hpp文件中添加删除共享内存函数,并在服务端调用该函数,随着进程的退出,共享内存也会被释放。
//删除共享内存段
void delShm(int shmid)
{
int n = shmctl(shmid,IPC_RMID, nullptr);
assert(n!=-1);
(void)n;
}
共享内存的关联与解除
我们不是单单把共享内存创建出来就行了,我们还要将需要获取这块共享内存的进程与对应的共享内存关联起来,通信结束后,解除关联,最后进行共享内存的释放。
共享内存的关联
关联共享内存需要用的函数是:shmat,shmat函数的函数原型如下:
void *shmat(int shmid, const void *shmaddr, int shmflg);
解读参数:
- shmid:是由shmget(创建共享内存函数)返回的共享内存标识码-句柄。
- shmaddr:按我们的指定将共享内存映射到进程地址空间的某一地址处,通常设置为NULL,让OS自助决定一个合理的位置。
- shmflg:表示关联共享内存设置的属性。设置为0,默认具有读写权限;SHM_RDONLY表示只读操作
返回值:
- 调用成功,返回共享内存映射到虚拟地址进程空间的起始地址。
- 调用失败,返回-1
示例代码:
解除与共享内存的关联
取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:
int shmdt(const void *shmaddr);
解读参数:
- shmaddr:进程所关联的共享内存的起始地址,即调用shmat函数时得到的返回值
返回值:
- shmdt调用成功,返回0。
- shmdt调用失败,返回-1。
示例代码:
代码演示
下面是认识,使用,验证共享内存的简单的测试代码,以便我们更好的理解共享内存。
C++版封装
共享内存的大小
共享内存的大小是以PAGE页(4kb)为单位的,用户需要多大,OS给予多大,不过如果超过特定的页,OS直接向上对齐到4kb的整数倍。
共享内存的特点
共享内存没有任何的保护机制(同步互斥),当共享内存中无数据时,进程依旧可以从共享内存中读取数据,而我们之前学过的管道,当管道当中的数据被读完了,操作系统read接口就会返回-1。其原因在于管道是通过系统接口通信的,共享内存是直接通信。