Linux mmap原理

Posted 热爱编程的大忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux mmap原理相关的知识,希望对你有一定的参考价值。

Linux mmap原理


前言

mmap是linux操作系统提供给用户空间调用的内存映射函数,很多人仅仅只是知道可以通过mmap完成进程间的内存共享和减少用户态到内核态的数据拷贝次数,但是并没有深入理解mmap在操作系统内部是如何实现的,原理是什么。

本文想要和大家一起来聊聊mmap的原理,本文整体脉络如下:

  • linux段页式内存管理回顾
  • mmap原理

Linux段页式内存管理

这里的段页式内存管理主要基于linux 0.11进行讲解(作者本人并非主攻linux,所以只是对linux 0.11略有研究)

无论是现代操作系统还是最早的linux 0.11操作系统,在对于物理内存的管理,都是将物理内存按页划分,如下图所示:

按页划分的好处是可以避免内存碎片的产生。

物理内存按页划分是方便了操作系统管理内存,但是对于程序员来说,我们更希望看到的内存视图是类似于一个完整数组般:


并且由于一个完整的程序是分为了代码段,栈段和数据段的,当我们运行起这个程序时,该运行中的程序就被称为一个进程,我们更希望该进程下管理的程序数据在内存上是如下分布状态:


现在的问题就是站在程序员的视角,希望分段来管理内存,而操作系统更希望分页管理,现在就需要进行一波折中,也就像程序屏蔽底层对物理内存的分页管理,对外展示的内存外貌为一整块内存,这怎么办到呢?


这就需要采用段页式内存管理了,由于实际物理内存的管理还是需要分页管理,所以程序员视角看到的内存其实是一块虚拟内存,虚拟内存上的地址通过某种方式会映射到物理内存上的某一页的某块偏移地址上,而具体的映射方式采用的是多级页表的方式:

本文重点不在linux内存管理上,因此这部分内容不会细讲,如果想完整了解,可以看此篇文章:操作系统段页结合的实际内存管理–13

如何通过多级页表完成虚拟地址到物理页映射的,这里就不多展开了,想要完整了解的,看上面那篇文章。

对于linux 0.11而言,是把虚拟内存设置为了0-4G大小,而这块虚拟内存是被多个进程共享的,如下图所示:

因为每个进程的段空间不重叠,意味着各个进程的虚拟空间中的虚拟地址不会重叠,那么对应各个进程的虚拟地址解析得到的虚拟页号不会重叠,因此在linux 0.11中多个进程可以共享一套页表。

但是,对于现代32操作系统而言,每个进程都会单独占有4G虚拟内存,各个进程对应的页表是会产生重叠的,因此每个进程需要有自己的段表和页表。

mmap的实现不是基于linux 0.11的虚拟内存管理方式,而是更复杂的方式,这里大家需要记住上面标红的那段话。


mmap

mmap是一种内存映射的方法,这一功能可以用在文件的处理上,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系

在编程时可以使某个磁盘文件的内容看起来像是内存中的一个数组。如果文件由记录组成,而这些记录又能够用结构体来描述的话,可以通过访问结构数组来更新文件的内容。

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


进程的虚拟地址空间,由多个虚拟内存区域构成。

虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。

而为内存映射服务的地址空间处在堆栈之间的空余部分。

这里说的地址都是当前进程享有的一块完整的虚拟内存中的地址

内核为系统中的每个进程维护一个单独的任务结构(task_struct)。任务结构中的元素包含或者指向内核运行该进程所需的所有信息(PID、指向用户栈的指针、可执行目标文件的名字、程序计数器等)。Linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问,如下图所示:

这里可以简单将vm_area_struct结构体看做是描述当前进程内某个段信息的载体,例如: 当前段位于当前进程虚拟内存中哪段虚拟地址范围,访问标志啥的…

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

mm_struct:描述了虚拟内存的当前状态。pgd指向一级页表的基址(当内核运行这个进程时,pgd会被存放在CR3控制寄存器,也就是页表基址寄存器中),mmap指向一个vm_area_structs的链表,其中每个vm_area_structs都描述了当前虚拟地址空间的一个区域。vm_starts指向这个区域的起始处。vm_end指向这个区域的结束处。vm_prot描述这个区域内包含的所有页的读写许可权限。vm_flags描述这个区域内的页面是与其他进程共享的,还是这个进程私有的以及一些其他信息。vm_next指向链表的下一个区域结构。


mmap内存映射原理

文字概述

mmap内存映射的实现过程,总的来说可以分为三个阶段:

