第6章:共享内存架构
章节导读
如果说环境初始化是"准备舞台",那么共享内存就是"舞台的核心"。Java 和 Python 进程间的高性能通信完全依赖于 DataExchangeQueue 的精妙设计。
本章将深入讲解:
- mmap 内存映射的原理
- 环形缓冲区的设计
- 无锁算法的实现
- Unsafe 底层操作
通过本章,你将理解为什么 geaflow-infer 能达到微秒级延迟。
核心设计
markdown
问题: 如何在毫秒级延迟和通用性之间权衡?
答案: DataExchangeQueue
✓ 以微秒级延迟换取本机限制
✓ 以复杂的无锁实现换取高性能
✓ 以 Unsafe 危险性换取性能优势
✓ 这些权衡在推理场景中是值得的
最终结果:
- 延迟: 微秒级 (vs RPC 毫秒级)
- 吞吐: Gbps 级别
- 复杂度: 高,但被隔离在这个模块
- 可维护性: 通过接口隔离,易于理解
6.1 DataExchangeQueue 核心设计
设计目标
markdown
问题: Java 和 Python 进程如何高效传输大量数据?
候选方案对比:
方案 1: 文件系统
├─ 优点: 简单、可靠
└─ 缺点: 延迟高 (毫秒级)、吞吐量受限
方案 2: 网络 Socket
├─ 优点: 支持远程
└─ 缺点: 延迟高 (毫秒级)、有网络开销
方案 3: 消息队列 (如 Redis)
├─ 优点: 功能丰富
└─ 缺点: 延迟中等 (微秒级)、有序列化开销
方案 4: 共享内存 (DataExchangeQueue)
├─ 优点: 延迟极低 (微秒级)、零拷贝、吞吐量高
└─ 缺点: 仅限本机、需要复杂的并发控制
GeaFlow-Infer 的选择: 方案 4
原因: 推理通常在本机,对延迟敏感,需要高吞吐量
内存映射 (mmap) 的基础
scss
┌──────────────────────────────────────────────────┐
│ 传统的进程间通信 (IPC) │
├──────────────────────────────────────────────────┤
│ Java 进程 系统内核 Python 进程│
│ │ │ │
│ ├─ 写数据 │ │
│ │ (系统调用) │ │
│ │ ↓ │ │
│ │ [数据在内核缓冲] ←─→ [数据在内核缓冲] │
│ │ ↓ │ ↓ │
│ │ (系统调用) │ (系统调用) │
│ │ │ ↓ │
│ │ │ 读数据 │
│ │ │ ↓ │
│ 缺点: 多次数据拷贝,延迟高 结果进程接收│
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ 基于 mmap 的共享内存通信 │
├──────────────────────────────────────────────────┤
│ Java 进程 Linux 内核 Python 进程│
│ │ │ │
│ │ 1. mmap() 系统调用 │ │
│ ├─→ 请求内存映射 │ │
│ │ ↓ │ │
│ │ 2. 返回虚拟地址 │ │
│ │ ↓ │ 3. mmap() 系统调用 │
│ │ 获得虚拟地址 A ─────→ ←─ 获得虚拟地址 B │
│ │ (不同地址, │ (不同地址, │
│ │ 指向同一物理内存) │ 指向同一物理内存) │
│ │ │ │
│ │ 4. 直接写内存 │ │
│ ├─ *(void*)A = data │ ← 直接读内存 │
│ │ │ data = *(void*)B │
│ │ (无系统调用,无拷贝!)│ (无系统调用,无拷贝!) │
│ │ │ │
│ 优点: 零拷贝、延迟微秒级、吞吐量高 │
└──────────────────────────────────────────────────┘
内存映射的工作原理
ini
Step 1: 创建共享内存文件
Linux 文件系统
/dev/shm/geaflow_queue_input
(大小: 1MB, 内容: 全 0)
Step 2: Java 进程映射文件
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel channel = raf.getChannel();
MappedByteBuffer mbb = channel.map(
MapMode.READ_WRITE, 0, file.length());
结果:
Java 虚拟地址空间: 0x1000 - 0x2000
↓
页表 (Page Table)
↓
物理内存: Frame 100-200
↓
/dev/shm/geaflow_queue_input
Step 3: Python 进程映射文件
import mmap
with open('/dev/shm/geaflow_queue_input', 'r+b') as f:
mm = mmap.mmap(f.fileno(), 0)
# 或直接使用 mm[offset:offset+length]
结果:
Python 虚拟地址空间: 0x5000 - 0x6000
↓
页表 (Page Table)
↓
物理内存: Frame 100-200 (同一份!)
Step 4: 数据交换 (零拷贝!)
Java: Python:
mbb.put(index, data) ← 直接写内存
data = mm[index]
(直接读取, 看到 Java 写入的值)
6.2 无锁队列实现原理
Ring Buffer 设计
ini
┌────────────────────────────────────────┐
│ 环形缓冲区 (Ring Buffer) │
│ 大小: 2^N = 1MB (对齐) │
├────────────────────────────────────────┤
│ │
│ [写入数据1] │
│ [写入数据2] ← 写指针指向这里 │
│ [写入数据3] │
│ [写入数据4] │
│ ... │
│ [读取完毕的数据] ← 读指针指向这里 │
│ [可写入的空间] │
│ │
│ 大小=2^20, 掩码=0xFFFFF │
│ 指针值: 0 ~ 2^30 (不清零) │
│ 实际地址: ptr & 掩码 │
│ │
└────────────────────────────────────────┘
示例:
queue size = 2^10 = 1024 bytes
mask = 0x3FF = 1023
write_ptr = 100, 实际地址 = 100 & 0x3FF = 100
write_ptr = 1100, 实际地址 = 1100 & 0x3FF = 76 (绕一圈)
write_ptr = 2100, 实际地址 = 2100 & 0x3FF = 52 (绕两圈)
优点: 无需清零指针,&mask 自动处理环绕
无锁并发的关键
arduino
问题: 多个 Writer 和 Reader 同时访问队列
┌──────────────────────────────────────┐
│ Writer Thread 1 Writer Thread 2 │
│ │ │ │
│ └────→ Queue ←───────┘ │
│ │ │
│ │ │
│ ┌──────┘ │
│ ↓ │
│ Reader Thread │
│ │
│ 不能用 Lock (synchronized) │
│ 原因: │
│ - 太慢 (原子操作之间可能被中断) │
│ - Java 锁只保证 JVM 内同步 │
│ - Python 的读取无法被保护 │
│ │
│ 解决方案: 无锁算法 │
│ - 使用原子操作 (CAS) │
│ - 使用内存屏障 │
│ - 利用 CPU 指令级别的原子性 │
└──────────────────────────────────────┘
内存屏障的作用
ini
情景: Writer 写数据,Reader 读数据
没有内存屏障:
Writer Reader
│ │
├─ write data[0] │
├─ write data[1] │
├─ write_ptr = 2 ✗ │
│ ├─ read_ptr ✓
│ (reorder!) ├─ read data[0]
│ 实际执行顺序: ├─ read data[1]
│ 1. write_ptr = 2 │
│ 2. write data[0] │ 问题: Reader 看到
│ 3. write data[1] │ write_ptr=2 但数据还没写!
使用内存屏障:
Writer Reader
│ │
├─ write data[0] │
├─ write data[1] │
├─ [内存屏障] │
│ ~~forcedFlush~~ │
├─ write_ptr = 2 ✓ │
│ ├─ [内存屏障]
│ ├─ read_ptr ✓
│ 顺序保证: ├─ read data[0]
│ 1. write data[0] ├─ read data[1]
│ 2. write data[1] │
│ 3. 屏障完成 │ ✓ 正确: Reader 先等待
│ 4. write_ptr = 2 │ 再读数据
Java Unsafe 中的方法:
putOrderedLong() ← Writer 使用,写屏障
getVolatileLong() ← Reader 使用,读屏障
6.3 内存布局与地址管理
共享内存的完整布局
scss
┌─────────────────────────────────────────────────────┐
│ DataExchangeQueue 的内存布局 │
├─────────────────────────────────────────────────────┤
│ [Cache Line 64字节] │
│ ┌─────────────────────────────────┐ │
│ │ startPointAddress (8 bytes) │ ← 标记开始 │
│ │ [填充到 Cache Line 边界] │ │
│ └─────────────────────────────────┘ │
│ │
│ [Cache Line 64字节] │
│ ┌─────────────────────────────────┐ │
│ │ capacityAddress (8 bytes) │ ← 队列大小 │
│ │ outputAddress (8 bytes) │ ← 写指针 │
│ │ [填充到 Cache Line 边界] │ │
│ └─────────────────────────────────┘ │
│ │
│ [Cache Line 64字节] │
│ ┌─────────────────────────────────┐ │
│ │ inputNextAddress (8 bytes) │ ← 下次读 │
│ │ outputNextAddress (8 bytes) │ ← 下次写 │
│ │ [填充到 Cache Line 边界] │ │
│ └─────────────────────────────────┘ │
│ │
│ [Cache Line 64字节] │
│ ┌─────────────────────────────────┐ │
│ │ endPointAddress (8 bytes) │ ← 标记结尾 │
│ │ [填充到 Cache Line 边界] │ │
│ └─────────────────────────────────┘ │
│ │
│ [Cache Line 64字节] │
│ ┌─────────────────────────────────┐ │
│ │ barrierAddress (8 bytes) │ ← 屏障 │
│ │ [填充到 Cache Line 边界] │ │
│ └─────────────────────────────────┘ │
│ │
│ [Cache Line 64字节] │
│ ┌─────────────────────────────────┐ │
│ │ currentBufferAddress (8 bytes) │ ← 当前缓冲 │
│ │ [填充到 Cache Line 边界] │ │
│ └─────────────────────────────────┘ │
│ │
│ [环形数据缓冲区] (实际大小: 通常 1MB) │
│ ┌─────────────────────────────────────────────┐ │
│ │ [数据1] [数据2] [数据3] ... [数据N] │ │
│ │ ^ read_ptr ^ write_ptr │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
设计亮点:
1. Cache Line 对齐 (64 字节)
原因: 避免 False Sharing
什么是 False Sharing?
Writer Thread 在 CPU 0 上修改 write_ptr
Reader Thread 在 CPU 1 上读 read_ptr
如果两个指针在同一 Cache Line,
CPU 0 的缓存失效会导致 CPU 1 的缓存也失效 (通过 Cache Coherency)
使用 Cache Line 对齐,确保指针在不同 Line,避免这个开销
2. 指针不清零
原因: 简化逻辑
write_ptr 可以不断增长: 0 → 1MB → 2MB → ...
实际地址: ptr & mask (自动绕回)
不需要检查"是否需要清零"
3. 多个缓冲区地址
原因: 支持多生产者-多消费者
outputAddress: 下一个要写入的位置 (Writer 使用)
inputNextAddress: 下一个要读取的位置 (Reader 使用)
地址计算
java
public final class DataExchangeQueue {
// 初始化时计算各地址
public DataExchangeQueue(String mapKey, int capacity,
boolean reset) {
// 1. 获取内存映射
this.memoryMapper = new MemoryMapper(mapKey, bufferCapacity);
this.mapAddress = memoryMapper.getMapAddress();
// 2. 对齐到 Cache Line
this.initialRawAddress = Pow2.align(mapAddress,
PortableJvmInfo.CACHE_LINE_SIZE); // 通常 64 字节
// 3. 计算各个字段的地址
this.startPointAddress = initialRawAddress;
this.capacityAddress = startPointAddress
+ PortableJvmInfo.CACHE_LINE_SIZE;
this.outputAddress = startPointAddress
+ 2L * PortableJvmInfo.CACHE_LINE_SIZE;
this.inputNextAddress = outputAddress + 8; // 同一 cache line
this.outputNextAddress = startPointAddress + 8; // 另一 line
this.endPointAddress = outputAddress
+ PortableJvmInfo.CACHE_LINE_SIZE;
this.barrierAddress = endPointAddress + 8;
this.currentBufferAddress = barrierAddress + 8;
// 4. 初始化队列
if (reset) {
reset();
}
}
/**
* 初始化队列状态
*/
private void reset() {
UNSAFE.putOrderedLong(null, startPointAddress, 0);
UNSAFE.putOrderedLong(null, capacityAddress, queueCapacity);
UNSAFE.putOrderedLong(null, outputAddress, 0);
UNSAFE.putOrderedLong(null, inputNextAddress, 0);
UNSAFE.putOrderedLong(null, outputNextAddress, 0);
}
}
6.4 Unsafe 底层操作详解
Unsafe 是什么?
objectivec
sun.misc.Unsafe 是 Java 提供的一个"后门" API
├─ 直接操纵内存 (绕过 GC, 安全检查)
├─ 提供原子操作 (CAS, volatile)
├─ 访问任意对象的任意字段
└─ 原本用于 JVM 内部实现,现在被开发者广泛使用
Unsafe 的危险性:
⚠️ 可能导致 JVM 崩溃
⚠️ 破坏 Java 的内存安全
⚠️ 代码难以维护
⚠️ Java 9+ 开始限制使用
但是,对于 geaflow-infer:
✓ 需要微秒级延迟,无法使用 Java Lock
✓ 需要跨进程通信,Unsafe 提供底层能力
✓ 开发者理解风险,谨慎使用
核心 Unsafe 操作
java
// 1. 获取 Unsafe 实例 (反射)
Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe UNSAFE = (Unsafe) field.get(null);
// 2. 写操作 (带内存屏障)
UNSAFE.putOrderedLong(object, offset, value);
// 相当于: *(long*)(object + offset) = value
// 带屏障: 确保写操作后的所有内存操作完成
// 3. 读操作 (带内存屏障)
long value = UNSAFE.getVolatileLong(object, offset);
// 相当于: return *(long*)(object + offset)
// 带屏障: 确保读操作前的所有内存操作完成
// 4. 写字节
UNSAFE.putByte(address, value);
UNSAFE.putByte(object, offset, value);
// 5. 读字节
byte value = UNSAFE.getByte(address);
byte value = UNSAFE.getByte(object, offset);
// 6. 比较并交换 (CAS - 无锁的基础)
boolean success = UNSAFE.compareAndSwapLong(
object, // 对象
offset, // 字段偏移
expect, // 期望值
update // 新值
);
// 原子操作: 如果 *(object+offset) == expect,
// 则设置为 update,返回 true
// 否则返回 false
实际使用示例
java
// 来自 DataExchangeQueue 的实际代码
public class DataExchangeQueue {
private static final Unsafe UNSAFE;
private static final long OUTPUT_ADDRESS_OFFSET;
private static final long INPUT_NEXT_ADDRESS_OFFSET;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
// 计算字段偏移 (用于后续 putOrderedLong/getVolatileLong)
OUTPUT_ADDRESS_OFFSET = UNSAFE.objectFieldOffset(
DataExchangeQueue.class.getDeclaredField("outputAddress")
);
INPUT_NEXT_ADDRESS_OFFSET = UNSAFE.objectFieldOffset(
DataExchangeQueue.class.getDeclaredField("inputNextAddress")
);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 写入数据
*/
public void put(byte[] data) {
long writeIndex = UNSAFE.getVolatileLong(this,
OUTPUT_ADDRESS_OFFSET);
long nextIndex = writeIndex + data.length;
// 检查是否越界
if ((nextIndex & ~mask) != (writeIndex & ~mask)) {
throw new BufferFullException();
}
// 写数据到共享内存
for (int i = 0; i < data.length; i++) {
long address = initialRawAddress + (writeIndex + i) & mask;
UNSAFE.putByte(address, data[i]);
}
// 更新写指针 (带屏障,其他进程能看到)
UNSAFE.putOrderedLong(this, OUTPUT_ADDRESS_OFFSET, nextIndex);
}
/**
* 读取数据
*/
public byte[] get() {
// 读写指针 (带屏障)
long writeIndex = UNSAFE.getVolatileLong(this,
OUTPUT_ADDRESS_OFFSET);
long readIndex = UNSAFE.getVolatileLong(this,
INPUT_NEXT_ADDRESS_OFFSET);
if (writeIndex == readIndex) {
return null; // 无数据
}
// 获取数据长度 (存储在数据前 4 字节)
long lenAddress = initialRawAddress + (readIndex & mask);
int len = UNSAFE.getInt(lenAddress);
// 读数据
byte[] data = new byte[len];
for (int i = 0; i < len; i++) {
long address = initialRawAddress +
((readIndex + i + 4) & mask);
data[i] = UNSAFE.getByte(address);
}
// 更新读指针 (带屏障)
UNSAFE.putOrderedLong(this, INPUT_NEXT_ADDRESS_OFFSET,
readIndex + len + 4);
return data;
}
}
Unsafe 的性能特性
erlang
操作延迟对比:
操作 延迟 (纳秒)
─────────────────────────────
普通字段访问 1-10 ns
Unsafe.getByte() 5-20 ns
Unsafe.getLong() 10-30 ns
getVolatileLong() 20-50 ns (带内存屏障)
putOrderedLong() 20-50 ns (带内存屏障)
synchronized Lock 100-500 ns
Lock (java.util.concurrent) 200-1000 ns
结论: 即使加上内存屏障,Unsafe 操作仍然比 Lock 快 10 倍以上