共享内存原理

本文主要说一下共享内存的原理

本文源码来自于 glibc 2.27

一、共享内存的底层实现

我们看一下 shm_open 的源码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int shm_open (const char *name, int oflag, mode_t mode) {
SHM_GET_NAME (EINVAL, -1, "");
oflag |= O_NOFOLLOW | O_CLOEXEC;
/* Disable asynchronous cancellation. */
int state;
pthread_setcancelstate (PTHREAD_CANCEL_DISABLE, &state);
int fd = open (shm_name, oflag, mode);
if (fd == -1 && __glibc_unlikely (errno == EISDIR))
/* It might be better to fold this error with EINVAL since
directory names are just another example for unsuitable shared
object names and the standard does not mention EISDIR. */
__set_errno (EINVAL);
pthread_setcancelstate (state, NULL);
return fd;
}

可以看到 shm_open 的底层其实也是调用的 open 函数,打开的一个文件描述符。其中 shm_name 由宏定义 SHM_GET_NAME 来获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#define	_PATH_DEV	"/dev/"
# define SHMDIR (_PATH_DEV "shm/")

const char * __shm_directory (size_t *len) {
*len = sizeof SHMDIR - 1;
return SHMDIR;
}

#define SHM_GET_NAME(errno_for_invalid, retval_for_invalid, prefix) \
size_t shm_dirlen; \
const char *shm_dir = __shm_directory (&shm_dirlen); \
/* If we don't know what directory to use, there is nothing we can do. */ \
if (__glibc_unlikely (shm_dir == NULL)) \
{ \
__set_errno (ENOSYS); \
return retval_for_invalid; \
} \
/* Construct the filename. */ \
while (name[0] == '/') \
++name; \
size_t namelen = strlen (name) + 1; \
/* Validate the filename. */ \
if (namelen == 1 || namelen >= NAME_MAX || strchr (name, '/') != NULL) \
{ \
__set_errno (errno_for_invalid); \
return retval_for_invalid; \
} \
char *shm_name = __alloca (shm_dirlen + sizeof prefix - 1 + namelen); \
__mempcpy (__mempcpy (__mempcpy (shm_name, shm_dir, shm_dirlen), \
prefix, sizeof prefix - 1), \
name, namelen)

从以上的代码,我们可以清晰的看出,shm_name 的组成,一部分是 /dev/shm/,另外一部分是用户传入的 name。最终一起组成文件的地址 /dev/shm/user_name

二、为什么共享内存要放在 /dev/shm 下呢?

/dev/shm 一般是一种特殊的文件系统 tmpfs

1
2
3
4
# df -h
Filesystem Size Used Avail Use% Mounted on
tmpfs 1.9G 0 1.9G 0% /dev/shm
...

tmpfs 的解释:

1
2
3
4
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 实例,那么存储的内容将全部丢失。

那么如果机器重启,共享内存会丢失,毕竟是内存嘛

那么创建共享内存的进程要是重启呢?共享内存还会在吗?那么现在产生了一个问题:虚拟内存必须要映射到物理内存,才可以使用,tmpfs 的虚拟内存会映射到哪里呢?

三、“共享内存” 位于哪里?

先来看第一个问题:

1
2
3
4
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 会显示为 cached,不会显示为 shared 或其他。可以通过 df 和 du 命令查看 tmpfs 真实使用的物理内存和 swap 的大小。

我们来做实验论证一下。我的机器是 ubuntu22.04

  • 我们首先通过 df、free 来查看当前的 /dev/shm 占有空间和 buffer/Cache 的占用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # df -h
    Filesystem Size Used Avail Use% Mounted on
    tmpfs 1.9G 0 1.9G 0% /dev/shm
    ...

    # free -h
    total used free shared buff/cache available
    Mem: 3.8Gi 998Mi 773Mi 43Mi 2.0Gi 2.5Gi
    Swap: 3.6Gi 9.0Mi 3.6Gi

    我们看到此时 /dev/shm 中没有空间占用,buff/cache 为 2G

  • 然后我们在 /dev/shm 中创建一个 100MB 的文件

    1
    # dd if=/dev/zero of=/dev/shm/hello bs=1M count=100
  • 再来查看占用情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    df -h
    Filesystem Size Used Avail Use% Mounted on
    tmpfs 1.9G 100M 1.8G 6% /dev/shm
    ...

    # free -h
    total used free shared buff/cache available
    Mem: 3.8Gi 992Mi 678Mi 143Mi 2.1Gi 2.4Gi
    Swap: 3.6Gi 9.0Mi 3.6Gi

    我们发现 /dev/shm 此时占用 100M,而 buff/cache 增加了 100M。验证了的确 /dev/shm 是被创建在 buff/cache 中的

  • 我们再来测试删除情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # free -m
    total used free shared buff/cache available
    Mem: 3871 996 668 143 2205 2449
    Swap: 3715 9 3706
    # rm /dev/shm/hello
    # free -m
    total used free shared buff/cache available
    Mem: 3871 1013 751 43 2105 2532
    Swap: 3715 9 3706

至此,我们通过实验论证了 tmpfs 文件系统中内存空间占用,位于 buff/cache 中。

也回答了创建共享内存的进程即使 crash 掉,也不会影响共享内存,因为共享内存存在于操作系统内核的 buff/cache 中。

四、注意

在使用共享内存时,注意要挂载

1
2
3
4
5
6
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.

程序员在使用 POSIX 的时候,记得在 /etc/fstab 中编辑一下,把 tmpfs 分区进行挂载。