Linux系统编程之 内存映射mmap

前面有文章提到过mmap,但是觉得还不够,完全可以再写一篇文档专题讲述。

  

1、概述

mmap()系统调用在调用进程的虚拟地址空间中创建一个新内存映射。映射分为两种。

(1)文件映射:文件映射将一个文件的一部分直接映射到调用进程的虚拟内存中。一旦一个文件被映射之后就可以通过在相应的内存区域中操作字节来访问文件内容了。

映射的分页会在需要的时候从文件中(自动)加载。

(2)匿名映射:一个匿名映射没有对应的文件

相反,这种映射的分页会被初始化为0。(一个内容总是被初始化为0的虚拟文件的映射)

 

一个进程的映射中的内存可以与其他进程中的映射共享(即各个进程的页表条目指向RAM中相同分页)(fork())。      

当多个进程共享相同内存分页时,每个进程可能会看到其他进程对分页内容做出的改变,这取决于映射是私有还是共享的。

(1)私有映射MAP_PRIVATE):对映射的改变其他进程不可见,变更将不会在底层文件上进行。内核使用写时复制技术,当进程试图修改内容时,创建一个新的分页。

(2)共享映射MAP_SHARED):变更对所有共享进程可见,发生在底层文件上。

 

2、创建映射

#include<sys/mman.h>
void * mmap(void *addr,
            size_t length,
            int prot,
            int flags,
            int fd,
            off_t offset);        

该函数各参数的作用图示如下

addr参数制定了映射被放置的虚拟地址。如果将addr设为NULL,那么内核将会为映射选择一个合适的地址。这是创建映射的首选方法。

成功时会返回新映射的起始地址。发生错误时返回MAP_FAILED。

lenth参数指定了映射的字节数。lenth会被内核提升为分页大小下一个倍数

prot参数是一个位掩码,指定了施加于应设置上的保护信息,其取值是PROT_NONE(区域无法访问)或者为下列三者组合(OR)

描述
PROT_READ 区域内容可读
PROT_WRITE 区域内容可写
PROT_EXEC 区域内容可执行

flags参数为 MAP_PRIVATEMAP_SHARED

fd,offset用于文件映射的匿名映射忽略)。fd是一个标识被映射的文件的文件描述符。offset指定了映射在文件中的起点,必须是系统分页大小的倍数。

#include<sys/stat.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>

int
main(int argc, char*argv[]){ struct stat st; int fd; char *addr;
fd
= open(argv[1], O_RDWR); fstat(fd, &st);//获取文件信息 addr = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); write(STDOUT_FILENO, addr, st.st_size);//将内容输出到终端
return 0; }

关于内存保护

如果一个进程在访问一个内存区域违反了该区域的保护位(PROT),那么内核将会向该进程发送一个SIGSEGV信号。

内存保护信息在进程私有的虚拟内存表中。因此,不同进程可能会有不同的保护位来映射同一个内存区域。

 

3、解除映射

#include<sys/mman.h>
int munmap(void *addr, size_t length);

addr参数是待解除映射的地址范围的起始地址,必须与一个分页边界对齐。

length参数是一个非负整数,它指定了待解除映射区域的大小(字节数)。

当一个进程最终或执行了一个exec()之后进程所有的映射会自动解除

为确保一个共享文件映射的内容会被写入底层文件中,在使用munmap()解除一个映射之前需要调用msync()

 

复制文件

open中flag要和mmap中一致 不然会mmap失败

#include<sys/stat.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main(int argc, char *argv[]) { struct stat st; void *addr1; void *addr2; int fd1 = open(argv[1], O_RDONLY, 0777); int fd2 = open(argv[2], O_CREAT | O_RDWR, 0777); fstat(fd1, &st); //fflush(stdout); ftruncate(fd2, st.st_size); addr1 = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd1, 0); addr2 = mmap(NULL, st.st_size, PROT_WRITE | PROT_READ, MAP_SHARED, fd2, 0); if(addr1 == MAP_FAILED && addr2 == MAP_FAILED)
return 0; memcpy(addr2, addr1, st.st_size); close(fd1); close(fd2); return 0; }

 

4、同步映射

#include<sys/mman.h>
int msync (void *addr, size_t len, int flags);

将mmap()生成的映射在内存的任何修改写回到磁盘中,从而实现同步内存中的映射和被映射的文件。即也是一种文件同步的方式,功能等价于系统调用fsync()。

参数flags控制同步操作的行为,它的值按位或操作。

  MS_SYNC:    操作同步进行,直到所有页写回磁盘后,该调用才返回。

  MS_ASYNC:   操作异步进行,该调用马上返回,更新操作由系统调度完成的。

  MS_INVALIDATE:指定所有其他该块映射的拷贝都将失效。后期的操作都将直接同步到磁盘。

MS_ASYNC 和 MS_SYNC 二者必须选且只能选其一。

 

5、mmap()的优点

常规文件操作,也就是buffer IO,为了提高读写效率保护磁盘,使用了页缓存机制。

这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。

这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。

写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

 

而使用mmap()操作文件中,创建新的虚拟内存区域建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作

而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

 

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。

mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程(频繁的系统调用和上下文切换)。因此mmap效率更高

 

但缺点是:创建和维护映射以及相关的内核数据结构(VMA)有一定的开销,而且映射区域必须是页的整数倍。

所以,基于以上理由,处理大文件时,mmap()的优势就会非常显著

 

6、映射到设备文件/dev/zero

Linux系统给我们提供了创建匿名映射区的方法,需要借助标志位参数flags来指定,使用 MAP_ANONYMOUS 或 MAP_ANON

但是,以上的宏只能在linux使用,在类unix上没有此宏,要实现匿名映射可以用如下方法

  ① fd = open("/dev/zero", O_RDWR);

  ② p = mmap(NULL, size, PROT_READ | PROT_WRITE, MMAP_SHARED, fd, 0);

 

举例进行说明:

 #include <stdio.h>
 #include <string.h>
 #include <sys/mman.h>
 #include <fcntl.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <stdlib.h>
 #include <unistd.h>
 
 int main(void)
 {
 
         int fd;
         int*p;
         pid_t pid;
         int global_val = 10;
 
         fd = open("/dev/zero", O_RDWR);   
         p = mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);// 4 根据自己定义映射区大小
 
          close(fd); //
          pid = fork();
          if(pid == 0)
          {
                  *p=10000;
                  global_val=9999;
                  printf("son   ---*p:%d,global_val:%d\n", *p, global_val);
          }
          else
          {
                  sleep(1);
                  printf("father---*p:%d,global_val:%d\n", *p, global_val);
                  wait(NULL); 
                  munmap(p, 4); //
          } 

         return 0;
  }

运行结果如下:

 

 

 

 

 

参考资料:

《linux系统编程》第2版

https://blog.csdn.net/qq_26072739/article/details/104184839

posted on 2020-09-02 11:28  orange-C  阅读(580)  评论(0编辑  收藏  举报

导航