本文是对 https://kernel.dk/io_uring.pdf 第4部分的翻译:
一起来看io_uring 内部的数据结构
目录
4.2 通信通道 Communication Channel : ring buffer的细节
4.0 开始了解 io_uring
设计之初,性能的考量就被纳入计划中。否则接口一旦固定,就很难再修改了;我们不需要在提交和完成事件上有内存拷贝,或者像aio一样有间接的内存使用。应用和内核要共享IO的结构和完成的事件;
由于不需要复制,内核和应用程序必须更好的共享IO结构和完成事件,两者之间的同步必须有一定机制来进行管理:满足我们需求的一种数据结构是单生产者和单消费者环形缓冲区。使用共享环形缓冲区,我们可以消除应用程序和内核之间共享锁定的需要,而不需要巧妙地使用内存顺序和障碍。
异步接口有两个基本操作:提交请求和完成事件;提交请求:应用程序产生数据,内核消费;完成事件:内核产生完成的事件,应用程序消费事件作进一步处理。因此我们需要2个ring buffer来提供有效的通信。两个ring buffer被命名为提交队列(SQ)和完成队列(CQ)。
4.1 数据结构 CQ与SQ
completion侧:它需要携带与操作结果相关的信息, cqe代表Completion Queue Event。
struct io_uring_cqe {
__u64 user_data; /* sqe->data submission passed back */
__s32 res; /* result code for this event */
__u32 flags;
};
user_data
字段来自提交的请求
并且可以包含程序识别该请求所需的任何信息
一种常见的使用场景是使其成为指向请求
的指针
内核不会修改这个字段,只是简单的直接从提交(submission)
传递给完成事件(completion event)
res
保留了请求的结果,可以认为他就像系统调用返回的值,对于正常的读写操作,会类似于 read 和 write 的返回值,对于成功的操作,他会包含传输的字节数,如果出现异常,他会包含一个负的错误值。例如,发生了 IO error,res
将会包含-EIO
flags
可以携带此操作相关的元数据,现在这个字段还未使用
submit侧因为需要提供可扩展性,结构更复杂一些:
提交侧的结构被称为Submission Queue Entry简称sqe
struct io_uring_seq {
__u8 opcode;
__u8 flags;
__u16 ioprio;
__u32 fd;
__u64 off;
__u64 addr;
__u32 len;
union {
__kernel_rwf_t rw_flags;
__u32 fsync_flags;
__u16 poll_events;
__u32 sync_range_flags;
__u32 msg_flags;
};
__u64 user_data;
union {
__u16 buf_index;
__u64 __pad2[3];
};
};
opcode
:表示本次请求的操作码,例如一个矢量读取的操作码IORING_OP_READV
flags:
标识符(IORING_SETUP_IOPOLL,IOSQE_FIXED_FILE等)ioprio:
代表请求的优先级fd:
文件描述符off:
操作的偏移量addr
:- 如果
op-code
描述的是传输数据的操作,addr
包含了该操作执行相应IO的地址
如果操作是向量化读写,addr是所使用的指向iovec
数组结构的指针(例如preadv的指针); - 对于非向量 IO 传输,
addr
必须直接包含地址;
- 如果
len
表示非向量 IO 传输的字节数或者对于向量 IO 传输,表示 addr 指向的向量个数 (iovecs 数组的长度)user_data
:所有op-code通用,并且不会被内核修改。当该请求完成时复制到完成事件cqe
中;- buf_index:结构末端的填充,以帮助SQE在内存中对齐64字节。将来也可用于放入更多的请求描述信息; (比如一组kv值,或者checksum信息)。
4.2 通信通道 Communication Channel : ring buffer的细节
CQ
CQE被放在数组中,对内核和应用程序可见。
但是CQE是由内核生成的(完成事件),因此时间上只有内核在修改CQE。每次有新的事件会发布到CQ ring buffer,更新尾部,当应用程序get events时,使用头部。因此,如果tail和head不同,应用就知道还有一个或者更多events 可以被消费。
环行计数器(ring counter) 是32 位整数,并且当完成事件数超过环的容量时会自然计算环项索引,这种方法的优势之一是我们可以利用环的完整大小,而无需另外管理环已满的标志,因此要求环必须是 2 的幂等
ring buffer的size:2的幂
要查找事件的index,应用程序必须使用ring buffer的大小掩码屏蔽当前尾部索引。这
通常如下所示:
unsigned head;
head = cqring->head;
read_barrier();
// ring buffer没有满
if (head != cqring->tail) {
struct io_uring_cqe *cqe;
unsigned index;
// 计算 索引位置
index = head & (cqring->mask);
cqe = &cqing->cqes[index];
head++;
}
cqring->had = head;
write_barrier();
其中ring→cqes[] 是共享数组的具体结构;后面会继续探讨相关的设置管理和读写barrier机制;
SQ
提交请求:应用添加entry到ring buffer的tail。内核从head消费entry。SQE的操作和CQE操作相反。
与CQ不同的是,CQ ring buffer直接索引共享的数组cqes,但是提交侧有一个间接的array在中间。提交
操作的ring buffer是此数组的索引,而该数组又包含 sqes
的索引。原因:一些应用程序可能在应用内部嵌入请求的entry,这样处理起来更灵活,同时保留在一个操作中提交多个SQE的能力。
struct io_uring_seq *sqe;
unsigned tail, index;
tail = sqring->tail;
index = tail & (*sqring->ring_mask);
sqe = &sqring->specs[index];
init_io(sqe);
sqring->array[index] = index;
tail++
write_barrier();
sqring->tail = tail;
write_barrier();