共享内存和文件内存映射的区别

1. 共享内存和文件内存映射有什么区别?

我们的系统中,本来使用的是共享内存来进行进程间通信,共享内存文件的位置在/dev/shm/.但是后面要迁移至某平台,而据说平台的容器中并没有这个分区,所以无法使用共享内存。大佬给支招,“可以用mmap内存映射,把文件映射到内存中,和原来用共享内存差不多”。经过我一番折腾,发现,把原来使用boost库共享内存的接口改为使用文件内存映射,一共改动不超过10行。用文件内存映射的方式运行程序,未见任何异常,而要映射的文件,却可以不用放在/dev/shm/下。大佬又说:“我见过的系统基本都用的文件内存映射,没怎么见过用共享内存的,不知道为什么这个系统开始设计时候一定要用共享内存”。这引起了我的好奇,文件内存映射和共享内存到底有什么区别?

网上有关这部分的文章,没几篇原创,看来看去就那几篇来回复制,说的也不明不白,大概就是告诉你怎么用,看完之后更是懵逼。于是查阅编程手册。

各种编程手册,相关内容基本分成三部分:

1. 内存映射->文件内存映射,内存映射分为2种,文件内存映射和匿名映射。内存映射相关的数据结构如何具体的可以看这篇文章:*。编程手册中基本是介绍如何使用mmap系统调用把一个文件映射到内存中。使用方法一般是open()一个文件之后把文件描述符传入mmap()进行映射。

2. 共享内存:主要告诉你,共享内存是最快的进程间通信方式,然后:

2.1 Posix接口,共享内存的POSIX接口,使用方法:shm_open()、mmap()

2.2 System V接口,使用方法shmget()、shmmat()....

看完之后,有了个大概轮廓,就POSIX接口的共享内存来说,他们的底层都是调用mmap,不同的只是共享内存打开文件调用shm_open(),而内存映射调用open()。那shm_open有什么神奇的呢?看了下glibc2.29中的源码:

/* Open shared memory object. */ 
int 
shm_open (const char *name, int oflag, mode_t mode) 
{ 
SHM_GET_NAME (EINVAL, -1, ""); 
oflag |= O_NOFOLLOW | O_CLOEXEC; 
int state; 
pthread_setcancelstate (PTHREAD_CANCEL_DISABLE, &state); 
int fd = open (shm_name, oflag, mode); 
...... 
pthread_setcancelstate (state, NULL); 
return fd; 
} 

代码里赫然写着open,看来shm_open只不过是封装了一个更安全的open而已。

源码也同时解决了我一个大疑问:

2. 为什么共享内存一定要放在/dev/shm/下?

上面代码中,shm_open传入的参数叫name,然而到open的时候却变成了shm_name,但是前后却找不到shm_name定义的地方。大佬们写的代码都这么神奇的吗?仔细一看,原来在上边的宏里:

SHM_GET_NAME (EINVAL, -1, ""); 

宏中声明了shm_name,构造出shm_dir,shm_dir的内容就是/dev/shm/,之后通过和name拼接:

__mempcpy (__mempcpy (__mempcpy (shm_name, shm_dir, shm_dirlen), \
prefix, sizeof prefix - 1),name,namelen) 

生成了shm_name。

(老实说,不大明白为什么要通过定义一个宏的方式实现这块,按说也可以通过函数调用来达到降低代码重复率的问题啊,这个宏没有任何入参,代码的可读性感觉不大好,容易让人迷惑)

但是这只是解释了是怎么干的,却没有解释,为什么要这么干,为什么GlibC要在代码里写死把共享内存放在这个分区下呢?

继续在网上找,找来找去,也没有说清楚的。大多数都只是陈述事实而已。但是其中的一些附带描述引起了我的注意,描述中多次提到,/dev/shm/使用了一种特殊的文件系统tmpfs,它是虚拟的,并不是磁盘上的真实的分区,它如何快,如何好,如何厉害等等……通过df命令查看/dev/shm/分区,类型确实写着tmpfs,而使用文件内存映射生成的文件,却平平无奇,看不出有什么特殊的。莫非,共享内存和文件内存映射的区别在这里?共享内存使用了一种特殊的文件系统,而文件内存映射没有吗?但是他们明明传入mmap的参数也是一样的啊……

3. 我目前所知的mmap:

mmap干了什么,研究这个问题之前,我知道个大概。

每个进程,在内核中都有对应的数据结构管理。按照《Linux内核源码情景分析》里的形容,这个数据结构就好像一个人的户口一样。你这个人在物理上存不存在不知道,但是只要你在公安局注册了户口,那么在法律上你就是存在的。进程也一样,你在不在物理内存上,不确定,但是你在内核里进程管理的数据结构上注册了,那内核就认为你是存在的。

在内核里管理进程的数据结构是task_struct,管理内存的数据结构是mm_struct,具体到这个进程的虚拟空间哪块分配了,分配的空间地址是从哪到哪,里面内容是运行时临时分配用的(匿名映射)还是跟一个文件关联上(文件映射),那是由mm_struct里面的vma_struct组成一棵树来管理的。每个vma_struct就表示一块在内核注册了的虚拟内存。这个结构又根据一系列数据结构,和文件系统中的某个文件关联上了,这就建立起了虚拟内存和磁盘上文件之间的联系。

