JDK21中使用FFM异步读取MSG_ZEROCOPY错误队列原理
- 前言
- FFM异步读取MSG_ZEROCOPY错误队列原理
-
- [Linux 内核 `sk_error_queue` 与 `epoll` 事件触发机制](#Linux 内核
sk_error_queue与epoll事件触发机制) - [C System ABI 与 Panama FFM 内存布局映射](#C System ABI 与 Panama FFM 内存布局映射)
- 零拷贝通道与上下文生命周期管理器
- [Epoll 事件驱动异步 Reactor (`EpollZerocopyReactor`)](#Epoll 事件驱动异步 Reactor (
EpollZerocopyReactor)) - 端到端执行流程与内存池集成示例
- 关键技术特性与性能对照
- [Linux 内核 `sk_error_queue` 与 `epoll` 事件触发机制](#Linux 内核
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
FFM异步读取MSG_ZEROCOPY错误队列原理
Linux 内核 sk_error_queue 与 epoll 事件触发机制
当套接字开启 SO_ZEROCOPY 后,内核发送网络帧(sk_buff)不再进行用户态到内核态的内存拷贝,而是通过 get_user_pages() 锁定 MemorySegment 对应的物理页。DMA 传输完成并收到网卡 TX Interrupt 后,内核释放 SKB 并触发 sock_zerocopy_callback()。
sock_zerocopy_callback() 将构造一个 sock_extended_err 结构体压入套接字的错误队列(sk_error_queue),随后调用 sk->sk_error_report(sk)。在 epoll 子系统中,套接字错误队列出现挂起数据会直接激活 EPOLLERR 事件(在某些内核路径中也可能附带 EPOLLPRI 带外数据事件)。
+---------------------------------------------------------------------------------------+
| Linux Kernel Network Subsystem |
| |
| [ NIC DMA Tx Complete ] |
| │ |
| ▼ |
| sk_buff Freed ──> sock_zerocopy_callback() |
| │ |
| ├── 1. Push struct sock_extended_err -> sk_error_queue |
| └── 2. Trigger sk->sk_error_report() |
| │ |
| ▼ |
| Wakeup epoll Wait Queue |
| │ |
+---------------------------------------┼-----------------------------------------------+
│
Returns Event: EPOLLERR | EPOLLPRI
│
+---------------------------------------┼-----------------------------------------------+
| User Space (Panama FFM Reactor Engine)│ |
| ▼ |
| 1. epoll_wait() returns socketFd with EPOLLERR |
| 2. Drain Loop: recvmsg(fd, &msg, MSG_ERRQUEUE) in Non-Blocking Mode |
| 3. Parse Ancillary Data (cmsghdr) -> extract range [ee_info, ee_data] |
| 4. Unpin & Recycle MemorySegments in range |
+---------------------------------------------------------------------------------------+
C System ABI 与 Panama FFM 内存布局映射
在 Linux x86_64 架构下,struct epoll_event 具有 __attribute__((__packed__)) 紧凑对齐特性,其尺寸为 12 字节。在 FFM 中必须显式定义 .withByteAlignment(4),否则将产生内存错位。
java
package com.engine.network.epoll.nativeabi;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import static java.lang.foreign.ValueLayout.*;
/**
* Linux x86_64 C System ABI Layout 与 Downcall 句柄映射 (JDK 22+ Panama FFM)
*/
public final class LinuxNativeAbi {
// -------------------------------------------------------------------------
// Socket / Epoll 常量
// -------------------------------------------------------------------------
public static final int SOL_SOCKET = 1;
public static final int SO_ZEROCOPY = 60;
public static final int MSG_ZEROCOPY = 0x04000000;
public static final int MSG_ERRQUEUE = 0x2000;
public static final int MSG_DONTWAIT = 0x40;
public static final byte SO_EE_ORIGIN_ZEROCOPY = 5;
public static final int SOL_IP = 0;
public static final int IP_RECVERR = 11;
public static final int EPOLLIN = 0x001;
public static final int EPOLLPRI = 0x002;
public static final int EPOLLOUT = 0x004;
public static final int EPOLLERR = 0x008;
public static final int EPOLLHUP = 0x010;
public static final int EPOLLET = 1 << 31; // 边缘触发模式 (Edge-Triggered)
public static final int EPOLL_CTL_ADD = 1;
public static final int EPOLL_CTL_MOD = 2;
public static final int EPOLL_CTL_DEL = 3;
public static final int EPOLL_CLOEXEC = 02000000;
public static final int F_GETFL = 3;
public static final int F_SETFL = 4;
public static final int O_NONBLOCK = 04000;
// -------------------------------------------------------------------------
// MemoryLayout: struct epoll_event (x86_64 Packed Layout, 12 Bytes)
// struct epoll_event {
// uint32_t events; // Offset 0, Size 4
// epoll_data_t data; // Offset 4, Size 8 (Union: void*, int fd, uint64_t)
// }
// -------------------------------------------------------------------------
public static final StructLayout EPOLL_EVENT_LAYOUT = MemoryLayout.structLayout(
JAVA_INT.withName("events"),
JAVA_LONG.withByteAlignment(4).withName("data")
);
public static final long OFF_EPOLL_EVENTS = EPOLL_EVENT_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("events"));
public static final long OFF_EPOLL_DATA = EPOLL_EVENT_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("data"));
// -------------------------------------------------------------------------
// MemoryLayout: struct iovec { void *iov_base; size_t iov_len; }
// -------------------------------------------------------------------------
public static final StructLayout IOVEC_LAYOUT = MemoryLayout.structLayout(
ADDRESS.withName("iov_base"),
JAVA_LONG.withName("iov_len")
);
public static final long OFF_IOV_BASE = IOVEC_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("iov_base"));
public static final long OFF_IOV_LEN = IOVEC_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("iov_len"));
// -------------------------------------------------------------------------
// MemoryLayout: struct msghdr (56 Bytes on x86_64)
// -------------------------------------------------------------------------
public static final StructLayout MSGHDR_LAYOUT = MemoryLayout.structLayout(
ADDRESS.withName("msg_name"),
JAVA_INT.withName("msg_namelen"),
MemoryLayout.paddingLayout(4),
ADDRESS.withName("msg_iov"),
JAVA_LONG.withName("msg_iovlen"),
ADDRESS.withName("msg_control"),
JAVA_LONG.withName("msg_controllen"),
JAVA_INT.withName("msg_flags"),
MemoryLayout.paddingLayout(4)
);
public static final long OFF_MSG_IOV = MSGHDR_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("msg_iov"));
public static final long OFF_MSG_IOVLEN = MSGHDR_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("msg_iovlen"));
public static final long OFF_MSG_CONTROL = MSGHDR_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("msg_control"));
public static final long OFF_MSG_CONTROLLEN = MSGHDR_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("msg_controllen"));
// -------------------------------------------------------------------------
// MemoryLayout: struct cmsghdr (16 Bytes on x86_64)
// -------------------------------------------------------------------------
public static final StructLayout CMSGHDR_LAYOUT = MemoryLayout.structLayout(
JAVA_LONG.withName("cmsg_len"),
JAVA_INT.withName("cmsg_level"),
JAVA_INT.withName("cmsg_type")
);
public static final long OFF_CMSG_LEN = CMSGHDR_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("cmsg_len"));
public static final long OFF_CMSG_LEVEL = CMSGHDR_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("cmsg_level"));
public static final long OFF_CMSG_TYPE = CMSGHDR_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("cmsg_type"));
// -------------------------------------------------------------------------
// MemoryLayout: struct sock_extended_err (16 Bytes)
// -------------------------------------------------------------------------
public static final StructLayout SOCK_EXTENDED_ERR_LAYOUT = MemoryLayout.structLayout(
JAVA_INT.withName("ee_errno"),
JAVA_BYTE.withName("ee_origin"),
JAVA_BYTE.withName("ee_type"),
JAVA_BYTE.withName("ee_code"),
JAVA_BYTE.withName("ee_pad"),
JAVA_INT.withName("ee_info"), // lo_id: 零拷贝完成起始 ID
JAVA_INT.withName("ee_data") // hi_id: 零拷贝完成终止 ID
);
public static final long OFF_EE_ORIGIN = SOCK_EXTENDED_ERR_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("ee_origin"));
public static final long OFF_EE_INFO = SOCK_EXTENDED_ERR_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("ee_info"));
public static final long OFF_EE_DATA = SOCK_EXTENDED_ERR_LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("ee_data"));
// -------------------------------------------------------------------------
// POSIX Native System Call Downcall Handles
// -------------------------------------------------------------------------
public static final MethodHandle EPOLL_CREATE1;
public static final MethodHandle EPOLL_CTL;
public static final MethodHandle EPOLL_WAIT;
public static final MethodHandle RECVMSG;
public static final MethodHandle SEND;
public static final MethodHandle SETSOCKOPT;
public static final MethodHandle FCNTL;
static {
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
EPOLL_CREATE1 = linker.downcallHandle(
stdlib.find("epoll_create1").orElseThrow(),
FunctionDescriptor.of(JAVA_INT, JAVA_INT)
);
EPOLL_CTL = linker.downcallHandle(
stdlib.find("epoll_ctl").orElseThrow(),
FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT, JAVA_INT, ADDRESS)
);
EPOLL_WAIT = linker.downcallHandle(
stdlib.find("epoll_wait").orElseThrow(),
FunctionDescriptor.of(JAVA_INT, JAVA_INT, ADDRESS, JAVA_INT, JAVA_INT)
);
RECVMSG = linker.downcallHandle(
stdlib.find("recvmsg").orElseThrow(),
FunctionDescriptor.of(JAVA_LONG, JAVA_INT, ADDRESS, JAVA_INT)
);
SEND = linker.downcallHandle(
stdlib.find("send").orElseThrow(),
FunctionDescriptor.of(JAVA_LONG, JAVA_INT, ADDRESS, JAVA_LONG, JAVA_INT)
);
SETSOCKOPT = linker.downcallHandle(
stdlib.find("setsockopt").orElseThrow(),
FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT, JAVA_INT, ADDRESS, JAVA_INT)
);
FCNTL = linker.downcallHandle(
stdlib.find("fcntl").orElseThrow(),
FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT, JAVA_LONG)
);
}
}
零拷贝通道与上下文生命周期管理器
该类负责对原生 Socket 进行非阻塞模式配置、SO_ZEROCOPY 开关的使能,并维护由内核递增分配的 zerocopy_id 与 Java 堆外 MemorySegment 回调之间的关联生命周期。
java
package com.engine.network.epoll.zerocopy;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import static com.engine.network.epoll.nativeabi.LinuxNativeAbi.*;
/**
* 零拷贝发送通道上下文,负责关联 zerocopy_id 与 MemorySegment 生命期
*/
public final class ZeroCopySocketChannel implements AutoCloseable {
private final int socketFd;
private final AtomicInteger zerocopyIdSequence = new AtomicInteger(0);
private final ConcurrentHashMap<Integer, ZeroCopyFrame> pendingFrames = new ConcurrentHashMap<>();
public record ZeroCopyFrame(
int id,
MemorySegment segment,
Consumer<MemorySegment> onUnpinCallback
) {}
public ZeroCopySocketChannel(int socketFd) throws Throwable {
this.socketFd = socketFd;
initSocketOptions();
}
private void initSocketOptions() throws Throwable {
// 1. 设置套接字为非阻塞模式 (O_NONBLOCK)
int flags = (int) FCNTL.invokeExact(socketFd, F_GETFL, 0L);
if (flags < 0) {
throw new RuntimeException("fcntl F_GETFL 失败");
}
int res = (int) FCNTL.invokeExact(socketFd, F_SETFL, (long) (flags | O_NONBLOCK));
if (res < 0) {
throw new RuntimeException("fcntl F_SETFL O_NONBLOCK 失败");
}
// 2. 使能套接字的 SO_ZEROCOPY 选项
try (Arena localArena = Arena.ofConfined()) {
MemorySegment optVal = localArena.allocate(JAVA_INT, 1);
int optRes = (int) SETSOCKOPT.invokeExact(socketFd, SOL_SOCKET, SO_ZEROCOPY, optVal, (int) optVal.byteSize());
if (optRes < 0) {
throw new RuntimeException("setsockopt SO_ZEROCOPY 开启失败");
}
}
}
/**
* 发送 Native MemorySegment
*/
public long sendZeroCopy(MemorySegment segment, Consumer<MemorySegment> onUnpin) throws Throwable {
if (!segment.isNative()) {
throw new IllegalArgumentException("MSG_ZEROCOPY 必须使用 Native MemorySegment");
}
int currentId = zerocopyIdSequence.getAndIncrement();
ZeroCopyFrame frame = new ZeroCopyFrame(currentId, segment, onUnpin);
pendingFrames.put(currentId, frame);
long bytesSent = (long) SEND.invokeExact(
socketFd,
segment.address(),
segment.byteSize(),
MSG_ZEROCOPY
);
if (bytesSent < 0) {
pendingFrames.remove(currentId);
throw new RuntimeException("send(MSG_ZEROCOPY) 系统调用失败, bytesSent=" + bytesSent);
}
return bytesSent;
}
/**
* 边缘触发 (EPOLLET) 下非阻塞读取错误队列并清空所有完成通知
*/
public int drainErrorQueue() throws Throwable {
int freedCount = 0;
try (Arena localArena = Arena.ofConfined()) {
// 分配 struct msghdr 与 Auxiliary Control Data 空间
MemorySegment msgHeader = localArena.allocate(MSGHDR_LAYOUT);
MemorySegment controlBuffer = localArena.allocate(512);
// 构造 recvmsg 必须附带的 dummy struct iovec
MemorySegment iov = localArena.allocate(IOVEC_LAYOUT);
MemorySegment dummyBuf = localArena.allocate(64);
iov.set(ADDRESS, OFF_IOV_BASE, dummyBuf);
iov.set(JAVA_LONG, OFF_IOV_LEN, dummyBuf.byteSize());
msgHeader.set(ADDRESS, OFF_MSG_IOV, iov);
msgHeader.set(JAVA_LONG, OFF_MSG_IOVLEN, 1);
msgHeader.set(ADDRESS, OFF_MSG_CONTROL, controlBuffer);
msgHeader.set(JAVA_LONG, OFF_MSG_CONTROLLEN, controlBuffer.byteSize());
// 循环拉取直至错误队列返回 EAGAIN / EWOULDBLOCK
while (true) {
long res = (long) RECVMSG.invokeExact(socketFd, msgHeader, MSG_ERRQUEUE | MSG_DONTWAIT);
if (res < 0) {
// 错误队列已排空,退出循环
break;
}
// 解析 Control Message (cmsghdr)
long controlAddr = msgHeader.get(ADDRESS, OFF_MSG_CONTROL).address();
long controlLen = msgHeader.get(JAVA_LONG, OFF_MSG_CONTROLLEN);
if (controlLen >= CMSGHDR_LAYOUT.byteSize()) {
MemorySegment cmsgSeg = MemorySegment.ofAddress(controlAddr).reinterpret(controlLen, localArena, null);
int level = cmsgSeg.get(JAVA_INT, OFF_CMSG_LEVEL);
int type = cmsgSeg.get(JAVA_INT, OFF_CMSG_TYPE);
if (type == IP_RECVERR || level == SOL_SOCKET) {
// 提取 struct sock_extended_err 内容
long errOffset = CMSGHDR_LAYOUT.byteSize();
MemorySegment errStruct = cmsgSeg.asSlice(errOffset, SOCK_EXTENDED_ERR_LAYOUT.byteSize());
byte origin = errStruct.get(JAVA_BYTE, OFF_EE_ORIGIN);
if (origin == SO_EE_ORIGIN_ZEROCOPY) {
// 内核解卡范围闭区间 [ee_info, ee_data]
int rangeLo = errStruct.get(JAVA_INT, OFF_EE_INFO);
int rangeHi = errStruct.get(JAVA_INT, OFF_EE_DATA);
for (int id = rangeLo; id <= rangeHi; id++) {
ZeroCopyFrame frame = pendingFrames.remove(id);
if (frame != null) {
freedCount++;
if (frame.onUnpinCallback() != null) {
frame.onUnpinCallback().accept(frame.segment());
}
}
}
}
}
}
}
}
return freedCount;
}
public int getSocketFd() {
return socketFd;
}
@Override
public void close() {
pendingFrames.clear();
}
}
Epoll 事件驱动异步 Reactor (EpollZerocopyReactor)
java
package com.engine.network.epoll.zerocopy;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.util.concurrent.ConcurrentHashMap;
import static com.engine.network.epoll.nativeabi.LinuxNativeAbi.*;
/**
* 绑定 EPOLLERR | EPOLLPRI 事件的异步零拷贝回收 Reactor 反应堆
*/
public final class EpollZerocopyReactor implements AutoCloseable {
private final int epollFd;
private final Arena reactorArena;
private final Thread eventLoopThread;
private final ConcurrentHashMap<Integer, ZeroCopySocketChannel> channelMap = new ConcurrentHashMap<>();
private volatile boolean running = true;
public EpollZerocopyReactor() throws Throwable {
this.epollFd = (int) EPOLL_CREATE1.invokeExact(EPOLL_CLOEXEC);
if (this.epollFd < 0) {
throw new RuntimeException("epoll_create1 执行失败, fd=" + epollFd);
}
this.reactorArena = Arena.ofShared();
this.eventLoopThread = Thread.ofPlatform()
.name("epoll-zerocopy-reactor")
.start(this::eventLoop);
}
/**
* 将 ZeroCopySocketChannel 注册至 epoll,绑定 EPOLLERR | EPOLLPRI 并使用边缘触发 (EPOLLET)
*/
public void registerChannel(ZeroCopySocketChannel channel) throws Throwable {
int fd = channel.getSocketFd();
channelMap.put(fd, channel);
try (Arena localArena = Arena.ofConfined()) {
MemorySegment event = localArena.allocate(EPOLL_EVENT_LAYOUT);
// 监听 EPOLLOUT (写就绪) + EPOLLERR / EPOLLPRI (错误队列有解卡通知) + EPOLLET (边缘触发)
int interestEvents = EPOLLOUT | EPOLLERR | EPOLLPRI | EPOLLET;
event.set(ValueLayout.JAVA_INT, OFF_EPOLL_EVENTS, interestEvents);
event.set(ValueLayout.JAVA_LONG, OFF_EPOLL_DATA, (long) fd);
int res = (int) EPOLL_CTL.invokeExact(epollFd, EPOLL_CTL_ADD, fd, event);
if (res < 0) {
channelMap.remove(fd);
throw new RuntimeException("epoll_ctl EPOLL_CTL_ADD 注册失败, ret=" + res);
}
}
}
/**
* Reactor 主事件循环
*/
private void eventLoop() {
final int MAX_EVENTS = 128;
// 在 shared arena 预分配 epoll_event 批处理数组,避免循环内产生 JVM 堆对象
MemorySegment eventsArray = reactorArena.allocate(EPOLL_EVENT_LAYOUT, MAX_EVENTS);
while (running) {
try {
// 阻塞等待 epoll 事件唤醒 (超时设为 50ms)
int ready = (int) EPOLL_WAIT.invokeExact(epollFd, eventsArray, MAX_EVENTS, 50);
for (int i = 0; i < ready; i++) {
MemorySegment eventSlice = eventsArray.asSlice(i * EPOLL_EVENT_LAYOUT.byteSize(), EPOLL_EVENT_LAYOUT);
int events = eventSlice.get(ValueLayout.JAVA_INT, OFF_EPOLL_EVENTS);
int fd = (int) eventSlice.get(ValueLayout.JAVA_LONG, OFF_EPOLL_DATA);
ZeroCopySocketChannel channel = channelMap.get(fd);
if (channel == null) continue;
// 核心路径:捕获 EPOLLERR 或 EPOLLPRI 事件
// 此时说明内核 sk_error_queue 已经收到 sock_zerocopy_callback() 填入的通知
if ((events & (EPOLLERR | EPOLLPRI)) != 0) {
int freedSegments = channel.drainErrorQueue();
if (freedSegments > 0) {
// 成功回收内存帧,可触发监控指标或流控调节
}
}
// 普通可写事件路径
if ((events & EPOLLOUT) != 0) {
// 驱动 Pipe/Socket 待发送环形队列的写出
}
}
} catch (Throwable t) {
if (running) {
t.printStackTrace();
}
}
}
}
public void unregisterChannel(int socketFd) throws Throwable {
channelMap.remove(socketFd);
try (Arena localArena = Arena.ofConfined()) {
EPOLL_CTL.invokeExact(epollFd, EPOLL_CTL_DEL, socketFd, MemorySegment.NULL);
}
}
@Override
public void close() {
this.running = false;
if (eventLoopThread != null) {
eventLoopThread.interrupt();
}
if (reactorArena.scope().isAlive()) {
reactorArena.close();
}
}
}
端到端执行流程与内存池集成示例
java
package com.engine.network.epoll;
import com.engine.network.epoll.zerocopy.EpollZerocopyReactor;
import com.engine.network.epoll.zerocopy.ZeroCopySocketChannel;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.concurrent.CountDownLatch;
public class EpollZeroCopyPipelineDemo {
public static void main(String[] args) throws Throwable {
int nativeSocketFd = connectToRemoteHost("127.0.0.1", 9090);
try (EpollZerocopyReactor reactor = new EpollZerocopyReactor();
ZeroCopySocketChannel channel = new ZeroCopySocketChannel(nativeSocketFd);
Arena offHeapPoolArena = Arena.ofShared()) {
// 1. 将套接字通道注册至 epoll reactor
reactor.registerChannel(channel);
// 2. 从堆外内存池分配一个 2MB 的物理页 Chunk
MemorySegment payloadSegment = offHeapPoolArena.allocate(2 * 1024 * 1024);
payloadSegment.fill((byte) 0x7E);
CountDownLatch unpinLatch = new CountDownLatch(1);
// 3. 执行零拷贝发送,并传入异步解卡回调
channel.sendZeroCopy(payloadSegment, freedSegment -> {
System.out.println("[内核通知]: 物理页已由 NIC DMA 发送完毕并成功解卡!");
System.out.println("归还 MemorySegment 地址: 0x" + Long.toHexString(freedSegment.address()));
unpinLatch.countDown();
});
// 4. 等待异步 epoll 事件触发 recvmsg(MSG_ERRQUEUE) 完成回收
unpinLatch.await();
System.out.println("端到端异步 MSG_ZEROCOPY 回收流程完成。");
}
}
private static int connectToRemoteHost(String host, int port) {
// POSIX socket(), connect() 逻辑...
return 5; // 假设返回 socket fd = 5
}
}
关键技术特性与性能对照
| 维度 | 传统 select / 轮询查询 MSG_ERRQUEUE |
本方案:Panama FFM + epoll_wait (EPOLLERR) |
|---|---|---|
| 通知触发机制 | 用户态主动 Thread.sleep / 定时轮询 | 内核中断回调驱动 :仅在 sk_error_queue 进数据时精准唤醒 |
| 系统调用开销 | 存在大量空轮询 recvmsg 尝试 |
0 额外轮询 :epoll_wait 挂起,有通知时单次批量 Drain |
| 内存映射精度 | JNI 字节数组拷贝或 ByteBuffer 包装 | MemoryLayout.structLayout 精确匹配 C packed 布局 (withByteAlignment(4)) |
| GC 压力 | JNI/JDK 内部生成临时对象 | 0 GC :事件数组与 msghdr 均由 Arena 挂载在堆外映射区 |
| 批处理效率 | 单次读取单个 ID | 范围归还 :解析 [ee_info, ee_data] 闭区间,单次系统调用解卡上百个 Segment |