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

Linux内存映射原理

目录

一、为什么需要mmap,传统读文件的缺陷是什么?

二、mm_struct的各个区域划分

三、内存映射的基本原理

(1)分配虚拟地址区间

(2)建立地址、文件的映射关系

(3)缺页中断:触发数据加载

四、文件映射的关键特性:不止步于“读写文件”

(1)两种映射类型:文件映射与匿名映射

(2)映射标志:控制 “修改是否同步到文件”

(3)数据同步:何时写回磁盘?

四、文件映射的优势

1. 减少数据拷贝,提升效率​

2. 简化编程:用 “内存操作” 代替 “文件操作”​

3. 支持高效共享:多进程共享数据​

五、内存映射的使用场景

1. 大文件处理(GB 级)​

2. 进程间通信(IPC)​

3. 设备操作(内存映射 IO)        ​


        在Linux中,内存映射(mmap)是一种让进程像访问物理内存一样操作文件或者其他设备的机制。他跳过了传统文件读写的内核缓冲区拷贝,大幅提升了数据交互的效率,同时也是进程间通信的高效手段。对于新手而言,理解mmap原理不仅能掌握一种高效的进程间通信手段,还能加深对虚拟地址空间的理解。

一、为什么需要mmap,传统读文件的缺陷是什么?

        我们先来看看传统的read/write是如何读写文件的。

当读取一个文件的时候,数据会经历3次拷贝:

(1)内核把磁盘数据读入到读缓冲区(内核空间的一块内存)

(2)内核再把数据从内核空间的读缓冲区拷贝到用户空间定义的内存中(C语言层面的缓冲区)。

(3)从C语言层面的缓冲区读到上层应用中。

        写入一个文件时则恰恰相反,不过这两种都有一个很明显的问题:数据要在用户空间和内核空间之间来回拷贝,如果处理大文件(比如几个 GB 的日志、数据库文件),频繁的拷贝会严重消耗 CPU 和内存带宽。

        而内存映射的核心思想是:把文件或设备的一部分直接 “映射” 到进程的虚拟地址空间。此后,进程操作这块虚拟内存时,就像直接操作文件或设备本身 —— 无需read/write,也无需数据拷贝。

二、mm_struct的各个区域划分

        在之前的文章中我们曾提及过用户虚拟地址空间的划分,即又mm_struct宏观管理、vm_area_struct精细管理。

struct mm_struct {/* 1. 内存区域管理核心 */struct vm_area_struct *mmap;       // 所有内存区域链表(堆/栈/文件映射等)struct rb_root mm_rb;              // 内存区域红黑树(快速查找)int map_count;                     // 内存区域总数/* 2. 程序代码与数据段(可执行文件加载区域) */unsigned long start_code;          // 代码段起始地址(.text段)unsigned long end_code;            // 代码段结束地址unsigned long start_data;          // 数据段起始地址(.data/.bss段)unsigned long end_data;            // 数据段结束地址/* 3. 堆区域 */unsigned long start_brk;           // 堆起始地址(固定)unsigned long brk;                 // 堆当前结束地址(可扩展)/* 4. 栈区域 */unsigned long start_stack;         // 用户栈起始地址(高地址)unsigned long stack_limit;         // 栈的最低地址限制(栈向下生长的边界)/* 5. 文件映射与共享内存区域 */unsigned long mmap_base;           // 文件映射区起始地址(mmap分配的地址从此开始)struct list_head mmap_shared;      // 共享映射区域链表(如共享库、共享内存)/* 6. 命令行参数与环境变量区域(用户态初始化数据) */unsigned long arg_start;           // 命令行参数起始地址unsigned long arg_end;             // 命令行参数结束地址unsigned long env_start;           // 环境变量起始地址unsigned long env_end;             // 环境变量结束地址/* 7. 页表与地址空间控制 */pgd_t *pgd;                        // 页全局目录(虚拟地址转物理地址的根)unsigned long task_size;           // 进程虚拟地址空间总大小(如32位4GB)/* 8. 引用与同步 */atomic_long_t mm_users;            // 共享该内存空间的进程数(如线程)atomic_long_t mm_count;            // 自身引用计数struct rw_semaphore mmap_sem;      // 保护内存区域操作的信号量
};