所以mmap所做的事,就是在内核对应的进程数据结构里新建了一个vma_struct结构体,然后通过一系列操作把文件和这个vma_struct结构体联系起来了而已。实际上mmap进行完之后,只是注册,并没有真的在物理内存上分配一块空间给这个文件。而是在程序运行时,真的要访问这个文件/这块内存时,才会引发缺页中断,把相应的文件中的内容写入到物理内存。

以上是我已知的mmap流程。从这里丝毫看不出,tmpfs、共享内存、内存映射之间的联系。

4. tmpfs是什么?

网上的描述基本上说这个东西是一个虚拟的文件系统,然后。。。

看的我云里雾里,干脆去内核里搜一下,结果一搜直接在Document里找到一个tmpfs.txt,好嘛得来全不费工夫。

文档没有详细解释实现细节,但是就从一个使用者的角度来说,已经基本解答了上面的疑惑。

Tmpfs is a file system which keeps all files in virtual memory. 
Everything in tmpfs is temporary in the sense that no files will 
be created on your hard drive. If you unmount a tmpfs instance, 
everything stored therein is lost. 

这段说tmpfs是一个文件系统,所有的文件都存在于虚拟内存中,物理磁盘上没有任何文件。如果把一个tmpfs实例解挂了,其中存储的内容就丢失了。这段和网上说的如出一辙,对一个新手来说一样的云里雾里。什么叫在虚拟内存里?虚拟内存不就是个抽象吗?顶多就是内核的一个数据结构啊。没有文件在磁盘上?那文件去哪了?在物理内存上吗?不大对啊,物理内存直接当硬盘使吗?

Since tmpfs lives completely in the page cache and on swap, all tmpfs
pages currently in memory will show up as cached. It will not show up
as shared or something like that. Further on you can check the actual 
RAM+swap use of a tmpfs instance with df(1) and du(1). 

这段说tmpfs文件系统完全存活在page cache和swap中,当前在内存中的tmpfs页会显示为cacheed,不会被显示为shared或其他。可以通过df和du命令查看tmpfs实例真实使用的物理内存+swap大小。

什么是page cache和swap,我也是后来才知道的。当时并不完全理解。但是后边说的cached和shared,我倒是常见。

随便找一台测试机器,用free -g命令看下:

[xxxxx ~]# free -g 
total used free shared buff/cache available 
Mem: 124 0 33 0 90 82 
Swap: 0 17575006175232 17179869183

shared为0,buff/cache为90G

然后删除/dev/shm/下的几个文件看看:

[xxxxx ~]# rm /dev/shm/News_Share_Memory_V10 
[xxxxx ~]# free -g 
total used free shared buff/cache available 
Mem: 124 0 36 0 87 84 
Swap: 0 17575006175232 17179869183 
[xxxxx ~]# rm /dev/shm/Video_Share_Memory_V10 
[xxxxx ~]# free -g 
total used free shared buff/cache available 
Mem: 124 0 39 0 84 88 
Swap: 0 17575006175232 17179869183 

嗯,大小果真对的上,证明文中所言非虚,tmpfs文件系统中的实例,会显示在cache中,而不是shared,尽管我们会管这个叫”共享内存“。

下面还有两段有用的:

There is always a kernel internal mount which you will not see at 
all. This is used for shared anonymous mappings and SYSV shared 
memory. 
This mount does not depend on CONFIG_TMPFS. If CONFIG_TMPFS is not 
set, the user visible part of tmpfs is not build. But the internal 
mechanisms are always present. 

这段是说着,内核内部会有一个挂载,你是看不到的,当你使用匿名共享内存映射或者System V共享内存的时候,会用到这个隐藏的挂载。而且,即使内核中的编译选项CONFIG_TMPFS没有设置,tmpfs那没有编译,这个内部隐藏的机制依然有效。

这点对我们来说意义重大,这意味着,共享内存和tmpfs文件系统,都是使用了内核提供的机制来实现的,共享内存的实现,和tmpfs系统是没有依赖关系的。那么这个机制到底是啥呢?到底是怎么实现的呢?在此挖个坑。

glibc 2.2 and above expects tmpfs to be mounted at /dev/shm for 
POSIX shared memory (shm_open, shm_unlink). Adding the following 
line to /etc/fstab should take care of this: 
tmpfs /dev/shm tmpfs defaults 0 0 
Remember to create the directory that you intend to mount tmpfs on 
if necessary. 

这段就更直白了,glibc在实现POSIX共享内存时,会预设tmpfs文件系统挂载在/dev/shm/,所以用POSIX共享内存的时候,记得在/etc/fstab里编辑一下,把这个分区给挂载上。

5. 总结

至此,最初的疑惑已经解决了。

共享内存和文件内存映射有什么区别?

首先是共享内存和文件内存映射的接口、用法不一样,GLIBC的POSIX的共享内存实现会默认把共享内存文件放在/dev/shm/分区下,如果没有这个分区,需要手动挂载一下。

然后就是共享内存和文件内存映射,在内核中的实现原理,都使用了内核的cache和swap机制,完全没有区别。

至于文中衍生的其他疑惑,在下一篇文章中继续研究。

编辑于 06-18