mmap随笔

5 阅读4分钟

一共有两层映射,别搞混

第一层:虚拟地址 → 物理内存(所有程序都有,不是 MMAP 专属)

这是 CPU + OS 的基础机制:

你写的任何代码:
  int x = 100;   // x 的地址是 0x7fff1000(虚拟地址)

CPU 执行时:
  MMU 查页表
  0x7fff1000(虚拟)→ 0x3a2b4000(物理内存条上的真实位置)

这层映射每个进程都有,让每个进程以为自己独占全部内存,互不干扰。


第二层:虚拟地址 → 文件(这才是 MMAP 做的事)

普通情况下,虚拟地址只映射物理内存,不认识文件。MMAP 做的是:在 OS 内核里登记一条记录:

"进程的虚拟地址区间 [0x7f000000, 0x7f000018)
对应的是文件 prices.dat 的第 0~24 字节"

这条记录存在 vm_area_struct(VMA)里

两层叠在一起的完整图

你的代码
buffer.get(1)
= 读虚拟地址 0x7f000008
        │
        │ ① MMU 查页表
        ▼
   页表里有记录吗?
   ┌────────┴────────┐
  没有               有
   │                  │
   ▼                  ▼
② 缺页中断          直接拿到物理地址
   OS 查 VMA        纳秒级返回 ✓
   发现这段虚拟地址
   对应 prices.dat 第8字节
        │
        ▼
③ OS 把文件第0页(4KB)
   读入 Page Cache
        │
        ▼
④ 页表登记:
   虚拟地址 0x7f000000
   → 物理页帧 0x3a2b4000
        │
        ▼
⑤ 重新执行,命中页表
   直接读物理内存 ✓


用门牌号类比两层映射

第一层(虚拟→物理):
  门牌号(虚拟地址)→ 真实房间位置(物理内存)
  每栋楼(进程)有自己的门牌号系统,互不影响

第二层(MMAP 的虚拟→文件):
  在登记处(VMA)记一笔:
  "101房间(虚拟地址段)= 仓库(文件)第3排货架(文件偏移)"

  你去 101 房间取东西时:
    如果房间是空的(缺页)→ 工人去仓库搬来放进来
    如果房间有东西(命中)→ 直接拿走

一句话

MMAP 建立的是"虚拟地址区间 → 文件某段的偏移位置"的映射

访问虚拟地址 0x7f000008 时,OS 通过这个映射知道: "去 prices.dat 文件第 8 字节的位置取数据" ,然后触发缺页把那部分内容加载进来,再让页表指向它。

Demo

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;

public class MmapDemo {

    public static void main(String[] args) throws Exception {
        String filePath = "/tmp/mmap_demo/prices.dat";

        // ============ 第一步:建立 MMAP 映射 ============
        System.out.println("=== 第一步:创建文件并建立 MMAP 映射 ===");
        RandomAccessFile raf = new RandomAccessFile(filePath, "rw");
        FileChannel channel = raf.getChannel();

        // 映射 3 个 long 的空间(3 × 8 = 24 字节)
        // 就像打开一个传送门,连接到文件的 0~24 字节区域
        LongBuffer buffer = channel.map(
            FileChannel.MapMode.READ_WRITE,
            0,      // 文件起始偏移
            24      // 映射 24 字节
        ).asLongBuffer();

        System.out.println("传送门已建立!文件大小:" + new File(filePath).length() + " 字节");

        // ============ 第二步:写入初始价格 ============
        System.out.println("\n=== 第二步:写入商品初始价格 ===");
        // 就像直接往货架上放东西,没有任何"搬运"过程
        buffer.put(0, Double.doubleToLongBits(28.0));  // 黄焖鸡 docId=0
        buffer.put(1, Double.doubleToLongBits(22.5));  // 麻辣烫 docId=1
        buffer.put(2, Double.doubleToLongBits(15.0));  // 炸鸡腿 docId=2

        System.out.println("黄焖鸡 价格=" + Double.longBitsToDouble(buffer.get(0)));
        System.out.println("麻辣烫 价格=" + Double.longBitsToDouble(buffer.get(1)));
        System.out.println("炸鸡腿 价格=" + Double.longBitsToDouble(buffer.get(2)));

        // ============ 第三步:模拟原地更新价格 ============
        System.out.println("\n=== 第三步:麻辣烫降价!22.5 → 19.9 ===");
        buffer.put(1, Double.doubleToLongBits(19.9));  // 直接改内存,等于改文件
        System.out.println("更新后麻辣烫 价格=" + Double.longBitsToDouble(buffer.get(1)));

        // 强制刷盘(msync)
        channel.force(false);
        System.out.println("已 msync 刷盘");

    }
}

MMAP 建立的映射(登记在内核 VMA 表里)

Key(键): 进程的虚拟地址区间
Value(值): 文件描述符 + 文件内偏移量 + 长度

具体例子

channel.map(READ_WRITE, 0, 24)
返回虚拟地址: 0x7f000000

内核 VMA 表里记录:

Key:   [0x7f000000, 0x7f000018)   ← 虚拟地址范围(24字节)
       
Value: {
         文件: prices.dat(通过 fd 标识),
         偏移: 0,                        ← 从文件第0字节开始
         长度: 24,                       ← 映射24字节
         权限: READ_WRITE                ← 可读可写
       }

白话翻译

"进程虚拟地址 0x7f000000~0x7f000018 这段内存
 对应的不是普通内存,而是 prices.dat 文件的第 0~24 字节"

访问时怎么用这个映射

程序: buffer.get(1)  = 读虚拟地址 0x7f000008MMU: 查页表,发现缺页OS: 查 VMA 表
    Key=0x7f000008 落在 [0x7f000000, 0x7f000018) 区间
    Value=prices.dat 文件偏移 0
            ↓
OS: 去磁盘读 prices.dat 的第 0 页(4KB),加载到 Page CacheOS: 更新页表,让虚拟地址 0x7f000000 指向这个物理页程序: 继续执行,从物理页读到数据


一句话

MMAP 的映射是:K = 虚拟地址范围,V = (文件,偏移,长度) ,告诉操作系统"这段虚拟地址的数据来自那个文件的哪个位置"。