mmap详解

一、什么是 mmap?

mmap(memory map)是操作系统(主要是类 Unix 系统)提供的一种内存映射文件机制。它允许进程将文件或其他对象直接映射到自己的虚拟地址空间,从而可以像访问内存一样访问文件内容,而不需要显式的 read/write 系统调用。


二、mmap 的基本用法

在 Linux 下,mmap 的原型为:

 

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • addr:指定映射的起始地址,通常为 NULL,由内核决定。
  • length:映射的长度。
  • prot:访问权限(如 PROT_READ、PROT_WRITE)。
  • flags:映射类型(如 MAP_SHARED、MAP_PRIVATE)。
  • fd:文件描述符。
  • offset:文件偏移量。

三、mmap 的底层原理

1. 虚拟内存与页表

  • 现代操作系统为每个进程分配虚拟地址空间,实际物理内存由内核管理。
  • 虚拟地址到物理地址的映射由页表维护,通常以 4KB 为一页。

2. mmap 的实现流程

(1)建立映射

当进程调用 mmap 时,内核会:

  • 在进程的虚拟地址空间中分配一段连续的虚拟地址区域(VMA,Virtual Memory Area)。
  • 记录这个 VMA 与文件的映射关系(文件 inode、偏移量等)。
(2)页面缺页异常(Page Fault)
  • 进程首次访问映射区域时,发现该虚拟地址没有物理页(页表项无效),触发缺页异常
  • 内核捕获异常,根据 VMA 信息,找到对应的文件页,从文件系统读取数据(或分配空页),将其加载到物理内存,并建立页表映射。
  • 后续访问直接命中物理内存,无需系统调用。
(3)同步与回写
  • 对于 MAP_SHARED,内存页的修改会被标记为“脏页”,内核会在合适时机(如 msync 或 munmap)将修改内容回写到磁盘文件。
  • MAP_PRIVATE 的写入采用写时复制(Copy-On-Write, COW),不会影响原文件。

3. 内核数据结构

  • VMA(vm_area_struct):描述一段虚拟内存区域及其属性。
  • 文件页缓存(page cache):文件内容读到内存后,存放在页缓存中,供 mmap 及普通 IO 共享。
  • 页表:维护虚拟页与物理页的映射关系。

四、mmap 的优势

  1. 高效 IO:减少用户空间与内核空间的多次拷贝,避免 read/write 的缓冲区复制。
  2. 共享内存:多个进程可通过 mmap 实现高效的进程间通信。
  3. 按需加载:只访问到的页面才会被实际加载,节省内存。
  4. 零拷贝:某些场景下,数据可直接在物理内存中操作,无需显式拷贝。

五、应用场景

  • 大文件处理(如数据库、视频流)
  • 进程间通信(共享内存段)
  • 内存映射设备(如显卡、外设寄存器)
  • 文件缓存

六、底层细节与优化

1. 缺页异常处理

  • mmap 区域的访问依赖于“延迟加载”,只有访问到某一页时才真正读入内存,减少 IO。
  • 如果文件被多进程 mmap,内核只会维护一份物理页缓存,节省内存。

2. COW(写时复制)

  • MAP_PRIVATE 时,写操作会触发 COW,内核会分配新的物理页,原页保持只读。

3. 页回收与同步

  • 内核的页回收机制会定期将脏页回写到磁盘,保证数据一致性。
  • 用户可通过 msync 主动同步。

4. 文件大小与映射长度

  • 映射长度超过文件大小时,超出部分访问会触发 SIGBUS 信号。

七、与 read/write 的对比

  • read/write 需要多次用户空间和内核空间的数据复制,效率较低。
  • mmap 只需一次内核空间到用户空间的映射,访问数据如同访问普通内存,效率高。

八、常见问题

  • 映射区的释放:需要 munmap。
  • 文件删除与映射:即使文件被删除,映射区仍然可用,直到所有进程解除映射。
  • 多进程同步:需注意并发写入的同步问题。

九、流程图示(简化)

进程调用 mmap
   |
   v
内核建立 VMA 映射关系
   |
   v
进程访问映射区
   |
   v
缺页异常 -> 内核加载文件页到物理内存 -> 建立页表映射
   |
   v
进程直接访问物理内存

十、类比说明 mmap

1. 类比:图书馆借书

假设你在图书馆看书,有两种方式:

  • 传统方式(read/write):你每次想看书的一部分内容,都要去前台借出来,带回自己的座位看,看完再还回去。每次搬运都很麻烦。
  • mmap 方式:你直接在书架旁边,把书摊开放在桌子上,随时翻看哪一页都行。你不需要每次都跑到前台借书,只需要翻到你想看的那一页,图书馆管理员会在你翻到那一页时把那一页递给你。

