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;
}
相关推荐
HLC++9 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室10 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy13 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕13 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫15 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生15 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1116 小时前
linux基础IO
linux·运维·服务器
BelongPanda18 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯18 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983419 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器