【Linux系统加餐】 mmap 文件映射全解:从底层原理、API 到实战开发(含 malloc 模拟实现)


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言:
  • [一. mmap 到底是什么?](#一. mmap 到底是什么?)
    • [1.1 核心优势](#1.1 核心优势)
    • [1.2 映射的内存布局](#1.2 映射的内存布局)
  • [二. mmap 与 munmap API 全解析](#二. mmap 与 munmap API 全解析)
    • [2.1 函数原型](#2.1 函数原型)
    • [2.2 mmap 参数介绍](#2.2 mmap 参数介绍)
    • [2.3 返回值说明](#2.3 返回值说明)
    • [2.4 核心标志位深度辨析:MAP_SHARED vs MAP_PRIVATE](#2.4 核心标志位深度辨析:MAP_SHARED vs MAP_PRIVATE)
  • [三. mmap 实战开发(PDF 完整代码复刻 + 详细注释)](#三. mmap 实战开发(PDF 完整代码复刻 + 详细注释))
    • [3.1 实战 1:基于 mmap 的文件写入](#3.1 实战 1:基于 mmap 的文件写入)
    • [3.2 实战 2:基于 mmap 的文件读取](#3.2 实战 2:基于 mmap 的文件读取)
    • [3.3 实战 3:用 mmap 极简模拟 malloc/free 实现](#3.3 实战 3:用 mmap 极简模拟 malloc/free 实现)
  • [四. mmap 使用避坑指南(开发必看)](#四. mmap 使用避坑指南(开发必看))
  • [五. 传统 read/write vs mmap 怎么选?(仅供参考)](#五. 传统 read/write vs mmap 怎么选?(仅供参考))
  • 结尾:

前言:

大家好,我是深耕 Linux 内核与系统开发的博主。在 Linux 高性能开发中,mmap是一个极具魔力的系统调用 ------ 它能让我们直接通过内存操作读写文件,省去传统read/write的内核态与用户态数据拷贝开销,还能实现进程间共享内存、自定义内存分配等高级功能。本文从核心原理、API 参数、实战代码到避坑指南全覆盖,所有代码均可直接编译运行,兼顾学习理解与工业级开发参考。


一. mmap 到底是什么?

mmap全称memory map,即内存映射 ,是 Linux 提供的系统调用,核心能力是:将一个文件或设备的内容,直接映射到进程的虚拟地址空间中。

映射完成后,进程对这段虚拟内存的读写操作,会被内核自动同步到对应的文件 / 设备上,无需再调用传统的read/write系统调用。

1.1 核心优势

  • 零拷贝高效访问 :传统read/write需要先把数据从磁盘拷贝到内核缓冲区,再拷贝到用户态内存;而mmap直接建立文件与用户虚拟地址的映射,只需要一次拷贝,大幅提升大文件读写效率。
  • 统一访问形式:操作文件就像操作内存一样,直接通过指针读写,无需繁琐的文件偏移操作。
  • 天然支持共享内存:多个进程映射同一个文件,可直接实现进程间数据共享,是 Linux 进程间通信(IPC)的经典实现方式。
  • 灵活的内存管理 :可实现匿名映射,用于自定义内存分配,替代malloc的部分场景。

1.2 映射的内存布局

在进程的虚拟地址空间中,mmap的映射区域位于堆区和栈区之间的共享区(mmap 区域),和动态库的加载区域一致。

二. mmap 与 munmap API 全解析

2.1 函数原型

cpp 复制代码
#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);

2.2 mmap 参数介绍


2.3 返回值说明

  • mmap成功:返回指向映射区域起始地址的指针;
  • mmap失败:返回MAP_FAILED(即(void *)-1),并设置errno指示错误原因;
  • munmap成功:返回 0;
  • munmap失败:返回 - 1,并设置errno

2.4 核心标志位深度辨析:MAP_SHARED vs MAP_PRIVATE

这是mmap最核心的两个标志位,决定了映射的行为模式,必须分清:

特性 MAP_SHARED(共享映射) MAP_PRIVATE(私有映射)
修改同步 对内存的修改会同步到底层文件 修改不会同步到文件,触发写时拷贝
多进程可见 对其他映射同一文件的进程可见 对其他进程不可见,修改仅当前进程有效
适用场景 进程间共享内存、大文件读写修改 只读文件映射、私有内存分配、不希望修改源文件的场景

三. mmap 实战开发(PDF 完整代码复刻 + 详细注释)

3.1 实战 1:基于 mmap 的文件写入

该示例通过mmap映射文件,直接向映射内存写入数据,无需write系统调用,数据会自动同步到文件。

关键注意事项:

  • 要实现写入映射,文件必须以O_RDWR模式打开(读写模式);
  • 空文件无法直接映射,必须通过ftruncate设置文件大小,保证映射的长度有对应的文件存储空间;
  • 映射长度必须是页大小整数倍。
cpp 复制代码
#include<iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>

const int PAGE_SIZE = 4096; // 其实最后最小都是 4096,一定要是4096的倍数,否则会报错
// write_mmap filename
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " filename" << std::endl;
        return 1;
    }
    // 1.打开目标文件, mmap需要自己先打开文件
    int fd = ::open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        std::cerr << "Failed to open file: " << argv[1] << std::endl;
        return 2;
    }

    // 2. 我们需要手动调整一个文件的大小,方便我们进行合法的mmap
    if(::ftruncate(fd, PAGE_SIZE) < 0)
    {
        std::cerr << "Failed to ftruncate file: " << argv[1] << std::endl;
        return 3;
    }

    // 3. 进行mmap操作
    char *shmaddr = (char*)::mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(shmaddr == MAP_FAILED)
    {
        std::cerr << "Failed to mmap file: " << argv[1] << std::endl;
        return 4;
    }

    // 4. 正在进行文件操作
    for (char c = 'a'; c <= 'z'; c++) 
    {
        shmaddr[c - 'a'] = c;
        sleep(1);
    }

    // 5. 关闭文件映射
    if(::munmap(shmaddr, PAGE_SIZE) == -1)
    {
        std::cerr << "Failed to munmap file: " << argv[1] << std::endl;
        return 5;
    }

    // 6. 关闭文件描述符
    ::close(fd);
    return 0;
}



3.2 实战 2:基于 mmap 的文件读取

该示例通过mmap映射已有文件,直接读取映射内存即可获取文件内容,无需read系统调用。

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

const int PAGE_SIZE = 4096; // 其实最后最小都是 4096,一定要是4096的倍数,否则会报错
// read_mmap filename
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " filename" << std::endl;
        return 1;
    }
    // 1.打开目标文件, mmap需要自己先打开文件
    int fd = ::open(argv[1], O_RDONLY);
    if(fd < 0)
    {
        std::cerr << "Failed to open file: " << argv[1] << std::endl;
        return 2;
    }

    // 2. 获取文件的大小
    struct stat st;
    if(::fstat(fd, &st) < 0)
    {
        std::cerr << "Failed to fstat file: " << argv[1] << std::endl;
        return 3;
    }

    // 3. 进行mmap操作
    char *shmaddr = (char*)::mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
    if(shmaddr == MAP_FAILED)
    {
        std::cerr << "Failed to mmap file: " << argv[1] << std::endl;
        return 4;
    }

    // 4. 正在进行文件操作
    std::cout << shmaddr << std::endl;

    // 5. 关闭文件映射
    if(::munmap(shmaddr, st.st_size) == -1)
    {
        std::cerr << "Failed to munmap file: " << argv[1] << std::endl;
        return 5;
    }

    // 6. 关闭文件描述符
    ::close(fd);
    return 0;
}


哎,为啥没读到我们后面之前填充的那些东西呢,因为那些是用的0值填充

3.3 实战 3:用 mmap 极简模拟 malloc/free 实现

malloc的底层实现,在分配大内存时,本质就是通过mmap的匿名映射实现的。我们可以通过mmap+munmap,极简模拟mallocfree的核心功能。

核心原理

  • 匿名映射 :通过MAP_PRIVATE | MAP_ANONYMOUS标志创建,不关联任何文件,仅分配一段私有的空白内存;
  • my_malloc:调用mmap分配指定大小的内存,返回内存首地址;
  • my_free:调用munmap释放映射的内存。
cpp 复制代码
#include<iostream>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>

// 极简malloc实现
void* my_malloc(size_t size)
{
    if(size > 0)
    {
        void* addr = (void*)::mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if(addr == MAP_FAILED)
        {
            std::cerr << "Failed to mmap " << size << std::endl;
            return nullptr;
        }
        return addr;
    }
    return nullptr;
}

void my_free(void* start, size_t size)
{
    if(start != nullptr && size > 0)
    {
        int ret = ::munmap(start, size);
        if(ret == -1)
        {
            std::cerr << "Failed to munmap " << size << std::endl;
        }
    }
}
int main()
{
    char* p = (char*)my_malloc(1024);
    if(p == nullptr)
    {
        std::cerr << "Failed to malloc 1024 bytes" << std::endl;
        return 1;
    }
    // 使用分配的内存,简单打印指针值
    printf("Allocated memory at address: %p\n", p);
    // 在这里使用ptr指向的内存
    memset(p, 'A', 1024);

    for(int i = 0; i < 1024; i++)
    {
        printf("%c ", p[i]);
        fflush(stdout);
        sleep(1);
    }

    // 释放内存
    my_free(p, 1024);
    return 0;
}


进阶验证:gdb 查看内存映射

我们可以通过 gdb 调试,查看mmap前后进程的地址空间映射变化:

cpp 复制代码
# 带调试信息编译
gcc -g my_malloc.c -o my_malloc
# gdb调试
gdb ./my_malloc

在 gdb 中执行以下命令:

cpp 复制代码
# 在printf分配地址处打断点
b 39
# 运行程序
r
# 查看映射前的地址空间
info proc mapping
# 单步执行,完成mmap
n
# 再次查看地址空间,能看到新增的mmap匿名映射区域
info proc mapping





可以清晰看到,mmap后进程的地址空间中,新增了一段匿名映射区域,就是我们分配的内存。


四. mmap 使用避坑指南(开发必看)

  • 必须保证页大小对齐

    • lengthoffset必须是系统页大小的整数倍,否则会调用失败;
    • 可通过sysconf(_SC_PAGESIZE)获取系统真实页大小,不要硬编码 4KB。
  • 文件打开权限与映射权限必须匹配

    • 要设置PROT_WRITE可写权限,文件必须以O_RDWR模式打开,仅O_WRONLYO_RDONLY会映射失败;
    • 只读映射PROT_READ,文件至少要有O_RDONLY权限。
  • 空文件必须提前设置大小

    • 空文件大小为 0,直接映射会触发总线错误(SIGBUS);
    • 必须通过ftruncate/lseek+write提前给文件分配足够的空间,再进行映射。
  • 映射解除后禁止再访问

    • 调用munmap后,映射区域会被回收,再访问该地址会触发段错误(SIGSEGV)。
  • MAP_SHARED 修改同步时机

    • 共享映射的修改不会实时同步到磁盘,内核会根据脏页刷新策略自动同步;
    • 若需要强制同步,可调用msync函数主动刷盘。
  • 线程安全问题

    • 多个进程 / 线程同时修改共享映射的同一块内存,会出现竞态条件,需要通过信号量、互斥锁做同步。

五. 传统 read/write vs mmap 怎么选?(仅供参考)

特性 read/write mmap
数据拷贝 2 次拷贝(磁盘→内核缓冲区→用户态) 1 次拷贝(磁盘→用户内存)
随机访问 效率低,需要频繁 lseek+read 效率高,直接指针偏移访问
大文件处理 内存占用低,适合流式读写 性能优势极大,适合随机读写
小文件处理 开销小,使用简单 有页大小对齐的内存浪费,优势不明显
编程复杂度 简单,接口易用 相对复杂,需要处理对齐、权限等问题
异常处理 系统调用返回错误,不会直接崩溃 非法访问会触发 SIGBUS/SIGSEGV,直接终止进程

最佳选择建议

  • ✅ 大文件随机读写、频繁修改文件内容:优先选mmap
  • ✅ 进程间共享内存、多进程通信:必须用mmap共享映射;
  • ✅ 自定义内存分配、大块内存申请:用mmap匿名映射;
  • ❌ 小文件一次性流式读写、顺序读写:用read/write更简单;
  • ❌ 对程序稳定性要求极高,不能接受崩溃的场景:优先read/write,异常处理更可控。

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:mmap是 Linux 系统开发中极具威力的工具,它打破了 "文件操作" 和 "内存操作" 的壁垒,既能实现高性能的文件读写,又能完成进程间共享内存、自定义内存管理等高级功能。本文完整覆盖了 mmap 的核心原理、API、实战代码和避坑指南,无论是学习理解还是开发参考,都能直接使用。后续我会继续分享基于 mmap 的 LRU 缓存实现、进程间共享内存通信等进阶内容,欢迎点赞、收藏、关注,一起深耕 Linux 系统开发!

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
做咩啊~1 天前
6.增加一个flat网段
服务器·openstack
不愿透露姓名的大鹏1 天前
Oracle归档日志爆满急救指南
linux·数据库·oracle·dba
飞Link1 天前
逆向兼容的桥梁:3to2 自动化降级工具实现全解析
运维·开发语言·python·自动化
W.W.H.1 天前
嵌入式常见的面试题1
linux·网络·经验分享·网络协议·tcp/ip
木白CPP1 天前
DMA-Buffer内核驱动API文档
linux
itman3011 天前
C语言怎么学?从写程序到玩指针的实操攻略
c语言·指针·结构体·编程学习·资源推荐
HXQ_晴天1 天前
Linux 系统的交互式进程监控工具htop
linux·服务器·网络
君义_noip1 天前
信息学奥赛一本通 4150:【GESP2509七级】⾦币收集 | 洛谷 P14078 [GESP202509 七级] 金币收集
c++·算法·gesp·信息学奥赛·csp-s
Ricky_Theseus1 天前
静态链接与动态链接
c++
song8581 天前
韦东山开发手册阅读笔记(五)
linux