一共有两层映射,别搞混
第一层:虚拟地址 → 物理内存(所有程序都有,不是 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) = 读虚拟地址 0x7f000008
↓
MMU: 查页表,发现缺页
↓
OS: 查 VMA 表
Key=0x7f000008 落在 [0x7f000000, 0x7f000018) 区间
Value=prices.dat 文件偏移 0
↓
OS: 去磁盘读 prices.dat 的第 0 页(4KB),加载到 Page Cache
↓
OS: 更新页表,让虚拟地址 0x7f000000 指向这个物理页
↓
程序: 继续执行,从物理页读到数据
一句话
MMAP 的映射是:K = 虚拟地址范围,V = (文件,偏移,长度) ,告诉操作系统"这段虚拟地址的数据来自那个文件的哪个位置"。