io_uring介绍(2)数据结构

​本文是对 https://kernel.dk/io_uring.pdf  第4部分的翻译:

一起来看io_uring 内部的数据结构

目录

4.0 开始了解 io_uring

4.1 数据结构 CQ与SQ

4.2 通信通道 Communication Channel : ring buffer的细节

CQ

SQ


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();

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值