大致的示意图如下:

        可以看到,一个进程PCB中会有一个mm_struct,而一个mm_struct会划分为多个区域,其中就有一个区域叫做文件映射区,但是文件映射区和其他的区域大不相同,比如堆区、栈区有自己的指针,每一个进程必定会有,所以有brk、start_stack这种指针。而文件映射区纯粹依赖于vm_area_struct链表的管理,在没有文件映射的情况下,不会创建vm_area_struct来精细描述,也就没有文件映射区了。(所有区域中仅仅栈、堆必定有专用指针,其余任何区域都纯粹依赖于vm_area_struct来管理,特点是使用时才创建,不存在时则无这个区域)

        注意一点,如果有多个文件映射到这个进程,则每一个文件都有一个vm_area_atruct结构体,因为在上图中我们可以看出来一个vm_area_struct结构体只有一个struct file指针(即一个文件指针)。

       

三、内存映射的基本原理

        内存映射的本质是在进程的虚拟地址空间和文件 / 设备的物理存储之间,建立一座 “映射桥梁”。具体可以拆分为三个关键步骤:

(1)分配虚拟地址区间

        当进程调用mmap函数时,内核会在进程的虚拟地址空间中,划分出一块连续的 “空闲虚拟地址区间”,尽管虚拟地址空间已经有一部分被划分为了各个区域和自己的vm_area_struct,但是总归有没有使用到的地方,就给了文件映射区,这个区域通常我们让内核自己去找,而非手动设置。这块区间的大小与要映射的文件 / 设备大小(或指定的长度)一致,比如映射一个 10MB 的文件,就会分配 10MB 的虚拟地址。​

        注意:此时只是分配了 “虚拟地址”,并没有实际占用物理内存,就像给你一张 “提货单”,但货物还没送到仓库。

(2)建立地址、文件的映射关系

        内核会在进程的 “页表”(记录虚拟地址与物理地址对应关系的表格)中,添加一系列 “映射记录”:虚拟地址区间中的每个 “页”(通常 4KB),都会对应文件中的一个 “块”(比如文件的第 0-4095 字节对应虚拟地址的第 0-4095 字节)。​

此时,虚拟地址和文件内容的映射关系已建立,但数据还没加载到物理内存。​

        注意:这一步并未填写页表,页表仍然为空。只是会根据mmap的各个参数,填写到vm_area_struct中,如文件偏移量、映射长度等。

(3)缺页中断:触发数据加载

        当进程访问这块虚拟地址时(比如读取某个字节),CPU 会通过页表查找对应的物理地址。但此时物理地址还未分配(因为数据没加载),CPU 会触发 “缺页中断”,让内核处理:​

  • 内核会从磁盘读取文件中对应的块,加载到物理内存的某个页框(物理内存的最小单位);​
  • 填写页表,将虚拟地址与刚分配的物理页框关联;​
  • 中断结束后,进程继续访问,此时就能读到物理内存中的数据了。​

后续再访问同一部分数据时,因为已经在物理内存中,就不会触发缺页中断,直接通过页表映射访问即可。

四、文件映射的关键特性:不止步于“读写文件”

内存映射的强大之处,在于它的灵活性和多场景适配,核心特性包括:

(1)两种映射类型:文件映射与匿名映射

  • 文件映射:将磁盘文件映射到虚拟地址空间,进程对虚拟地址的修改会同步到文件(取决于映射标志)。这是最常用的场景,比如编辑大文件时,编辑器会通过 mmap 映射文件,避免一次性加载整个文件到内存。​
  • 匿名映射:不关联任何文件,而是映射一块 “匿名内存”(由内核分配物理内存)。这种方式常用于进程间通信(通过共享匿名内存),或动态分配大块内存(比malloc更高效)。
  • 在进程间通信方面,匿名映射比文件映射更加优秀,因为他会省去磁盘IO和文件管理的开销。普通的文件映射用于进程间通信,本质是多个进程映射到同一个物理地址,但是这个地址里面的内容最终还是会从物理内存拷贝到磁盘中,如果频繁修改,则可能产生大量的开销。同时,如果你仅仅使用他通信,最后还需要处理文件残留的问题。

        如果是匿名映射则少了写回磁盘的部分:简单来说,匿名映射是轻量级、纯内存的共享方式,适合临时、高频的进程通信;而映射普通文件则更适合需要持久化数据的场景。        

(2)映射标志:控制 “修改是否同步到文件”

  • MAP_SHARED:进程对虚拟地址的修改会同步到文件,且其他映射该文件的进程也能看到修改(共享变更)。比如多进程协作编辑同一个文件时,用这个标志能实时同步变更。​
  • MAP_PRIVATE:进程的修改不会同步到文件,也不会被其他进程看到(私有副本)。内核会在进程首次修改时,创建物理内存的 “私有副本”(写时复制,Copy-On-Write),避免影响原文件和其他进程。常用于临时读写文件,但不想污染源文件。

