20251129 - 详细解析Linux的mmap(内存映射)

详细解析Linux的mmap(内存映射)

mmap(Memory Map,内存映射)是 Linux 系统编程中最迷人、最强大,但也最容易出错的机制之一。

它打破了"文件是文件,内存是内存"的界限,让程序员可以像操作内存数组一样直接操作文件,甚至直接操作硬件(在嵌入式开发中尤为重要)。

以下是对 mmap 的全方位深度解析。

1. 核心概念:什么是内存映射?

在传统的 read/write 模型中,如果想修改文件:

  1. 内核态:内核把数据从磁盘读到内核高速缓存(Page Cache)。
  2. 拷贝 :内核把数据从 Page Cache **拷贝**到用户进程的缓冲区(User Buffer)。
  3. 修改:用户在 User Buffer 修改数据。
  4. 拷贝 :用户把数据拷贝回内核。
  5. 内核态:内核把数据刷回磁盘。

关键词:内核->Page Cache(页缓存)->用户缓存并修改->Page Cache(页缓存)->内核缓存->磁盘

mmap 模型:

mmap 请求内核:"请把这个文件直接'投影'到我的虚拟内存里。"

  • 零拷贝 (Zero Copy):内核直接把文件的 Page Cache 映射到用户的虚拟地址空间。
  • 直接操作:用户进程直接读写这段内存,实际上就是在读写内核的 Page Cache。
  • 自动同步:内核负责在后台将 Page Cache 的变动刷新到磁盘。

一句话总结:mmap 建立了"进程虚拟内存"与"文件对象"之间的一座桥梁。

2. 函数原型与关键参数

头文件:<sys/mman.h>

c 复制代码
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数 含义 详解
addr 映射起始地址 通常设为 NULL,让内核自动选择一块空闲地址。
length 映射长度 想要映射多少字节(通常是文件大小)。
prot 保护权限 PROT_READ (可读), PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE (不可访问)。必须与 open 时的权限匹配。
flags 映射标志 关键参数! 决定了内存修改是否同步回文件(见下文)。
fd 文件描述符 open() 返回的句柄。
offset 文件偏移量 从文件的哪里开始映射。必须是分页大小(通常4KB)的整数倍(如 0, 4096...)。

返回值:

  • 成功:返回映射区的首地址指针。
  • 失败:返回 MAP_FAILED (即 (void *)-1),注意不是 NULL

3. 两种核心模式:MAP_SHARED vs MAP_PRIVATE

这是 flags 参数中最关键的选择:

A. MAP_SHARED(变动是共享的)
  • 含义 :你对内存的修改写回磁盘文件。
  • 可见性:其他映射了该文件的进程也能立即看到你的修改。
  • 用途
    • 文件操作 :替代 read/write 修改文件。
    • 进程间通信 (IPC) :两个进程 mmap 同一个文件,实现最高效的数据共享。
    • 硬件操作 :嵌入式中映射 /dev/mem 来操作寄存器。
B. MAP_PRIVATE(写时复制,Copy-On-Write)
  • 含义 :你对内存的修改不会写回磁盘文件。
  • 机制:当你第一次尝试修改数据时,内核会悄悄拷贝一份数据给你(私有副本),你修改的是副本。
  • 用途
    • 加载动态库.so 文件加载到内存时,代码段是共享的,但数据段是私有的。
    • 调试器:在不破坏源文件的情况下修改内存中的程序逻辑。

4. 嵌入式开发专属:mmap 操作硬件寄存器

在您的 i.MX6ULL 开发中,mmap 是驱动开发和裸机应用的核心。

原理:

CPU 有物理地址空间,硬件外设(如 GPIO 控制器)映射在特定的物理地址上(查数据手册可知,例如 0x20AC000)。Linux 用户态程序不能直接访问物理地址,必须用 mmap 将物理地址映射成虚拟地址。

伪代码示例:

c 复制代码
int fd = open("/dev/mem", O_RDWR); // 打开物理内存设备
// 将物理地址 0x20AC000 开始的 4KB 映射到虚拟内存
unsigned char *gpio_base = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x20AC000);

