介绍
除了标准的文件 IO,例如 open, read, write,内核还提供接口允许应用将文件 map 到内存。使得内存中的一个字节与文件中的一个字节一一对应。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap执行相反的操作,删除特定地址区域的对象映射。
两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
- 优势
- 读写文件避免了
read()
和write()
系统调用,也避免了数据的拷贝。 - 除了潜在的页错误,读写 map 后的文件不引起系统调用或者上下文切换。就像访问内存一样简单。
- 多个进程 map 同一个对象,可以共享数据。
- 可以直接使用指针来跳转到文件某个位置,不必使用
lseek()
系统调用。
- 读写文件避免了
- 劣势
- 内存浪费。由于必须要使用整数页的内存。
- 导致难以找到连续的内存区域
- 创建和维护映射和相关的数据结构的额外开销。在大文件和频繁访问的文件中,这个开销相比 read write 的 copy 开销小。
页面对齐
内存拥有独立权限的最小单位就是页。因此,mmap 的最小单位也是页。addr
和 offset
参数都必须页对齐,len
会被 roundup。被 roundup 的多余的内存会以 \0
填充。对这一部分的写入操作不会影响文件。我们可以通过如下方式获取本机的页面大小:
mmap函数说明
函数原型为:
void * mmap (void *addr, size_t len, int prot, int flags, int fd, off_t offset);
函数功能:
mmap的作用是映射文件描述符fd指定文件的 [off,off + len]区域至调用进程的[addr, addr + len]的内存区域, 如下图所示:
函数参数:
-
addr
这个参数是建议地址(hint),需要映射的共享内存起始地址,一般传入NULL。 -
len
映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起
-
prot
表明对这块内存的保护方式,不可与文件访问方式冲突。PROT_NONE
无权限,基本没有用PROT_READ
读权限PROT_WRITE
写权限PROT_EXEC
执行权限 -
flags
描述了映射的类型。MAP_FIXED
开启这个选项,则 addr 参数指定的地址是作为必须而不是建议。如果由于空间不足等问题无法映射则调用失败。不建议使用。MAP_PRIVATE
表明这个映射不是共享的。文件使用 copy on write 机制映射,任何内存中的改动并不反映到文件之中。也不反映到其他映射了这个文件的进程之中。如果只需要读取某个文件而不改变文件内容,可以使用这种模式。MAP_SHARED
和其他进程共享这个文件。往内存中写入相当于往文件中写入。会影响映射了这个文件的其他进程。与MAP_PRIVATE
冲突。 -
fd
参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的
MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的
进程间通信)。
-
offset
文件偏移,参数一般设为0,表示从文件头开始映射。
返回值:
成功执行时,mmap()返回被映射区的指针,munmap()返回0。
失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为以下的某个值
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区
munmap函数说明
函数头文件:#include<unistd.h> #include<sys/mman.h>
函数原型:int munmap( void * start, size_t
length )
函数功能: 解除内存映射
参数说明:start 是调用mmap()时返回的地址,length 是映射区的大小, length 必须是 mmap 时的 length,如果小于当初 mmap 时的那个 length。当进程结束或使用exec相关函数来执行其他程序时,映射内存会自动解除,但关闭相应的文件描述符不会解除映射。当映射关系解除后,对原来映射地址的访问将导致段错误发生。
函数返回值:成功:0,失败:-1
msync函数说明
函数头文件:#include<unistd.h> #include<sys/mman.h>
函数原型:int msync ( void * addr , size_t len, int flags) /标志和权限/
函数功能:进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
函数参数说明:
addr:文件映射到进程空间的地址;
len:映射空间的大小;
flags:刷新的参数设置,可以取值MS_ASYNC/ MS_SYNC/ MS_INVALIDATE
其中:
取值为MS_ASYNC(异步)时,调用会立即返回,不等到更新的完成;
取值为MS_SYNC(同步)时,调用会等到更新完成之后返回;
取MS_INVALIDATE(通知使用该共享区域的进程,数据已经改变)时,在共享内容更改之后,使得文件的其他映射失效,从而使得共享该文件的其他进程去重新获取最新值;
函数返回值
成功则返回0;失败则返回-1;
可能的错误
EBUSY/ EINVAL/ ENOMEM
函数的注意事项:
1.创建映射区的过程中,隐含着一次对映射文件的读操作,因此在打开文件时必须包括读权限,才能创建映射区;
2.当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对文件的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制;
3.映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。在建立好映射区之后就可以关闭文件了,以后在对映射区的读写操作不再需要文件描述符了,因为映射关系已经被mmap系统调用确定了。
4.特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!不能为0。mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。malloc(或new)分配的地址大小(堆空间)都可以指定为0(使用完后,需要free(或delete)释放),但是mmap不可以!!
5.munmap传入的地址一定是mmap的返回地址,坚决杜绝指针++操作(必须完全对应);
6.文件偏移量必须为4K的整数倍,因为MMU完成了从线性地址到物理地址的映射,映射时以页为单位进行的,一页一页进行映射;
7.mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
因此,对于新创建的文件,必须对文件进行拓展(ftruncate、truncate和lseek),然后才能创建映射区。对于lseek、mmap等函数的指针移动都是以字节为单位的(char为1个字节)。
8. 最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小;
9. 可以用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。
注:在linux中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程可以对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;但是,如果对一个页面以外的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有效地址空间大小不会超过文件大小及一个页面大小的和。
10. 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。
创建内存映射的两种方法:
1. mmap MAP_ANONYMOUS
在支持MAP_ANONYMOUS的系统上,直接用匿名共享内存就可以,
mmap(NULL, sizeof(int), PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0);
有些系统不支持匿名内存映射,则能够使用fopen打开/dev/zero文件,然后对该文件进行映射。能够相同达到匿名内存映射的效果。
fd=open("/dev/zero",O_RDWR);
if(fd==-1){
printf("open /dev/zero null\n");
return -1;
}
addr=mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(addr == NULL){
printf("mmap error\n");
return -1;
}
mmap实例说明
- aaaaaaaaa
- bbbbbbbbb
- ccccccccc
- ddddddddd
1 通过共享映射的方式修改文件
- #include <sys/mman.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <error.h>
- #define BUF_SIZE 100
- int main(int argc, char **argv)
- {
- int fd, nread, i;
- struct stat sb;
- char *mapped, buf[BUF_SIZE];
- for (i = 0; i < BUF_SIZE; i++) {
- buf[i] = '#';
- }
- /* 打开文件 */
- if ((fd = open(argv[1], O_RDWR)) < 0) {
- perror("open");
- }
- /* 获取文件的属性 */
- if ((fstat(fd, &sb)) == -1) {
- perror("fstat");
- }
- /* 将文件映射至进程的地址空间 */
- if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ |
- PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) {
- perror("mmap");
- }
- /* 映射完后, 关闭文件也可以操纵内存 */
- close(fd);
- printf("%s", mapped);
- /* 修改一个字符,同步到磁盘文件 */
- mapped[20] = '9';
- if ((msync((void *)mapped, sb.st_size, MS_SYNC)) == -1) {
- perror("msync");
- }
- /* 释放存储映射区 */
- if ((munmap((void *)mapped, sb.st_size)) == -1) {
- perror("munmap");
- }
- return 0;
- }
2 私有映射无法修改文件
/* 将文件映射至进程的地址空间 */
if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ |
PROT_WRITE, MAP_PRIVATE, fd, 0)) == (void *)-1) {
perror("mmap");
}
3 使用共享映射实现两个进程之间的通信
两个程序映射同一个文件到自己的地址空间, 进程A先运行, 每隔两秒读取映射区域, 看是否发生变化.
进程B后运行, 它修改映射区域, 然后推出, 此时进程A能够观察到存储映射区的变化
进程A的代码:
- #include <sys/mman.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <error.h>
- #define BUF_SIZE 100
- int main(int argc, char **argv)
- {
- int fd, nread, i;
- struct stat sb;
- char *mapped, buf[BUF_SIZE];
- for (i = 0; i < BUF_SIZE; i++) {
- buf[i] = '#';
- }
- /* 打开文件 */
- if ((fd = open(argv[1], O_RDWR)) < 0) {
- perror("open");
- }
- /* 获取文件的属性 */
- if ((fstat(fd, &sb)) == -1) {
- perror("fstat");
- }
- /* 将文件映射至进程的地址空间 */
- if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ |
- PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) {
- perror("mmap");
- }
- /* 文件已在内存, 关闭文件也可以操纵内存 */
- close(fd);
- /* 每隔两秒查看存储映射区是否被修改 */
- while (1) {
- printf("%s\n", mapped);
- sleep(2);
- }
- return 0;
- }
进程B的代码:
- #include <sys/mman.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <error.h>
- #define BUF_SIZE 100
- int main(int argc, char **argv)
- {
- int fd, nread, i;
- struct stat sb;
- char *mapped, buf[BUF_SIZE];
- for (i = 0; i < BUF_SIZE; i++) {
- buf[i] = '#';
- }
- /* 打开文件 */
- if ((fd = open(argv[1], O_RDWR)) < 0) {
- perror("open");
- }
- /* 获取文件的属性 */
- if ((fstat(fd, &sb)) == -1) {
- perror("fstat");
- }
- /* 私有文件映射将无法修改文件 */
- if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ |
- PROT_WRITE, MAP_PRIVATE, fd, 0)) == (void *)-1) {
- perror("mmap");
- }
- /* 映射完后, 关闭文件也可以操纵内存 */
- close(fd);
- /* 修改一个字符 */
- mapped[20] = '9';
- return 0;
- }
4 通过匿名映射实现父子进程通信
- #include <sys/mman.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #define BUF_SIZE 100
- int main(int argc, char** argv)
- {
- char *p_map;
- /* 匿名映射,创建一块内存供父子进程通信 */
- p_map = (char *)mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE,
- MAP_SHARED | MAP_ANONYMOUS, -1, 0);
- if(fork() == 0) {
- sleep(1);
- printf("child got a message: %s\n", p_map);
- sprintf(p_map, "%s", "hi, dad, this is son");
- munmap(p_map, BUF_SIZE); //实际上,进程终止时,会自动解除映射。
- exit(0);
- }
- sprintf(p_map, "%s", "hi, this is father");
- sleep(2);
- printf("parent got a message: %s\n", p_map);
- return 0;
- }
mmap()返回地址的访问
linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大
小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明:
总结一下就是, 文件大小, mmap的参数 len 都不能决定进程能访问的大小, 而是容纳文件被映射部分的最小页面数决定
进程能访问的大小. 下面看一个实例:
- #include <sys/mman.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <stdio.h>
- int main(int argc, char** argv)
- {
- int fd,i;
- int pagesize,offset;
- char *p_map;
- struct stat sb;
- /* 取得page size */
- pagesize = sysconf(_SC_PAGESIZE);
- printf("pagesize is %d\n",pagesize);
- /* 打开文件 */
- fd = open(argv[1], O_RDWR, 00777);
- fstat(fd, &sb);
- printf("file size is %zd\n", (size_t)sb.st_size);
- offset = 0;
- p_map = (char *)mmap(NULL, pagesize * 2, PROT_READ|PROT_WRITE,
- MAP_SHARED, fd, offset);
- close(fd);
- p_map[sb.st_size] = '9'; /* 导致总线错误 */
- p_map[pagesize] = '9'; /* 导致段错误 */
- munmap(p_map, pagesize * 2);
- return 0;
- }
参考资料:
https://www.jianshu.com/p/187eada7b900
https://blog.csdn.net/bluehawksky/article/details/39805565
https://blog.csdn.net/weixin_33862993/article/details/85977165
https://blog.csdn.net/qq_33883085/article/details/88919041