(3)数据同步:何时写回磁盘?

        用MAP_SHARED映射时,修改不会立即写入磁盘,而是先存在物理内存的 “页缓存” 中,由内核在合适的时机(比如内存不足、调用msync函数、关闭映射)同步到磁盘。这与传统文件读写的 “延迟写” 机制一致,保证效率的同时减少磁盘 IO。

四、文件映射的优势

        相比read/write等传统 IO 函数,内存映射的核心优势体现在:​

1. 减少数据拷贝,提升效率​

        传统 IO 需要 “用户缓冲区←→内核缓冲区” 的拷贝,而内存映射通过虚拟地址直接访问内核页缓存(物理内存中的文件数据),跳过了用户态与内核态之间的拷贝。对于大文件(比如 1GB 以上),这种效率提升非常明显。​

2. 简化编程:用 “内存操作” 代替 “文件操作”​

进程可以像操作数组一样操作文件,比如:​

// 映射文件后,直接通过指针修改虚拟地址
char *addr = mmap(...);
addr[100] = 'A';  // 相当于修改文件的第100个字节

无需调用lseek定位、read/write传输,代码更简洁。

3. 支持高效共享:多进程共享数据​

        多个进程可以映射同一个文件(MAP_SHARED标志),实现 “共享内存”:一个进程的修改会自动同步到其他进程(因为共享物理内存中的页缓存)。这比管道、消息队列等 IPC 方式更高效,是数据库、分布式系统中进程协作的常用手段。

五、内存映射的使用场景

内存映射不是 “银弹”,但在以下场景中能发挥最大价值:​

1. 大文件处理(GB 级)​

        比如数据库读取超大表文件、日志系统分析海量日志。传统 IO 会因频繁拷贝导致效率低下,而 mmap 只需加载当前访问的部分数据(按需加载),适合处理远大于物理内存的文件。​

2. 进程间通信(IPC)​

        多进程需要高频共享数据时(比如渲染引擎的多个线程共享帧缓存),用MAP_SHARED映射同一个文件(或匿名内存),比用管道传输数据更高效(无拷贝、低延迟)。​

3. 设备操作(内存映射 IO)        ​

        Linux 中很多设备(如显卡、网卡)的寄存器和缓冲区会被映射到物理地址空间,进程可以通过 mmap 将这些物理地址映射到虚拟地址,直接操作设备(比如向显卡寄存器写入像素数据),这是设备驱动开发的核心方式。

http://www.lryc.cn/news/596928.html

相关文章:

  • 第十讲:stack、queue、priority_queue以及deque
  • Python-初学openCV——图像预处理(一)
  • 如何在 npm 上发布 Element Plus 二次封装组件
  • Parasoft为金融服务打造统一测试平台,提升安全、合规与交付效率
  • C++ Primer(第5版)- Chapter 7. Classes -005
  • ESP32的ADF详解:6. Audio Processing的API
  • Android 测试全指南:单元测试与UI测试框架详解
  • ESP-NOW实战:ESP32一对多无线通信方案(支持ESP8266兼容)
  • 【Spring Cloud Gateway 实战系列】进阶篇:过滤器高级用法、动态路由配置与性能优化
  • Android Studio中调用USB摄像头
  • CRMEB 单商户PRO多商户通用去版权教程
  • win11安装erlang和rabbitmq
  • SpringBoot 使用Rabbitmq
  • RabbitMQ--@RabbitListener及@RabbitHandle
  • OceanBase数据库
  • RabbitMQ有多少种Exchange?
  • LLM 幻觉一般是由于什么产生的,在模型什么部位产生
  • Java学习第六十九部分——RabbitMQ
  • iOS WebView 远程调试实战 解决表单输入被键盘遮挡和焦点丢失问题
  • 期权遇到股票分红会调整价格吗?
  • 【机器学习深度学习】比较 LLaMA-Factory、vLLM 和 LMDeploy 的量化导出:为何 LLaMA-Factory 不是首选?
  • OpenCV(01)基本图像操作、绘制,读取视频
  • Redis MCP 安装与配置完整指南
  • Spring Boot全局异常处理:一网打尽Controller层异常,@RestControllerAdvice解析
  • Unreal5从入门到精通之使用 Python 编写虚幻编辑器脚本
  • Linux进程控制:掌握系统的核心脉络
  • 《设计模式之禅》笔记摘录 - 9.责任链模式
  • Xorg占用显卡内存问题和编译opencv GPU版本
  • 基于LNMP分布式个人云存储
  • Docker 容器中的 HEAD 请求缺失 header?从 Content-MD5 缺失聊起