Linux 内核 5.1 引入的 io_uring 是继 AIO 之后 Linux 原生异步 IO 的集大成者。它通过一对共享内存环形缓冲区——Submission Queue(SQ)和 Completion Queue(CQ)——实现了用户态与内核态之间的零拷贝通信,彻底消除了传统 epoll + read/write 系统调用的上下文切换开销。Netty 4.2 在 IoHandler/IoHandle 抽象层的基础上,实现了完整的 io_uring 传输后端,将 IORING_OP_ACCEPT、IORING_OP_RECV、IORING_OP_SEND、IORING_OP_SEND_ZC 等操作码映射为 IoUringIoOps,通过 IoUringIoHandler 将 SQ 提交与 CQ 轮询集成到 SingleThreadIoEventLoop 的事件循环中。更引人注目的是,Netty 4.2 还引入了 IoUringBufferRing——基于内核 buf_ring 机制的固定缓冲区环形队列,配合 IOSQE_BUFFER_SELECT 标志,实现了真正意义上的内核级零拷贝读取。
-
IoUringIoHandler 如何用 eventfd 替代 JDK NIO 的
Selector.wakeup(),实现无 Selector 的 IO 唤醒机制? -
SubmissionQueue.enqueueSqe()如何将 IoUringIoOps 的 13 个字段精确映射到内核 io_uring_sqe 结构体的 64 字节布局,submitAndGet()又如何通过 io_uring_enter 系统调用批量提交 SQ 并等待 CQ 完成? -
CompletionQueue.process()如何通过 VarHandle 的 getVolatile/setRelease 实现无锁的 CQ 消费,IORING_CQE_F_MORE和IORING_CQE_F_SOCK_NONEMPTY标志位如何影响多轮读循环的调度? -
IoUringBufferRing 的 buf_ring 机制如何通过
io_uring_register(IORING_REGISTER_PBUF_RING)注册预分配缓冲区,配合IOSQE_BUFFER_SELECT让内核直接将数据写入预分配缓冲区,实现零拷贝读取? -
IoUringSocketChannel.IoUringSocketUnsafe 如何利用
IORING_OP_SEND_ZC和IORING_OP_SENDMSG_ZC实现零拷贝发送,IORING_CQE_F_NOTIF通知机制如何管理零拷贝缓冲区的生命周期?本文将沿着"内核机制 → 数据结构 → 事件循环 → Channel 实现 → BufferRing 零拷贝"的递进逻辑,从 io_uring 的 SQ/CQ 双环形缓冲区原理出发,逐一分析 RingBuffer、SubmissionQueue、CompletionQueue 的 Java 实现,再以
IoUringIoHandler.run()为核心展示 SQ 提交 → CQ 轮询 → 事件分发的完整流程,最后深入 IoUringBufferRing 的零拷贝读取和 IoUringSocketChannel 的零拷贝发送机制。
一、io_uring 内核原理速览:SQ/CQ 环形缓冲区与共享内存
在深入 Netty 的 io_uring 实现之前,有必要先理解 io_uring 本身的核心设计。所谓 io_uring,是 Linux 内核提供的一套全新的异步 IO 接口,其核心思想是:用一对通过 mmap 映射到用户态的共享内存环形缓冲区,替代传统的系统调用往返,实现用户态与内核态之间的零拷贝通信。
1.1 SQ/CQ 双环形缓冲区
io_uring 的核心数据结构是互为镜像的两个环形队列:
-
Submission Queue(SQ) :用户态向 SQ 尾部追加 SQE(Submission Queue Entry),描述一个 IO 操作(读、写、accept、connect 等),更新 SQ tail 指针后,通过 io_uring_enter 系统调用通知内核消费。内核从 SQ head 取出 SQE 执行异步 IO 操作。
-
Completion Queue(CQ) :内核完成 IO 操作后,将 CQE(Completion Queue Entry)写入 CQ 尾部,更新 CQ tail 指针。用户态从 CQ head 读取 CQE,获取操作结果(res 返回值)、标志位(flags)和用户数据(user_data),无需任何额外系统调用。
下面这张线性图展示了 SQ/CQ 双环形缓冲区的数据流向和生产者-消费者关系:
io\_uring\_setup 系统调用负责创建这对环形缓冲区:`entries` 指定 SQ 大小,flags 指定特性(`IORING_SETUP_SINGLE_ISSUER`、`IORING_SETUP_DEFER_TASKRUN`、`IORING_SETUP_NO_SQARRAY` 等),返回 ring\_fd 文件描述符。io\_uring\_enter 系统调用则是唯一的同步点:`to_submit` 表示提交的 SQE 数量,`min_complete` 表示最少等待的 CQE 数量,flags 控制行为(`IORING_ENTER_GETEVENTS` 等待完成事件)。
1.2 SQE 与 CQE 的结构体布局
SQE 结构体 io_uring_sqe 恰好 64 字节,字段按偏移排列:
| 字段 | 偏移 | 大小 | 说明 |
|---|---|---|---|
| opcode | 0 | 1 | 操作码(IORING_OP_RECV、IORING_OP_SEND 等) |
| flags | 1 | 1 | 标志位(IOSQE_BUFFER_SELECT、IOSQE_IO_DRAIN 等) |
| ioprio | 2 | 2 | IO 优先级 |
| fd | 4 | 4 | 文件描述符 |
| union1 | 8 | 8 | 联合体(off/addr2) |
| union2 | 16 | 8 | 联合体(addr/splice_off) |
| len | 24 | 4 | 数据长度 |
| union3 | 28 | 4 | 联合体(rw_flags/...) |
| user_data | 32 | 8 | 用户数据(关键:用于分发 CQE 到对应 Channel) |
| union4 | 40 | 2 | 联合体(buf_index/group) |
| personality | 42 | 2 | 凭证标识 |
| union5 | 44 | 4 | 联合体(splice_fd_in/...) |
| union6 | 48 | 8 | 联合体(addr3/optval/cmd) |
CQE 结构体 io_uring_cqe 标准为 16 字节:user_data(8 字节)、res(4 字节)、flags(4 字节)。启用 IORING_SETUP_CQE32 时扩展为 32 字节,增加 big_cqe[] 扩展数据。
1.3 io_uring 相比 epoll 的核心优势
与传统的 epoll + read/write 模型相比,io_uring 带来了几个根本性的变化。首先是批量提交:一次 io_uring_enter 可以提交多个 SQE,内核一次性消费,消除了多次系统调用的开销。其次是零拷贝:buf_ring 固定缓冲区配合 IOSQE_BUFFER_SELECT 让内核直接写入用户预分配的内存,SEND_ZC/SENDMSG_ZC 让内核 DMA 直接传输数据。第三是无锁队列:SQ 单生产者(每个 EventLoop 独立提交)、CQ 单消费者(每个 EventLoop 独立消费),无需锁同步。第四是减少系统调用:CQ 轮询不需要系统调用,只需读取共享内存中的 CQ head/tail 指针。
下面这张线性图直观对比了 epoll 模型与 io_uring 模型在系统调用次数和数据拷贝路径上的差异:
下面这张架构图展示了 io_uring 的共享内存模型:
二、IoUring 环境检测:内核特性探测与能力矩阵
IoUring 类是整个 io_uring 传输的"哨兵",它在 static { ... } 静态初始化块中完成内核版本检查、16 个必需操作码 + 多项特性标志探测和系统属性解析,最终生成一份完整的内核能力矩阵。如果内核不满足最低要求,UNAVAILABILITY_CAUSE 会被设置为对应的异常,后续任何 IoUringIoHandler 的构造都会因为 ensureAvailability() 检查而失败。
2.1 静态初始化的三阶段流程
静态初始化块的执行流程可以概括为三个阶段。
第一阶段:内核版本检查。 通过 Native.kernelVersion() 获取内核版本字符串,Native.checkKernelVersion() 验证最低版本要求(Linux 5.9+)。同时检查 PlatformDependent.javaVersion() >= 9,因为 Netty 的 io_uring 实现依赖 Java 9+ 的 VarHandle 内存操作。
第二阶段:创建临时 ring 进行特性探测。 调用 Native.createRingBuffer(1, 0) 创建一个临时 ring(ringSize=1, setupFlags=0),然后进行强制检查 IORING_FEAT_SUBMIT_STABLE——若内核不支持此特性(< 5.9),直接抛出 UnsupportedOperationException。随后,通过 Native.ioUringProbe() 获取 io_uring_probe 结构体,按 opcode 位掩码逐一检测 16 个必需操作码是否受支持。
第三阶段:系统属性解析与日志输出。 解析 io.netty.iouring.* 系统属性,设置默认配置参数,最后根据探测结果输出日志(可用时输出支持的特性列表,不可用时输出错误原因)。
2.2 特性探测的两类方式
Netty 采用两种方式探测内核特性。Setup flags 探测通过 Native.ioUringSetupSupportsFlags() 测试 io_uring_setup 是否支持特定的 flag 组合,用于探测 IORING_SETUP_SUBMIT_ALL、IORING_SETUP_CQE_MIXED、IORING_SETUP_CQSIZE、IORING_SETUP_SINGLE_ISSUER、IORING_SETUP_DEFER_TASKRUN、IORING_SETUP_NO_SQARRAY 等。
Probe 探测通过 Native.ioUringProbe() 获取 io_uring_probe 结构体,按 opcode 位掩码检查是否支持特定操作码,用于探测 CQE_F_SOCK_NONEMPTY(套接字是否仍有数据)、SPLICE(零拷贝文件传输)、SEND_ZC/SENDMSG_ZC(零拷贝发送)、ACCEPT_MULTISHOT(一次提交多次 ACCEPT)、RECV_MULTISHOT(一次提交多次 RECV)、RECVSEND_BUNDLE(批量收发)、POLL_ADD_MULTISHOT(一次提交多次 POLL)、REGISTER_IOWQ_MAX_WORKERS(IO 工作线程数控制)、REGISTER_BUFFER_RING/REGISTER_BUFFER_RING_INC(固定缓冲区环形队列)等。
下面展示 IoUring 静态初始化块的核心探测逻辑:
io.netty.channel.uring.IoUring#static
// 静态初始化块:探测内核版本、创建临时 ring、检测 16 个必需操作码 + 多项特性标志
static {
logger = InternalLoggerFactory.getInstance(IoUring.class);
Throwable cause = null;
// 16 个必需操作码 + 多项特性标志的局部变量声明
boolean socketNonEmptySupported = false;
boolean spliceSupported = false;
boolean sendZcSupported = false;
// ... 省略其他变量声明
try {
// 第一阶段:检查是否显式禁用原生传输
if (SystemPropertyUtil.getBoolean("io.netty.transport.noNative", false)) {
cause = new UnsupportedOperationException(
"Native transport was explicit disabled with -Dio.netty.transport.noNative=true");
} else {
kernelVersion = Native.kernelVersion();
// 检查内核版本是否满足最低要求(5.9+)
Native.checkKernelVersion(kernelVersion);
if (PlatformDependent.javaVersion() >= 9) {
RingBuffer ringBuffer = null;
try {
// 第二阶段:创建临时 ring 探测内核特性
ringBuffer = Native.createRingBuffer(1, 0);
// 强制检查:SUBMIT_STABLE 是 Netty io_uring 的硬性依赖
if ((ringBuffer.features() & Native.IORING_FEAT_SUBMIT_STABLE) == 0) {
throw new UnsupportedOperationException(
"IORING_FEAT_SUBMIT_STABLE not supported!");
}
// 通过 io_uring_probe 获取内核支持的操作码列表
Native.IoUringProbe ioUringProbe = Native.ioUringProbe(ringBuffer.fd());
Native.checkAllIOSupported(ioUringProbe);
// 逐一探测各操作码是否受支持
socketNonEmptySupported = Native.isCqeFSockNonEmptySupported(ioUringProbe);
spliceSupported = Native.isSpliceSupported(ioUringProbe);
sendZcSupported = Native.isSendZcSupported(ioUringProbe);
sendmsgZcSupported = Native.isSendmsgZcSupported(ioUringProbe);
// ... 省略其他探测
// 通过 ringBuffer.features() 位掩码探测内核特性
recvsendBundleSupported = (ringBuffer.features()
& Native.IORING_FEAT_RECVSEND_BUNDLE) != 0;
// Setup flags 探测
submitAllSupported = Native.ioUringSetupSupportsFlags(
Native.IORING_SETUP_SUBMIT_ALL);
singleIssuerSupported = Native.ioUringSetupSupportsFlags(
Native.IORING_SETUP_SINGLE_ISSUER);
// ... 省略其他 setup flags 探测
} finally {
if (ringBuffer != null) {
try { ringBuffer.close(); } catch (Exception ignore) { }
}
}
}
}
} catch (Throwable t) {
cause = t;
}
// 第三阶段:赋值静态 final 字段 + 系统属性解析
UNAVAILABILITY_CAUSE = cause;
IORING_CQE_F_SOCK_NONEMPTY_SUPPORTED = socketNonEmptySupported;
// ... 省略其他静态 final 赋值
}
2.3 系统属性开关与默认配置
Netty 为每个内核特性提供了对应的系统属性开关,允许用户在运行时按需禁用某些特性。以下是完整的属性列表及其默认值:
| 系统属性 | 默认值 | 说明 |
|---|---|---|
| io.netty.iouring.acceptMultiShotEnabled | true | 启用 multi-shot ACCEPT |
| io.netty.iouring.recvMultiShotEnabled | true | 启用 multi-shot RECV |
| io.netty.iouring.recvsendBundleEnabled | false | 启用 RECVSEND_BUNDLE(默认关闭,因已知内核 bug) |
| io.netty.iouring.pollAddMultishotEnabled | true | 启用 multi-shot POLL_ADD |
| io.netty.iouring.enterNoIoWaitEnabled | false | 启用 ENTER_NO_IOWAIT |
| io.netty.iouring.ringSize | 128 | SQ 环形队列大小 |
| io.netty.iouring.cqSize | 4096 | CQ 环形队列大小 |
默认配置参数:DEFAULT_RING_SIZE = 128(SQ 环形队列大小),DEFAULT_CQ_SIZE = 4096(CQ 环形队列大小,仅在 IORING_SETUP_CQ_SIZE_SUPPORTED 时生效),DEFAULT_PENDING_OPS_INITIAL_CAPACITY(pendingOps 初始容量,默认等于 ringSize)。
2.4 兼容性检查
isAvailable() 和 ensureAvailability() 是外部判断 io_uring 是否可用的关键入口。isAvailable() 返回 UNAVAILABILITY_CAUSE == null,ensureAvailability() 在不可用时抛出 UnsatisfiedLinkError:
io.netty.channel.uring.IoUring#isAvailable
// 判断 io_uring 原生库是否可用
public static boolean isAvailable() {
return UNAVAILABILITY_CAUSE == null;
}
io.netty.channel.uring.IoUring#ensureAvailability
// 确保原生库已加载,否则抛出 UnsatisfiedLinkError
public static void ensureAvailability() {
if (UNAVAILABILITY_CAUSE != null) {
throw (Error) new UnsatisfiedLinkError(
"failed to load the required native library").initCause(UNAVAILABILITY_CAUSE);
}
}
IoUringIoHandler 构造器的第一行就调用 IoUring.ensureAvailability(),确保只有在原生库加载成功的前提下才能创建 IoUringIoHandler 实例。这套"探测-验证-开关"三层机制,保证了 Netty 的 io_uring 传输在不同内核版本下都能优雅降级。
三、RingBuffer 核心数据结构:SubmissionQueue 与 CompletionQueue 的 JNI 实现
在前两节中,我们理解了 io_uring 的内核原理和 Netty 的环境检测机制。现在进入 Netty 的 Java 封装层,首先分析 RingBuffer——它是对 io_uring 实例的 Java 对象封装,聚合了 SubmissionQueue 和 CompletionQueue 两个核心数据结构。
3.1 RingBuffer 的创建与销毁
RingBuffer 的创建通过 Native.createRingBuffer(ringSize, cqSize, setupFlags) 完成,底层通过 JNI 调用 io_uring_setup 系统调用创建 io_uring 实例,并 mmap 映射 SQ 和 CQ 内存区域。RingBuffer 持有 features 位掩码(内核特性标志),enable() 方法在 IoUringIoHandler.initialize() 阶段调用 Native.ioUringRegisterEnableRings(fd()) 启用 ring 并注册 ring_fd。
销毁流程则相反:close() 设置 closed=true,关闭 SubmissionQueue 和 CompletionQueue,调用 Native.ioUringExit() 解除 mmap 映射并关闭 ring_fd。
3.2 SubmissionQueue 的 SQE 填充逻辑
SubmissionQueue 通过 ByteBuffer 直接操作内核共享内存区域。其核心字段包括:kHead/kTail(内核共享的 SQ 头尾指针,通过 VarHandle 原子操作读写)、submissionQueueArray(SQE 数组 ByteBuffer,每个 SQE 64 字节)、ringEntries/ringMask(环形队列大小和掩码)、head/tail(用户态缓存的内核头尾指针)。
下面这张时序图展示了 SubmissionQueue.enqueueSqe() 提交 SQE 和 CompletionQueue.process() 消费 CQE 的完整流程:
上面的时序图清晰地展示了三个层次:SQ 入队(enqueueSqe())、SQ 提交(submitAndGet())和 CQ 消费(process())。接下来按这个顺序逐一深入。
enqueueSqe() 的核心逻辑是:按 io_uring_sqe 结构体布局逐字段写入 submissionQueueArray。13 个字段各占特定偏移:opcode(偏移 0)、flags(偏移 1)、ioPrio(偏移 2)、fd(偏移 4)、union1(偏移 8)、union2(偏移 16)、len(偏移 24)、union3(偏移 28)、user_data(偏移 32)、union4(偏移 40)、personality(偏移 42)、union5(偏移 44)、union6(偏移 48)。总大小恰好 64 字节。
io.netty.channel.uring.SubmissionQueue#enqueueSqe
// 将 IoUringIoOps 的 13 个字段按 io_uring_sqe 布局写入共享内存
long enqueueSqe(byte opcode, byte flags, short ioPrio, int fd, long union1,
long union2, int len, int union3, long udata, short union4,
short personality, int union5, long union6) {
checkClosed();
// 环形队列满时,先提交已填充的 SQE 腾出空间
int pending = tail - head;
if (pending == ringEntries) {
int submitted = submit();
if (submitted == 0) {
throw new RuntimeException("SQ ring full and no submissions accepted");
}
}
// 计算当前 tail 在环形队列中的索引
int sqe = sqeIndex(tail++, ringMask);
// 按 io_uring_sqe 结构体偏移逐字段写入
submissionQueueArray.put(sqe + SQE_OP_CODE_FIELD, opcode);
submissionQueueArray.put(sqe + SQE_FLAGS_FIELD, flags);
submissionQueueArray.putShort(sqe + SQE_IOPRIO_FIELD, ioPrio);
submissionQueueArray.putInt(sqe + SQE_FD_FIELD, fd);
submissionQueueArray.putLong(sqe + SQE_UNION1_FIELD, union1);
submissionQueueArray.putLong(sqe + SQE_UNION2_FIELD, union2);
submissionQueueArray.putInt(sqe + SQE_LEN_FIELD, len);
submissionQueueArray.putInt(sqe + SQE_UNION3_FIELD, union3);
submissionQueueArray.putLong(sqe + SQE_USER_DATA_FIELD, udata);
submissionQueueArray.putShort(sqe + SQE_UNION4_FIELD, union4);
submissionQueueArray.putShort(sqe + SQE_PERSONALITY_FIELD, personality);
submissionQueueArray.putInt(sqe + SQE_UNION5_FIELD, union5);
submissionQueueArray.putLong(sqe + SQE_UNION6_FIELD, union6);
return udata;
}
3.3 SubmissionQueue 的提交三部曲
submit() 方法执行经典的三步操作:写 tail 通知内核(release)→ io_uring_enter 系统调用 → 读 head 获取内核消费进度(acquire) 。INT_HANDLE.setRelease(kTail, 0, tail) 使用 release 语义保证 tail 之前的 SQE 写入对内核可见,INT_HANDLE.getVolatile(kHead, 0) 使用 acquire 语义保证读取到内核最新的消费进度。
io.netty.channel.uring.SubmissionQueue#submit
// 提交三部曲:写 tail(release)→ io_uring_enter → 读 head(acquire)
private int submit(int toSubmit, int minComplete, int flags) {
// 第一步:release 语义写入 tail,通知内核有新 SQE 就绪
INT_HANDLE.setRelease(kTail, 0, tail);
// 第二步:io_uring_enter 系统调用,提交 SQE 并等待 CQE
int ret = ioUringEnter(toSubmit, minComplete, flags);
// 第三步:acquire 语义读取 head,获取内核的消费进度
head = (int) INT_HANDLE.getVolatile(kHead, 0);
if (ret != toSubmit) {
if (ret < 0) {
throw new UncheckedIOException(Errors.newIOException("io_uring_enter", ret));
}
}
return ret;
}
submitAndGet() 与 submitAndGetNow() 的关键区别在于 minComplete 参数:submitAndGet() 传入 minComplete=1 和 IORING_ENTER_GETEVENTS,阻塞等待至少一个完成事件;submitAndGetNow() 传入 minComplete=0,不做阻塞等待。此外,当 IORING_SETUP_SUBMIT_ALL 不受支持时,ioUringEnter() 需要循环调用 io_uring_enter 直到所有 SQE 都提交完毕,因为内核可能在内联执行失败时停止提交。
3.4 CompletionQueue 的无锁消费循环
CompletionQueue 的核心字段与 SubmissionQueue 对称:khead/ktail(内核共享的 CQ 头尾指针)、completionQueueArray(CQE 数组 ByteBuffer)、ringHead(用户态缓存的内核 CQ 头指针)、extraCqeData(CQE32 扩展数据)。
process() 方法的消费循环设计精妙。首先通过 INT_HANDLE.getVolatile(ktail, 0) 获取内核写入的 tail(acquire 语义),然后 while (ringHead != tail) 遍历所有就绪 CQE,读取 user_data、res、flags,调用 callback.handle() 分发事件。每处理一个 CQE 后,ringHead++。当 IORING_CQE_F_32 标志位存在时(CQE_MIXED 模式),ringHead 需要额外递增一次跳过扩展数据。当 IORING_CQE_F_SKIP 标志位存在时,跳过该 CQE 不处理。循环结束后,INT_HANDLE.setRelease(khead, 0, ringHead) 使用 release 语义更新 head,通知内核 CQE 已被消费。
io.netty.channel.uring.CompletionQueue#process
// 无锁消费 CQE:acquire 读 tail → 遍历 CQE → release 写 head
long process(CompletionCallback callback) {
if (closed) {
return 0;
}
// acquire 语义:读取内核写入的最新 tail
int tail = (int) INT_HANDLE.getVolatile(ktail, 0);
try {
int total = 0;
int realIo = 0;
// 遍历所有就绪的 CQE
while (ringHead != tail) {
int cqeIdx = cqeIdx(ringHead, ringMask);
int cqePosition = cqeIdx * cqeLength;
long udata = completionQueueArray.getLong(cqePosition + CQE_USER_DATA_FIELD);
int res = completionQueueArray.getInt(cqePosition + CQE_RES_FIELD);
int flags = completionQueueArray.getInt(cqePosition + CQE_FLAGS_FIELD);
ringHead++;
// 处理 CQE32 扩展数据(32 字节 CQE 模式)
final ByteBuffer extraCqeData;
if ((flags & Native.IORING_CQE_F_32) != 0) {
extraCqeData = extraCqeData(cqeIdx + 1);
ringHead++; // 跳过扩展数据
} else if (cqeLength == Native.CQE32_SIZE) {
extraCqeData = extraCqeData(cqeIdx + 1);
} else {
extraCqeData = null;
}
// 跳过标记的 CQE
if ((flags & Native.IORING_CQE_F_SKIP) == 0) {
total++;
// callback.handle() 返回 true 表示真实 IO 完成
if (callback.handle(res, flags, udata, extraCqeData)) {
realIo++;
}
}
// 再次获取 tail,因为处理过程中可能触发了新的提交
if (ringHead == tail) {
tail = (int) INT_HANDLE.getVolatile(ktail, 0);
}
}
// 高 32 位:总完成事件数;低 32 位:真实 IO 完成事件数
return ((long) total << 32) | (realIo & 0xFFFFFFFFL);
} finally {
// release 语义:确保内核仅在 CQE 读取完成后才看到新的 head
INT_HANDLE.setRelease(khead, 0, ringHead);
}
}
process() 返回值的打包设计也很巧妙:((long) total << 32) | (realIo & 0xFFFFFFFFL),高 32 位存储总完成事件数(含内部事件如 eventfd/timeout),低 32 位存储真实 IO 完成事件数(callback.handle() 返回 true 的计数)。调用方通过 total = (int)(packed >>> 32) 和 realIo = (int) packed 解包。
四、IoUringIoHandler 事件循环:从 SQ 提交到 CQ 分发
前三节从内核原理到 RingBuffer 数据结构,为理解 io_uring 的 Java 封装层打下了基础。现在进入核心引擎——IoUringIoHandler,它是 io_uring 传输的"调度中心",将 SQ 提交、CQ 轮询和事件分发串联为完整的事件循环。
4.1 IoUringIoHandler 的构造器
IoUringIoHandler 是 final class IoUringIoHandler implements IoHandler,其构造器按以下顺序初始化:
第一步:确保原生库可用。 构造器入口调用 IoUring.ensureAvailability(),若原生库加载失败则直接抛出异常,后续所有初始化都无需执行。
第二步:计算 setupFlags 并创建 RingBuffer。 通过 Native.setupFlags(config.singleIssuer()) 计算 setup 标志位。默认情况下,singleIssuer() 返回 true,因此 IORING_SETUP_SINGLE_ISSUER 和 IORING_SETUP_DEFER_TASKRUN 都会被设置(DEFER_TASKRUN 需要与 SINGLE_ISSUER 组合使用)。CQ 大小默认是 ringSize 的两倍,若用户显式配置了 cqSize 且内核支持 IORING_SETUP_CQSIZE,则通过 setupFlags |= Native.IORING_SETUP_CQSIZE 启用自定义 CQ 大小。随后调用 Native.createRingBuffer(ringSize, cqSize, setupFlags) 创建 RingBuffer。
第三步:可选注册 IOWQ_MAX_WORKERS。 若内核支持 IORING_REGISTER_IOWQ_MAX_WORKERS 且用户配置了 needRegisterIowqMaxWorker(),则通过 Native.ioUringRegisterIoWqMaxWorkers() 控制 IO 工作线程数量。
第四步:初始化 BufferRing。 遍历 config.getInternBufferRingConfigs() 中的每个 IoUringBufferRingConfig,通过 newBufferRing() 创建 IoUringBufferRing 并存入 registeredIoUringBufferRing 映射表。newBufferRing() 方法内部调用 Native.ioUringRegisterBufRing() 注册 buf_ring,然后将返回的内存地址包装为 IoUringBufferRing 对象。
第五步:初始化 eventfd 唤醒机制。 通过 Native.newBlockingEventFd() 创建阻塞式 eventfd 文件描述符,分配 8 字节的直接缓冲区 eventfdReadBuf(用于存储 eventfd 读取值),以及 16 字节的 timeoutMemory(__kernel_timespec 结构体,用于定时器)。
第六步:创建辅助数据结构。 初始化 registrations(IntObjectHashMap,存储注册的 Channel)、pendingOps(PendingOpMap,慢路径完成映射)、iovArray(IovArray,用于批量写入的 iovec 数组)、msgHdrMemoryArray(MsgHdrMemoryArray,用于 sendmsg/recvmsg 的 msghdr 结构体)。
io.netty.channel.uring.IoUringIoHandler#IoUringIoHandler
// 构造器:按顺序初始化 RingBuffer、BufferRing、eventfd、辅助数据结构
IoUringIoHandler(ThreadAwareExecutor executor, IoUringIoHandlerConfig config) {
IoUring.ensureAvailability();
this.executor = requireNonNull(executor, "executor");
requireNonNull(config, "config");
int setupFlags = Native.setupFlags(config.singleIssuer());
int cqSize = 2 * config.getRingSize();
if (config.needSetupCqeSize()) {
assert IoUring.isSetupCqeSizeSupported();
setupFlags |= Native.IORING_SETUP_CQSIZE;
cqSize = config.getCqSize();
}
this.ringBuffer = Native.createRingBuffer(config.getRingSize(), cqSize, setupFlags);
// 可选:注册 IO 工作线程数量限制
if (IoUring.isRegisterIowqMaxWorkersSupported() && config.needRegisterIowqMaxWorker()) {
int maxBoundedWorker = Math.max(config.getMaxBoundedWorker(), 0);
int maxUnboundedWorker = Math.max(config.getMaxUnboundedWorker(), 0);
int result = Native.ioUringRegisterIoWqMaxWorkers(
ringBuffer.fd(), maxBoundedWorker, maxUnboundedWorker);
// ...
}
// 初始化 BufferRing
registeredIoUringBufferRing = new IntObjectHashMap<>();
Collection<IoUringBufferRingConfig> bufferRingConfigs = config.getInternBufferRingConfigs();
if (bufferRingConfigs != null && !bufferRingConfigs.isEmpty()) {
for (IoUringBufferRingConfig bufferRingConfig : bufferRingConfigs) {
IoUringBufferRing ring = newBufferRing(ringBuffer.fd(), bufferRingConfig);
registeredIoUringBufferRing.put(bufferRingConfig.bufferGroupId(), ring);
}
}
// 初始化 eventfd 唤醒机制
registrations = new IntObjectHashMap<>();
pendingOps = new PendingOpMap(IoUring.DEFAULT_PENDING_OPS_INITIAL_CAPACITY);
eventfd = Native.newBlockingEventFd();
eventfdReadBufCleanable = Buffer.allocateDirectBufferWithNativeOrder(Long.BYTES);
eventfdReadBuf = eventfdReadBufCleanable.buffer();
eventfdReadBufAddress = Buffer.memoryAddress(eventfdReadBuf);
// ...
}
4.2 initialize() 与 BufferRing 的初始填充
initialize() 方法在 IoUringIoHandler 首次被 SingleThreadIoEventLoop 调度时调用。它首先调用 ringBuffer.enable() 启用 ring(底层调用 Native.ioUringRegisterEnableRings(fd()) 和 tryRegisterRingFd()),然后遍历所有 registeredIoUringBufferRing 调用 bufferRing.initialize() 填充首批缓冲区。
io.netty.channel.uring.IoUringIoHandler#initialize
@Override
public void initialize() {
ringBuffer.enable();
for (IoUringBufferRing bufferRing : registeredIoUringBufferRing.values()) {
bufferRing.initialize();
}
}
4.3 run() 事件循环的完整流程
run() 是 IoUringIoHandler 的核心方法,由 SingleThreadIoEventLoop 每轮事件循环调用。其完整流程如下:
-
检查
closeCompleted,若已完成则直接返回 0 -
获取 SubmissionQueue 和 CompletionQueue
-
判断是否有就绪 CQE 或能否阻塞,分两条路径:
- 阻塞路径(无就绪 CQE 且
canBlock()为 true):先通过submitEventFdRead()提交 eventfd 读请求,再调用submitAndWaitWithTimeout()阻塞等待完成事件 - 非阻塞路径(有就绪 CQE 或有待处理任务):调用
submitAndClearNow()非阻塞提交
- 阻塞路径(无就绪 CQE 且
-
调用
processCompletionsAndHandleOverflow()消费 CQE 并处理溢出
下面这张时序图展示了完整的 run() 事件循环:
阻塞路径(无就绪 CQE 且 canBlock): 当 CompletionQueue 中没有就绪的 CQE,且 context.canBlock() 返回 true 时,IoUringIoHandler 进入阻塞等待模式。首先通过 submitEventFdRead() 提交一个 IORING_OP_READ 操作读取 eventfd 的值(当其他线程调用 wakeup() 时,eventfd 会变为可读),然后调用 submitAndWaitWithTimeout() 提交定时器并阻塞等待。submitAndWaitWithTimeout() 内部:若 timeoutNanoSeconds 不为 -1,将秒和纳秒写入预分配的 timeoutMemory(__kernel_timespec 结构体),然后调用 submissionQueue.addTimeout() 提交 IORING_OP_TIMEOUT SQE(标记为 RINGFD_TOKEN),最后调用 submissionQueue.submitAndGet() 提交并等待完成事件。
非阻塞路径(有就绪 CQE 或无法阻塞): 当已有 CQE 就绪,或 context.canBlock() 返回 false(有待处理任务)时,IoUringIoHandler 走非阻塞路径。它调用 submitAndClearNow()(内部调用 submissionQueue.submitAndGetNow())进行非阻塞提交,然后立即进入 CQE 消费循环。
processCompletionsAndHandleOverflow() 批量消费: processCompletionsAndHandleOverflow() 通过一个最高 128 次的循环,每次调用 completionQueue.process(callback) 消费一批 CQE。handle() 方法作为回调,按 udata 分发到不同路径(详见 4.4 节)。循环中检查 IORING_SQ_CQ_OVERFLOW 标志,若 CQ 溢出则打印警告日志。当 total == 0(本轮无 CQE 消费)且 needSubmit() 返回 false(无新的 SQE 需要提交)时退出循环。否则调用 submitAndClearNow0() 清空 iovArray 和 msgHdrMemoryArray 并提交 SQE。
io.netty.channel.uring.IoUringIoHandler#run
@Override
public int run(IoHandlerContext context) {
if (closeCompleted) {
if (context.shouldReportActiveIoTime()) {
context.reportActiveIoTime(0);
}
return 0;
}
SubmissionQueue submissionQueue = ringBuffer.ioUringSubmissionQueue();
CompletionQueue completionQueue = ringBuffer.ioUringCompletionQueue();
if (!completionQueue.hasCompletions() && context.canBlock()) {
if (eventfdReadSubmitted == 0) {
submitEventFdRead();
}
long timeoutNanos = context.deadlineNanos() == -1 ? -1
: context.delayNanos(System.nanoTime());
submitAndWaitWithTimeout(submissionQueue, false, timeoutNanos);
} else {
submitAndClearNow(submissionQueue);
}
int ioCompletions;
if (context.shouldReportActiveIoTime()) {
long activeIoStartTimeNanos = System.nanoTime();
ioCompletions = processCompletionsAndHandleOverflow(
submissionQueue, completionQueue, this::handle);
long activeIoEndTimeNanos = System.nanoTime();
context.reportActiveIoTime(activeIoEndTimeNanos - activeIoStartTimeNanos);
} else {
ioCompletions = processCompletionsAndHandleOverflow(
submissionQueue, completionQueue, this::handle);
}
return ioCompletions;
}
4.4 handle() 的 fast/slow path 分发
handle() 是 CQE 分发的核心回调,它按 udata(user_data token)的值决定分发路径:
-
EVENTFD_TOKEN:eventfd 读完成,调用handleEventFdRead()重置eventfdAsyncNotify并重新提交 eventfd 读请求,返回 false(非真实 IO 完成)。 -
RINGFD_TOKEN:ring 内部事件(timeout/NOP 完成),直接忽略,返回 false。 -
udata >= 0(fast path) :user_data 完整编码了 registrationId、opcode、data,通过UserData.decodeId(udata)解码 ID,从 registrations 中查找 DefaultIoUringIoRegistration 并调用handle(),返回 true。 -
udata < 0(slow path) :user_data 是 PendingOpMap token,通过pendingOps.findSlot(udata)查找对应的 registrationId 和 opcode。若IORING_CQE_F_MORE未设置,调用pendingOps.release(slot)释放 slot。返回 true。fast path 与 slow path 的分流条件由
canUseFastPath()决定:当IoUringIoOps.userData()的值能放入short(16 位)时走 fast path,此时UserData.encode(id, opcode, (short) userData)将三个信息打包到 64 位 user_data 中。超出 16 位范围时走 slow path,通过PendingOpMap.nextToken()分配 token,额外存储 registrationId、opcode 和 userData。
io.netty.channel.uring.IoUringIoHandler#handle
private boolean handle(int res, int flags, long udata, ByteBuffer extraCqeData) {
try {
if (udata == EVENTFD_TOKEN) {
handleEventFdRead();
return false;
}
if (udata == RINGFD_TOKEN) {
return false;
}
if (udata >= 0) {
handleFastPath(res, flags, udata, extraCqeData);
return true;
}
handleSlowPath(res, flags, udata, extraCqeData);
return true;
} catch (Error e) {
throw e;
} catch (Throwable throwable) {
handleLoopException(throwable);
return true;
}
}
4.5 wakeup() 的 eventfd 机制
IoUringIoHandler 不使用 JDK 的 Selector.wakeup(),而是通过 Linux 的 eventfd 实现无 Selector 的 IO 唤醒。eventfd 是一个由内核维护的计数器文件描述符,写入操作递增计数器,读取操作获取并清零计数器。当计数器非零时,对 eventfd 的 read 操作不会阻塞(或 poll 返回可读事件)。
wakeup() 方法由非 EventLoop 线程调用,流程如下:
-
CAS 设置通知标志:
eventfdAsyncNotify.getAndSet(true)确保只有一个线程负责写入 eventfd。 -
获取写锁:通过
wakeupWritersCAS 自旋,确保在closeWakeupGate()关闭 eventfd 时不会并发写入。若WAKEUP_CLOSED位已设置,则直接返回(无需唤醒已关闭的 loop)。 -
写入 eventfd:
Native.eventFdWrite(eventfd.intValue(), 1L)写入 1,使 eventfd 变为可读。 -
释放写锁:
wakeupWriters.decrementAndGet()释放写锁。当 eventfd 变为可读后,IoUringIoHandler 在
run()中提交的IORING_OP_READ(由submitEventFdRead()提交,udata = EVENTFD_TOKEN)会完成,触发handleEventFdRead():重置eventfdAsyncNotify为 false,并重新提交submitEventFdRead()为下一次唤醒做准备。
io.netty.channel.uring.IoUringIoHandler#wakeup
@Override
public void wakeup() {
if (!executor.isExecutorThread(Thread.currentThread()) &&
!eventfdAsyncNotify.getAndSet(true)) {
int s;
do {
s = wakeupWriters.get();
if ((s & WAKEUP_CLOSED) != 0) {
return;
}
} while (!wakeupWriters.compareAndSet(s, s + 1));
try {
Native.eventFdWrite(eventfd.intValue(), 1L);
} finally {
wakeupWriters.decrementAndGet();
}
}
}
4.6 prepareToDestroy() 与 destroy()
prepareToDestroy() 和 destroy() 构成两阶段销毁流程。
prepareToDestroy() 阶段: 设置 shuttingDown = true,复制 registrations 列表,逐个调用 registration.close()(触发 handle.close(),最终调用 AbstractIoUringChannel.doClose() 提交 IORING_OP_CLOSE)。然后通过 eventFdWrite() 写入 eventfd 确保读请求完成,再提交 addNop(IOSQE_IO_DRAIN) 作为 drain 屏障(等待所有已提交操作完成),最后 submitAndGet() 提交并等待,循环消费剩余 CQE 直到队列为空。
// IoUringIoHandler.java
@Override
public void prepareToDestroy() {
shuttingDown = true;
CompletionQueue completionQueue = ringBuffer.ioUringCompletionQueue();
SubmissionQueue submissionQueue = ringBuffer.ioUringSubmissionQueue();
List<DefaultIoUringIoRegistration> copy = new ArrayList<>(registrations.values());
for (DefaultIoUringIoRegistration registration : copy) {
registration.close();
}
// 写入 eventfd 确保已提交的读请求能看到完成事件
Native.eventFdWrite(eventfd.intValue(), 1L);
// drain 屏障:等待所有已提交操作完成后再销毁
submissionQueue.addNop((byte) Native.IOSQE_IO_DRAIN, RINGFD_TOKEN);
submissionQueue.submitAndGet();
while (completionQueue.hasCompletions()) {
processCompletionsAndHandleOverflow(submissionQueue, completionQueue, this::handle);
if (submissionQueue.count() > 0) {
submissionQueue.submitAndGetNow();
}
}
}
destroy() 阶段: 首先调用 drainEventFd() 排空 eventfd 待处理事件。drainEventFd() 设置 eventFdClosing = true,检查是否有未处理的 eventfd 事件,若存在则循环消费直到 EVENTFD_TOKEN 被处理。然后取消任何未完成的 eventfd 读请求。接着,destroy() 提交 IOSQE_IO_DRAIN | IOSQE_LINK 链式 NOP 加 200ms 超时,消费最后一批 CQE。最后关闭所有 IoUringBufferRing,调用 completeRingClose() 关闭 ring、eventfd 和清理资源。
// IoUringIoHandler.java
@Override
public void destroy() {
SubmissionQueue submissionQueue = ringBuffer.ioUringSubmissionQueue();
CompletionQueue completionQueue = ringBuffer.ioUringCompletionQueue();
drainEventFd();
if (submissionQueue.remaining() < 2) {
submissionQueue.submit();
}
// 链式 drain + 200ms 超时,消费最后一批 CQE
submissionQueue.addNop((byte) (Native.IOSQE_IO_DRAIN | Native.IOSQE_LINK), RINGFD_TOKEN);
submitAndWaitWithTimeout(submissionQueue, true, TimeUnit.MILLISECONDS.toNanos(200));
completionQueue.process(this::handle);
for (IoUringBufferRing ioUringBufferRing : registeredIoUringBufferRing.values()) {
ioUringBufferRing.close();
}
completeRingClose();
}
private void completeRingClose() {
if (closeCompleted) {
return;
}
closeCompleted = true;
ringBuffer.close();
closeWakeupGate();
try {
eventfd.close();
} catch (IOException e) {
logger.warn("Failed to close eventfd", e);
}
eventfdReadBufCleanable.clean();
timeoutMemoryCleanable.clean();
iovArray.release();
// ...
}
4.7 newFactory() 工厂方法
newFactory() 提供三个工厂方法:newFactory()(默认配置)、newFactory(int ringSize)(指定 ringSize)、newFactory(IoUringIoHandlerConfig config)(完全自定义配置)。isChangingThreadSupported() 返回 !singleIssuer(),即 SINGLE_ISSUER 模式下不支持线程切换。
io.netty.channel.uring.IoUringIoHandler#newFactory
public static IoHandlerFactory newFactory() {
return newFactory(new IoUringIoHandlerConfig());
}
public static IoHandlerFactory newFactory(int ringSize) {
IoUringIoHandlerConfig configuration = new IoUringIoHandlerConfig();
configuration.setRingSize(ringSize);
return eventLoop -> new IoUringIoHandler(eventLoop, configuration);
}
public static IoHandlerFactory newFactory(IoUringIoHandlerConfig config) {
IoUring.ensureAvailability();
final IoUringIoHandlerConfig copy = ObjectUtil.checkNotNull(config, "config").verifyAndClone();
return new IoHandlerFactory() {
@Override
public IoHandler newHandler(ThreadAwareExecutor eventLoop) {
return new IoUringIoHandler(eventLoop, copy);
}
@Override
public boolean isChangingThreadSupported() {
return !copy.singleIssuer();
}
};
}
五、IoUringIoHandle、IoUringIoOps 与 IoUringIoEvent:io_uring 的 IO 操作抽象
在 IoUringIoHandler 的事件循环中,CQE 通过 user_data 分发到 DefaultIoUringIoRegistration,再由 IoUringIoHandle 处理后产生 IoUringIoEvent。这三个接口——IoUringIoHandle、IoUringIoOps、IoUringIoEvent——构成了 io_uring 传输的 IO 操作抽象层。
5.1 IoUringIoHandle 标记接口
IoUringIoHandle 是一个继承自 IoHandle 的空接口,本身不定义任何方法,仅用于标记 io_uring 传输的 IoHandle 类型。AbstractUringUnsafe 实现了此接口,使得所有 io_uring Channel 的 Unsafe 都能被 IoUringIoHandler 识别和接受。
// transport-classes-io_uring/src/main/java/io/netty/channel/uring/IoUringIoHandle.java
// io_uring 传输的 IoHandle 标记接口,本身不定义任何方法
public interface IoUringIoHandle extends IoHandle {
}
AbstractUringUnsafe 通过 implements 关键字标记自己为 IoUringIoHandle:
// transport-classes-io_uring/src/main/java/io/netty/channel/uring/AbstractIoUringChannel.java
// AbstractUringUnsafe 实现 IoUringIoHandle,标记为 io_uring 传输的 Unsafe
protected abstract class AbstractUringUnsafe extends AbstractUnsafe implements IoUringIoHandle {
// ...
}
IoUringIoHandler 在 isCompatible() 中通过 IoUringIoHandle.class.isAssignableFrom() 检查传入的 handle 类型是否兼容,只有实现了 IoUringIoHandle 的 Unsafe 才能被接受。
5.2 IoUringIoOps 的 15 个工厂方法
IoUringIoOps 直接映射 io_uring_sqe 结构体的 13 个字段,提供 15 个静态工厂方法,每个方法对应一种 io_uring 操作码。需要注意的是,newConnect() 和 newRecv() 各有两个重载版本,因此共有 15 个工厂方法:
| 工厂方法 | 操作码 | 用途 |
|---|---|---|
| newAccept() | IORING_OP_ACCEPT | 接受新连接 |
| newConnect() (2 重载) | IORING_OP_CONNECT | 发起连接 |
| newRecv() (2 重载) | IORING_OP_RECV | 接收数据 |
| newSend() | IORING_OP_SEND | 发送数据 |
| newWritev() | IORING_OP_WRITEV | 批量发送(iovec 数组) |
| newWrite() | IORING_OP_WRITE | 写入数据 |
| newSendmsg() | IORING_OP_SENDMSG | 发送消息(msghdr) |
| newRecvmsg() | IORING_OP_RECVMSG | 接收消息(msghdr) |
| newSendZc() | IORING_OP_SEND_ZC | 零拷贝发送 |
| newSendmsgZc() | IORING_OP_SENDMSG_ZC | 零拷贝批量发送 |
| newPollAdd() | IORING_OP_POLL_ADD | 注册 poll 事件 |
| newAsyncCancel() | IORING_OP_ASYNC_CANCEL | 取消异步操作 |
| newClose() | IORING_OP_CLOSE | 关闭文件描述符 |
| newShutdown() | IORING_OP_SHUTDOWN | 关闭套接字 |
| newSplice() | IORING_OP_SPLICE | 零拷贝文件传输 |
每个工厂方法按 io_uring_sqe 的语义填充字段。以 newSendZc() 为例:
io.netty.channel.uring.IoUringIoOps#newSendZc
static IoUringIoOps newSendZc(
int fd, long memoryAddress, int length,
int flags, short data, int zcFlags) {
// zcFlags 作为 ioPrio 传入,控制零拷贝行为
return new IoUringIoOps(Native.IORING_OP_SEND_ZC, (byte) 0, (byte) zcFlags, fd,
0, memoryAddress, length, flags, data, (short) 0, (short) 0, 0, 0);
}
5.3 IoUringIoEvent 的设计
IoUringIoEvent 是 IoEvent 的实现,包含 res(操作结果)、flags(CQE 标志位)、opcode(触发此事件的操作码)、userData(提交时传入的用户数据)、extraCqeData(CQE32 扩展数据)。关键的 update() 方法用于内部复用,减少对象创建:
io.netty.channel.uring.IoUringIoEvent#update
// 内部复用,避免每次事件都创建新对象
void update(int res, int flags, byte opcode, long userData, ByteBuffer extraCqeData) {
this.res = res;
this.flags = flags;
this.opcode = opcode;
this.userData = userData;
this.extraCqeData = extraCqeData;
}
在 DefaultIoUringIoRegistration 中,IoUringIoEvent 作为成员变量 event 在构造时创建一次,后续每次 handle() 调用都通过 event.update() 更新字段,然后传递给 handle.handle(this, event)。
5.4 DefaultIoUringIoRegistration 的 submit/cancel/handle
DefaultIoUringIoRegistration 是 IoUringIoHandler 的内部类,实现了 IoRegistration 接口,管理 Channel 的注册生命周期。
submit() 的线程安全: submit() 首先检查 executor.isExecutorThread(Thread.currentThread()),若当前线程不是 EventLoop 线程,则通过 executor.execute() 异步提交,保证 SQ 的单生产者语义。然后根据 canUseFastPath() 判断走 fast path 还是 slow path:
- fast path:userData 可放入 short 范围时,将
id、opcode、(short) userData打包为packedSeq,调用submitFastPath0()直接入队。 - slow path:userData 超出 short 范围时,通过
pendingOps.nextToken()分配 token,调用submitSlowPath0()额外注册映射。
io.netty.channel.uring.DefaultIoUringIoRegistration#submit
@Override
public long submit(IoOps ops) {
IoUringIoOps ioOps = (IoUringIoOps) ops;
if (!isValid()) {
return INVALID_ID;
}
long userData = ioOps.userData();
if (canUseFastPath(userData)) {
long packedSeq = UserData.encode(id, ioOps.opcode(), (short) userData);
if (executor.isExecutorThread(Thread.currentThread())) {
submitFastPath0(ioOps, packedSeq);
} else {
executor.execute(() -> submitFastPath0(ioOps, packedSeq));
}
return packedSeq;
}
long token = pendingOps.nextToken();
if (executor.isExecutorThread(Thread.currentThread())) {
submitSlowPath0(ioOps, token, userData);
} else {
executor.execute(() -> submitSlowPath0(ioOps, token, userData));
}
return token;
}
cancel() 的幂等设计: canceled.compareAndSet(false, true) CAS 保证只取消一次。若 outstandingCompletions > 0(仍有未完成的 completion),设置 removeLater = true 延迟移除,等待所有未完成 completion 处理完毕后再从 registrations 中移除。
handle() 的事件处理: event.update(res, flags, op, userData, extraCqeData) 复用 IoUringIoEvent 对象,然后 handle.handle(this, event) 调用 AbstractUringUnsafe.handle()。若 IORING_CQE_F_MORE 未设置,outstandingCompletions--。当 outstandingCompletions == 0 && removeLater 时,执行 remove()(从 registrations 移除并调用 handle.unregistered())。
5.5 IoUringIoOps 与 io_uring_sqe 的字段映射
IoUringIoOps 的 13 个字段与 io_uring_sqe 结构体一一对应:
| IoUringIoOps 字段 | io_uring_sqe 字段 | 大小(字节) |
|---|---|---|
| opcode | opcode | 1 |
| flags | flags | 1 |
| ioPrio | ioprio | 2 |
| fd | fd | 4 |
| union1 | union1(off/addr2) | 8 |
| union2 | union2(addr/splice_off) | 8 |
| len | len | 4 |
| union3 | union3(rw_flags/...) | 4 |
| data | user_data | 8 |
| union4 | union4(buf_index/group) | 2 |
| personality | personality | 2 |
| union5 | union5(splice_fd_in/...) | 4 |
| union6 | union6(addr3/optval/cmd) | 8 |
| 总计:13 字段 | 64 |
六、Channel 实现:AbstractIoUringChannel 的 IO 状态机
AbstractIoUringChannel 是 io_uring 传输中所有 Channel 的基类,它通过 ioState 位掩码管理 IO 调度状态,并借助内部类 AbstractUringUnsafe 实现按 opcode 的七路事件分发。
6.1 ioState 位掩码
ioState 的类型是 byte(不是 int),这是源码中的一个关键设计细节。byte 的 8 位足够容纳 6 个标志位(每个标志位占用 1 位),剩余 2 位作为保留:
io.netty.channel.uring.AbstractIoUringChannel#ioState
// A byte is enough for now.
private byte ioState;
private static final int POLL_IN_SCHEDULED = 1;
private static final int POLL_OUT_SCHEDULED = 1 << 2;
private static final int POLL_RDHUP_SCHEDULED = 1 << 3;
private static final int WRITE_SCHEDULED = 1 << 4;
private static final int READ_SCHEDULED = 1 << 5;
private static final int CONNECT_SCHEDULED = 1 << 6;
注意 POLL_IN_SCHEDULED 使用位 0(值为 1),POLL_OUT_SCHEDULED 使用位 2(值为 4),位 1 未被使用——这是刻意留下的间隙。每个标志位表示对应类型的操作是否已提交到 SQ 但尚未完成:POLL_IN_SCHEDULED 表示已提交 IORING_OP_POLL_ADD 等待 POLLIN 事件;READ_SCHEDULED 表示已提交 IORING_OP_RECV 等待数据到达;WRITE_SCHEDULED 表示已提交 IORING_OP_SEND/SEND_ZC 等待写完成;CONNECT_SCHEDULED 表示已提交 IORING_OP_CONNECT 等待连接建立。
6.2 handle() 的七路事件分发
AbstractUringUnsafe.handle() 是 IO 事件分发的中枢,按 opcode 分派到七个分支:
RECV/ACCEPT/RECVMSG/READ→readComplete():读操作完成WRITEV/SEND/SENDMSG/WRITE/SPLICE/SEND_ZC/SENDMSG_ZC→writeComplete():写操作完成POLL_ADD→pollAddComplete():poll 事件触发ASYNC_CANCEL→cancelComplete0():取消操作完成CONNECT→connectComplete():连接操作完成CLOSE→ close 处理:关闭操作完成default→ 忽略未知 opcode
下面这张时序图展示了完整的七路分发:
io.netty.channel.uring.AbstractUringUnsafe#handle
@Override
public final void handle(IoRegistration registration, IoEvent ioEvent) {
IoUringIoEvent event = (IoUringIoEvent) ioEvent;
byte op = event.opcode();
int res = event.res();
int flags = event.flags();
short data = (short) event.userData();
switch (op) {
case Native.IORING_OP_RECV:
case Native.IORING_OP_ACCEPT:
case Native.IORING_OP_RECVMSG:
case Native.IORING_OP_READ:
readComplete(op, res, flags, data);
break;
case Native.IORING_OP_WRITEV:
case Native.IORING_OP_SEND:
case Native.IORING_OP_SENDMSG:
case Native.IORING_OP_WRITE:
case Native.IORING_OP_SPLICE:
case Native.IORING_OP_SEND_ZC:
case Native.IORING_OP_SENDMSG_ZC:
writeComplete(op, res, flags, data);
break;
case Native.IORING_OP_POLL_ADD:
pollAddComplete(res, flags, data);
break;
case Native.IORING_OP_ASYNC_CANCEL:
cancelComplete0(op, res, flags, data);
break;
case Native.IORING_OP_CONNECT:
connectComplete(op, res, flags, data);
break;
case Native.IORING_OP_CLOSE:
if (res != Native.ERRNO_ECANCELED_NEGATIVE) {
if (delayedClose != null) {
delayedClose.promise.setSuccess();
}
closed = true;
}
break;
default:
break;
}
handleDelayedClosed();
if (ioState == 0 && (closed || !isRegistered())) {
registration.cancel();
}
}
6.3 pollAddComplete() 三路分发
pollAddComplete() 按 res 中的 POLLOUT、POLLIN、POLLRDHUP 位进行三路分发。三个分支的处理逻辑如下:
POLLOUT→pollOut():若connectPromise != null(正在连接中),调用socket.finishConnect()完成连接,然后fulfillConnectPromise()和pipeline().fireChannelActive()。否则检查!socket.isOutputShutdown(),调用super.flush0()继续刷新写缓冲。POLLIN→pollIn():若 readPending 为 true,调用scheduleFirstReadIfNeeded()发起读操作。否则设置socketHasMoreData = true,等待用户调用read()时直接触发读。POLLRDHUP→pollRdHup():标记对端关闭连接,设置recvBufAllocHandle().rdHupReceived(),若 Channel 仍 active 则调用scheduleFirstReadIfNeeded()继续读取剩余数据,否则调用shutdownInput(false)关闭输入。
// AbstractIoUringChannel.java
private void pollAddComplete(int res, int flags, short data) {
if ((res & Native.POLLOUT) != 0) {
pollOut(res);
}
if ((res & Native.POLLIN) != 0) {
pollIn(res, flags, data);
}
if ((res & Native.POLLRDHUP) != 0) {
pollRdHup(res);
}
}
6.4 readComplete() 读循环状态机
readComplete() 是读循环的核心,管理 READ_SCHEDULED 位和 readPending 标志的状态转换。核心流程拆解:
- 多轮读(multi-shot)检测:
numOutstandingReads == -1表示使用了 multi-shot 模式。 - rearm 判断:
(flags & IORING_CQE_F_MORE) == 0时,需要重新武装(rearm),清除READ_SCHEDULED位。 - 非 multi-shot 递减:
--numOutstandingReads == 0时清除 readPending 和READ_SCHEDULED。 - socketIsEmpty 检测:通过
socketIsEmpty(flags)判断 socket 是否可保证为空,利用IORING_CQE_F_SOCK_NONEMPTY标志位。 - 调用 readComplete0() :具体读处理由子类实现。
- finally 块中的读循环决策:根据
recvBufAllocHandle().isReadComplete()和 multi-shot 模式决定是否继续读循环。
// AbstractIoUringChannel.java
private void readComplete(byte op, int res, int flags, short data) {
boolean multishot = numOutstandingReads == -1;
boolean rearm = (flags & Native.IORING_CQE_F_MORE) == 0;
if (rearm) {
ioState &= ~READ_SCHEDULED;
}
boolean pending = readPending;
if (multishot) {
readPending = false;
} else if (--numOutstandingReads == 0) {
readPending = false;
ioState &= ~READ_SCHEDULED;
}
inReadComplete = true;
try {
socketIsEmpty = socketIsEmpty(flags);
socketHasMoreData = IoUring.isCqeFSockNonEmptySupported() &&
(flags & Native.IORING_CQE_F_SOCK_NONEMPTY) != 0;
readComplete0(op, res, flags, data, numOutstandingReads);
} finally {
try {
if (recvBufAllocHandle().isReadComplete()) {
recvBufAllocHandle().reset(config());
if (!multishot) {
if (readPending) { doBeginReadNow(); }
} else {
if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
if (pending) { readPending = true; doBeginReadNow(); }
} else if (rearm) {
doBeginReadNow();
} else if (!readPending) {
cancelOutstandingReads(registration, numOutstandingReads);
}
}
} else if (res == Native.ERRNO_ECANCELED_NEGATIVE) {
if (pending) { readPending = true; doBeginReadNow(); }
} else if (multishot && rearm) {
doBeginReadNow();
}
} finally {
inReadComplete = false;
socketIsEmpty = false;
}
}
}
6.5 writeComplete() 写循环
writeComplete() 管理写完成后的状态清理和后续写调度。核心流程拆解:
- TFO 场景检测:若
CONNECT_SCHEDULED位已设置,说明writeComplete()是由 TFO 的sendmsg完成触发,特殊处理连接完成逻辑。 - 非通知递减:
(flags & IORING_CQE_F_NOTIF) == 0时,--numOutstandingWrites。 - 调用 writeComplete0() :具体写处理由子类实现。
- 写不完时注册 POLLOUT:
!writtenAll && (ioState & POLL_OUT_SCHEDULED) == 0时调用schedulePollOut()。 - 写完后继续调度:
numOutstandingWrites == 0时清除WRITE_SCHEDULED位,若writtenAll则尝试scheduleWriteIfNeeded()继续写。
// AbstractIoUringChannel.java
private void writeComplete(byte op, int res, int flags, short data) {
if ((ioState & CONNECT_SCHEDULED) != 0) {
// TFO 场景:writeComplete 由 sendmsg 完成 triggered
freeMsgHdrArray();
if (res > 0) {
outboundBuffer().removeBytes(res);
connectComplete(op, 0, flags, data);
} else if (res == ERRNO_EINPROGRESS_NEGATIVE || res == 0) {
submitConnect((InetSocketAddress) requestedRemoteAddress);
} else {
connectComplete(op, res, flags, data);
}
return;
}
if ((flags & Native.IORING_CQE_F_NOTIF) == 0) {
assert numOutstandingWrites > 0;
--numOutstandingWrites;
}
boolean writtenAll = writeComplete0(op, res, flags, data, numOutstandingWrites);
if (!writtenAll && (ioState & POLL_OUT_SCHEDULED) == 0) {
schedulePollOut();
}
if (numOutstandingWrites == 0) {
ioState &= ~WRITE_SCHEDULED;
if (writtenAll && (ioState & POLL_OUT_SCHEDULED) == 0) {
scheduleWriteIfNeeded(unsafe().outboundBuffer(), false);
}
}
}
6.6 connectComplete() 连接处理
connectComplete() 按三种结果处理:
EINPROGRESS或EALREADY:连接未完成,调用schedulePollOut()注册POLLOUT等待连接完成。res == 0:连接成功,调用fulfillConnectPromise()触发pipeline().fireChannelActive(),若 readPending 为 true 则doBeginReadNow()。- 错误:通过
Errors.throwConnectException()抛出异常,fulfillConnectPromise(connectPromise, cause)通知失败。
// AbstractIoUringChannel.java
void connectComplete(byte op, int res, int flags, short data) {
ioState &= ~CONNECT_SCHEDULED;
freeRemoteAddressMemory();
if (res == ERRNO_EINPROGRESS_NEGATIVE || res == ERROR_EALREADY_NEGATIVE) {
// 连接未完成,注册 POLLOUT 等待
schedulePollOut();
} else {
try {
if (res == 0) {
fulfillConnectPromise(connectPromise, active);
if (readPending) { doBeginReadNow(); }
} else {
try {
Errors.throwConnectException("io_uring connect", res);
} catch (Throwable cause) {
fulfillConnectPromise(connectPromise, cause);
}
}
} finally {
cancelConnectTimeoutFuture();
connectPromise = null;
}
}
}
6.7 doClose() 延迟关闭机制
doClose() 采用延迟关闭策略:
// AbstractIoUringChannel.java
@Override
protected void doClose() throws Exception {
active = false;
if (registration != null) {
if (socket.markClosed()) {
int fd = fd().intValue();
IoUringIoOps ops = IoUringIoOps.newClose(fd, (byte) 0, nextOpsId());
registration.submit(ops);
}
} else {
socket.close();
ioUringUnsafe().unregistered();
}
}
当 registration != null(Channel 已注册)时,不直接关闭 socket,而是通过 IoUringIoOps.newClose(fd, 0, nextOpsId()) 提交 IORING_OP_CLOSE 操作。实际的关闭通过 close() 方法中的延迟机制完成:
// AbstractIoUringChannel.java
protected void close(ChannelPromise promise, Throwable cause, ClosedChannelException closeCause) {
if (closeFuture().isDone()) {
safeSetSuccess(promise);
return;
}
if (delayedClose == null) {
delayedClose = new DelayedClose(promise.isVoid() ? newPromise() : promise, cause, closeCause);
} else {
delayedClose.promise.addListener(new PromiseNotifier<>(false, promise));
return;
}
// ...
cancelOps(cancelConnect);
if (canCloseNow()) {
closeNow();
}
}
private boolean canCloseNow() {
return canCloseNow0() && (ioState & (WRITE_SCHEDULED | READ_SCHEDULED)) == 0;
}
private void handleDelayedClosed() {
if (delayedClose != null && canCloseNow()) {
closeNow();
}
}
如果当前有未完成的 READ 或 WRITE 操作(ioState & (WRITE_SCHEDULED | READ_SCHEDULED) != 0),关闭操作会被延迟(delayedClose 存储关闭请求),等待最后一个完成事件触发 handleDelayedClosed() 调用 closeNow()。cancelOps() 取消所有未完成的 POLL/READ/WRITE/CONNECT 操作,canCloseNow() 判断 WRITE_SCHEDULED | READ_SCHEDULED 均为 0 时才执行关闭。
七、IoUringBufferRing:固定缓冲区环形队列与零拷贝读取
IoUringBufferRing 是 Netty 4.2 io_uring 传输中最具特色的组件,它基于内核 buf_ring 机制实现零拷贝读取。传统 recv 需要用户态预先分配 ByteBuf,内核将数据拷贝到用户态缓冲区;而 buf_ring 机制通过 io_uring_register(IORING_REGISTER_PBUF_RING) 在内核注册一组预分配缓冲区,提交 IORING_OP_RECV 时设置 IOSQE_BUFFER_SELECT 标志和 buf_group 索引,内核完成 IO 后直接选择空闲缓冲区写入数据,用户态通过 CQE 的 flags 位获取 bid(buffer id),无需任何数据拷贝。
7.1 构造器与核心字段
IoUringBufferRing 的构造器接收以下参数:
ringFd:io_uring 实例的文件描述符ioUringBufRing:mmap 映射的 buf_ring 内存 ByteBuffer(内核直接写入)entries:缓冲区数量,必须为偶数batchSize:批量填充大小,必须为偶数bufferGroupId:缓冲区组 ID,用于IOSQE_BUFFER_SELECT的buf_group字段incremental:是否支持增量模式allocator:IoUringBufferRingAllocator 分配器batchAllocation:是否使用批量分配
io.netty.channel.uring.IoUringBufferRing#IoUringBufferRing
IoUringBufferRing(int ringFd, ByteBuffer ioUringBufRing,
short entries, int batchSize, short bufferGroupId, boolean incremental,
IoUringBufferRingAllocator allocator, boolean batchAllocation) {
assert entries % 2 == 0;
assert batchSize % 2 == 0;
this.batchSize = batchSize;
this.ioUringBufRing = ioUringBufRing;
this.tailFieldPosition = Native.IO_URING_BUFFER_RING_TAIL;
this.entries = entries;
this.mask = (short) (entries - 1);
this.bufferGroupId = bufferGroupId;
this.ringFd = ringFd;
this.buffers = new ByteBuf[entries];
this.incremental = incremental;
this.allocator = allocator;
this.batchAllocation = batchAllocation;
this.ringConsumer = new RingConsumer();
this.exhaustedEvent = new IoUringBufferRingExhaustedEvent(bufferGroupId);
}
7.2 RingConsumer.fill() 与 add()
RingConsumer 是 IoUringBufferRing 的内部类,实现了 Consumer 接口,负责将 ByteBuf 填充到内核的 buf_ring 中。
fill(startBid, numBuffers) 方法批量填充缓冲区:首先通过 SHORT_HANDLE.get(ioUringBufRing, tailFieldPosition) 获取当前 tail 位置,然后根据 batchAllocation 标志决定使用批量分配还是逐次分配。批量分配时调用 allocator.allocateBatch(this, numBuffers),逐次分配时循环调用 allocator.allocate() 并通过 add() 写入。填充完成后,使用 SHORT_HANDLE.setRelease() 的 release 语义更新 tail,通知内核缓冲区已就绪。
add(tail, bid, offset, byteBuf) 方法将单个 ByteBuf 写入 buf_ring:计算 ringIndex = (tail + offset) & mask,获取缓冲区内存地址和可写大小,按 io_uring_buf 结构体布局写入 addr(8 字节)、len(4 字节)、bid(2 字节),并将 byteBuf 存入 buffers[bid]。
io.netty.channel.uring.RingConsumer#add
private void add(int tail, short bid, int offset, ByteBuf byteBuf) {
short ringIndex = (short) ((tail + offset) & mask);
assert buffers[bid] == null;
long memoryAddress = IoUring.memoryAddress(byteBuf) + byteBuf.writerIndex();
int writable = byteBuf.writableBytes();
int position = Native.SIZEOF_IOURING_BUF * ringIndex;
ioUringBufRing.putLong(position + Native.IOURING_BUFFER_OFFSETOF_ADDR, memoryAddress);
ioUringBufRing.putInt(position + Native.IOURING_BUFFER_OFFSETOF_LEN, writable);
ioUringBufRing.putShort(position + Native.IOURING_BUFFER_OFFSETOF_BID, bid);
buffers[bid] = byteBuf;
}
7.3 useBuffer() 零拷贝消费
useBuffer(short bid, int read, boolean more) 是缓冲区消费的核心方法。当 CQE 返回时,bid 从 flags >> IORING_CQE_BUFFER_SHIFT 提取,read 是实际读取的字节数:
- 从 buffers[bid] 取出 ByteBuf。
- 调用
allocator.lastBytesRead(attempted, read)反馈实际读取量。 retainedSlice(writerIndex, read)切片出已读数据(零拷贝:直接引用原始缓冲区内存)。- 更新
byteBuf.writerIndex。 - 若
incremental && more && byteBuf.isWritable(),保留缓冲区供后续使用(incremental 模式)。 - 否则,清空 buffers[bid],释放
byteBuf,usableBuffers递减。 - 当
usableBuffers == 0时触发fill()重新填充缓冲区。若needExpand为 true,则扩容后再填充。
io.netty.channel.uring.IoUringBufferRing#useBuffer
ByteBuf useBuffer(short bid, int read, boolean more) {
assert read > 0;
ByteBuf byteBuf = buffers[bid];
allocator.lastBytesRead(byteBuf.writableBytes(), read);
// 零拷贝:直接 retainedSlice 引用原始缓冲区内存
ByteBuf buffer = byteBuf.retainedSlice(byteBuf.writerIndex(), read);
byteBuf.writerIndex(byteBuf.writerIndex() + read);
if (incremental && more && byteBuf.isWritable()) {
return buffer;
}
buffers[bid] = null;
byteBuf.release();
if (--usableBuffers == 0) {
int numBuffers = allocatedBuffers;
if (needExpand) {
needExpand = false;
numBuffers += calculateNextBufferBatch();
}
fill((short) 0, numBuffers);
allocatedBuffers = numBuffers;
} else if (!batchAllocation) {
fill(bid);
if (needExpand && lastGeneratedBid == bid) {
needExpand = false;
int numBuffers = calculateNextBufferBatch();
fill((short) (bid + 1), numBuffers);
allocatedBuffers += numBuffers;
}
}
return buffer;
}
7.4 expand() 与 ERRNO_NOBUFS 处理
当内核发现 buf_ring 中无可用缓冲区时,CQE 的 res 返回 -ENOBUFS(即 ERRNO_NOBUFS_NEGATIVE)。readComplete0() 中检测此错误码后调用 bufferRing.expand() 尝试扩容。expand() 设置 needExpand 标记并返回是否还有扩容空间:
// transport-classes-io_uring/src/main/java/io/netty/channel/uring/IoUringBufferRing.java
boolean expand() {
needExpand = true; // 标记需要扩容,后续 useBuffer() 中懒消费
return allocatedBuffers < buffers.length; // 返回是否还有空闲槽位
}
如果 expand() 返回 false(缓冲区槽已满无法扩容),则触发 IoUringBufferRingExhaustedEvent 用户事件通知,提示用户增大 bufferRingSize 配置。无论是否成功扩容,都会重新调度读操作:
// transport-classes-io_uring/src/main/java/io/netty/channel/uring/AbstractIoUringStreamChannel.java
if (res == Native.ERRNO_NOBUFS_NEGATIVE) {
// 内核返回 NOBUFS,尝试扩容 buf_ring
if (!bufferRing.expand()) {
// 槽位已满无法扩容,通知用户增大 bufferRingSize
pipeline.fireUserEventTriggered(bufferRing.getExhaustedEvent());
}
// 不计入实际读取量,重新调度读操作
scheduleRead(allocHandle.isFirstRead());
return;
}
needExpand 标记会在后续 useBuffer() 中被消费——当 usableBuffers 归零时检查 needExpand,如果为 true 则在 refill 时增加一批缓冲区(numBuffers += calculateNextBufferBatch()),实现懒扩容。
八、BufferRing 分配器:Fixed 固定分配与 Adaptive 自适应分配
IoUringBufferRingAllocator 接口定义了 IoUringBufferRing 的缓冲区分配策略,包含三个方法:allocate()(分配单个 ByteBuf)、allocateBatch(Consumer<ByteBuf>, int)(批量分配)、lastBytesRead(int attempted, int actual)(反馈实际读取量)。Netty 提供了两种实现:IoUringFixedBufferRingAllocator 和 IoUringAdaptiveBufferRingAllocator。
8.1 AbstractIoUringBufferRingAllocator 抽象类
AbstractIoUringBufferRingAllocator 提供了 allocate() 和 allocateBatch() 的通用实现,并引入 largeAllocation 模式。在 largeAllocation 模式下,一次分配 bufferSize * number 的大块内存,然后通过 retainedSlice() 切片交付,减少 GC 压力。否则逐次分配每个缓冲区。
io.netty.channel.uring.AbstractIoUringBufferRingAllocator#allocateBatch
@Override
public final void allocateBatch(Consumer<ByteBuf> consumer, int number) {
if (largeAllocation) {
int bufferSize = nextBufferSize();
ByteBuf buffer = allocator.directBuffer(nextBufferSize() * number);
try {
for (int i = 0; i < number; i++) {
consumer.accept(buffer
.retainedSlice(i * bufferSize, bufferSize)
.setIndex(0, 0)
);
}
} finally {
buffer.release();
}
} else {
IoUringBufferRingAllocator.super.allocateBatch(consumer, number);
}
}
8.2 IoUringFixedBufferRingAllocator
IoUringFixedBufferRingAllocator 采用固定大小分配策略,nextBufferSize() 始终返回构造时指定的 bufferSize。适用于帧大小已知的协议(如 Thrift、Protobuf 固定帧长度)。lastBytesRead() 继承自 AbstractIoUringBufferRingAllocator 的默认空实现(NOOP)。
io.netty.channel.uring.IoUringFixedBufferRingAllocator#nextBufferSize
@Override
protected int nextBufferSize() {
return bufferSize;
}
8.3 IoUringAdaptiveBufferRingAllocator
IoUringAdaptiveBufferRingAllocator 采用自适应大小分配策略,内部使用 AdaptiveCalculator。默认参数为 minimum=1024、initial=4096、maximum=65536。
nextBufferSize() 返回 calculator.nextSize()(当前建议大小)。lastBytesRead() 仅在 attempted == actual(缓冲区被填满)时调用 calculator.record(actual),因为缓冲区被填满才说明可能需要更大的缓冲区:
io.netty.channel.uring.IoUringAdaptiveBufferRingAllocator#lastBytesRead
@Override
public void lastBytesRead(int attempted, int actual) {
// 仅在缓冲区被填满时才记录,说明可能需要更大的缓冲区
if (attempted == actual) {
calculator.record(actual);
}
}
AdaptiveCalculator 的自适应策略:当 attempted == actual(缓冲区被填满)时上调 nextSize(),未填满时下调,在 minimum 和 maximum 之间动态调整。首次读取使用 initial=4096 的保守估计,若消息更大则后续自动扩容。
两种分配器的适用场景:Fixed 适合帧大小已知的协议,Adaptive 适合消息大小变化大的场景(如 HTTP 请求体)。
九、IoUringSocketChannel:零拷贝发送与 TFO 快速连接
IoUringSocketChannel 是 io_uring 传输中面向连接的 Socket Channel 实现,其内部类 IoUringSocketUnsafe 在 IoUringStreamUnsafe 的基础上增加了零拷贝发送和 TFO(TCP Fast Open)快速连接支持。
9.1 SEND_ZC 与 SENDMSG_ZC 零拷贝发送
scheduleWriteSingle() 在以下条件同时满足时走零拷贝路径:IoUring.isSendZcSupported() 为 true 且消息大小超过 shouldWriteZeroCopy() 阈值。零拷贝发送使用 IoUringIoOps.newSendZc() 提交 IORING_OP_SEND_ZC 操作码,内核通过 DMA 直接传输数据,无需用户态拷贝。
scheduleWriteMultiple() 在 IoUring.isSendmsgZcSupported() 为 true 且至少一个缓冲区超过阈值时走零拷贝批量路径。使用 IovArray 收集多个 ByteBuf 的 iovec,MsgHdrMemory 构建 msghdr,然后通过 IoUringIoOps.newSendmsgZc() 提交 IORING_OP_SENDMSG_ZC 操作码。
io.netty.channel.uring.IoUringSocketUnsafe#scheduleWriteSingle
@Override
protected int scheduleWriteSingle(Object msg) {
assert writeId == 0;
if (IoUring.isSendZcSupported() && msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
int length = buf.readableBytes();
if (((IoUringSocketChannelConfig) config()).shouldWriteZeroCopy(length)) {
long address = IoUring.memoryAddress(buf) + buf.readerIndex();
IoUringIoOps ops = IoUringIoOps.newSendZc(
fd().intValue(), address, length, 0, nextOpsId(), 0);
byte opCode = ops.opcode();
writeId = registration().submit(ops);
writeOpCode = opCode;
if (writeId == 0) {
return 0;
}
return 1;
}
}
return super.scheduleWriteSingle(msg);
}
9.2 IORING_CQE_F_NOTIF 两阶段通知机制
IORING_CQE_F_NOTIF 是零拷贝发送的核心机制。当使用 IORING_OP_SEND_ZC 或 IORING_OP_SENDMSG_ZC 时,内核会为每个操作产生两个 CQE:
-
第一阶段(写完成) :
IORING_CQE_F_NOTIF未设置,res 表示实际发送的字节数。若IORING_CQE_F_MORE也被设置,说明缓冲区仍在被内核使用,需要retain()并存入 zcWriteQueue,插入ZC_BATCH_MARKER分隔批次。 -
第二阶段(缓冲区释放) :
IORING_CQE_F_NOTIF已设置,表示内核不再使用缓冲区,从 zcWriteQueue 中循环release()直到遇到ZC_BATCH_MARKER。下面这张时序图展示了两阶段通知的完整流程:
`handleWriteCompleteZeroCopy()` 方法实现两阶段处理:
io.netty.channel.uring.IoUringSocketUnsafe#handleWriteCompleteZeroCopy
// 零拷贝写完成的两阶段处理:NOTIF 未设置时 retain 入队,NOTIF 设置时 release 出队
private boolean handleWriteCompleteZeroCopy(byte op, ChannelOutboundBuffer channelOutboundBuffer,
int res, int flags) {
if ((flags & Native.IORING_CQE_F_NOTIF) == 0) {
// 第一阶段:写操作完成,但内核仍持有缓冲区
writeId = 0;
writeOpCode = 0;
boolean more = (flags & Native.IORING_CQE_F_MORE) != 0;
if (more) {
// IORING_CQE_F_MORE:内核还会发送 NOTIF 通知,retain 缓冲区延迟释放
if (zcWriteQueue == null) {
zcWriteQueue = new ArrayDeque<>(8);
}
}
if (res >= 0) {
if (more) {
do {
ByteBuf currentBuffer = (ByteBuf) channelOutboundBuffer.current();
zcWriteQueue.add(currentBuffer);
currentBuffer.retain();
int readable = currentBuffer.readableBytes();
int skip = Math.min(readable, res);
currentBuffer.skipBytes(skip);
channelOutboundBuffer.progress(readable);
if (readable <= res) {
channelOutboundBuffer.remove();
}
res -= readable;
} while (res > 0);
zcWriteQueue.add(ZC_BATCH_MARKER);
} else {
channelOutboundBuffer.removeBytes(res);
}
return true;
}
// ... 错误处理
} else {
// 第二阶段:IORING_CQE_F_NOTIF 设置,内核不再需要缓冲区,安全释放
Object o;
while ((o = zcWriteQueue.poll()) != ZC_BATCH_MARKER) {
if (o instanceof ByteBuf) {
((ByteBuf) o).release();
}
}
}
return false;
}
9.3 TCP_FASTOPEN_CONNECT(TFO)支持
当 ChannelOption.TCP_FASTOPEN_CONNECT 为 true 时,connect() 不会先建立 TCP 连接再发送数据,而是通过 IoUringIoOps.newSendmsg() 提交一个带 MSG_FASTOPEN 标志的 sendmsg 操作,将连接建立和首包数据一并发送。连接前先从 ChannelOutboundBuffer 中取出待发送的首个 ByteBuf 作为 initialData,通过 fillTFOInitData() 填充到 msghdr 中,然后提交 IORING_OP_SENDMSG:
// transport-classes-io_uring/src/main/java/io/netty/channel/uring/AbstractIoUringChannel.java
// 检测 TCP_FASTOPEN_CONNECT 选项,取出首个待发送 ByteBuf 作为 TFO 首包
if (IoUring.isTcpFastOpenClientSideAvailable() &&
config().getOption(ChannelOption.TCP_FASTOPEN_CONNECT) == Boolean.TRUE) {
ChannelOutboundBuffer outbound = unsafe().outboundBuffer();
outbound.addFlush();
Object curr;
if ((curr = outbound.current()) instanceof ByteBuf) {
initialData = (ByteBuf) curr;
}
}
if (initialData != null) {
// 有首包数据,提交带 MSG_FASTOPEN 的 sendmsg(连接+数据一并发送)
msgHdrMemoryArray = new MsgHdrMemoryArray((short) 1);
MsgHdrMemory hdr = msgHdrMemoryArray.hdr(0);
fillTFOInitData(hdr, inetSocketAddress, initialData);
IoUringIoOps ops = IoUringIoOps.newSendmsg(fd, (byte) 0, Native.MSG_FASTOPEN,
hdr.address(), hdr.idx());
connectId = registration.submit(ops);
if (connectId == 0) {
freeMsgHdrArray();
}
} else {
// 无首包数据,走普通 connect
submitConnect(inetSocketAddress);
}
// ...
ioState |= CONNECT_SCHEDULED; // 标记 connect 已调度,供 writeComplete() 识别 TFO 场景
TFO 的首包数据完成回调走的是 writeComplete() 而非 connectComplete(),因为提交的操作码是 IORING_OP_SENDMSG。writeComplete() 开头检测 CONNECT_SCHEDULED 标志,如果已设置说明是 TFO 场景,按 res 值分三种情况处理:
res > 0:首包已发送且连接已建立,removeBytes(res)消费已发送数据,调用connectComplete(op, 0, ...)以 res=0 表示连接成功。EINPROGRESS或res == 0:客户端没有 TFO cookie,内核只建立了连接但未发送数据,回退到submitConnect()提交普通的IORING_OP_CONNECT。- 其他错误:直接调用
connectComplete()传递错误码。
// transport-classes-io_uring/src/main/java/io/netty/channel/uring/AbstractIoUringChannel.java
private void writeComplete(byte op, int res, int flags, short data) {
if ((ioState & CONNECT_SCHEDULED) != 0) {
// TFO 场景:sendmsg 完成后走到这里
freeMsgHdrArray();
if (res > 0) {
// 首包发送成功,连接已建立
outboundBuffer().removeBytes(res);
connectComplete(op, 0, flags, data);
} else if (res == ERRNO_EINPROGRESS_NEGATIVE || res == 0) {
// 无 TFO cookie,回退到普通 connect
submitConnect((InetSocketAddress) requestedRemoteAddress);
} else {
// 连接失败,传递错误码
connectComplete(op, res, flags, data);
}
return;
}
// ... 正常写完成逻辑
}
十、整体链路串联
将 io_uring 传输的各个环节串联起来,可以看出一条完整的链路闭环:
-
IoUringIoHandler 构造:通过
Native.createRingBuffer()创建 io_uring 实例,Native.ioUringRegisterBufRing()注册 BufferRing,创建 eventfd 初始化唤醒机制,并完成 PendingOpMap / IovArray / MsgHdrMemoryArray 的初始化 -
initialize() 初始化:调用
ringBuffer.enable()启用 ring,调用bufferRing.initialize()填充初始缓冲区 -
run() 事件循环:分阻塞路径(
submitEventFdRead()→submitAndWaitWithTimeout()等待 CQE)和非阻塞路径(submitAndClearNow()立即提交),最终通过processCompletionsAndHandleOverflow()批量消费 CQE -
handle() 四种分发:EVENTFD_TOKEN 走
handleEventFdRead()重置唤醒;RINGFD_TOKEN 忽略内部事件;fast path 通过UserData.decode()实现 O(1) 分发到registration.handle();slow path 通过pendingOps.findSlot()查表分发 -
AbstractUringUnsafe.handle() 七路分发:
readComplete()触发fireChannelRead()(有 BufferRing 时走useBuffer()→retainedSlice()零拷贝读);writeComplete()触发removeBytes()(零拷贝时走handleWriteCompleteZeroCopy()两阶段通知);pollAddComplete()分发到pollOut()/pollIn()/pollRdHup();connectComplete()调用fulfillConnectPromise();另有cancelComplete0()和OP_CLOSE→handleDelayedClosed()回顾全文,Netty 4.2 的 io_uring 传输可以凝练为三大核心机制:
"环形队列" —— RingBuffer 封装 SubmissionQueue 和 CompletionQueue,通过 VarHandle 的 getVolatile/setRelease 实现无锁的 SQ 生产和 CQ 消费,io_uring_enter 是唯一的系统调用入口。
SubmissionQueue.enqueueSqe()将 IoUringIoOps 的 13 个字段精确映射到 io_uring_sqe 的 64 字节布局,CompletionQueue.process()的无锁消费循环通过 acquire/release 语义保证线程安全。"事件分发" —— IoUringIoHandler 通过
handle()的 fast/slow path 分流将 CQE 分发给 DefaultIoUringIoRegistration。Fast path 利用UserData.encode()将 id/op/data 打包为 64 位 user_data,实现 O(1) 的 registration 查找;Slow path 通过 PendingOpMap 管理额外映射。AbstractUringUnsafe.handle()按 opcode 七路分发到具体的 IO 方法,ioState 位掩码精确管理 POLL/READ/WRITE/CONNECT 的调度状态。"零拷贝" —— IoUringBufferRing 的 buf_ring +
IOSQE_BUFFER_SELECT让内核直接将数据写入预分配缓冲区,useBuffer()通过retainedSlice()零拷贝交付数据。IORING_OP_SEND_ZC/SENDMSG_ZC让内核 DMA 直接传输发送数据,IORING_CQE_F_NOTIF两阶段通知机制管理缓冲区生命周期。IORING_OP_SPLICE在内核内直接将文件数据"拼接"到 socket,无需用户态中转。io_uring 传输与 NIO/Epoll 传输的核心差异:
| 维度 | NIO(Java NIO) | Epoll(Netty Epoll) | io_uring(Netty IoUring) |
|---|---|---|---|
| IO 模型 | 同步非阻塞(Selector + read/write) | 同步非阻塞(EpollEventLoop + read/write) | 异步(SQ 提交 + CQ 轮询) |
| 唤醒机制 | Selector.wakeup() | eventfd + SocketWritableByteChannel | eventfd + IORING_OP_READ |
| 事件注册 | SelectionKey.interestOps | EpollEventLoop 的 events | IoRegistration.submit() 直接提交 |
| 提交方式 | 每次 read/write 一次系统调用 | 每次 read/write 一次系统调用 | 批量提交,一次 io_uring_enter 多个 SQE |
| 读取方式 | 用户分配 ByteBuf,内核拷贝 | 用户分配 ByteBuf,内核拷贝 | buf_ring + IOSQE_BUFFER_SELECT 零拷贝 |
| 写入方式 | 用户提供 ByteBuf,内核拷贝 | 用户提供 ByteBuf,内核拷贝 | SEND_ZC/SENDMSG_ZC DMA 直接发送 |
| 文件传输 | 用户态 read + write | 用户态 splice/零拷贝 | IORING_OP_SPLICE 内核内拼接 |
全文小结
本文聚焦 Netty 4.2 中 io_uring 传输的实现,从内核机制与 Java 封装两个维度,深入分析了 RingBuffer 环形缓冲区、IoUringIoHandler 事件循环、IoUringBufferRing 零拷贝读取和 IoUringSocketChannel 零拷贝发送的完整机制。
在内核层面,io_uring 通过 SQ/CQ 双环形缓冲区共享内存映射,实现用户态与内核态的零拷贝通信,io_uring_enter 系统调用是唯一的同步点。在数据结构层面,RingBuffer 聚合 SubmissionQueue 和 CompletionQueue,SubmissionQueue.enqueueSqe() 将 IoUringIoOps 的 13 个字段精确映射到 io_uring_sqe 的 64 字节布局,CompletionQueue.process() 通过 VarHandle 的 getVolatile/setRelease 实现无锁 CQ 消费。在事件循环层面,IoUringIoHandler.run() 通过阻塞/非阻塞两条路径驱动 SQ 提交与 CQ 轮询,handle() 的四种分发路径(eventfd/ringfd/fast/slow)将 CQE 精准路由到目标 Channel。在 IO 操作抽象层面,IoUringIoOps 的 15 个静态工厂方法映射 IORING_OP_* 操作码,UserData 的 56 位编码实现 fast path 的 O(1) 查找。在 Channel 实现层面,AbstractUringUnsafe.handle() 按 opcode 七路分发到 readComplete()/writeComplete()/pollAddComplete() 等,ioState 位掩码精确管理 IO 调度状态,delayedClose 延迟关闭机制确保优雅下线。在零拷贝读取层面,IoUringBufferRing 通过 io_uring_register(IORING_REGISTER_PBUF_RING) 注册预分配缓冲区,useBuffer() 通过 retainedSlice() 零拷贝交付数据,AdaptiveCalculator 动态调整缓冲区大小。在零拷贝发送层面,IoUringSocketUnsafe 通过 IORING_OP_SEND_ZC/SENDMSG_ZC 实现 DMA 直接发送,IORING_CQE_F_NOTIF 两阶段通知机制配合 zcWriteQueue 和 ZC_BATCH_MARKER 精确管理缓冲区生命周期。
原创不易,如果本文对您有帮助,带来了些许灵感或启发,烦请动动小手点赞、关注、转发、收藏。这是作者持续更新的动力源泉,衷心感谢您的支持。我会尽量在工作之余,为大家带来更高品质的内容,努力保持周更。