ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux的io_uring配合Registered Buffers的零拷贝优化原理剖析

Linux的io_uring配合Registered Buffers的零拷贝优化原理剖析 Linux的io_uring配合Registered Buffers的零拷贝优化原理剖析前言io_uring配合Registered Buffers的零拷贝优化1. 内核级源码分析与 GUP 消除机制1.1 动态 GUP 开销普通 IORING_OP_SEND_ZC1.2 预注册固定缓冲区机制IORING_REGISTER_BUFFERS IORING_OP_SEND_ZC1.3 Linux 内核核心源码剖析A. 缓冲区注册数据结构io_uring/rsrc.hB. 运行时导入逻辑io_uring/net.c2. 基于 liburing 的完整 C 语言示例代码3. 性能优化与极致调优对比极致场景下的数据通路对比前言本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限文中内容难免存在疏漏恳请读者不吝指正。io_uring配合Registered Buffers的零拷贝优化1. 内核级源码分析与 GUP 消除机制在 Linux 网络子系统中传统IORING_OP_SEND_ZC与结合IORING_REGISTER_BUFFERS的零拷贝发送在内核数据路径上有本质的区别。深度的性能差异来自于内核如何处理用户态虚拟地址VA到物理页帧struct page的映射。1.1 动态 GUP 开销普通IORING_OP_SEND_ZC当发起普通的IORING_OP_SEND_ZC时内核在每次提交 I/O 时必须执行以下步骤用户态提交 SQE (VA: 0x7f9a1000, Len: 128KB) │ ▼ ┌─────────────────────────────────────────────────────────┐ | 内核 io_send_zc() (io_uring/net.c) | └────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ | 动态调用 iov_iter_extract_pages() - get_user_pages_fast()| | 1. 获取当前进程的 mm_struct 与 mmap_lock | | 2. 逐级遍历多级页表 (PGD - P4D - PUD - PMD - PTE) | | 3. 检查 PTE 权限 (Present, User, Write/Read) | | 4. 增加物理页 atomic_inc(page-_refcount) (原子锁开销) | └────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ | 构建 bio_vec / SKB 碎片并提交网卡 SG-DMA | └─────────────────────────────────────────────────────────┘主要瓶颈MMU 页表遍历开销每次 I/O 请求都需遍历进程页表。原子锁与缓存失效高并发下频繁执行page_ref_inc()导致跨 CPU 核心的 L1/L2 Cache Line 伪共享False Sharing与原子指令争抢。并发内存锁争抢在多线程高并发分配/释放内存时频繁触发 GUP 可能会引入对mm-mmap_lock的 Read-Lock 竞争。1.2 预注册固定缓冲区机制IORING_REGISTER_BUFFERSIORING_OP_SEND_ZC通过预先注册缓冲区映射关系在初始化阶段即被持久化钉扎Pinned在内核io_ring_ctx上下文中启动阶段: io_uring_register_buffers() │ ▼ ┌─────────────────────────────────────────────────────────┐ | 内核 io_sqe_buffers_register() (io_uring/rsrc.c) | | 1. 调用 pin_user_pages() 一次性锁定所有物理页 | | 2. 分配 struct io_mapped_ubuf 结构 | | 3. 将物理页指针 (struct page*) 固化填入 imu-bvec[] | | 4. 挂载至 ctx-user_bufs[buf_index] 数组 | └─────────────────────────────────────────────────────────┘ ----------------------------------------------------------- 运行时数据路径: IORING_OP_SEND_ZC IORING_RECVSEND_FIXED_BUF │ ▼ ┌─────────────────────────────────────────────────────────┐ | 内核 io_send_zc() (io_uring/net.c) | └────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ | 零 GUP 快速路径: io_import_fixed() | | 1. 根据 buf_index 直接查表: ctx-user_bufs[buf_index] | | 2. 直接引用预先构建好的 bio_vec 数组 | | 3. 彻底跳过 get_user_pages_fast() / 0 页表遍历 | | 4. 0 原子引用计数开销 / 0 MMU 锁争抢 | └────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ | 构建 SKB 碎片并提交网卡 SG-DMA | └─────────────────────────────────────────────────────────┘1.3 Linux 内核核心源码剖析A. 缓冲区注册数据结构io_uring/rsrc.h内核使用struct io_mapped_ubuf保存已钉扎的物理页与bio_vec映射// linux/io_uring/rsrc.hstructio_mapped_ubuf{u64 ubuf;// 用户态虚拟内存起始地址 (VA)u32 len;// 缓冲区长度unsignedintnr_bvecs;// 该缓冲区占用的物理页数量structbio_vecbvec[];// 预先建立好的物理页与偏移量数组 (直接供给 DMA)};B. 运行时导入逻辑io_uring/net.c在io_send_zc()处理函数中内核根据是否设置了REQ_F_FIXED_FILE/IORING_RECVSEND_FIXED_BUF走不同的分支// linux/io_uring/net.cintio_send_zc(structio_kiocb*req,unsignedintissue_flags){structio_send_zc*zcio_kiocb_to_cmd(req,structio_send_zc);structmsghdr*msgkmsg-msg;// 预注册缓冲区快速路径 (Fixed Buffer Path)if(req-flagsREQ_F_FIXED_FILE){// 直接使用预分配的 struct io_mapped_ubuf 充填 msg_iter// O(1) 复杂度完全不触发 GUP 操作retio_import_fixed(ITER_SOURCE,msg-msg_iter,req-ctx-user_bufs[zc-buf_index],zc-addr,zc-len);if(unlikely(ret))returnret;}else{// 普通路径动态 GUP 提取物理页遍历页表并增加引用计数retimport_ubuf(ITER_SOURCE,zc-addr,zc-len,msg-msg_iter);if(unlikely(ret))returnret;}// ... 后续构建 SKB 并挂载 Zero-Copy Notification 结构的逻辑 ...}2. 基于liburing的完整 C 语言示例代码以下代码实现了生产级的无锁缓冲区池管理结合IORING_REGISTER_BUFFERS与IORING_OP_SEND_ZC并内嵌了源码级别的逐行分析说明。#define_GNU_SOURCE#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includeerrno.h#includestdbool.h#includesys/socket.h#includesys/uio.h#includesys/resource.h#includeliburing.h/* * 系统级常量配置 * */#defineQUEUE_DEPTH32#defineBUF_COUNT8#defineBUF_SIZE(128*1024)// 128KB 传输大包确保发挥 Zero-Copy 优势#definePAGE_ALIGNMENT4096// 物理页对齐尺寸 (4KB)/* * 状态机定义与无锁内存池数据结构 * *//** * brief 预注册缓冲区的生命周期状态机 */typedefenum{BUF_STATE_FREE0,// 空闲用户态完全掌控可写入新数据或重新调度发送BUF_STATE_SUBMITTED,// 已提交SQE 已入队/正在提交给内核严禁修改/释放BUF_STATE_WAIT_NOTIF// 挂起中已收到 CQE 1 (网络栈已接管)正等待 CQE 2 (DMA 完成)}buf_state_t;/** * brief 缓冲区上下文结构体 (包含注册索引与关联数据) */typedefstruct{uint16_tbuf_index;// 对应 io_uring 注册缓冲区的数组下标 (0 ~ BUF_COUNT-1)buf_state_tstate;// 原子生命周期状态size_tlen;// 请求发送字节数ssize_tbytes_sent;// 实际发送字节数 (来自 CQE 1 的 cqe-res)char*data;// 页对齐的用户态虚拟内存指针}registered_buf_ctx_t;/** * brief 应用层无锁固定缓冲区池 (单线程 Reactor 架构下无锁) */typedefstruct{registered_buf_ctx_tbufs[BUF_COUNT];structioveciov[BUF_COUNT];// 传递给 io_uring_register_buffers 的描述符数组uint32_tfree_stack[BUF_COUNT];inttop;}fixed_buffer_pool_t;/* * 函数声明与工具实现 * *//** * brief 提升进程的 RLIMIT_MEMLOCK 配额 (防止预注册时 pin_user_pages 报 -ENOMEM) */staticvoidbump_memlock_limit(void){structrlimitrlim{RLIM_INFINITY,RLIM_INFINITY};if(setrlimit(RLIMIT_MEMLOCK,rlim)0){perror([警告] 无法设置 RLIMIT_MEMLOCK 为 INFINITY预注册可能因配额不足失败);}}/** * brief 初始化物理页对齐的缓冲区池并向 io_uring 内核注册 */staticintinit_and_register_buffer_pool(structio_uring*ring,fixed_buffer_pool_t*pool){pool-top0;for(inti0;iBUF_COUNT;i){pool-bufs[i].buf_indexi;pool-bufs[i].stateBUF_STATE_FREE;pool-bufs[i].lenBUF_SIZE;pool-bufs[i].bytes_sent0;/* * 源码分析 1为什么必须使用 posix_memalign 进行页对齐 * 内核的 io_sqe_buffers_register() 最终通过 pin_user_pages() 锁定物理页。 * 如果虚拟内存起始地址未按 4096 字节对齐内核必须额外处理首尾跨页碎片 * 这会降低构建 bio_vec 描述符的效率。页对齐可确保 100% 匹配物理页框 (Page Frame)。 */if(posix_memalign((void**)pool-bufs[i].data,PAGE_ALIGNMENT,BUF_SIZE)!0){perror(posix_memalign 分配内存失败);return-1;}// 填充测试数据memset(pool-bufs[i].data,A(i%26),BUF_SIZE);// 充填 iovec 结构数组pool-iov[i].iov_basepool-bufs[i].data;pool-iov[i].iov_lenBUF_SIZE;// 压入无锁栈pool-free_stack[pool-top]i;}/* * 源码分析 2io_uring_register_buffers 系统调用 * 此接口会将 pool-iov 提交至内核。内核在此处执行了一次性的 GUP (get_user_pages) * 并构建全局 struct io_mapped_ubuf 映射表存入 ring 上下文中。 * 后续所有 I/O 提交将彻底消除运行时 GUP 与 MMU 页表遍历开销 */intretio_uring_register_buffers(ring,pool-iov,BUF_COUNT);if(ret0){fprintf(stderr,io_uring_register_buffers 失败: %s\n,strerror(-ret));returnret;}printf([1] 预注册完成已一次性锁定 %d 个 %dKB 的物理页内存区间。\n,BUF_COUNT,BUF_SIZE/1024);return0;}staticregistered_buf_ctx_t*buffer_pool_pop(fixed_buffer_pool_t*pool){if(pool-top0)returnNULL;uint32_tidxpool-free_stack[--pool-top];returnpool-bufs[idx];}staticvoidbuffer_pool_push(fixed_buffer_pool_t*pool,registered_buf_ctx_t*buf){buf-stateBUF_STATE_FREE;buf-bytes_sent0;pool-free_stack[pool-top]buf-buf_index;}staticintcreate_socket_pair(intsv[2]){if(socketpair(AF_UNIX,SOCK_STREAM,0,sv)0){perror(socketpair 创建失败);return-1;}// 放大 Socket Send/Recv Buffer 避免发送队列满阻塞测试intoptval8*1024*1024;setsockopt(sv[0],SOL_SOCKET,SO_SNDBUF,optval,sizeof(optval));setsockopt(sv[1],SOL_SOCKET,SO_RCVBUF,optval,sizeof(optval));return0;}/* * 主程序结合注册缓冲区与零拷贝发送演示双 CQE 解析 * */intmain(void){structio_uringring;fixed_buffer_pool_tpool;intsv[2];bump_memlock_limit();if(create_socket_pair(sv)0)exit(EXIT_FAILURE);// 初始化 io_uringif(io_uring_queue_init(QUEUE_DEPTH,ring,0)0){perror(io_uring_queue_init 失败);exit(EXIT_FAILURE);}// 初始化页对齐 Buffer 池并注册至内核if(init_and_register_buffer_pool(ring,pool)0){exit(EXIT_FAILURE);}printf(\n [2] 提交阶段使用 IORING_OP_SEND_ZC FIXED_BUF 发起数据传输 \n);// 从无锁池弹出一个预注册 Bufferregistered_buf_ctx_t*buf_ctxbuffer_pool_pop(pool);if(!buf_ctx){fprintf(stderr,无可用 Buffer\n);exit(EXIT_FAILURE);}structio_uring_sqe*sqeio_uring_get_sqe(ring);if(!sqe){fprintf(stderr,SQE 结构分配失败\n);exit(EXIT_FAILURE);}/* * 源码分析 3io_uring_prep_send_zc_fixed 机制详解 * 参数定义: sqe, sockfd, buf, len, msg_flags, zc_flags, buf_index * * 内部代码行为解析: * 1. 设置 sqe-opcode IORING_OP_SEND_ZC * 2. 设置 sqe-buf_index buf_index (指定预注册数组索引) * 3. 在 sqe-ioprio 中打上 IORING_RECVSEND_FIXED_BUF 标记位 * * 内核接收到该 SQE 后 * 触发 req-flags | REQ_F_FIXED_FILE在 io_send_zc() 中走 io_import_fixed() * 快速分支直接通过 ctx-user_bufs[buf_index] 获取硬件 DMA 物理页地址 */io_uring_prep_send_zc_fixed(sqe,sv[0],buf_ctx-data,buf_ctx-len,0,0,buf_ctx-buf_index);/* * 源码分析 4无锁上下文挂载 (user_data) * 将用户态 registered_buf_ctx_t 指针直接存储在 SQE user_data 中。 * 当 CQE 返回时内核将原样返回该 64 位整型实现 0 锁、O(1) 时间强转恢复指针 */io_uring_sqe_set_data64(sqe,(uint64_t)(uintptr_t)buf_ctx);buf_ctx-stateBUF_STATE_SUBMITTED;printf([SQE 发送] Buffer Index: %u | Address: %p | 运行时 GUP 开销: 0 (完全跳过)\n,buf_ctx-buf_index,(void*)buf_ctx-data);// 刷新 Submission Queue 提交至内核io_uring_submit(ring);printf(\n [3] 事件循环无锁双 CQE 解析与 Buffer 安全回收 \n);intcompleted_cqes0;while(completed_cqes2){structio_uring_cqe*cqe;// 阻塞等待完成事件出队intretio_uring_wait_cqe(ring,cqe);if(ret0){fprintf(stderr,io_uring_wait_cqe 错误: %s\n,strerror(-ret));break;}// 无锁指针还原从 cqe-user_data 强转还原结构体registered_buf_ctx_t*b(registered_buf_ctx_t*)(uintptr_t)io_uring_cqe_get_data64(cqe);/* * 源码分析 5双 CQE 标志位无锁判定逻辑 * * - IORING_CQE_F_MORE (0x02): * 表示当前 I/O 请求尚未完全终结后面至少还有一个 CQE (即 notification CQE) 会到达。 * 带有此标志的为【CQE 1系统调用完成通知】。 * * - IORING_CQE_F_NOTIF (0x08): * 内核 6.0 显式标记当前 CQE 为【CQE 2Zero-Copy DMA 释放通知】。 * 此时网卡驱动已完成物理页的 SG-DMA 读取且 SKB 在内核网络栈中的引用计数降为 0。 */bool has_more(cqe-flagsIORING_CQE_F_MORE)!0;bool is_notif(cqe-flagsIORING_CQE_F_NOTIF)!0;printf( 收到 CQE #%d | user_data(Buf Index): %u | res: %d | flags: 0x%x (MORE:%d, NOTIF:%d)\n,completed_cqes1,b-buf_index,cqe-res,cqe-flags,has_more,is_notif);/* * 分支一处理 CQE 1 (Syscall Complete) */if(has_more){if(cqe-res0){fprintf(stderr, └── [CQE 1 异常] 系统调用排队发送失败: %s\n,strerror(-cqe-res));}else{b-bytes_sentcqe-res;b-stateBUF_STATE_WAIT_NOTIF;printf( └── [CQE 1 语义] 数据已成功入队套接字发送队列字节数: %zd\n,b-bytes_sent);printf( └── ⚠️ [内存安全警示] 物理页仍挂载在网卡 DMA 描述符中绝对禁止重写/释放\n);}}/* * 分支二处理 CQE 2 (DMA Complete / Notification) */else{if(b-stateBUF_STATE_WAIT_NOTIF||is_notif){printf( └── [CQE 2 语义] 收到硬件 DMA 释放通知SKB 引用计数归零。\n);// 执行无锁安全回收压回应用层无锁栈buffer_pool_push(pool,b);printf( └── ✅ [安全回收完成] Buffer Index %u 状态恢复为 BUF_STATE_FREE可安全写入新数据\n,b-buf_index);}else{// 异常降级处理 (如未排队直接报错只产生单 CQE)printf( └── [单 CQE 降级] 请求直接终结执行直接回收。\n);buffer_pool_push(pool,b);}}completed_cqes;// 标记 CQE 已消费更新共享 CQ 环 head 指针 (无锁内存屏障指令)io_uring_cqe_seen(ring,cqe);}printf(\n [4] 清理阶段注销内核预注册缓冲区 \n);/* * 源码分析 6注销映射 * 解绑内核 io_mapped_ubuf 结构减少物理页引用计数 (Unpin) */io_uring_unregister_buffers(ring);close(sv[0]);close(sv[1]);io_uring_queue_exit(ring);for(inti0;iBUF_COUNT;i){free(pool.bufs[i].data);}printf([5] 程序正常终结资源已全部清理。\n);return0;}3. 性能优化与极致调优对比为了在生产环境中获得最高的单核吞吐量Mops通常会将IORING_REGISTER_BUFFERS与其他io_uring高级特性组合使用极致性能优化链路 (Full Offload) ┌──────────────────────────────────────────────────────────────────────────────────┐ │ 1. IORING_REGISTER_BUFFERS ──► 消除 GUP 页表遍历与 MMU 锁 │ │ 2. IORING_REGISTER_FILES ──► 消除 fget()/fput() 文件描述符引用计数原子锁 │ │ 3. IORING_SETUP_SQPOLL ──► 消除 io_uring_enter() 内核系统调用开销 │ │ 4. Single-Producer/Consumer ──► 共享 CQ 环无锁解析 (0 锁争抢) │ └──────────────────────────────────────────────────────────────────────────────────┘极致场景下的数据通路对比特性维度传统sendmsg()普通IORING_OP_SEND_ZC结合REGISTER_BUFFERSREGISTER_FILES内存拷贝存在 (User→ \to→SKB)0 拷贝0 拷贝页表 Pinning (GUP)动态 (Kernel Space Copy)动态(get_user_pages_fast)0 开销(初始化预钉扎)原子引用计数锁存在 (SKB / Socket)存在 (page-_refcount增减)0 开销(引用固定页)FD 锁开销每次fget_light()每次fget_light()0 开销(固定文件索引)系统调用 CPU 开销每次 I/O 触发 Syscall批处理 Syscall0 Syscall(搭配SQPOLL)
返回列表