// 现在可以直接操作寄存器点灯了!
*(volatile unsigned int *)(gpio_base + 0x04) = 0xFF; // 写寄存器

5. mmapread/write 的性能对比

很多初学者认为 mmap 总是比 read/write 快,这是误区

场景 推荐方式 原因
小文件 / 顺序读写 read / write mmap 建立映射和销毁映射有较大的系统开销(Setup Cost)。对于简单读写,系统调用反而更快。
大文件 / 随机读写 mmap read/write 需要频繁移动文件指针 (lseek) 并进行多次内存拷贝。mmap 直接通过指针跳转,效率极高。
频繁修改文件 mmap 修改内存比调用 write 写入磁盘快得多。
流式数据 (Pipe/Socket) read / write mmap 只能映射可定位的文件(seekable),不能映射管道或套接字。

6. 实战中的"坑"与注意事项

A. 缺页异常 (Page Fault) 是隐形成本

mmap 调用返回极快,因为此时内核并没有真的把文件读入内存。

  • 当你第一次访问 ptr[0] 时,CPU 触发缺页异常。

  • 内核捕获异常,暂停你的进程,去磁盘读取数据到 Page Cache,修改页表。

  • 恢复进程执行。

    这意味着:mmap 是"懒加载"的。

B. 文件大小限制 (SIGBUS 错误)

mmap 不能改变文件大小

  • 假设文件只有 100 字节。
  • mmap 了 4096 字节(一页)。
  • 你可以读写 0~99 字节。
  • 如果你写 ptr[200] :虽然它在映射的 4KB 页内,但它超过了文件实际大小。你会收到 SIGBUS 信号,程序崩溃。
  • 解决:mmap 之前,先用 lseek + write 或者 ftruncate() 把文件撑大。
C. 同步问题

MAP_SHARED 并不保证立即可靠地写入磁盘,它只是写到了内核缓存。

  • 如果系统断电,数据可能丢失。
  • 强制同步: 使用 msync(addr, len, MS_SYNC) 函数,类似于文件 I/O 的 fsync
D. 解除映射

使用完毕后,必须调用:

c 复制代码
munmap(void *addr, size_t length);

如果不调用,进程结束后内核才会自动回收,因此长期运行的服务会造成虚拟内存泄漏。

7. 总结代码示例

这是一个标准的用 mmap 修改文件的模板:

c 复制代码
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *filepath = "data.bin";
    
    // 1. 打开文件
    int fd = open(filepath, O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    // 2. 获取文件大小
    struct stat sb;
    if (fstat(fd, &sb) == -1) { perror("fstat"); return 1; }

    // 3. 执行内存映射
    // 注意:MAP_SHARED 才能写回文件
    char *mapped = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    
    if (mapped == MAP_FAILED) { perror("mmap"); return 1; }

    // 4. 像操作数组一样操作文件
    printf("Old: %c\n", mapped[0]);
    mapped[0] = 'X'; // 修改内存,实际上就是修改文件
    
    // 5. (可选) 强制同步回磁盘
    msync(mapped, sb.st_size, MS_SYNC);

    // 6. 解除映射
    munmap(mapped, sb.st_size);
    
    // 7. 关闭文件
    close(fd);
    
    return 0;
}
相关推荐
Joren的学习记录3 小时前
【Linux运维大神系列】docker详解(四)
linux·运维·docker
老王熬夜敲代码3 小时前
网络中数据传输的具体过程
linux·网络·笔记
世转神风-4 小时前
linux使用终端打开当前文件夹界面
linux
刘某的Cloud5 小时前
列表、元组、字典、集合-组合数据类型
linux·开发语言·python
学烹饪的小胡桃5 小时前
【运维学习】实时性能监控工具 WGCLOUD v3.6.2 更新介绍
linux·运维·服务器·学习·工单系统
知识分享小能手5 小时前
Ubuntu入门学习教程,从入门到精通,Ubuntu 22.04的桌面环境 (4)
linux·学习·ubuntu
Lueeee.5 小时前
图解字符驱动模块设计思路
linux
白露与泡影6 小时前
使用systemd,把服务装进 Linux 心脏里~
linux·运维·python
CQ_YM6 小时前
Linux管道通信
linux·c语言·管道·ipc·管道通信