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 将这些物理地址映射到虚拟地址,直接操作设备(比如向显卡寄存器写入像素数据),这是设备驱动开发的核心方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值