Netty 4.2.x 源码深度解析 (十五):io_uring 传输 —— RingBuffer 与零拷贝 IO

0 阅读42分钟

cover-15-v4.jpg

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 双环形缓冲区的数据流向和生产者-消费者关系:

image-20260928174739060.png

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 字节,字段按偏移排列:

字段偏移大小说明
opcode01操作码(IORING_OP_RECV、IORING_OP_SEND 等)
flags11标志位(IOSQE_BUFFER_SELECT、IOSQE_IO_DRAIN 等)
ioprio22IO 优先级
fd44文件描述符
union188联合体(off/addr2)
union2168联合体(addr/splice_off)
len244数据长度
union3284联合体(rw_flags/...)
user_data328用户数据(关键:用于分发 CQE 到对应 Channel)
union4402联合体(buf_index/group)
personality422凭证标识
union5444联合体(splice_fd_in/...)
union6488联合体(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 模型在系统调用次数和数据拷贝路径上的差异: image-20260928181302819.png

下面这张架构图展示了 io_uring 的共享内存模型:

image-20260928183301095.png

二、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.acceptMultiShotEnabledtrue启用 multi-shot ACCEPT
io.netty.iouring.recvMultiShotEnabledtrue启用 multi-shot RECV
io.netty.iouring.recvsendBundleEnabledfalse启用 RECVSEND_BUNDLE(默认关闭,因已知内核 bug)
io.netty.iouring.pollAddMultishotEnabledtrue启用 multi-shot POLL_ADD
io.netty.iouring.enterNoIoWaitEnabledfalse启用 ENTER_NO_IOWAIT
io.netty.iouring.ringSize128SQ 环形队列大小
io.netty.iouring.cqSize4096CQ 环形队列大小

默认配置参数: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 的完整流程:

IO_Uring-PubSubProcess.drawio.png

上面的时序图清晰地展示了三个层次: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 每轮事件循环调用。其完整流程如下:

  1. 检查 closeCompleted,若已完成则直接返回 0

  2. 获取 SubmissionQueue 和 CompletionQueue

  3. 判断是否有就绪 CQE 或能否阻塞,分两条路径:

    • 阻塞路径(无就绪 CQE 且 canBlock() 为 true):先通过 submitEventFdRead() 提交 eventfd 读请求,再调用 submitAndWaitWithTimeout() 阻塞等待完成事件
    • 非阻塞路径(有就绪 CQE 或有待处理任务):调用 submitAndClearNow() 非阻塞提交
  4. 调用 processCompletionsAndHandleOverflow() 消费 CQE 并处理溢出

下面这张时序图展示了完整的 run() 事件循环:

IO_Uring-IoUringIoHandler.run().drawio.png

阻塞路径(无就绪 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 线程调用,流程如下:

  1. CAS 设置通知标志:eventfdAsyncNotify.getAndSet(true) 确保只有一个线程负责写入 eventfd。

  2. 获取写锁:通过 wakeupWriters CAS 自旋,确保在 closeWakeupGate() 关闭 eventfd 时不会并发写入。若 WAKEUP_CLOSED 位已设置,则直接返回(无需唤醒已关闭的 loop)。

  3. 写入 eventfd:Native.eventFdWrite(eventfd.intValue(), 1L) 写入 1,使 eventfd 变为可读。

  4. 释放写锁: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 字段大小(字节)
opcodeopcode1
flagsflags1
ioPrioioprio2
fdfd4
union1union1(off/addr2)8
union2union2(addr/splice_off)8
lenlen4
union3union3(rw_flags/...)4
datauser_data8
union4union4(buf_index/group)2
personalitypersonality2
union5union5(splice_fd_in/...)4
union6union6(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 分派到七个分支:

  1. RECV/ACCEPT/RECVMSG/READ → readComplete():读操作完成
  2. WRITEV/SEND/SENDMSG/WRITE/SPLICE/SEND_ZC/SENDMSG_ZC → writeComplete():写操作完成
  3. POLL_ADD → pollAddComplete():poll 事件触发
  4. ASYNC_CANCEL → cancelComplete0():取消操作完成
  5. CONNECT → connectComplete():连接操作完成
  6. CLOSE → close 处理:关闭操作完成
  7. default → 忽略未知 opcode

下面这张时序图展示了完整的七路分发:

IO_Uring-Dispatch.drawio.png

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 标志的状态转换。核心流程拆解:

  1. 多轮读(multi-shot)检测:numOutstandingReads == -1 表示使用了 multi-shot 模式。
  2. rearm 判断:(flags & IORING_CQE_F_MORE) == 0 时,需要重新武装(rearm),清除 READ_SCHEDULED 位。
  3. 非 multi-shot 递减:--numOutstandingReads == 0 时清除 readPending 和 READ_SCHEDULED。
  4. socketIsEmpty 检测:通过 socketIsEmpty(flags) 判断 socket 是否可保证为空,利用 IORING_CQE_F_SOCK_NONEMPTY 标志位。
  5. 调用 readComplete0() :具体读处理由子类实现。
  6. 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() 管理写完成后的状态清理和后续写调度。核心流程拆解:

  1. TFO 场景检测:若 CONNECT_SCHEDULED 位已设置,说明 writeComplete() 是由 TFO 的 sendmsg 完成触发,特殊处理连接完成逻辑。
  2. 非通知递减:(flags & IORING_CQE_F_NOTIF) == 0 时,--numOutstandingWrites。
  3. 调用 writeComplete0() :具体写处理由子类实现。
  4. 写不完时注册 POLLOUT:!writtenAll && (ioState & POLL_OUT_SCHEDULED) == 0 时调用 schedulePollOut()。
  5. 写完后继续调度: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 是实际读取的字节数:

  1. 从 buffers[bid] 取出 ByteBuf。
  2. 调用 allocator.lastBytesRead(attempted, read) 反馈实际读取量。
  3. retainedSlice(writerIndex, read) 切片出已读数据(零拷贝:直接引用原始缓冲区内存)。
  4. 更新 byteBuf.writerIndex。
  5. 若 incremental && more && byteBuf.isWritable(),保留缓冲区供后续使用(incremental 模式)。
  6. 否则,清空 buffers[bid],释放 byteBuf,usableBuffers 递减。
  7. 当 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:

  1. 第一阶段(写完成) :IORING_CQE_F_NOTIF 未设置,res 表示实际发送的字节数。若 IORING_CQE_F_MORE 也被设置,说明缓冲区仍在被内核使用,需要 retain() 并存入 zcWriteQueue,插入 ZC_BATCH_MARKER 分隔批次。

  2. 第二阶段(缓冲区释放) :IORING_CQE_F_NOTIF 已设置,表示内核不再使用缓冲区,从 zcWriteQueue 中循环 release() 直到遇到 ZC_BATCH_MARKER。

    下面这张时序图展示了两阶段通知的完整流程:

IO_Uring-TwoPhareNotice.drawio.png

`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 传输的各个环节串联起来,可以看出一条完整的链路闭环:

  1. IoUringIoHandler 构造:通过 Native.createRingBuffer() 创建 io_uring 实例,Native.ioUringRegisterBufRing() 注册 BufferRing,创建 eventfd 初始化唤醒机制,并完成 PendingOpMap / IovArray / MsgHdrMemoryArray 的初始化

  2. initialize() 初始化:调用 ringBuffer.enable() 启用 ring,调用 bufferRing.initialize() 填充初始缓冲区

  3. run() 事件循环:分阻塞路径(submitEventFdRead() → submitAndWaitWithTimeout() 等待 CQE)和非阻塞路径(submitAndClearNow() 立即提交),最终通过 processCompletionsAndHandleOverflow() 批量消费 CQE

  4. handle() 四种分发:EVENTFD_TOKEN 走 handleEventFdRead() 重置唤醒;RINGFD_TOKEN 忽略内部事件;fast path 通过 UserData.decode() 实现 O(1) 分发到 registration.handle();slow path 通过 pendingOps.findSlot() 查表分发

  5. 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 + SocketWritableByteChanneleventfd + IORING_OP_READ
事件注册SelectionKey.interestOpsEpollEventLoop 的 eventsIoRegistration.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 精确管理缓冲区生命周期。


原创不易,如果本文对您有帮助,带来了些许灵感或启发,烦请动动小手点赞、关注、转发、收藏。这是作者持续更新的动力源泉,衷心感谢您的支持。我会尽量在工作之余,为大家带来更高品质的内容,努力保持周更。