(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

  1. 进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

  2. 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址。

  3. 为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化。

  4. 将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中。

(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

  1. 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。

  2. 通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。

  3. 内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
    inode相关知识可以看此篇文章进行学习

  4. 通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。

第8步就是将在虚拟地址空间申请的那片虚拟地址和实际物理页建立映射关系

(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

这其实也算是一种懒加载思想的体现

  1. 进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。

  2. 缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。

  3. 调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。

  4. 之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

注意: 修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。


mmap函数参数介绍

mmap (内存映射)函数的作用是建立一段可以被两个或更多个程序读写的内存。一个程序对它所做出的修改可以被其他程序看见。这要通过使用带有特殊权限集的虚拟内存段来实现。对这类虚拟内存段的读写会使操作系统去读写磁盘文件中与之对应的部分。 mmap 函数创建一个指向一段内存区域的指针,该内存区域与可以通过一个打开的文件描述符访问的文件的内容相关联。mmap 函数原型如下:

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 可以通过传递 offset 参数来改变经共享内存段访问的文件中数据的起始偏移值。
  • 打开的文件描述符由 fd 参数给出。
  • 可以访问的数据量(即内存段的长度)由 length 参数设置。
  • 可以通过 addr 参数来请求使用某个特定的内存地址。如果它的取值是零,结果指针就将自动分配。这是推荐的做法,否则会降低程序的可移植性,因为不同系统上的可用地址范围是不一样的。
  • prot 参数用于设置内存段的访问权限。它是下列常数值的按位或的结果:
    • PROT_READ 内存段可读。
    • PROT_WRITE 内存段可写。
    • PROT_EXEC 内存段可执行。
    • PROT_NONE 内存段不能被访问。
  • flags 参数控制程序对该内存段的改变所造成的影响:


msync 函数的作用是:把在该内存段的某个部分或整段中的修改写回到被映射的文件中(或者从被映射文件里读出)。

#include <sys/mman.h>

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

内存段需要修改的部分由作为参数传递过来的起始地址 addr 和长度 len 确定。flags 参数控制着执行修改的具体方式,可以使用的选项如下:

  • MS_ASYNC 采用异步写方式
  • MS_SYNC 采用同步写方式
  • MS_INVALIDATE 从文件中读回数据

munmap 函数的作用是释放内存段:

#include <sys/mman.h>

int munmap(void *addr, size_t length);

源码解析

mmap 的全称是 memory map,中文意思是 内存映射。其用途是将文件映射到内存中,然后可以通过对映射区的内存进行读写操作,其效果等同于对文件进行读写操作。

下面我们通过一幅图来对 mmap 的原理进行阐述:


从上图可以看出,mmap 的原理就是将虚拟内存空间映射到文件的页缓存,我们可以知道:对文件进行读写时需要经过页缓存进行中转的。所以当虚拟内存地址映射到文件的页缓存后,就可以直接通过读写映射区内存来对文件进行读写操作。

mmap 实现:

1. 文件映射

当我们使用 mmap() 系统调用对文件进行映射时,将会触发调用 do_mmap_pgoff() 内核函数来完成工作,我们来看看 do_mmap_pgoff() 函数的实现(经过精简后):

unsigned long
do_mmap_pgoff(struct file *file, unsigned long addr, 
              unsigned long len, unsigned long prot, 
              unsigned long flags, unsigned long pgoff)

    ...
    // 1. 获取一个未被使用的虚拟内存区
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (addr & ~PAGE_MASK)
        return addr;

    ...
    // 2. 调用 mmap_region() 函数继续进行映射操作
    return mmap_region(file, addr, len, flags, vm_flags, pgoff, accountable);

经过精简后的 do_mmap_pgoff() 函数主要完成 2 个工作:

  • 首先,调用 get_unmapped_area() 函数来获取进程没被使用的虚拟内存区,并且返回此内存区的首地址。
  • 然后,调用 mmap_region() 函数继续进行映射操作。

在 32 位的操作系统中,每个进程都有 4GB 的虚拟内存空间,应用程序在使用内存前,需要先向操作系统发起申请内存的操作。操作系统会从进程的虚拟内存空间中查找未被使用的内存地址,并且返回给应用程序。
操作系统会记录进程正在使用中的虚拟内存地址,如果内存地址没被登记,说明此内存地址是空闲的(未被使用)。

我们继续来看看 mmap_region() 函数的实现,代码如下(经过精简后):

unsigned long
mmap_region(struct file *file, unsigned long addr,
            unsigned long len, unsigned long flags,
            unsigned int vm_flags, unsigned long pgoff,
            int accountable)

    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev;
    int correct_wcount = 0;
    int error;
    ...

    // 1. 申请一个虚拟内存区管理结构(vma)
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    ...

    // 2. 设置vma结构各个字段的值
    vma->vm_mm = mm;
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_flags = vm_flags;
    vma->vm_page_prot = protection_map[vm_flags & (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)];
    vma->vm_pgoff = pgoff;

    if (file) 
        ...
        vma->vm_file = file;

        /* 3. 此处是内存映射的关键点,调用文件对象的 mmap() 回调函数来设置vma结构的 fault() 回调函数。
         *    vma对象的 fault() 回调函数的作用是:
         *        - 当访问的虚拟内存没有映射到物理内存时,
         *        - 将会调用 fault() 回调函数对虚拟内存地址映射到物理内存地址。
         */
        error = file->f_op->mmap(file, vma);
        ...
    
    ...

    // 4. 把 vma 结构连接到进程虚拟内存区的链表和红黑树中。
    vma_link(mm, vma, prev, rb_link, rb_parent);
    ...

    return addr;

mmap_region() 函数主要完成以下 4 件事情:

  • 申请一个 vm_area_struct 结构(vma),内核使用 vma 来管理进程的虚拟内存地址
  • 设置 vma 结构各个字段的值。
  • 通过调用文件对象的 mmap() 回调函数来设置vma结构的 fault() 回调函数,一般文件对象的 mmap() 回调函数为:generic_file_mmap()
  • 把新创建的 vma 结构连接到进程的虚拟内存区链表和红黑树中。

内核使用 vm_area_struct 结构来管理进程的虚拟内存地址。当进程需要使用内存时,首先要向操作系统进行申请,操作系统会使用 vm_area_struct 结构来记录被分配出去的内存区的大小、起始地址和权限等。

我们来看看 vm_area_struct 结构的定义:

struct vm_area_struct 
    struct mm_struct *vm_mm;
    unsigned long vm_start;              // 内存区的开始地址
    unsigned long vm_end;                // 内存区的结束地址
    struct vm_area_struct *vm_next;      // 把进程所有已分配的内存区链接起来
    pgprot_t vm_page_prot;               // 内存区的权限
    ...
    struct rb_node vm_rb;                // 为了加快查找内存区而建立的红黑树
    ...
    struct vm_operations_struct *vm_ops; // 内存区的操作回调函数集

    unsigned long vm_pgoff;
    struct file *vm_file;                // 如果映射到文件,将指向映射的文件对象
    ...
;

struct vm_operations_struct 
    // 当虚拟内存区没有映射到物理内存地址时,将会触发缺页异常,
    // 而在缺页异常处理函数中,将会调用此回调函数来对虚拟内存映射到物理内存。
    int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
    ...
;

当把文件映射到虚拟内存空间时,需要把 vma 结构的 vm_file 字段设置为要映射的文件对象,然后调用文件对象的 mmap() 回调函数来设置 vma 结构的 fault() 回调函数。

vma 结构的 fault() 回调函数的作用是:当虚拟内存区没有映射到物理内存地址时,将会触发缺页异常。而在缺页异常处理中,将会调用此回调函数来对虚拟内存映射到物理内存。

我们来看看 generic_file_mmap() 函数是怎么设置 vma 结构的 fault() 回调函数的:

struct vm_operations_struct generic_file_vm_ops = 
    .fault = filemap_fault, // 将 fault() 回调函数设置为:filemap_fault()
;

int generic_file_mmap(struct file *file, struct vm_area_struct *vma)

    ...
    vma->vm_ops = &generic_file_vm_ops;
    ...
    return 0;

至此,文件映射的过程已经分析完毕。我们来看看其调用链:

sys_mmap()
└→ do_mmap_pgoff()
   └→ mmap_region()
      └→ generic_file_mmap()

2. 缺页异常

前面介绍了 mmap() 系统调用的处理过程,可以发现 mmap() 只是将 vmavm_file 字段设置为被映射的文件对象,并且将 vmafault() 回调函数设置为 filemap_fault()。也就是说,mmap() 系统调用并没有对虚拟内存进行任何的映射操作。

虚拟内存必须映射到物理内存才能使用。如果访问没有映射到物理内存的虚拟内存地址,CPU 将会触发缺页异常。也就是说,虚拟内存并不能直接映射到磁盘中的文件。

那么 mmap() 是怎么将文件映射到虚拟内存中呢?

读写文件时并不是直接对磁盘上的文件进行操作的,而是通过 页缓存 作为中转的,而页缓存就是物理内存中的内存页。所以,mmap() 可以通过将文件的页缓存映射到虚拟内存空间来实现对文件的映射。

但我们在 mmap() 系统调用的实现中,也没看到将文件页缓存映射到虚拟内存空间。那么映射过程是在什么时候发生的呢?

答案就是:缺页异常

由于 mmap() 系统调用并没有直接将文件的页缓存映射到虚拟内存中,所以当访问到没有映射的虚拟内存地址时,将会触发 缺页异常。当 CPU 触发缺页异常时,将会调用 do_page_fault() 函数来修复触发异常的虚拟内存地址。

我们主要来看看 do_page_fault() 函数对文件映射的实现部分,其调用链如下:

do_page_fault()
└→ handle_mm_fault()
   └→ handle_pte_fault()
      └→ do_linear_fault()
         └→ __do_fault()

所以我们直接来看看 __do_fault() 函数的实现:

static int
__do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
           unsigned long address, pmd_t *pmd, pgoff_t pgoff,
           unsigned int flags, pte_t orig_pte)

    ...
    vmf.virtual_address = address & PAGE_MASK; // 要映射的虚拟内存地址
    vmf.pgoff = pgoff;                         // 映射到文件的偏移量
    vmf.flags = flags;                         // 标志位
    vmf.page = NULL;                           // 映射到虚拟内存中的物理内存页

    // 1. 如果虚拟内存管理区提供了 falut() 回调函数,那么将调用此函数来获取要映射的物理内存页,
    //    我们在 mmap() 系统调用的实现中看到,已经将其设置为 filemap_fault() 函数了。
    if (likely(vma->vm_ops->fault)) 
        ret = vma->vm_ops->fault(vma, &vmf);
        ...
    
    ...

    if (likely(pte_same(*page_table, orig_pte))) 
        ...
        // 2. 通过物理内存页生成一个页表项值(可以参考内存映射一文)
        entry = mk_pte(page, vma->vm_page_prot);
        if (flags & FAULT_FLAG_WRITE)
            entry = maybe_mkwrite(pte_mkdirty(entry), vma);

        // 3. 将虚拟内存地址映射到物理内存(也就是将进程的页表项设置为刚生成的页表项的值)
        set_pte_at(mm, address, page_table, entry);
        ...
    
    ...

    return ret;

__do_fault() 函数对处理文件映射部分主要分为 3 个步骤:

  • 调用虚拟内存管理区结构(vma)的 fault() 回调函数(也就是 filemap_fault() 函数)来获取到文件的页缓存。
  • 将虚拟内存地址映射到页缓存的物理内存页(也就是将进程的页表项设置为上面生成的页表项的值)。

对于 filemap_fault() 函数是怎样读取文件页缓存的,本文不作解释,有兴趣的可以自行阅读源码。

最后,我们以一幅图来描述一下虚拟内存是如何与文件进行映射的:

从上图可以看出,mmap() 是通过将虚拟内存地址映射到文件的页缓存来实现的。当对映射后的虚拟内存进行读写操作时,其效果等价于直接对文件的页缓存进行读写操作。对文件的页缓存进行读写操作,也等价于对文件进行读写操作。


mmap 和常规文件操作的区别

常规文件调用过程:

  1. 进程发起读文件请求。

  2. 内核通过查找进程文件描述符表,定位到内核已打开文件集上的文件信息,从而找到此文件的inode。

  3. inode在address_space上查找要请求的文件页是否已经缓存在页缓存中。如果存在,则直接返回这片文件页的内容。

  4. 如果不存在,则通过inode定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页面过程,进而将页缓存中的数据发给用户进程。

像 read()/write() 这些系统调用,首先需要进入内核空间,然后把文件内容读入到缓存中,然后再对缓存进行读写操作,最后由内核定时同步到文件中。

总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。


而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到页Buffer的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间虚拟地址直接映射到页Buffer物理页上,从而实现用户空间无需进入内核即可访问页Buffer。因此mmap效率更高。

调用 mmap() 系统调用对文件进行映射后,用户对映射后的内存进行读写实际上是对文件缓存的读写,所以减少了一次系统调用,从而加速了对文件读写的效率。

如果当前进程拥有的虚拟地址空间中,存在某部分区域的虚拟地址是直接解析到内核空间中页buffer拥有的物理地址上的,也就是当前进程内存在一部分虚拟地址空间和内核空间中页buffer对应的虚拟地址空间映射到了同一块物理地址上,如下图:

那么后续通过DMA从磁盘将文件数据加载到页Buffer Pool后,进程一不就直接可以通过自身内部那段虚拟地址空间直接获取到文件数据了吗?
对于常规的文件读写方式来说,由于对应的进程在用户空间中没有那段直接映射到物理地址中页buffer存储位置的虚拟空间,所以就无法在用户空间内直接访问到内核空间中的页buffer,就必须使用系统调用进行访问了。


由上文讨论可知,mmap优点共有一下几点:

  1. 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

  2. 提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。

同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。

  1. 可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。

mmap 使用的细节

  1. 使用mmap需要注意的一个关键点是,mmap映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是4k字节)。原因是,内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap从磁盘到虚拟地址空间的映射也必须是页。

  2. 内核可以跟踪被内存映射的底层对象(文件)的大小,进程可以合法的访问在当前文件大小以内又在内存映射区以内的那些字节。也就是说,如果文件的大小一直在扩张,只要在映射区域范围内的数据,进程都可以合法得到,这和映射建立时文件的大小无关。

  3. 映射建立之后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。同时可用于进程间通信的有效地址空间不完全受限于被映射文件的大小,因为是按页映射。


小结

  • 对于传统的linux系统文件操作

其特点为:

  1. 使用页缓存机制,提高读写效率和保护磁盘
  2. 读文件时,先将文件从磁盘拷贝到缓存,由于页缓存区是在内核空间,不能被用户空间直接访问,所以需要将页缓存区数据再次拷贝到用户空间,有2次文件拷贝工作
  • 使用内存映射文件读/写的流程:

其特点为:

  1. 用户空间与内核空间的交互式通过映射的区域直接交互,用内存的读取代替I/O读写,文件读写效率高
  2. 可实现高效的大规模数据传输
  • 在Linux系统中,根据内存映射的本质和特点,其应用场景在于
  1. 实现内存共享,如跨进程通信
  2. 提高数据读/写效率:如读写操作
  • 对于进程间的通信,其工作流程如下图所示

  1. 创建一块共享的接收区,实现地址映射关系
  2. 发送进程数据到自身的虚拟内存区域,数据拷贝1次
  3. 由于发送进程的虚拟地址空间与接收进程的虚拟内存地址存在映射关系,所以发送到的数据也存放到接收进程的虚拟内存中,即实现了跨进程间通信

总结:

  • 内存映射的读写操作主要的过程如下:
  1. 创建虚拟映射区域,其在当前进程的虚拟地址空间中,寻找一段满足大小要求的虚拟地址,并且为此虚拟地址分配一个虚拟内存区域(vm_area_struct结构),初始化该虚拟内存区域,插入到进程虚拟地址区域的链表和红黑树中
  2. 实现地址映射关系,建立页表,该过程在mmap函数中并未实现,此时只是创建了映射关系,并不将任何文件数据拷贝至主存中,真正的数据拷贝是通过进程发起读写操作时
  3. 进程访问该映射空间,实现文件内容到物理内存的数据拷贝,当进程读写访问该映射地址时,如果进程写操作改变了内容,并不会立即更新,而是一定时间后系统会自动会写脏数据到对应硬盘的地址空间
  • 使用mmap来创建文件映射,由于只建立了进程地址空间VMA,并没有马上分配page cache和建立映射关系。那么就会导致一个问题,当创建一个很大的VMA,会频繁发生缺页中断。
  1. 内存映射机制mmap是POSIX标准的系统调用,有匿名映射和文件映射两种。
  2. 匿名映射使用进程的虚拟内存空间,它和malloc(3)类似,实际上有些malloc实现会使用mmap匿名映射分配内存,不过匿名映射不是POSIX标准中规定的。
  3. 文件映射有MAP_PRIVATE和MAP_SHARED两种。前者使用COW的方式,把文件映射到当前的进程空间,修改操作不会改动源文件。后者直接把文件映射到当前的进程空间,所有的修改会直接反应到文件的page cache,然后由内核自动同步到映射文件上。
  • 相比于IO函数调用,基于文件的mmap的一大优点是把文件映射到进程的地址空间,避免了数据从用户缓冲区到内核page cache缓冲区的复制过程;当然还有一个优点就是不需要频繁的read/write系统调用。

以上是关于Linux mmap原理的主要内容,如果未能解决你的问题,请参考以下文章

Linux|Mmap的实现原理和应用

Linux内存管理:mmap原理

Linux|Mmap的实现原理和应用

Android中mmap原理及应用简析

linux内存管理概述

Linux之共享内存shm和内存映射mmap