角声满天秋色里
塞上燕脂凝夜紫
之前有写过一篇文章 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“提交”到指定的文件里。
**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的解释:
简要翻译:tmpfs是一种虚拟文件系统,允许直接在RAM上直接创建文件。当我们用以下命令挂载一个tmpfs类型的文件系统时,tmpfs就会自动创建
sudo mount -t tmpfs -o size=10M tmpfs /mnt/mytmpfs
该文件系统存在以下几个属性:
- 当物理内存不足时,tmpfs可以使用swap的空间
- tmpfs只会使用一定的物理空间和swap空间
- 如果重新挂载,可以改变tmpfs的大小
如果tmpfs文件系统被卸载,则上面存储的内容也会丢失。
总结一下的话,tmpfs被当成一种存储在内存的中的文件系统来使用。
tmpfs和共享内存的关系
还是看文档:
文档中透露几个相关信息:
- 为了能够应用在用户空间,内核必须提供配置选项
- 一种内置的共享内存文件系统用于实现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_unlink
和 close
,会直接导致共享区域被关闭。
#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
文件:
其文件大小正好就是我们指定的 1024
,然后看下文件内容:
可以看到写入了字母 a
,说明共享内存写入成功。
如果把这个文件删掉,会发生什么?
可以看到 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);