【Linux】mmap的介绍和使用

参考文章:https://cloud.tencent.com/developer/article/2420465

一、mmap介绍

  • mmap (Memory Map) 是一种将文件或设备映射到内存的系统调用,允许应用程序直接通过内存地址访问文件数据,无需使用 read/write 等系统调用

  • 虽然 mmap() 最初是为映射文件而设计的,但它实际上是一个通用映射工具。它可用于将任何适当的对象(例如内存、文件、设备等)映射到进程的地址空间。

  • 以文件映射到内存为例,实现这样的映射后,进程虚拟地址空间中一段内存地址将与文件磁盘地址一一对应,进程就可以采用指针的方式读写这段内存,系统会自动回写脏 页到对应的磁盘文件。

二、mmap原理

2.1 mmap映射过程

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

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

    • 进程在用户空间调用函数 mmap
    • 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续虚拟地址
    • 为此虚拟区分配一个 vm_area_struct 结构,接着对这个结构的各个域进行初始化
    • 将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
  2. 调用内核空间的系统调用函数 mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的映射

    • 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核"已打开文件集"中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
    • 通过该文件的文件结构体,链接到file_operations 模块,调用内核函数 mmap,其原型为int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
    • 内核 mmap 函数通过虚拟文件系统 inode 模块定位到文件磁盘物理地址。
    • 通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中
  3. 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存的拷贝

    • 进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址对应的物理内存页面上没有数据。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存,因此引发缺页异常。
    • 缺页异常进行一系列判断,确定无非法操作后,内核发起调页过程。
    • 调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用 nopage 函数把所缺的页从磁盘载入主存。
    • 之后进程即可对这片主存进行读写,如果写操作改变了其内容,一定时间后系统会自动回写脏页到对应磁盘地址,即完成了写入到文件的过程。

2.2 mmap对比常规文件操作

常规文件操作,这里指的是read/write操作,通常有以下的执行过程,以read为例

  1. 进程发起读文件请求
  2. 内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的 inode
  3. inodeaddress_space 上查找要请求的文件页是否已经缓存在页缓存。如果存在,则直接返回这片文件页的内容
  4. 如果不存在,则通过 inode 定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页请求,进而将页缓存中的数据发给用户进程

总的来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制:

  • 读文件时需要先将文件页从磁盘拷贝到页缓存。
  • 由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到用户空间内存
  • 这样,通过了两次数据拷贝,才能完成进程对文件内容的获取任务

写操作也是一样:

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

而使用 mmap 操作文件:

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

系统调用 建立页表映射 访问触发缺页中断 加载文件数据 磁盘文件 应用程序 内核 虚拟内存 物理内存 页缓存 Page Cache 硬盘

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

