Linux之mmap原理分析

510 阅读5分钟

1 简述

mmap系统调用能够将文件映射到内存空间,然后可以通过读写内存来读写文件。Android的binder机制也是基于mmap实现。

2 使用示例

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
参数说明:

  • start:指定要映射的内存地址,一般设置为 NULL 让操作系统自动选择合适的内存地址。
  • length:映射地址空间的字节数,它从被映射文件开头 offset 个字节开始算起。
  • prot:指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读), PROT_WRITE(可写), PROT_EXEC(可执行), PROT_NONE(不可访问)。
  • flags:由以下几个常值指定:MAP_SHARED (共享的) MAP_PRIVATE(私有的), MAP_FIXED(表示必须使用 start 参数作为开始地址,如果失败不进行修正),其中,MAP_SHARED , MAP_PRIVATE必选其一,而 MAP_FIXED 则不推荐使用。
  • fd:表示要映射的文件句柄。
  • offset:表示映射文件的偏移量,一般设置为 0 表示从文件头部开始映射。

函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <zconf.h>
#include <string.h>

int main() {
    printf("Hello, World!\n");

    const char *file = "c_test/mmap_demo";

    // 读写方式打开,所有人都有读写可执行权限
    int m_fd = open(file, O_RDWR | O_CREAT, S_IRWXU);
    ftruncate(m_fd, 128); // 改变文件大小

    char *addr = mmap(0, 128, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED, m_fd, 0);

    memset(addr, 128, 0);
    sprintf(addr, "hello_modify!\n");
    // 写到内存,如果想让内存写入文件需要调下面两个中的一个
    munmap(addr, 128);
    //    msync(...);
    close(m_fd);

    return 0;
}

3 原理

虚拟内存与物理内存

32位linux操作系统中每个进程都有4GB的虚拟内存空间,但是物理内存空间(内存条)大小是固定的,配置的多少就是多少,所有进程都共用内存条上的空间。所以虚拟内存空间需要映射到物理内存空间才能使用。
内存的单位是内存页,这个自行查看操作系统。

虚拟内存管理

在Linux操作系统中,管理虚拟内存的基本单元是vm_area_struct,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍。

vm_area_struct {
    struct mm_struct * vm_mm;
    unsigned long vm_start;
    unsigned long vm_end;

    /* linked list of VM areas per task, sorted by address */
    struct vm_area_struct *vm_next;

    pgprot_t vm_page_prot;
    unsigned long vm_flags;

    /* AVL tree of VM areas per task, sorted by address */
    short vm_avl_height;
    struct vm_area_struct * vm_avl_left;
    struct vm_area_struct * vm_avl_right;

    vm_area_struct *vm_next_share;
    struct vm_area_struct **vm_pprev_share;

    struct vm_operations_struct * vm_ops;
    unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */
    struct file * vm_file;
    unsigned long vm_raend;
    void * vm_private_data; /* was vm_pte (shared mem) */
};
  • vm_startvm_end 指向虚拟内存一段连续空间的地址
  • vm_next 指向下一个vm_area_struct地址(链表结构)
  • vm_file 指向被映射的文件
  • vm_avl_heightvm_avl_leftvm_avl_right树高、左子结点、右子结点实现AVL树,提升查询效率。

未命名文件.png

  1. task_struct 用于管理进程。
  2. mm_struct是内存描述符,在每个mm_struct又都有一个pgd_t * 使其指向页表,然后通过页表实现从虚拟地址到物理地址的映射。mmap指向虚拟内存管理单元链表vm_area_struct

调用mmap

当用户程序调用mmap后。函数会在当前进程的空间中找到适合的vm_area_struct来描述自己将要映射的区域。这个区域的作用就是将mmap函数中文件描述符所指向的具体文件中内容映射过来。
mmap的执行,仅仅是在内核中建立了文件与虚拟内存空间的对应关系。用户访问这些虚拟内存空间时,页面表里面是没有这些空间的表项的。当用户程序试图访问这些映射的空间时,就会产生缺页异常。

缺页异常

缺页异常一般有两种情况:

  • 程序设计的不当导致访问了非法的地址
  • 访问的地址是合法的,但是该地址还未分配物理页框.

mmap仅仅只是建立了虚拟内存,并未分配物理内存,所以会缺页异常,异常后,会进入缺页异常流程(do_page_fault),将文件从高速缓存映射到物理内存。
然后我们操作文件,就是直接操作内存中的数据,不需要再read、write。
由于物理内存是所有进程共享的,所有mmap也会应用到进程间通信中。

注意

需要注意的是,我们通过mmap,可以做到像操作内存数据一样直接操作文件内容。但是我们的修改是在内存中生效,要想数据写回磁盘,还需要在操作后,调用msyncmunmap

4 总结

在unix/linux平台下读写文件,有两种方式:

  1. 正常操作,open文件,接着使用read系统调用读取文件的全部或一部分。于是内核将文件的内容从磁盘上读取到内核页高速缓冲,再从内核高速缓冲读取到用户进程的地址空间。这么做需要在内核和用户空间之间做多次数据拷贝。而且当多个进程同时读取一个文件时,则每一个进程在自己的地址空间都有这个文件的副本,这样也造成了物理内存的浪费。

内存管理.png

  1. 内存映射,也就是用mmap去建立虚拟内存,然后通过缺页异常处理,在物理内存创建文件存储区域。

内存管理.png
可以看出,mmap减少了内核态到用户态的拷贝,且进程AB操作同一个文件,都是直接操作内存,大大提升了读写效率。