mmap 就像第二种方式:按需借阅,只有你真正需要时,才把内容搬到你面前。这就是“缺页异常”机制。


2. 内核处理细节(更深入)

(1)VMA 与页表
  • 进程调用 mmap 后,内核会在进程的虚拟地址空间里建立一个 VMA(虚拟内存区域),并把这个区域标记为和某个文件关联。
  • 页表还没有建立实际的物理页映射,只有当你访问到某个地址时,才触发缺页异常。
(2)Page Fault 处理流程
  • 当进程访问 mmap 区域的某个地址时,CPU 发现该虚拟地址没有对应的物理页(页表项无效),于是抛出 page fault(缺页异常)。
  • 内核捕获异常,查找该地址属于哪个 VMA,确认是和某个文件关联。
  • 内核查找文件系统,把对应的数据页读到物理内存(可能从磁盘读取,也可能已经在页缓存里)。
  • 更新进程的页表,把虚拟地址映射到刚刚读入的物理页。
  • 进程继续运行,仿佛这块内存一直都在。
(3)写入时的 COW(写时复制)
  • 如果是 MAP_PRIVATE,写入时会触发写时复制:原来的物理页保持只读,内核会分配一个新的物理页,把数据复制过去,进程的页表指向新的页。
(4)共享机制
  • 多个进程 mmap 同一个文件时,内核会维护一份物理页缓存,所有映射都可以访问同样的数据,实现高效共享。

3. mmap 的高级应用

(1)共享内存
  • 进程可以 mmap 一个匿名区域(不关联文件,MAP_ANONYMOUS),用于进程间通信。多个进程 mmap 同一个区域,就能实现共享内存。
(2)大文件处理
  • 处理超大文件时,mmap 只需映射部分内容,随用随取,不必一次性读入全部数据,节省内存。
(3)零拷贝
  • 某些网络服务(如 nginx)用 mmap + sendfile 实现零拷贝,提高 IO 性能。

4. mmap 的局限与注意事项

  • 同步问题:多个进程写入同一个 mmap 区域时,需要加锁,否则可能导致数据不一致。
  • 资源管理:mmap 的区域释放要用 munmap,否则会导致内存泄漏。
  • 信号处理:访问越界或文件被截断后再访问,会触发 SIGSEGV 或 SIGBUS 信号。
  • 性能调优:频繁小块写入可能不如直接 write,mmap 更适合大块数据或随机访问场景。

