第 5 章 · 02 uring.h io_uring 异步 I/O


文档摘要

第 5 章 · 02 uring.h iouring 异步 I/O 本节摘要: 是 Colibrì 的 Linux iouring 异步 I/O 实现,一个头文件自包含全部逻辑。 用 系统调用建环, 把 SQ ring、CQ ring、SQEs 三段共享内存映射进用户态; / 提供 SQ tail / CQ head 的生产者-消费者同步; 提交定位读、 / 收割 CQE,全程无用户态自旋循环。整个 ring 由单线程拥有, 守卫保证非 Linux 平台编译干净。 内容来源:原项目源码 (全文 137 行) ⚠️ 注意:iouring 是 Linux 5.1+ 的现代异步 I/O 接口,相对传统 /线程池有质的飞跃——批量提交、内核态完成、共享内存环形缓冲、零拷贝。

第 5 章 · 02 uring.h io_uring 异步 I/O

本节摘要:uring.h 是 Colibrì 的 Linux io_uring 异步 I/O 实现,一个头文件自包含全部逻辑。coli_uring_initSYS_io_uring_setup 系统调用建环,mmap 把 SQ ring、CQ ring、SQEs 三段共享内存映射进用户态;__atomic_load_acquire/__atomic_store_release 提供 SQ tail / CQ head 的生产者-消费者同步;coli_uring_prep_read 提交定位读、coli_uring_enter/coli_uring_peek 收割 CQE,全程无用户态自旋循环。整个 ring 由单线程拥有,__linux__ 守卫保证非 Linux 平台编译干净。

内容来源:原项目源码 c/uring.h(全文 137 行)

⚠️ 注意:io_uring 是 Linux 5.1+ 的现代异步 I/O 接口,相对传统 aio/线程池有质的飞跃——批量提交、内核态完成、共享内存环形缓冲、零拷贝。Colibrì 没有用任何封装库(liburing 也没用),而是直接 syscall + mmap,这让它对 io_uring 的机制理解极深。读懂这个头文件,就读懂了"零依赖纯 C"如何驾驭现代内核异步。

学习目标

  1. 读懂 ColiUring 结构体各字段含义。
  2. 理解 coli_uring_init 的 setup + 三段 mmap 流程。
  3. 理解 __atomic_load_acquire/__atomic_store_release 在 SQ/CQ 同步中的作用。
  4. 读懂 coli_uring_prep_read 提交 + coli_uring_peek 收割。
  5. 理解为什么用 io_uring 而不是线程池,以及 __linux__ 守卫。

一、ColiUring 结构:ring 拥有的全部状态

文件开头先 __linux__ 守卫,只在 Linux 编译这段代码:

1 #ifndef COLI_URING_H 2 #define COLI_URING_H 3 /* Minimal Linux io_uring reader. Colibri owns the ring from one thread, queues 4 * a batch of positioned reads, and reaps CQEs without a userspace spin loop. */ 5 #ifdef __linux__

注释三句话点明设计意图:minimal、单线程拥有、批量定位读 + 收割 CQE 无自旋。ColiUring 结构体把整个 ring 的状态打包到一起:

20 typedef struct { 21 int fd; 22 struct io_uring_params p; 23 void *sq_map, *cq_map, *sqes_map; 24 size_t sq_map_sz, cq_map_sz, sqes_map_sz; 25 unsigned *sq_head, *sq_tail, *sq_mask, *sq_entries, *sq_array; 26 unsigned *cq_head, *cq_tail, *cq_mask, *cq_entries; 27 struct io_uring_sqe *sqes; 28 struct io_uring_cqe *cqes; 29 } ColiUring;

fd 是 ring 的文件描述符;p 是 io_uring_params,setup 时内核回填偏移信息。三段 mmap 映射:sq_map(submission queue ring)、cq_map(completion queue ring)、sqes_map(SQE 数组)。后面 5+5 个 unsigned * 是 SQ/CQ ring 的 head、tail、mask、entries 指针(SQ 还多一个 array),最后是 sqes/cqes 两个数组指针。所有指针都指向 mmap 出来的共享内存,用户态和内核态共享同一份 ring。

二、内存序:acquire/release 配对生产者-消费者

io_uring 的 SQ/CQ 是经典的单生产者-单消费者环形缓冲,用户态和内核态各扮一角。同步必须用正确的内存序:

31 static inline unsigned coli_uring_load_acquire(unsigned *p){ 32 return __atomic_load_n(p,__ATOMIC_ACQUIRE); 33 } 34 static inline void coli_uring_store_release(unsigned *p,unsigned v){ 35 __atomic_store_n(p,v,__ATOMIC_RELEASE); 36 }

