当前位置: 首页 > news >正文

Linux mmap内存映射

mmap 是 Linux 系统中一个非常重要且强大的系统调用,它提供了将文件或设备直接映射到进程地址空间的能力。这种机制在程序设计中用途广泛,从文件 I/O 到进程间通信,再到内存管理,都能看到它的身影。

核心概念

mmap 的核心思想是:让一块虚拟内存区域与一个文件(或其他对象)关联起来。当你读写这块内存时,系统会自动将数据同步到对应的文件中,或者从文件中加载数据到内存。这就像是给文件内容开了一个“直接操作”的窗口。

函数原型

c

#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数解析:

  • addr: 建议的映射起始地址。通常设为 NULL,让内核自动选择最适合的地址。如果指定,内核会将其作为参考。

  • length: 要映射的字节数。

  • prot: 指定内存区域的保护模式(不能与文件的打开模式冲突)。

    • PROT_EXEC: 页面可执行。

    • PROT_READ: 页面可读。

    • PROT_WRITE: 页面可写。

    • PROT_NONE: 页面不可访问。

  • flags: 控制映射行为的标志。这是理解 mmap 用法的关键。

    • MAP_SHARED: 共享映射。对映射区域的修改会写回文件,并且其他映射了同一文件的进程可以看到这些更改。用于文件 I/O 和 IPC。

    • MAP_PRIVATE: 私有映射。对映射区域的修改不会写回文件,而是创建一个该页的副本(Copy-on-Write)。用于加载动态库、创建进程的私有内存空间。

    • MAP_ANONYMOUS(或 MAP_ANON): 匿名映射。映射的区域不与任何文件关联,内容初始化为零。常用于分配大块内存(如 malloc 的底层实现)或 IPC。使用此标志时,fd 参数被忽略,通常设为 -1。

    • MAP_FIXED: 强制使用指定的 addr 作为映射地址,不建议使用,可能不安全。

    • MAP_LOCKED: 将映射页面锁定在内存中,防止被换出(swap out)。

    • MAP_POPULATE: 预先为映射填充页表,建立物理内存的映射,避免后续缺页中断。

  • fd: 要映射的文件的文件描述符。如果是匿名映射,则设为 -1。

  • offset: 从文件开头开始的偏移量,必须是系统页大小(通常为 4096 字节)的整数倍。

返回值:
成功时返回映射区域的起始地址;失败时返回 MAP_FAILED(即 (void *) -1)。

解除映射:munmap

c

#include <sys/mman.h>int munmap(void *addr, size_t length);

用于解除一个映射关系。addr 必须是 mmap 返回的地址,length 是之前映射的大小。进程退出时也会自动解除所有映射。

同步回磁盘:msync

c

#include <sys/mman.h>int msync(void *addr, size_t length, int flags);

如果你使用 MAP_SHARED,内核会在适当时候将脏页写回磁盘。但如果你需要确保数据立刻落盘,可以调用 msync

  • flags:

    • MS_SYNC: 同步写,阻塞直到所有修改都物理写入存储设备。

    • MS_ASYNC: 异步写,调度一次写操作但立即返回。

主要应用场景

1. 文件 I/O(内存映射文件)

这是最经典的用法。相比于传统的 read/write 系统调用,mmap 有显著优势:

  • 减少数据拷贝: 传统 I/O 需要将数据从内核缓冲区拷贝到用户缓冲区mmap 直接将文件映射到用户空间,操作内存就等于操作文件,省去了一次拷贝(CPU copy),尤其对大文件性能提升明显。

  • 随机访问更高效: 访问文件任意位置只需简单的指针操作,无需 lseek

  • 与内核共享缓存: 映射区域由内核的页缓存(Page Cache)支持,可以充分利用系统的缓存机制。

示例代码:读取文件

c

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>int main() {int fd = open("example.txt", O_RDONLY);if (fd == -1) {perror("open");return 1;}struct stat sb;if (fstat(fd, &sb) == -1) {perror("fstat");return 1;}// 将整个文件映射到内存char *mapped = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);if (mapped == MAP_FAILED) {perror("mmap");return 1;}// 现在可以像操作内存一样操作文件内容for (off_t i = 0; i < sb.st_size; i++) {putchar(mapped[i]); // 逐个字符打印}// 解除映射并关闭文件munmap(mapped, sb.st_size);close(fd);return 0;
}
2. 进程间通信 (IPC)

通过 mmap 可以实现高效的进程间共享内存。

  • 方式一(有文件背景): 两个进程使用 MAP_SHARED 映射同一个文件。他们对映射区域的修改会立刻被对方看到。

  • 方式二(匿名映射): 使用 MAP_SHARED | MAP_ANONYMOUS 创建一块与文件无关的共享内存区域。通常由父进程在 fork() 之前创建,子进程会自然继承这块映射,从而实现通信。

示例代码:匿名共享内存 IPC

c