5. 内核源码层面(更底层)

  • Linux 内核中,mmap 相关的核心代码在 mm/mmap.c、mm/filemap.c、fs/*.c 等文件。
  • VMA 结构体为 struct vm_area_struct,记录了映射区的起止地址、权限、关联文件等信息。
  • 缺页处理通过 do_page_fault(),最终调用 filemap_fault(),完成文件页的加载和页表更新。

十一、内核源码流程(Linux)

1. mmap 系统调用入口

用户空间调用 mmap(),最终会进入内核的 sys_mmap()(或 do_mmap()):

void *sys_mmap(...) {
    // 参数校验
    // ...
    // 进入 do_mmap
    return do_mmap(...)
}

2. VMA 结构体

每个进程的虚拟地址空间被切分成若干 VMA(struct vm_area_struct),每个 VMA 记录:

  • 起止地址
  • 权限(读/写/执行)
  • 文件映射信息(struct file *
  • 偏移量、flags 等

这些 VMA 被挂在进程的 mm_struct 的红黑树(或链表)里,便于查找。

3. 缺页异常处理

  • 当进程访问未分配物理页的虚拟地址时,CPU 触发 page fault,进入内核的 do_page_fault()
  • 内核查找该地址属于哪个 VMA,确认是文件映射。
  • 调用 VMA 的 vm_ops->fault(),通常是 filemap_fault(),完成文件内容的加载,建立页表映射。

4. 文件页缓存

  • 文件内容被读入页缓存(page cache),所有 mmap/read/write 都共享这份缓存,极大提升了多进程/多线程访问同一文件的效率。
  • 脏页(被修改但未写回磁盘的页)由后台进程(如 pdflush 或 writeback)定期同步回磁盘,也可用 msync() 手动触发。

5. COW(写时复制)

  • MAP_PRIVATE 时,写入会触发 COW,分配新物理页,保证原文件内容不被修改。
  • MAP_SHARED 时,直接修改页缓存,后续会同步到磁盘。

十二、性能瓶颈与优化

1. 优势

  • 大文件随机访问性能高,避免了多次用户态/内核态拷贝。
  • 多进程共享同一份页缓存,节省内存。

2. 瓶颈

  • 小文件高频写入,mmap 可能比 write 更慢(频繁缺页、脏页回写)。
  • 内存压力大时,页缓存容易被回收,导致频繁 page fault。
  • mmap 区域越界、文件被截断,易导致 SIGBUS/SIGSEGV。

3. 优化建议

  • 对于大文件、随机读写场景优先使用 mmap。
  • 写入后及时调用 msync(),保证数据持久化。
  • 合理设计 mmap 区域大小,避免一次性映射超大文件导致内存压力。
  • 多进程/多线程并发时,加锁保护 mmap 区域。

十三、工程实践案例

1. 数据库

如 SQLite、MongoDB、Redis 的 AOF 文件,常用 mmap 做持久化,提升写入性能。

2. 图像/视频处理

处理超大图片、视频文件时,mmap 只需映射部分区域,按需加载,节省内存。

3. 高性能服务器

Nginx、Apache 等高性能服务器,利用 mmap + sendfile 实现零拷贝,提升网络 IO 性能。

4. 共享内存

进程间通信(IPC),通过 mmap 映射匿名内存区域(MAP_ANONYMOUS),多个进程共享数据,效率远高于管道、消息队列。


十四、代码示例(C语言)

int fd = open("data.bin", O_RDWR);
size_t filesize = ...;
void *ptr = mmap(NULL, filesize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

// 直接像数组一样访问文件内容
char value = ((char *)ptr)[12345];

// 修改内容
((char *)ptr)[12345] = 'A';

// 持久化到磁盘
msync(ptr, filesize, MS_SYNC);

// 释放映射
munmap(ptr, filesize);
close(fd);

十五、常见问题解答

  1. 文件被删除后 mmap 区域还能用吗?
    可以,直到所有映射被解除,数据还在物理内存页里。

  2. mmap 区域越界会怎样?
    访问未映射区域会触发 SIGSEGV 或 SIGBUS,导致进程崩溃。

  3. 多进程写入 mmap 如何同步?
    需加锁(如 pthread_mutex),否则可能数据不一致。

  4. mmap 区域必须用 munmap 释放吗?
    是的,否则会内存泄漏。

十六、Java mmap 实现原理

在 Java 中,MappedByteBuffer 是对操作系统底层 mmap 的封装。它允许你将文件的一部分或全部直接映射到内存,读写操作直接反映到文件内容,底层由 JVM 通过 JNI 调用操作系统的 mmap 接口实现。

  • FileChannel.map() 方法用于创建映射。
  • 返回的 MappedByteBuffer 实例可以像数组一样 get/set 数据。

底层原理和 C 的 mmap 类似,Java 只是做了对象封装和安全性控制。


十七、Java mmap 代码示例

1. 基本读写示例

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MmapDemo {
    public static void main(String[] args) throws Exception {
        // 打开文件
        RandomAccessFile raf = new RandomAccessFile("test.dat", "rw");
        FileChannel channel = raf.getChannel();

        // 映射文件的前 1MB 到内存
        int size = 1024 * 1024;
        MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, size);

        // 写入数据
        buffer.put(0, (byte)123);
        buffer.put(1, (byte)45);

        // 读取数据
        byte value = buffer.get(0);
        System.out.println("Read value: " + value);

        // 刷新到磁盘(可选)
        buffer.force();

        // 关闭资源
        channel.close();
        raf.close();
    }
}

2. 只读映射

MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size);

3. 多进程/多线程共享

多个进程可以 mmap 同一个文件实现共享内存(需自行同步),多个线程可直接共享同一个 MappedByteBuffer 对象。


十八、注意事项

  1. 映射区域大小
    文件必须足够大,否则映射会失败。可以提前扩展文件大小。

  2. 同步问题
    buffer.force() 可以强制把修改刷新到磁盘,否则由操作系统决定何时同步。

  3. 资源释放
    MappedByteBuffer 的释放由 JVM 管理,不能直接 unmap,但可以通过反射 hack 或等待 GC。

  4. 越界访问
    访问未映射区域会抛出异常。

  5. 性能
    对于大文件、随机读写场景,mmap 性能非常高,适合日志、数据库、缓存等应用。


十九、工程应用场景

  • 日志文件高性能写入
  • 大文件处理(图片、视频、数据库文件)
  • 进程间通信(通过 mmap 同一个文件实现共享内存)

二十、进阶:MappedByteBuffer 强制释放(unmap)

由于 JVM 没有直接提供 unmap 方法,通常只能等待垃圾回收。但在一些极端场景下,可以用反射 hack:

import sun.misc.Cleaner;
import sun.nio.ch.DirectBuffer;

public static void unmap(MappedByteBuffer buffer) {
    if (buffer == null) return;
    Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
    if (cleaner != null) cleaner.clean();
}

注意:这种做法依赖于内部 API,不推荐在生产环境使用,JDK 9+ 可能不可用。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猩火燎猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值