前言
最近做项目需求的时候,需要从共享内存读取数据,因为第一次接触共享内存(Shared Memory),特地去做了很多调研,写篇文章记录下。共享内存这个概念有好几种定义,有硬件层面的、内核层面等等,因为是程序员,本文主要讨论软件层面的共享内存概念。
Contents
- 共享内存作用
- 硬件层面
- 软件层面
3.1 Unix-like系统支持
3.2 Windows系统支持
3.3 跨平台支持
3.4 编程语言支持 - 共享内存的几种方式
- 参考引用
为什么要使用共享内存
在计算机科学中,共享内存是可以被多个进程同时访问的内存,目的是给多个进程通信,特别是交换数据,提供一种高效方式和避免不必要的拷贝。单个进程内,多个线程基于同一片内存进行通信,这也可以被称为共享内存。
硬件层面
在计算机硬件中,共享内存是指一个Random Access Memory(RAM)块,可以被多处理器计算机系统中的不同中央处理单元(Central Processing Uints)访问。
共享内存系统可能使用:
- 统一内存访问(Uniform Memory Access,UMA):所有处理器统一共享物理内存
- 非统一内存访问(Non-Uniform Memory Access,NUMA):非统一内存访问:内存访问时间取决于处理器的内存位置
据了解,目前大部分服务器的实现都是UMA,所有处理器访问内存上任意位置的时间都是相同的
-
仅用于高速缓存的内存体系架构(Cache-Only Memory Arch,COMA):每个节点处理器的本地内存用于告诉缓存,而不用于实际的主存。例如 x86 cpu的L1/L2/L3 缓存。
//硬件相关知识,日后再考虑补充更新,这里先提到这么多
软件层面
在计算机软件中,共享内存的作用主要是用于 进程间通信(Inter-Process Communicate,IPC) 或者 用于节省内存空间。
- IPC
在RAM中创建一个允许其他进程相互访问的内存区域(segment),用于给同时运行的多个进程提供一种交换数据的方式。 - 节省内存
设想有上下文相关的Program1,Program2,Program3,都需要维护数据A,如果是独立维护方式,需要在内存中开辟 3*A,而通过了虚拟内存地址映射或在相关程序的明确支持下,将需要访问的数据(通常是虚拟内存地址重定向)重定向到单一实例,最终仅需要维护一份A数据。
由于两个进程可以像访问自身内存空间一样访问共享内存区,因此这是一种非常快速搞笑的通信方式。但缺点也很明显,可扩展性差,多个进程强依赖共享内存,耦合性高,必须要运行于同一台机器上,如果有需要解耦,则可以考虑Internet domain sockets(不是unix domain sockets哦),基于计算机网络通信的IPC方式,只要通信双方网络可达即可。
unix-like系统共享内存实现方式如下:
POSIX API
如果对于POSIX、UNIX这些名词疑惑可以参考下 POSIX定义
POSIX共享内存 application programming interface,API 参考Unix System V API
mmap
wiki定义
api定义/dev/shm
mmap实现
- 什么是mmap
NAME
mmap, munmap - map or unmap files or devices into memory
SYNOPSIS
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
DESCRIPTION
mmap() creates a new mapping in the virtual address space of the calling process. The starting address for the new mapping is speci‐
fied in addr. The length argument specifies the length of the mapping.
If addr is NULL, then the kernel chooses the address at which to create the mapping; this is the most portable method of creating a new
mapping. If addr is not NULL, then the kernel takes it as a hint about where to place the mapping; on Linux, the mapping will be cre‐
ated at a nearby page boundary. The address of the new mapping is returned as the result of the call.
The contents of a file mapping (as opposed to an anonymous mapping; see MAP_ANONYMOUS below), are initialized using length bytes starting at offset offset in the file (or other object) referred to by the file descriptor fd. offset must be a multiple of the page size as returned by sysconf(_SC_PAGE_SIZE).
...
mmap用于将文件或设备映射到进程地址空间内,使得进程在进程内可以直接访问。基于该特性,Linux系统用它实现多进程的内存共享功能。Linux产生子进程的系统调用是fork,根据fork的语义以及其实现可知,新进程的内存地址空间和父进程的地址空间是完全一致的,所以,Linux的mmap实现了一种可以在父子进程之间共享内存地址的方式,其使用方法是:
- 父进程将flags参数设置 MAP_SHARED 方式通过mmap申请一段内存。内存可以映射某个具体文件,也可以不映射具体文件(将fd设置为-1,flag设为 MAP_ANONYMOUS)
- 父进程调用fork产生子进程,之后在父子进程内都可以访问到mmap所返回的地址,从而实现共享内存。
- 父进程通过fork产生子进程后,两进程的地址空间是独立"生长"的。可以通过mmap的匿名映射模式轻松实现共享内存目的
- 通过mmap实现的共享内存,仅能在父子进程间通信,其他进程是无法得到该共享内存段地址的
下面代码通过mmap申请10M虚拟内存大小:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MEMSIZE 1024*1024*10
int main()
{
pid_t pid;
int count;
void *shm_p;
shm_p = mmap(NULL, MEMSIZE, PROT_WRITE|PROT_READ, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if (MAP_FAILED == shm_p) {
perror("mmap()");
exit(1);
}
bzero(shm_p, MEMSIZE);
sleep(3000);
munmap(shm_p, MEMSIZE);
exit(0);
}
可以看到,通过mmap申请的内存是被记录到shared和buff/cache中的:
[root@localhost shared_mem]# free -m
total used free shared buff/cache available
Mem: 972 321 118 9 532 469
Swap: 2047 27 2020
[root@localhost shared_mem]#
[root@localhost shared_mem]#
[root@localhost shared_mem]#
[root@localhost shared_mem]# ./mmap_mem &
[1] 62268
[root@localhost shared_mem]# free -m
total used free shared buff/cache available
Mem: 972 321 108 19 542 459
Swap: 2047 27 2020
System V 实现
考虑到mmap只适用于父子进程间内存共享的这一局限性,为了满足无关进程间共享内存的需求,Linux提供了更具通用性的手段:System V (XSI)共享内存。就是我们常用的shmctl 相关调用:
#include
#include
int shmget(key_t key, size_t size, int shmflg);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
- 相关参数理解
- key参数
key主要解决的是 "在系统中,如何让2个不相关的进程共同标识和使用一个内存段?"的问题, 一个现成的解决方案是使用 文件 。文件的存在可以使得无关进程进行数据交换,只需要记住文件路径和名字即可。通过fork产生的子进程会继承父进程的文件描述符列表,但子进程也可直接通过路径和文件名读写。但基于文件的通信(数据交换)效率太低,很少通过这种方式进行直接通信的。
在某些情况下,我们也可以不用通过一个key来生成共享内存。此时key参数填:IPC_PRIVATE,这样内核会在保证不产生冲突的共享内存段id的情况下新建一段共享内存。当然,这样调用则必然意味着是新创建,而不是打开已有得共享内存,所以flag位一定是IPC_CREAT。此时产生的共享内存只有一个shmid,而没有key,所以可以通过fork的方式将id传给子进程。
shmid参数
key的作用类似于文件名,shmget返回的int类型类似文件描述符。对于一个XSI的共享内存,key是系统全局唯一的,使用fork产生的子进程,可以直接通过shmid去访问相关共享内存段。key的产生
#include
#include
key_t ftok(const char *pathname, int proj_id);
(1)方式一:通过传进一个pathname和project_id,使用ftok调用产生的。这种方式适用于同个项目中相互通信场景,不过需要提前约定好文件路径、名称以及项目id。
(2)方式二:自定义方式key,key_t 其实是一个int类型的重新声明,直接传 自定义的key给shmget也是可以的
size
指定需要创建/读取的共享内存段的大小,一般创建和读取要保持一致shmflag
支持: IPC_CREAT、IPC_EXCL、SHM_HUGETLB、SHM_NORESERVE
如果同时指定IPC_CREAT、IPC_EXCL,且指定key已存在,则报错。
访问一个已经存在的共享内存,此时可以将shmflg置为0,不加任何标识打开。
- 使用
通过自定义shmkey创建System v 共享内存(不自动回收)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int count;
int *shm_p;
int shm_id;
/* 使用shm_key=1000创建一个共享内存,如果系统中已经存在
此共享内存则报错退出,创建出来的共享内存权限为0600 */
shm_id = shmget(1000, sizeof(int), IPC_CREAT|IPC_EXCL|0600);
if (shm_id < 0) {
perror("shmget()");
exit(1);
}
/* 将创建好的共享内存映射进父进程的地址以便访问。 */
shm_p = (int *)shmat(shm_id, NULL, 0);
if ((void *)shm_p == (void *)-1) {
perror("shmat()");
exit(1);
}
/* 共享内存赋值为1000 */
*shm_p = 1000;
/* 显示当前共享内存的值 */
printf("shm_p: %d\n", *shm_p);
/* 解除共享内存地质映射。 */
//shmdt() 与文件的close() 类似,并不会真正清除。
if (shmdt(shm_p) < 0) {
perror("shmdt()");
exit(1);
}
/* 删除共享内存。 */
//if (shmctl(shm_id, IPC_RMID, NULL) < 0) {
// perror("shmctl()");
// exit(1);
//}
exit(0);
}
验证:
[root@localhost shared_mem]# ipcs
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 nattch 状态
--------- 信号量数组 -----------
键 semid 拥有者 权限 nsems
[root@localhost shared_mem]#
[root@localhost shared_mem]#
[root@localhost shared_mem]# ./shm_mem
shm_p: 1000
[root@localhost shared_mem]#
[root@localhost shared_mem]#
[root@localhost shared_mem]#
[root@localhost shared_mem]#
[root@localhost shared_mem]# ipcs
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 nattch 状态
0x000003e8 3 root 600 4 0
--------- 信号量数组 -----------
键 semid 拥有者 权限 nsems
十六进制 0x000003e8 转换成十进制等于1000,验证通过。
读共享内存:
// 唯一差别在于 flags 参数,此时传0说明只读
/* 读取shm_key=1000 共享内存段,key不存在会报错 */
shm_id = shmget(1000, sizeof(int), 0);
if (shm_id < 0) {
perror("shmget()");
exit(1);
}
在使用之后要记得使用shmdt解除映射,否则对于长期运行的程序可能造成虚拟内存地址泄漏,导致没有可用地址可用。shmdt并不能删除共享内存段,而只是解除共享内存和进程虚拟地址的映射,只要shmid对应的共享内存还存在,就仍然可以继续使用shmat映射使用。想要删除一个共享内存需要使用shmctl的IPC_RMID指令处理。也可以在命令行中使用ipcrm删除指定的共享内存id或key。
[root@localhost shared_mem]# ipcrm --help
用法:
ipcrm [options]
ipcrm [...]
选项:
-m, --shmem-id 按 id 号移除共享内存段
-M, --shmem-key <键> 按键值移除共享内存段
-q, --queue-id 按 id 号移除消息队列
-Q, --queue-key <键> 按键值移除消息队列
-s, --semaphore-id 按 id 号移除信号量
-S, --semaphore-key <键> 按键值移除信号量
-a, --all[=] 全部移除
-v, --verbose 解释正在进行的操作
-h, --help 显示此帮助并退出
-V, --version 输出版本信息并退出
- 与mmap区别
XSI共享内存跟mmap在实现上并没有本质区别,System v共享内存实际上也会占用shared和buff/cache,实际上,在内核底层实现上,两种共享内存都是使用 tmpfs 方式实现的,所以两者对于物理内存的使用是一致的。唯一不同的就是前者适用于无关进程之间共享内存,后者仅适用于父子进程间。
POSIX 方式
参考理解:https://cloud.tencent.com/developer/article/1005543?from=article.detail.1005542
https://www.cnblogs.com/huxiao-tee/p/4660352.html
https://cloud.tencent.com/developer/article/1021157?from=information.detail.linux%20%E5%90%91%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98%E5%86%99%E6%95%B0%E6%8D%AE