#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main() {int *shared_var = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS, -1, 0);if (shared_var == MAP_FAILED) {perror("mmap");return 1;}*shared_var = 100; // 初始值pid_t pid = fork();if (pid == -1) {perror("fork");return 1;}if (pid == 0) { // 子进程printf("Child reads: %d\n", *shared_var);(*shared_var)++; // 修改共享内存printf("Child incremented to: %d\n", *shared_var);munmap(shared_var, sizeof(int)); // 子进程解除映射} else {         // 父进程wait(NULL);  // 等待子进程结束printf("Parent reads: %d\n", *shared_var);munmap(shared_var, sizeof(int)); // 父进程解除映射}return 0;
}

// 输出可能是:
// Child reads: 100
// Child incremented to: 101
// Parent reads: 101

3. 分配大块内存(malloc 的底层)

glibc 中的 malloc() 函数在需要分配非常大(例如超过 MMAP_THRESHOLD,默认 128KB)的内存块时,会使用 mmap 而不是 brk/sbrk。因为 mmap 分配的内存可以独立且方便地被 free() 归还给系统,而 brk 管理的堆内存则容易产生碎片。

4. 加载动态共享库

当你调用 dlopen() 或程序启动时加载 .so 文件,系统就是使用 mmap 将库文件的代码段(.text)和数据段(.data.bss)映射到进程的地址空间。这通常是 MAP_PRIVATE 映射,这样多个进程可以共享同一份库的物理内存页(写时复制),节省大量内存。

优点与缺点

优点:
  1. 高性能文件 I/O: 减少数据拷贝,尤其适合大文件或随机访问。

  2. 高效的 IPC: 共享内存是速度最快的 IPC 方式,因为数据不需要在进程间拷贝。

  3. 内存高效: 支持写时复制、共享库等高级特性,节省物理内存。

  4. 自然对齐: 映射的内存地址总是页对齐的。

缺点:
  1. 粒度不灵活: 映射大小必须是页大小的整数倍,可能造成内部碎片。

  2. 资源开销: 每次映射都会在进程内核数据结构(如 VMA 链表)中创建新条目,映射大量小文件不划算。

  3. 复杂性: 需要程序员自己处理同步问题(如对共享映射的访问需要加锁)。

  4. 延迟性错误: 对映射区域的访问可能触发 SIGBUS 或 SIGSEGV 信号(例如文件被截断时访问已映射的区域),错误处理比普通 I/O 更复杂。

总结

mmap 是 Linux 系统编程中一把强大的“瑞士军刀”。它通过巧妙地将文件、内存和进程地址空间结合起来,提供了远超传统 I/O 操作的灵活性和性能。理解并熟练运用 mmap,尤其是区分 MAP_SHAREDMAP_PRIVATE 和 MAP_ANONYMOUS 的用途,是成为一名高级 Linux/C 程序员的重要标志。

http://www.dtcms.com/a/342568.html

相关文章:

  • 集中式负载均衡 vs. 分布式负载均衡
  • 【赵渝强老师】Redis Cluster分布式集群
  • #千问海报大赛
  • 订单簿动力学与深度学习模型的融合大单识别与短期市场价格波动预测
  • Java多线程编程基础篇
  • 多级缓存一致性矩阵:ABP vNext 下的旁路 / 写穿 / 写回组合实战
  • Qt的moveToThread使用
  • SQL-leetcode—3451. 查找无效的 IP 地址
  • centos常用命令
  • Visual Studio Code (VS Code) 工作区配置文件的作用
  • CentOS7安装部署NexusRepository
  • 【Spring Cloud 微服务】2.守护神网关Gateway
  • 告别人工建模:AI 自动化 ETL 工具对比,数据 pipeline 搭建时间缩短 60% 的实践
  • 洛谷 P2656 采蘑菇-普及+/提高
  • k 均值聚类算法总结
  • 从入门到精通:Java设计模式——单例模式
  • 8.21IPSEC安全基础后篇,IKE工作过程
  • 【TWT】
  • 常德二院信创转型实战:全栈国产化提升医疗效率与安全
  • 嵌入式linux设备升级和sm32升级对比
  • 计算机视觉(opencv)实战六——图像形态学(腐蚀、膨胀、开运算、闭运算、梯度、顶帽、黑帽)
  • el-table-draggable拖拽实现表格内容排序
  • VUE从入门到精通二:ref、reactive、computed计算属性、watch监听、组件之间的通信
  • 网络与信息安全有哪些岗位:(5)安全开发工程师
  • Android14内核调试 - boot vendor_boot
  • Git常用操作大全(附git操作命令)
  • Matplotlib数据可视化实战:Matplotlib数据可视化入门与实践
  • golang实现的Rsa加密解密算法(go和java交互时双向加解密方案)
  • OpenCV 形态学操作详解:腐蚀、膨胀与开闭运算
  • Chrome/360 浏览器 WebUI 资源底层机制解析:共享资源与专属资源的奥秘