对比项 传统写入(write 内存映射写入(mmap
数据拷贝次数 两次(用户空间 → 内核空间 → 磁盘) 一次(内核空间 → 磁盘)
系统调用开销 每次写入都需调用write,开销较高 仅需mmapmunmap,写入时无系统调用
写入方式 通过write等函数传递数据 直接操作内存地址(如指针赋值、memcpy
内存占用 依赖内核缓冲区大小 映射区域占用虚拟内存(实际物理内存按需分配)
文件大小限制 无明确限制(受文件系统和磁盘容量限制) 需预先通过ftruncate设置文件大小
同步控制 通过fsync强制同步 通过msync强制同步
适用场景 随机写入、小数据块写入、动态调整文件大小 大数据块连续写入、频繁访问同一区域、文件与内存结构映射

性能对比

  • mmap 更高效
    • 大量连续写入(如文件复制、数据库批量写入)
    • 频繁随机访问(如内存数据库、索引文件)
    • 需与内存结构直接映射(如共享内存、进程间通信)
  • 传统写入更高效
    • 小数据块随机写入(如日志追加)
    • 文件大小动态变化(mmap需重新映射)

三、mmap应用

3.1 mmap常用API

常用的api是下面的三个,下面我们一一介绍

c 复制代码
#include <sys/mman.h>

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

mmap 函数原型

c 复制代码
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
1. addr:映射起始地址
  • 作用 :指定映射区域的起始地址。通常设为 NULL,让系统自动选择合适的地址。
  • 注意
    • 若不为 NULL,系统会尝试将映射区域起始于此地址,但最终地址可能因对齐要求而调整。
    • 大多数场景建议使用 NULL,避免兼容性问题。
2. length:映射区域大小(字节)
  • 作用:指定要映射的文件或设备的长度。
  • 要求
    • 必须为正整数。
    • 若文件长度小于 length,则映射区域超出文件部分的值为 0(称为 "文件空洞")。
    • 若文件长度大于 length,则仅映射前 length 字节。
3. prot:内存保护标志(访问权限)
  • 可选值(按位或组合)
    • PROT_READ:可读
    • PROT_WRITE:可写
    • PROT_EXEC:可执行
    • PROT_NONE:不可访问
  • 限制
    • 权限不能超过文件打开模式(如 open 以只读模式打开,则 prot 不能包含 PROT_WRITE)。
    • 示例:PROT_READ | PROT_WRITE 表示可读可写。
4. flags:映射类型和行为标志
  • 必选标志
    • MAP_SHARED:创建共享映射,对映射区域的修改会反映到文件,并被其他映射同一文件的进程看到。
    • MAP_PRIVATE:创建私有映射,对映射区域的修改仅对当前进程可见,不影响原文件(写时复制机制)。
  • 可选标志(常用)
    • MAP_ANONYMOUS:映射匿名内存(不关联文件),此时 fd 需设为 -1offset 需为 0
    • MAP_FIXED:强制使用 addr 指定的地址(不推荐,可能导致地址冲突)。
    • MAP_NORESERVE:不预分配物理内存(适用于大映射,仅在实际访问时分配)。
5. fd:文件描述符
  • 作用:指定要映射的文件或设备。
  • 要求
    • 需通过 openshm_open 等函数预先打开。
    • 若使用 MAP_ANONYMOUS,则 fd 必须为 -1
6. offset:文件偏移量
  • 作用:指定从文件的哪个位置开始映射(以字节为单位)。
  • 要求
    • 必须是系统页大小(通常为 4KB)的整数倍。
    • 示例:offset = 4096 表示从文件第 4096 字节处开始映射。
返回值
  • 成功 :返回映射区域的起始地址(类型需转换为 char* 或其他指针类型)。
  • 失败 :返回 MAP_FAILED(即 (void*)-1),并设置 errno 以指示错误类型。
常见错误码
  • EACCES:权限不足(如文件只读但 prot 包含 PROT_WRITE)。
  • EINVAL:参数无效(如 offset 不是页大小的整数倍)。
  • ENOMEM:内存不足或地址空间不足。
  • EBADFfd 不是有效的文件描述符。

munmap函数原型

c 复制代码
#include <sys/mman.h>

int munmap(void *addr, size_t length);
1. addr:映射区域起始地址
  • 作用 :指定要解除映射的内存区域的起始地址,必须与 mmap 返回的地址完全一致。
  • 注意
    • 若传入非法地址(如未映射的地址或部分映射的地址),会导致 EINVAL 错误。
    • 即使映射区域已被部分释放,也必须传入完整的起始地址。
2. length:映射区域长度
  • 作用 :指定要解除映射的内存区域的长度(字节),必须与 mmap 时指定的长度一致。
  • 注意
    • length 与原映射长度不一致,可能导致未定义行为。
    • 必须解除整个映射区域,不支持部分解除。
返回值
  • 成功 :返回 0
  • 失败 :返回 -1,并设置 errno 以指示错误类型。
常见错误码
  • EINVAL
    • addr 不是有效的映射起始地址。
    • length0
  • ENOMEM
    • 内核内部内存不足,无法完成操作

msync函数原型

c 复制代码
#include <sys/mman.h>

int msync(void *addr, size_t length, int flags);
1. addr:映射区域起始地址
  • 作用 :指定要同步的内存映射区域的起始地址,必须与 mmap 返回的地址一致。
  • 注意
    • 若传入非法地址,会导致 EINVAL 错误。
    • 地址需按页对齐(通常为 4KB 边界),否则行为未定义。
2. length:同步区域长度
  • 作用 :指定从 addr 开始的连续字节数,需同步的区域为 [addr, addr+length)
  • 特殊情况
    • length 为 0,则不执行同步操作,但函数仍可能返回成功(0)。
    • length 超过映射区域边界,会导致 EINVAL 错误。
3. flags:同步行为标志
  • 必选标志(三选一)
    • MS_SYNC :同步模式,阻塞直到所有数据写入磁盘。
      • 保证:调用返回成功时,数据已持久化到磁盘(即使系统崩溃也不会丢失)。
      • 性能影响:可能导致明显延迟,尤其在机械硬盘上。
    • MS_ASYNC :异步模式,立即返回,内核异步将数据写入磁盘。
      • 保证:仅标记数据需要写入,不等待实际磁盘操作完成。
      • 适用场景:性能敏感场景,可容忍一定数据丢失风险。
    • MS_INVALIDATE :使缓存失效,强制重新从磁盘读取数据。
      • 作用:用于刷新其他进程对文件的修改,确保后续读取到最新内容。
      • 注意 :与 MS_SYNC/MS_ASYNC 互斥,不可同时使用。
  • 可选标志(与上述标志组合)
    • MS_KERNEL:Linux 特有的标志,指示同步内核缓存(而非用户空间缓存)。
      • 通常无需显式指定,默认行为已包含此逻辑。
返回值
  • 成功 :返回 0
  • 失败 :返回 -1,并设置 errno 以指示错误类型。
常见错误码
  • EINVAL
    • flags 包含无效组合(如同时指定 MS_SYNCMS_INVALIDATE)。
    • addrlength 无效(如非页对齐地址)。
  • EBUSY
    • 映射区域正被内核锁定,无法立即同步 。
  • EIO
    • 磁盘 I/O 错误,可能导致数据未完全写入。

3.2 mmap读取文件

首先,我们准备一个文本文件,生成一些内容,用于测试

cpp 复制代码
int generateTest()
{
    int fd = open("1.txt", O_CREAT | O_WRONLY);
    if (fd == -1)
    {
        perror("open");
        return 1;
    }

    for (int i = 1; i <= 1000000; ++i)
    {
        std::string info = "This is a mmap test" + std::to_string(i) + "\n";
        write(fd, info.c_str(), info.size());
    }

    close(fd);

    return 0;
}

使用mmap进行文件读操作

cpp 复制代码
int test1()
{
    int fd = open("1.txt", O_RDONLY);
    struct stat sb;
    if (fstat(fd, &sb) == -1)
    {
        perror("fstat");
        return 1;
    }

    char *addr = (char *)mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (addr == MAP_FAILED)
    {
        perror("mmap");
        return 1;
    }

    std::cout << "===== File Content =====\r\n";
    std::cout << addr;

    if (munmap(addr, sb.st_size))
    {
        perror("munmap");
        return 1;
    }

    close(fd);

    return 0;
}

为了对比mmap的效率,我们这里新增使用read直接读取数据的例子

cpp 复制代码
int test2()
{
    int fd = open("1.txt", O_RDONLY);
    struct stat sb;
    if (fstat(fd, &sb) == -1)
    {
        perror("fstat");
        return 1;
    }

    std::cout << "===== File Content =====\r\n";
    char buffer[1024];

    int len = 0;
    while ((len = read(fd, buffer, sizeof(buffer) - 1)) != 0)
    {
        buffer[len] = '\0';
        std::cout << buffer;
    }

    close(fd);

    return 0;
}

如果只是小文件,mmap体现不了效率,因为建立映射也需要额外的开销

如果是大文件读取,这里的效率就可以看出来了,mmap比直接read快了将近1000ms

3.3 mmap写文件

mmap写文件,首先需要使用ftruncate提前指定文件的大小,注意这个函数只是提前将文件(通过 fd 指定)的大小调整为 length 字节,而不会实际分配内存

函数原型如下:

c 复制代码
#include <unistd.h>

int ftruncate(int fd, off_t length);
  • 调整文件大小后,就可以使用mmap建立映射关系了,得到的内存可以像普通内存一样操作
  • 这里通过循环的memcpy将写入的信息拷贝到虚拟内存中,由内核将缓存写入磁盘

写入虚拟内存后,还可以使用msync同步到磁盘,这里指定策略为MS_SYNC,一直阻塞到内存刷入磁盘,如果不调用个函数,也会在特定情况下内核自动刷新到磁盘中

最后调用munmap取消内存映射,释放虚拟内存

cpp 复制代码
#include<sys/mman.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<iostream>
#include<cstring>
#include<unistd.h>

#define WRITE_LINES 100000
std::string strBuffer[WRITE_LINES + 1];

int main(){
    int fd = open("2.txt",O_RDWR| O_CREAT,0644);
    if(fd == -1){
        perror("open");
        return 1;
    }
    
    int length = 0;
    for(int i = 1 ; i <= WRITE_LINES ; ++i){
        std::string info = "Hello mmap:" + std::to_string(i) + "\n";
        strBuffer[i] = info;
        length += info.size();
    }

    if(ftruncate(fd,length) == -1){
        perror("ftruncate");
        return 1;
    }

    char* addr = (char*)mmap(NULL,length,PROT_READ | PROT_WRITE, MAP_SHARED,fd,0);
    if(addr == MAP_FAILED){
        perror("mmap");
        return 1;
    }

    int j = 0;
    for(int i = 1 ; i <= WRITE_LINES ; ++i){
        memcpy(addr + j,strBuffer[i].c_str(),strBuffer[i].size());
        j += strBuffer[i].size();
    }

    if(msync(addr,length,MS_SYNC) == -1){
        perror("msync");
        return 1;
    }

    munmap(addr,length);
    close(fd);

    return 0;
}

运行程序后,查看写入的文件,可以查看到对应的内容

shell 复制代码
cat 2.txt

3.4 mmap进程间通讯

mmap 也可以配合 MAP_ANONYMOUS 标志,实现父子进程间的内存共享通信

主要的步骤如下:

  1. 创建一块匿名共享内存区域
  2. 通过 fork() 创建子进程
  3. 子进程向共享内存写入消息
  4. 父进程等待子进程结束后,从共享内存读取消息
  5. 释放共享内存资源

注意,为了实现父子进程之间的通讯,mmap函数中必须使用MAP_ANONYMOUS标志,创建匿名映射(不关联文件),以及MAP_SHARED允许多进程共享该内存区域

关联后得到的内存块,就可以像system V共享内存那样使用了

c 复制代码
#include<iostream>
#include<unistd.h>
#include<sys/mman.h>
#include<sys/wait.h>
#include<cstring>

int main(){
    ssize_t size = 4096;
    char * shared = (char*)mmap(NULL,size,PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS,-1,0);

    if(shared == MAP_FAILED){
        perror("mmap");
        return 1;
    }

    pid_t pid = fork();
    if(pid == -1){
        perror("fork");
        return 1;
    }

    if(pid == 0){ 
        std::string info = "Hello from child process !";
        memcpy(shared,info.c_str(),info.length());
        return 0;
    }
    else{
        wait(NULL);
        std::cout << "Receive from child process : " << shared << std::endl;
    }
    munmap(shared,size);
    
    return 0;
}

运行结果如下:

更多资料:https://github.com/0voice

相关推荐
TE-茶叶蛋几秒前
React 服务器组件 (RSC)
服务器·前端·react.js
小哈里36 分钟前
【管理】持续交付2.0:业务引领的DevOps-精要增订本,读书笔记(理论模型,技术架构,业务价值)
运维·架构·devops·管理·交付
kfepiza37 分钟前
`/etc/samba/smb.conf`笔记250721
linux·网络协议
beyoundout1 小时前
LVS(Linux virtual server)-实现四层负载均衡
linux·服务器·lvs
Spike()1 小时前
LVS工作模式和算法的总结
linux·服务器·lvs
禁默1 小时前
《命令行参数与环境变量:从使用到原理的全方位解析》
linux
沙老师1 小时前
删除debian xdm自启动ibus的配置项
运维·服务器·debian
老马啸西风1 小时前
windows wsl ubuntu 如何安装 maven
linux·运维·windows·ubuntu·docker·k8s·maven
东木君_2 小时前
【Linux驱动-快速回顾】简单了解一下PinCtrl子系统:设备树如何被接解析与匹配
linux·运维·服务器
CZIDC2 小时前
博客摘录「 华为云平台-FusionSphere OpenStack 8.2.1 系统加固」2025年7月15日
linux·服务器·笔记·华为云·openstack