mmap(一) 用户空间

一,mmap基础概念

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图所示:

                                                图1 文件映射到进程地址空间

由上图可以看出,进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。

linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问,如下图所示:

                                                图2 进程虚拟地址空间分区

vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用要的信息,都可以从vm_area_struct中获得。mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。

二,mmap与标准IO(read、write)的效率比较

1,标准I/O

    大多数文件系统的默认I/O操作都是标准I/O。在Linux的缓存I/O机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。

    读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。

    写操作:将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync等同步命令。

    缓存I/O的优点:1)在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全;2)可以减少读盘的次数,从而提高性能。

    缓存I/O的缺点:数据在传输过程中需要在应用程序地址空间和缓存之间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。

下面我们read()操作为例,下图是read操作时流程

                                                                图3 标准I/O流程

它会导致数据先从磁盘拷贝到 Page Cache 中,然后再从 Page Cache 拷贝到应用程序的用户空间,这样就会多一次内存拷贝。系统这样设计主要是因为内存相对磁盘是高速设备,即使多拷贝 100 次,内存也比真正读一次硬盘要快。

写操作

    前面提到写操作讲数据从用户控件复制到内核空间的缓存中,数据什么时候写到磁盘由应用程序采用的写操作机制决定,默认是采用延迟写机制,应用程序只需要将数据写到页缓存就可以了,完全不需要等待数据全部被写入磁盘,系统会负责定期将页缓存数据写入磁盘。

从中可以看出来,缓存 I/O 可以很大程度减少真正读写磁盘的次数,从而提升性能。但是延迟写机制可能会导致数据丢失,那系统究竟会在什么时机真正把页缓存的数据写入磁盘呢?

Page Cache 中被修改的内存称为“脏页”,内核通过 flush 线程定期将数据写入磁盘。具体写入的条件我们可以通过 /proc/sys/vm 文件或者sysctl -a | grep vm 命令得到。

在实际应用中,如果某些数据我们觉得非常重要,是完全不允许有丢失风险的,这个时候我们应该采用同步写机制。在应用程序中使用 sync、fsync、msync 等系统调用时,内核都会立刻将相应的数据写回到磁盘。

2,mmap

mmap是指将硬盘上文件的位置与进程逻辑地址空间中一块大小相同的区域一一对应,当要访问内存中一段数据时,转换为访问文件的某一段数据。这种方式的目的同样是减少数据在用户空间和内核空间之间的拷贝操作。当大量数据需要传输的时候,采用内存映射方式去访问文件会获得比较好的效率。

    使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。

mmap的优点:

    减少系统调用。我们只需要一次 mmap() 系统调用,后续所有的调用像操作内存一样,而不会出现大量的 read/write 系统调用。

    减少数据拷贝。实际拷贝时候,内存文件映射将磁盘数据直接拷贝到用户进程内存空间只进行了一次拷贝,而普通的IO是先将文件拷贝到内核缓存空间,然后才拷贝到用户进程内存空间,进行了两次拷贝。

    可靠性高。mmap 把数据写入页缓存后,跟缓存 I/O 的延迟写机制一样,可以依靠内核线程定期写回磁盘。但是需要提的是,mmap 在内核崩溃、突然断电的情况下也一样有可能引起内容丢失,当然我们也可以使用 msync来强制同步写。

                                                图5 mmap数据读写流程

从上面的图看来,我们使用 mmap 仅仅只需要一次数据拷贝。看起来 mmap 的确可以秒杀普通的文件读写,那我们为什么不全都使用 mmap 呢?事实上,它也存在一些缺点:

    虚拟内存增大。mmap 会导致虚拟内存增大,我们的 APK、Dex、so 都是通过 mmap 读取。而目前大部分的应用还没支持 64 位,除去内核使用的地址空间,一般我们可以使用的虚拟内存空间只有 3GB 左右。如果 mmap 一个 1GB 的文件,应用很容易会出现虚拟内存不足所导致的 OOM。

    磁盘延迟。mmap 通过缺页中断向磁盘发起真正的磁盘 I/O,所以如果我们当前的问题是在于磁盘 I/O 的高延迟,那么用 mmap() 消除小小的系统调用开销是杯水车薪的。

三,mmap系统调用

函数原型

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

返回说明

成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1], error被设为以下的某个值:

1 EACCES:访问出错

2 EAGAIN:文件已被锁定,或者太多的内存已被锁定

3 EBADF:fd不是有效的文件描述词

4 EINVAL:一个或者多个参数无效

5 ENFILE:已达到系统对打开文件的限制

6 ENODEV:指定文件所在的文件系统不支持内存映射

7 ENOMEM:内存不足,或者进程已超出最大内存映射数量

8 EPERM:权能不足,操作不允许

9 ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志

10 SIGSEGV:试着向只读区写入

11 SIGBUS:试着访问不属于进程的内存区

参数

start:映射区的开始地址

length:映射区的长度

prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起

1 PROT_EXEC :页内容可以被执行

2 PROT_READ :页内容可以被读取

3 PROT_WRITE :页可以被写入

4 PROT_NONE :页不可访问

flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体

1 MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。

2 MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。

3 MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。

