共享内存和tmpfs

操作系统

Posted by bbkgl on July 26, 2020

角声满天秋色里

塞上燕脂凝夜紫

之前有写过一篇文章 mmap基本用法和共享内存,讲了通过mmap使用共享内存的基本操作,但是对共享内存原理(sys v, posix)基本没怎么介绍,这里当是自己学习,好好深挖一波。

最难受的是之前面过一次wxg二面,面了三个小时(确实是3小时。。。我这么菜完全不值得3小时),面试官当时问了我共享内存(System v, POSIX)两种实现原理有什么区别。听完问题就觉得完了,我连sys v,posix是什么都不知道,偶尔听到过,但完全没去查过,更别说 unix 接口的不同标准的实现了。

为了避免太尴尬,我说这个问题可以理解成共享内存的两套API “shmat/mmap” 之间的区别吗?面试官说可以。于是又胡扯了一堆,什么/dev/shm,虚拟文件系统,swap分区都扯上了。最后不出意外地地被挂了,现在想想还不如直接只把自己知道的列一下,而不是胡扯。。。

这篇文章 mmap基本用法和共享内存 是我面试前写的,感觉面试官应该是看了我博客以后,有意问的。

前言

部分参考 共享内存和文件内存映射的区别

既然是写文章,肯定存在资料搜集的过程,这个过程会阅读很多其他人的相关文章。。。阅读后,真的被很多抄来抄去的文章恶心到了,抄就算了,还抄个错的。。。

Linux中存在两种共享内存 (Shared Memory)机制:

System V shared memory(shmget/shmat/shmdt)

  • Original shared memory mechanism, still widely used
  • Sharing between unrelated processes

POSIX shared memory(shm_open/shm_unlink)

  • Sharing between unrelated processes, without overhead of filesystem I/O
  • Intended to be simpler and better than older APIs

还有一种就是内存映射/文件映射,也是用mmap,不知道属不属于 POSIX

有的文章把 “内存映射/文件映射” 算进 POSIX 共享内存。。。有的不算,不过我认为不算,毕竟其映射的文件不在 /dev/shm 下,而是通过page cache“提交”到指定的文件里。

image-20200726141654196

**Shared mappings – mmap(2) **

  • Shared anonymous mappings:Sharing between related processes only (related via fork())
  • Shared file mappings:Sharing between unrelated processes, backed by file in filesystem

这里依次对tmpfs,以及几种共享内存的实现原理进行讲解。

tmpfs

tmpfs是什么?

文章信息来源要权威,所以先看看linux文档对tmpfs的解释:

1595774853010

简要翻译:tmpfs是一种虚拟文件系统,允许直接在RAM上直接创建文件。当我们用以下命令挂载一个tmpfs类型的文件系统时,tmpfs就会自动创建

sudo mount -t tmpfs -o size=10M tmpfs /mnt/mytmpfs

该文件系统存在以下几个属性:

  • 当物理内存不足时,tmpfs可以使用swap的空间
  • tmpfs只会使用一定的物理空间和swap空间
  • 如果重新挂载,可以改变tmpfs的大小

如果tmpfs文件系统被卸载,则上面存储的内容也会丢失。

总结一下的话,tmpfs被当成一种存储在内存的中的文件系统来使用。

tmpfs和共享内存的关系

还是看文档:

1595775435506

文档中透露几个相关信息:

  • 为了能够应用在用户空间,内核必须提供配置选项
  • 一种内置的共享内存文件系统用于实现System V共享内存和匿名内存映射。
  • 一种挂载在 /dev/shm 的tmpfs文件系统用于实现 POSIX 共享内存,和共享信号量???
  • 通过 /proc/meminfo 可以查看/dev/shm 消耗的tmpfs的容量,以及通过 free 命令显示的 shared 列也可以看到(这两种方式其实是一样的值)

这个已经解决了我们一个很大的疑惑。。。两种常用的共享内存异同点到底在哪里:

  • 同:两种共享内存都通过tmpfs实现
  • 异: POSIX 共享内存 用户可以进行手动mount和配置,其映射的文件都在 /dev/shm 下,用户可见;而 System V 共享内存匿名内存映射 则由操作系统内核管理,用户不可见,其实也是挂载了文件系统,只是对用户不可见,且用户也不能直接配置。。。

