mmap()系统调用,用于将文件映射到进程的地址空间中。这个过程创建了一个虚拟内存区域把对文件的操作转为对内存的操作,以此避免更多的lseek()与read()、write()操作,这点对于大文件或者频繁访问的文件带来显著的性能提升。

传统的读写文件

  • 页缓存(page cache)是读写文件时的中间层,内核使用页缓存与文件的数据块关联起来。所以应用程序读写文件时,实际操作的是页缓存。

mmap读写文件

在理解mmap()之前,先了解几个核心概念:

  • 虚拟地址空间:每个进程都有自己独立的虚拟地址空间。程序中的内存地址都是虚拟地址,由操作系统和内存管理单元 (MMU) 负责将它们映射到物理内存。
  • 内存映射:mmap()的核心功能就是建立一个从文件到进程虚拟地址空间的映射。一旦映射建立,程序对这个虚拟地址空间的读写操作,实际上就是对文件的读写

通常使用mmap()的三种情况:

  • 提高I/O效率。
  • 匿名内存映射。
  • 共享内存进程间通信。

函数原型

#include <sys/mman.h>

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

参数说明:

  • addr: 指定映射区域的起始地址。通常传入NULL让内核自动选择。
  • length: 映射区域的长度,单位是字节。
  • prot: 内存保护标志,定义了映射区域的访问权限。可以组合使用:
    • PROT_EXEC: 映射区域可执行。
    • PROT_READ: 映射区域可读。
    • PROT_WRITE: 映射区域可写。
    • PROT_NONE: 映射区域不可访问。
  • flags: 映射的类型和其他行为。常用的标志有:
    • MAP_SHARED: 创建一个共享映射。对内存区域的修改会同步到文件中,并且对其他同样映射了此文件的进程也可见。
    • MAP_PRIVATE: 创建一个私有映射。对内存区域的修改不会同步到文件中,也不会影响其他进程。当发生写入时,操作系统会使用写时复制机制,为该进程创建一个私有的页面副本。
    • MAP_ANONYMOUS: 创建一个匿名映射,不与任何文件关联。这种映射通常用于进程间通信(IPC)或分配大块内存。
  • fd: 要映射的文件的文件描述符。如果flags中包含了MAP_ANONYMOUS,则fd必须为-1。
  • offset: 文件中需要映射的起始位置的偏移量。必须是系统页面大小的整数倍。

使用完mmap()创建的映射区域后,必须调用munmap()来释放它。

// addr: mmap()返回的起始地址
// length: 映射区域的长度,必须与mmap()调用时传入的length一致。
int munmap(void *addr, size_t length);

工作原理

mmap的核心原理在于页表映射

每个进程都有一个独立的虚拟地址空间。这个空间被划分为许多固定大小的块,称为页(page),通常为 4KB。当程序访问一个虚拟地址时,内存管理单元 (MMU) 会通过一个页表(page table)将虚拟地址翻译成对应的物理地址。

页表是一个数据结构,记录了虚拟页和物理页之间的映射关系。如果一个虚拟地址没有对应的物理页,就会触发一个缺页中断(page fault)。操作系统内核会捕获这个中断,并根据需要为该虚拟地址分配一个物理页。

mmap系统调用的最终目的是将设备或文件映射到用户进程的虚拟地址空间,实现用户进程对文件的直接读写,这个任务可以分为以下三步:

  1. 创建虚拟内存区域:内核在进程的虚拟地址空间中创建一个新的内存区域,这个区域的大小由length参数指定,起始地址由addr参数指定(如果addr为NULL,内核会自动选择一个合适的地址)。这个区域的访问权限由prot参数定义。(由内核mmap系统调用完成)
  2. 建立文件与内存的映射关系:建立虚拟地址空间和文件或设备的物理地址之间的映射。(由设备驱动完成)
    • 建立映射是通过修改进程页表来实现的。
  3. 按需加载数据:当进程访问映射区域时,如果访问的页面尚未加载到内存中,操作系统会触发一个缺页中断(page fault)。内核会捕获这个异常,并从文件中读取相应的数据,将其加载到内存中,然后更新页表,使得虚拟地址指向正确的物理内存地址。(由内核的缺页处理机制完成)