4 MAP_DENYWRITE //这个标志被忽略。

5 MAP_EXECUTABLE //同上

6 MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。

7 MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。

8 MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。

9 MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。

10 MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。

11 MAP_FILE //兼容标志,被忽略。

12 MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。

13 MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。

14 MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。

fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1

offset:被映射对象内容的起点

相关函数

int munmap( void * addr, size_t len )

成功执行时,munmap()返回0。失败时,munmap返回-1,error返回标志和mmap一致;

该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小;

当映射关系解除后,对原来映射地址的访问将导致段错误发生。

int msync( void *addr, size_t len, int flags )

一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。

可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

四,mmap用法

1,当要map的文件是/dev/mem的时候,就可以实现把物理地址映射到虚拟空间中【比如在SV验证的时候在linux应用层访问设备寄存器】
#define MAP_SIZE 4096
#define MAP_MASK (MAP_SIZE - 1)int dev_mem_fd = -1;
void* g_map_base = NULL;

void initMmap()
{
    dev_mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
}

void* MapPaddr2Vaddr(void* paddr)
{
   void* vaddr = NULL;
   void* vaddr_map_base = NULL;
   void* map_base = (long)paddr & ~MAP_MASK;
   vaddr_map_base = mmap(NULL, MAP_SIZE, PORT_READ |

   PORT_WRITE, MAP_SHARED, dev_mem_fd, (long)map_base);

   if(vaddr_map_base != MAP_FAILED) {
       vaddr = vaddr_map_base + ((long)paddr & MAP_MASK);
       g_map_base = vaddr_map_base;
   }

   return vaddr;
}

void UnmapAddr()
{
    if(g_map_base ) {
        munmap(g_map_base, MAP_SIZE);
    }
}

void WriteReg(void* paddr, uint32_t val)
{
    void* vaddr = MapPaddr2Vaddr(paddr);  
    *(volatile uint32_t*)vaddr = val;   
    __asm("DC CIVAC, %0\n", :: "r" ((uint64_t)vaddr));  
    UnmapAddr(); // makesure update sync to file
}
2,将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>

int main(int argc, char* argv[]) {
    int fd, offset;
    char* data;
    struct stat sbuf;

    if (argc != 2) {
        fprintf(stderr, "usage:mmapdemo offset\n");
        exit(1);
    }

    if ((fd = open("mmapdemo.c", O_RDONLY)) == -1) {//打开文件自身
        perror("open");
        exit(1);
    }

    if (stat("mmapdemo.c", &sbuf) == -1) {//文件大小,mmap的有效内存大小不超过该值
        perror("stat");
        exit(1);
    }

    offset = atoi(argv[1]);//文件偏移量

    if (offset < 0 || offset > sbuf.st_size - 1) {
        fprintf(stderr, "mmapdemo: offset must be in the range 0-%d\n",
                sbuf.st_size - 1);
        exit(1);
    }

    data = mmap((caddr_t)0, sbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);

    if (data == (caddr_t)(-1)) {
        perror("mmap");
        exit(1);
    }

    printf("byte at offset %d is '%c'\n", offset, data[offset]);

    return 0;
}
3,进程间通信 (进程间共享内存)
//map_normalfile1.cpp
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

struct People{
    char name[4];
    int age;
};

const int file_struct_cnt = 5;
const int mem_struct_cnt = 10;

int main(int argc, char* argv[]) {
    int fd = open(argv[1], O_CREAT | O_RDWR | O_TRUNC, 00777);
    lseek(fd, sizeof(People) * file_struct_cnt - 1, SEEK_SET);//文件大小为8*5
    write(fd, "", 1);

    //内存大小为8*10
    People* pmap = (People*)mmap(NULL, sizeof(People) * mem_struct_cnt, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    //内存赋值
    for (int i = 0; i < 10; ++i) {
        char c = 'a' + i;
        memcpy((pmap + i)->name, &c, 1);
        (pmap + i)->age = 20 + i;
    }

    printf("initialize over.\n");
    sleep(10);//等待map_normalfile2读取argv[1]
    if (munmap(pmap, sizeof(People) * 10) != 0) {
        printf("munmap error[%s]\n", strerror(errno));
        return -1;
    }

    return 0;
}

//map_normalfile2.cpp
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>

struct People{
    char name[4];
    int age;
};

const int mem_struct_cnt = 10;

int main(int argc, char* argv[]) {
    int fd = open(argv[1], O_CREAT | O_RDONLY, 00777);
    People* pmap = (People*)mmap(NULL, sizeof(People) * mem_struct_cnt, PROT_READ, MAP_SHARED, fd, 0);

    for (int i = 0; i < mem_struct_cnt; ++i) {
        printf("name:%s age:%d\n", (pmap + i)->name, (pmap + i)->age);
    }

    if (munmap(pmap, sizeof(People) * 10) != 0) {
        printf("munmap error[%s]\n", strerror(errno));
        return -1;
    }

    return 0;
}

参考链接:

https://www.cnblogs.com/huxiao-tee/p/4660352.html

https://www.cnblogs.com/feng9exe/p/15532553.html

https://www.cnblogs.com/Arnold-Zhang/p/15686868.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值