1、共享内存
共享内存允许两个或多个进程共享一给定的存储区,是最快的一种进程间通信机制。对于像管道和消息队列等通信方式,需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。共享内存系统仅在建立共享内存区域时需要系统调用;一旦建立共享内存,所有访问都可作为常规内存访问,无需借助内核
共享内存可以通过mmap()映射普通文件(特殊情况下还可以采用匿名映射)机制实现,也可以通过系统V(shm)共享内存机制实现。应用接口和原理很简单,内部机制复杂。共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。为了实现更安全通信,往往还与信号灯等同步机制共同使用。
C programming in the UNIX environment的编程手册,一般都会为进程间用共享内存的方法通信提供两组方法:由于POSIX标准比较通用,一般建议使用该标准定义的方法集。
1. POSIX定义的 :shm_open、shm_unlink、ftruncate、fstat、mmap、munmap、msync、mprotect
2. SYSTEM V定义的 :shmget、shmat、shmdt、shmctl、ftok
关于这两者的差异稍微提一下:
-
二者本质上是类似的,mmap可以看到文件的实体,而 shmget 对应的文件在交换分区上的 shm 文件系统内,无法直接 cat 查看
-
安全性:mmap 方式对应的真实文件,如果用户有权限即可查看,甚至删除
shmget 方式其实也一样,好了一层皮罢了(ipcrm -m …) -
一致性:mmap 方式下各进程映射文件的相同部分可以共享内存
shmget 时各个进程共享同一片内存区
不建议使用交叠的方式使用 mmap -
持续性:进程挂了重启不丢失内容,二者都可以做到
机器挂了重启,mmap 可以不丢失内容(文件内保存了OS同步过的映像),而 shmget 会丢失 -
易用性:mmap 的接口会简单一些
-
通用性:posix 的 mmap 会相对广泛一些
-
其他:mmap在某些内核版本下会频繁读写磁盘,需要注意一下
-
(1)mmap保存到实际硬盘,实际存储并没有反映到主存上。优点:储存量可以很大(多于主存)(这里一个问题,需要高手解答,会不会太多拷贝到主存里面???);缺点:进程间读取和写入速度要比主存的要慢。
(2)shm保存到物理存储器(主存),实际的储存量直接反映到主存上。优点,进程间访问速度(读写)比磁盘要快;缺点,储存量不能非常大(多于主存)
使用上看:如果分配的存储量不大,那么使用shm;如果存储量大,那么使用mmap。 -
mmap系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。
-
1、mmap是在磁盘上建立一个文件,每个进程地址空间中开辟出一块空间进行映射。而对于shm而言,shm每个进程最终会映射到同一块物理内存。shm保存在物理内存,这样读写的速度要比磁盘要快,但是存储量不是特别大。
2、相对于shm来说,mmap更加简单,调用更加方便,所以这也是大家都喜欢用的原因。
3、另外mmap有一个好处是当机器重启,因为mmap把文件保存在磁盘上,这个文件还保存了操作系统同步的映像,所以mmap不会丢失,但是shmget就会丢失。
如果你担心会因误删文件导致 mmap 出错,那就用 shmget 吧,否则的话直接mmap就可以了,用起来简单一些
2、共享内存 - POSIX - shm_open、shm_unlink、ftruncate、fstat、mmap、munmap、msync、mprotect
实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。
2.1 mmap
#include <unistd.h>
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offsize);
函数说明:mmap()用来将某个文件内容映射到内存中,对该内存区域的存取即是直接对该文件内容的读写。
参数说明:
参数 | 说明 |
---|---|
start | 指向欲对应的内存起始地址,通常设为NULL,代表让系统自动选定地址,对应成功后该地址会返回。 |
length | 代表将文件中多大的部分对应到内存。 |
prot | 代表映射区域的保护方式,有下列组合:
|
flags | 会影响映射区域的各种特性:
|
fd | open()返回的文件描述词,代表欲映射到内存的文件。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。 |
offset | 文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页(PAGE_SIZE)大小的整数倍。 |
返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。
错误代码:
- EBADF 参数fd 不是有效的文件描述词。
- EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED 则要有PROT_WRITE 以及该文件要能写入。
- EINVAL 参数start、length 或offset 有一个不合法。
- EAGAIN 文件被锁住,或是有太多内存被锁住。
- ENOMEM 内存不足。
注意:匿名内存映射 : mmap调用时,flags设为MAP_SHARED | MAP_ANON,fd设为-1,offset设为0即可
2.2 msync
默认情况下,内核采用虚拟内存算法保持内存映射文件与内存映射区的同步,前提是指定了MAP_SHARED标志,但这种同步可能不是立即生效的,而是在随后某个时间进行。但有时候我们修改完数据并进行下一步操作之前,需要确认数据已经同步完成,这时可调用msync函数。
#include<unistd.h>
#include<sys/mman.h>
int msync ( void *addr , size_t len, int flags)
参数说明:
参数 | 说明 |
---|---|
addr | mmap返回的数值 |
len | 指定映射区的长度,它需要与mmap中指定相同。 |
flags |
MS_ASYNC 、MS_SYNC 必须指定其一 |
返回值:成功则返回0;失败则返回-1;多进程的内存
可能的错误:EBUSY/ EINVAL/ ENOMEM
2.3 mprotect
mprotect()函数可以修改调用进程内存页的保护属性,如果调用进程尝试以违反保护属性的方式访问该内存,则内核会发出一个SIGSEGV信号给该进程。这个可以用来做多线程的内存保护功能。
#include<unistd.h>
#include<sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
参数说明:
参数 | 说明 |
---|---|
addr | mmap返回的数值 |
len | 指定映射区的长度,它需要与mmap中指定相同。 |
prot | 可以取以下几个值,并可以用“|”将几个属性结合起来使用:
|
返回值:成功则返回0;失败则返回-1;
可能的错误:
1)EACCES:无法设置内存段的保护属性。当通过 mmap(2) 映射一个文件为只读权限时,接着使用 mprotect() 标志为 PROT_WRITE这种情况就会发生。
2)EINVAL:addr不是有效指针,指向的不是某个内存页的开头,或者不是系统页大小的倍数。
3)ENOMEM:内核内部的结构体无法分配
如果调用进程内存访问行为侵犯了这些设置的保护属性,内核会为该进程产生 SIGSEGV (Segmentation fault,段错误)信号,并且终止该进程。
2.4 munmap
当进程结束或利用exec 相关函数来执行其他程序时,映射内存会自动解除,但关闭对应的文件描述词时不会解除映射。注意:当映射关系解除后,对原来映射地址的访问将导致段错误发生。
#include <unistd.h>
#include <sys/mman.h>
int munmap(void *start, size_t length);
参数说明:
参数 | 说明 |
---|---|
start | 映射内存起始地址,mmap返回的数值 |
len | 指定映射区的长度,它需要与mmap中指定相同。 |
返回值:成功则返回0;失败则返回-1;
可能的错误:errno被设为以下的某个值
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区
2.5 shm_open和shm_unlink函数
shm_open用于创建一个新的Posix共享内存对象或打开一个已存在的Posix共享内存对象。
shm_unlink用于从系统中删除一个Posix共享内存对象。
//成功返回非负描述符,失败返回-1
int shm_open(const char *name, int oflag, mode_t mode);
//成功返回0,失败返回-1
int shun_unlink(const char *name);
shm_open参数说明:
- oflag参数不能设置O_WRONLY标志
- 和mq_open、sem_open不同,shm_open的mode参数总是必须指定,当指定了O_CREAT标志时,mode为用户权限位,否则将mode设为0
shm_open的返回值是一个描述符,它随后用作mmap的第五个参数fd。
2.6 ftruncate和fstat函数
处理mmap的时候,普通文件或Posix共享内存对象的大小都可以通过调用ftruncate设置。
#include <unistd.h>
//成功返回0,失败返回-1
int ftruncate(int fd, off_t length):
- 对于普通文件,若文件长度大于length,额外的数据会被丢弃;若文件长度小于length,则扩展文件大小到length
- 对于Posix共享内存
- 对象,ftruncate把该对象的大小设置成length字节
我们调用ftruncate来指定新创建的Posix共享内存对象大小,或者修改已存在的Posix共享内存对象大小。
- 创建新的Posix共享内存对象时指定大小是必须的,否则访问mmap返回的地址会报bus error错误
- 当打开一个已存在的Posix共享内存对象时,可以调用fstat来获取该对象的信息
#include <sys/stat.h>
#include <sys/types.h>
//成功返回0,失败返回-1
int fstat(int fd, struct stat *buf);
stat结构有12个或以上的成员,然而当fd指代一个Posix共享内存对象时,只有四个成员含有信息:
struct stat
{
mode_t st_mode; //用户访问权限
uid_t st_uid; //user id of owner
gid_t st_gid; //group id of owner
off_t st_size; //文件大小
};
2.7 POSIX共享内存的方法
Posix.1提供了两种在任意进程间共享内存区的方法。
- 内存映射IO:该方法其实也可以用于无亲缘关系进程间共享内存
- Posix共享内存:这是Posix IPC的第三种机制
这两种技术都需要调用mmap,区别在于mmap参数fd的获取手段:
- 内存映射IO通过open获得
- Posix共享内存通过shm_open获得
3、共享内存 - SYSTEM V - shmat、shmctl、shmdt、shmget
共享内存应用中的问题及解决方法
https://www.ibm.com/developerworks/cn/aix/library/au-cn-sharemem/index.html