【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. inode 在 address_space 上查找要请求的文件页是否已经缓存在页缓存。如果存在,则直接返回这片文件页的内容
  4. 如果不存在,则通过 inode 定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页请求,进而将页缓存中的数据发给用户进程

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

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

写操作也是一样:

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

而使用 mmap 操作文件:

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

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

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

对比项 传统写入(write) 内存映射写入(mmap)
数据拷贝次数 两次(用户空间 → 内核空间 → 磁盘) 一次(内核空间 → 磁盘)
系统调用开销 每次写入都需调用write,开销较高 仅需mmap和munmap,写入时无系统调用
写入方式 通过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 需设为 -1,offset 需为 0。
    • MAP_FIXED:强制使用 addr 指定的地址(不推荐,可能导致地址冲突)。
    • MAP_NORESERVE:不预分配物理内存(适用于大映射,仅在实际访问时分配)。
5. fd:文件描述符
  • 作用:指定要映射的文件或设备。
  • 要求 :
    • 需通过 open、shm_open 等函数预先打开。
    • 若使用 MAP_ANONYMOUS,则 fd 必须为 -1。
6. offset:文件偏移量
  • 作用:指定从文件的哪个位置开始映射(以字节为单位)。
  • 要求 :
    • 必须是系统页大小(通常为 4KB)的整数倍。
    • 示例:offset = 4096 表示从文件第 4096 字节处开始映射。
返回值
  • 成功 :返回映射区域的起始地址(类型需转换为 char* 或其他指针类型)。
  • 失败 :返回 MAP_FAILED(即 (void*)-1),并设置 errno 以指示错误类型。
常见错误码
  • EACCES:权限不足(如文件只读但 prot 包含 PROT_WRITE)。
  • EINVAL:参数无效(如 offset 不是页大小的整数倍)。
  • ENOMEM:内存不足或地址空间不足。
  • EBADF:fd 不是有效的文件描述符。

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 不是有效的映射起始地址。
    • length 为 0。
  • 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_SYNC 和 MS_INVALIDATE)。
    • addr 或 length 无效(如非页对齐地址)。
  • 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

相关推荐
ajassi20001 小时前
AI语音智能体开发日记(十八)智能体服务器xiaozhi-esp32-server源码部署指南
运维·服务器·人工智能·ai·ai编程
‎ദ്ദിᵔ.˛.ᵔ₎1 小时前
linux 进程信号(下)
linux·运维·服务器
skywalk81632 小时前
在FreeBSD系统的linux仿真环境下安装和使用kiro 这个亚马逊的AI agent
linux·人工智能·freebsd·kiro
Mikko72 小时前
Linux 磁盘满了但 du 找不到?LVM 扩容 df 不变?fstab 写错开不了机?三台真机实测磁盘排查
linux·运维·centos
蓝胖的四次元口袋2 小时前
Jenkins知识梳理(2)
运维·jenkins
yumgpkpm2 小时前
Acceldata ODP 3.3.6.4 vs CDP Private Cloud Base 7.3.2 对比
大数据·运维·服务器·hadoop·华为·zookeeper·hbase
Madison-No72 小时前
多语言聊天大模型--测试报告
linux·git·python·selenium·jmeter·自动化·postman
Liuqy-052 小时前
特殊进程——孤儿、僵尸、守护进程
linux·运维·服务器
流浪0012 小时前
Linux系统篇47——线程(十二) POSIX 信号量与环形队列,把判空判满提前到访问之前
linux·操作系统·线程·信号量·环形队列
咖丨喱3 小时前
【MMC Core + hc16 Host(WiFi SDIO 如何落到控制器)】
linux