还有一种共享内存,也就是刚刚说的 文件映射共享内存

也就是我之前文章 mmap基本用法和共享内存 里将的通过mmap使用共享内存。

这种应该就是利用不同进程读写同一个文件,不过这个文件被各个进程映射到了各自的内存空间里,然后通过 page cache 进行“刷新和提交”。

POSIX 共享内存

可能只介绍POSIX 共享内存 ,因为实现起来简单。。。

写一个简单的程序,创建一个共享内存区域,然后写入一个字母 a

这里注意不要调用 shm_unlinkclose,会直接导致共享区域被关闭。

#include <iostream>
#include <sys/mman.h>
#include <fcntl.h>
#include <cstring>
#include <zconf.h>

constexpr int MAP_SIZE = 1024;

int main() {
    std::string fname = "bbkgl";
    int fd = shm_open(fname.c_str(), O_RDWR | O_CREAT, 0777);
    if (fd < 0) {
        std::cerr << "shm_open failed";
        return -1;
    }
    ftruncate(fd, MAP_SIZE);
    void *addr = mmap(nullptr, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr != MAP_FAILED) {
        int val = 97;
        std::memcpy(addr, &val, sizeof(int));
    }
//    shm_unlink(fname.c_str());
//    close(fd);
    return 0;
}

执行完就能发现在 /dev/shm 目录下多了一个 bbkgl 文件:

image-20200727133652000

其文件大小正好就是我们指定的 1024,然后看下文件内容:

image-20200727133803849

可以看到写入了字母 a,说明共享内存写入成功。

如果把这个文件删掉,会发生什么?

image-20200727134007473

可以看到 shared 的变化:154604 -> 154600 ,单位是k,说明正好少了4k。。。

实际上我们只申请了1k,这是因为虚拟空间页对齐就是4k,申请的最小单位也是4k。

删除后,这部分就还给了操作系统,所以 shared 少了4k。

其他进程如果要进行共享内存通信,打开同一个文件然后写入/读取就可以了。

这里有个问题:既然 shm_open 是在 /dev/shm 下创建一个文件,那直接用 open/dev/shm 下创建一个文件是不是能达到同样的效果???

答案:可以。

我们可以看下 shm_open 的实现:

/* 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;
  /* 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;
}

其实现是直接用 open 打开一个路径为 shm_name 的文件。

这个 shm_name 其实就是 /dev/shm/{name} 的拼接,可以看宏 SHM_GET_NAME 的实现:


#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)
#endif        /* shm-directory.h */

所以实际上shm_open 就是在 /dev/shm 下创建一个文件,借助之前说到的tmpfs实现共享内存。

结论

这里结论也参考 浅析Linux的共享内存与tmpfs文件系统,感谢大佬!

  • POSIX 共享内存System V 共享内存 都通过tmpfs实现
  • POSIX 共享内存System V 共享内存 使用的tmpfs实例不一样,即 POSIX 共享内存 的文件系统可以由用户(重新)挂载并设置大小,系统默认挂载为物理内存的 1/2;而 System V 共享内存 由操作系统内核在初始化时挂载,用户不可见,用户可以通过 ` ipcmk -M ` 设置大小,通过在 ` /proc/sys/kernel/shmmax` 文件中查看大小

其实很多信息在内核源码里都比较清楚了,下面是挂载 System V 共享内存 使用的tmpfs的部分:

//mm/shmem.c
static struct file_system_type shmem_fs_type = {
	.owner		= THIS_MODULE,
	.name		= "tmpfs",
	.get_sb		= shmem_get_sb,
	.kill_sb	= kill_litter_super,
};

int __init shmem_init(void)
{
...
	error = register_filesystem(&shmem_fs_type);
	if (error) {
		printk(KERN_ERR "Could not register tmpfs\n");
		goto out2;
	}
	///挂载tmpfs(用于SYS V)
	shm_mnt = vfs_kern_mount(&shmem_fs_type, MS_NOUSER,
				 shmem_fs_type.name, NULL);