acquire 读保证后续的普通读写不会被重排到它前面;release 写保证之前的普通读写不会被重排到它后面。两者配对形成 happens-before:用户态 store_release SQ tail 之后,内核 load_acquire SQ tail 一定能看到前面填好的 SQE 内容;反之亦然。

注意 36-37 行的 coli_uring_close:三段 munmap(SQ 和 CQ 在 SINGLE_MMAP 模式下是同一段,只 munmap 一次),然后 close(fd),最后 memset(r,0,sizeof(*r)); r->fd=-1; 把结构体重置成"已关闭"状态,防止重复关闭。

三、coli_uring_init:setup + 三段 mmap

47 static inline int coli_uring_init(ColiUring *r,unsigned entries){ 48 memset(r,0,sizeof(*r)); r->fd=-1; 49 r->fd=(int)syscall(SYS_io_uring_setup,entries,&r->p); 50 if(r->fd<0) return -1;

第 49 行 SYS_io_uring_setup 创建 ring,内核把 ring 的布局信息(各字段偏移、entries 数、features 位)回填到 r->p。接下来是三段 mmap。

54 if(r->p.features&IORING_FEAT_SINGLE_MMAP){ 55 size_t n=r->sq_map_sz>r->cq_map_sz?r->sq_map_sz:r->cq_map_sz; 56 r->sq_map=mmap(NULL,n,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_POPULATE,r->fd,IORING_OFF_SQ_RING); 57 if(r->sq_map==MAP_FAILED){ r->sq_map=NULL; coli_uring_close(r); return -1; } 58 r->sq_map_sz=n; r->cq_map=r->sq_map; r->cq_map_sz=n; 59 }else{ ...两段分别 mmap SQ 和 CQ... }

第 54 行处理 SINGLE_MMAP feature:内核把 SQ ring 和 CQ ring 放在同一段共享内存,只用一次 mmap 拿下两段(cq_map = sq_map)。否则(第 59 行 else 分支)分别 mmap SQ ring(偏移 IORING_OFF_SQ_RING)和 CQ ring(偏移 IORING_OFF_CQ_RING)。第 65-67 行单独 mmap SQE 数组(偏移 IORING_OFF_SQES),因为 SQE 不和 ring 同段。

第 69-80 行把 ring 内的字段指针全部算出来:

69 char *sq=(char*)r->sq_map,*cq=(char*)r->cq_map; 70 r->sq_head=(unsigned*)(sq+r->p.sq_off.head); 71 r->sq_tail=(unsigned*)(sq+r->p.sq_off.tail); ... 79 r->sqes=(struct io_uring_sqe*)r->sqes_map; 80 r->cqes=(struct io_uring_cqe*)(cq+r->p.cq_off.cqes);

每个字段用 setup 回填的偏移(sq_off.head、cq_off.cqes 等)定位到共享内存里的确切位置。这是 io_uring 跨内核版本兼容的关键:字段顺序可能变,但偏移由内核在 setup 时告诉用户态。

四、提交定位读 + 收割 CQE 无自旋

coli_uring_prep_read 提交一次定位读(pread 的异步版):

89 static inline int coli_uring_prep_read(ColiUring *r,int fd,void *buf,size_t len, 90 int64_t off,uint64_t user_data){ 91 if(!len || len>UINT32_MAX){ errno=EINVAL; return -1; } 92 unsigned head=coli_uring_load_acquire(r->sq_head); 93 unsigned tail=__atomic_load_n(r->sq_tail,__ATOMIC_RELAXED); 94 if(tail-head>=*r->sq_entries){ errno=EAGAIN; return -1; } 95 unsigned idx=tail&*r->sq_mask; 96 struct io_uring_sqe *sqe=&r->sqes[idx]; 97 memset(sqe,0,sizeof(*sqe)); 98 sqe->opcode=IORING_OP_READ; 99 /* Cold regular-file reads are allowed to execute inline during 100 * io_uring_enter() unless forced async. That serializes the submitter on 101 * filesystems without native nonblocking buffered reads and destroys the 102 * intended I/O/compute overlap. io-wq gives the ring a real bounded worker 103 * pool while CQEs retain completion ordering/ownership here. */ 104 sqe->flags=IOSQE_ASYNC; 105 sqe->fd=fd; 106 sqe->off=(uint64_t)off; 107 sqe->addr=(uint64_t)(uintptr_t)buf; 108 sqe->len=(uint32_t)len; 109 sqe->user_data=user_data; 110 r->sq_array[idx]=idx; 111 coli_uring_store_release(r->sq_tail,tail+1);

第 92-93 行:head 用 acquire 读(保证看到内核更新),tail 用 relaxed 读(只有自己写)。第 94 行检查 ring 是否满。第 95-110 行填一个 SQE,opcode 是 IORING_OP_READ(定位读),关键是第 104 行 IOSQE_ASYNC 标志——注释解释:冷的正则文件读允许在 io_uring_enter() 里同步执行,这会在没有原生非阻塞缓冲读的文件系统上把提交者串行化,毁掉 I/O/计算重叠;IOSQE_ASYNC 强制让 io-wq 内核工作线程池处理,保住异步语义。第 111 行 store_release tail,通知内核有新 SQE。

coli_uring_peek 收割一个 CQE,非阻塞:

127 static inline int coli_uring_peek(ColiUring *r,struct io_uring_cqe *out){ 128 unsigned head=__atomic_load_n(r->cq_head,__ATOMIC_RELAXED); 129 unsigned tail=coli_uring_load_acquire(r->cq_tail); 130 if(head==tail) return 0; 131 *out=r->cqes[head&*r->cq_mask]; 132 coli_uring_store_release(r->cq_head,head+1); 133 return 1;

第 129 行 acquire 读 CQ tail(看到内核填好的 CQE 内容),第 130 行空就返回 0,第 131-132 行取出 CQE、advance head 并 release 通知内核。整个过程没有用户态自旋循环——配合 coli_uring_enter(min_complete>0) 一次性阻塞等一批完成,再 peek 逐个收割。

五、为什么是 io_uring,为什么单线程拥有

为什么 io_uring 而不是线程池:线程池的代价是上下文切换 + 线程栈内存 + 锁竞争,而 io_uring 把完成事件交给内核态工作线程,用户态只看到一个共享内存环形队列——批量提交一次 syscall、收割一批 CQE,系统调用次数极少。对于"每 decode step 提交十几个专家读"这种场景,io_uring 比 read 线程池高效得多。

为什么 ring 由单线程拥有:io_uring 的 SQ 是单生产者设计,如果多线程同时填 SQE 需要 SQ 锁,违背 minimal 设计。Colibrì 让 I/O 线程独占 ring,计算线程把"需要的专家列表"丢给 I/O 线程,I/O 线程批量提交 + 收割,无锁。

__linux__ 守卫:io_uring 是 Linux 专属。整段代码包在 #ifdef __linux__ 里,非 Linux 平台(Windows、macOS)这段被跳过,引擎回退到同步 read 路径,保证全平台可编译。第 136 行 #endif /* __linux__ */ 配对外层 include guard。

💡 深潜要点:Colibrì 没有用 liburing,而是直接 syscall + mmap 手写 io_uring 客户端。这不是"重新发明轮子",而是"零依赖"原则的彻底贯彻——整个引擎不依赖任何非系统库,连 io_uring 这种复杂接口都自己拼。代价是代码更底层、可读性更难,收益是单二进制可移植、编译期零外部依赖、对 io_uring 机制有完全控制。IOSQE_ASYNC 这个标志的选择就是这种控制的体现——liburing 默认行为不一定符合 Colibrì 的重叠需求,自己拼 SQE 才能精确强制异步。

本节要点回顾

  1. ColiUring 把 fd、params、三段 mmap 映射、SQ/CQ 字段指针打包,全部指向共享内存。
  2. coli_uring_init: SYS_io_uring_setup 建环 + 三段 mmap(SINGLE_MMAP 时 SQ/CQ 同段) + 偏移定位字段。
  3. __atomic_load_acquire/__atomic_store_release 配对 SQ tail / CQ head 的生产者-消费者同步。
  4. coli_uring_prep_readIOSQE_ASYNC 强制 io-wq 异步,避免同步执行毁掉重叠;coli_uring_peek 收割 CQE 无自旋。
  5. io_uring 比线程池省(无切换/无栈/无锁),ring 单线程拥有,__linux__ 守卫保证非 Linux 编译干净。

下一节:异步 I/O 讲完了,我们看 O_DIRECT 与读写算重叠——O_DIRECT 绕过页缓存、readahead/PILOT 预取、批量专家联合读取、OMP 并行 pin/warmup,看清 Colibrì 如何把"I/O 当成引擎的一部分",拒绝假装存储延迟免费。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U