io_uring原理

2022-11-02  本文已影响0人  谭英智

简介

io_uring是2019年在linux新增的异步IO接口。

它的出现是为了替代linux的旧的异步IO接口aio。

它以高效率、适用面广碾压aio

io_uring可以访问direct io、buffer io和网络IO

它的性能近乎原生的内存操作

在磁盘访问方面,媲美spdk

在网络访问方面,以多倍的优势超越epoll

AIO存在的问题

io_uring整体架构图

io_uring_overview

描述:

读文件过程(以SQPOLL为例):

写文件过程(以SQPOLL为例):

可以看到,除了初始化io_uring发生了系统调用

read和write都是内存操作

SQ与CQ的设计,避免了用户态和内核态之间的内存拷贝、

唯一的开销是原子变量的同步

设计关键点

性能数据

Interface       QD      Polled          Latency         IOPS
--------------------------------------------------------------------------
io_uring        1       0                9.5usec         77K
io_uring        2       0                8.2usec        183K
io_uring        4       0                8.4usec        383K
io_uring        8       0               13.3usec        449K

libaio          1       0                9.7usec         74K
libaio          2       0                8.5usec        181K
libaio          4       0                8.5usec        373K
libaio          8       0               15.4usec        402K

io_uring        1       1                6.1usec        139K
io_uring        2       1                6.1usec        272K    
io_uring        4       1                6.3usec        519K
io_uring        8       1               11.5usec        592K

spdk            1       1                6.1usec        151K
spdk            2       1                6.2usec        293K
spdk            4       1                6.7usec        536K
spdk            8       1               12.6usec        586K

数据结构与API

CQe:完成队列事件

struct io_uring_cqe {
    __u64 user_data; //用户自定义数据,来自SQe
    __s32 res;       //返回的结果
    __u32 flags;    //保留字段
};

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];
    };
};

CQ与SQ的提交

unsigned head;
head = cqring->head;

read_barrier();

// 头不等于尾,环未满
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();

CQ、SQ是一个由数组组成的环形队列

通过变动head的索引来完成对队列的插入操作

CQ与SQ的消费

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

CQ、SQ是一个由数组组成的环形队列

通过变动tail的索引来完成对队列的弹出操作

io_uring原生API

原生API只有三个,由于在写的过程中设计过多的体系结构知识,例如读写屏障,并不容易使用,所以下面只简单描述

SQE排序

如果多个sqe没有特别说明,在内核中是乱序执行的

但某些场景,为了保证数据的完整性,例如在一系列写操作后,然后调用fsync

io_uring支持将sqe的flags字段设置成IOSQE_IO_DRAIN,然后将sqe提交到内核中,io_uring可以保证在所有之前的请求完成前是不会执行这个sqe的

链式sqes

sqe还提供一钟链式执行的顺序,保证sqes按顺序一个一个的执行

通过设置IOSQE_IO_LINK、IO_SQE_LINK来完成

liburing

用于简化原生API

轮询IO(IOPOLL)

通过轮询,可以减少睡眠产生的上下文切换

对于低延时和IOPS高的应用来说,轮询IO可以提供机制的性能

通过注册IORING_SETUP_IOPOLL

使用轮询后,应用不能通过检查CQ尾部来检查完成事件,必须通过不断调用io_uring_enter来检查并获取完成事件

内核轮询(SQPOLL)

启动SQPOLL后,应用对SQ的提交不需要调用io_uring_enter来通知内核

SQPOLL会自动检测,从而可以减少系统调用

为了避免再非活跃期浪费过多的CPU,当内核线程空闲一段时间后,它会自动睡眠

用户态需要检测内核线程是否睡眠,而决定是否去wakeup内核线程

// 添加新的 sqe 条目
add_more_io();

// 如果可轮询并且线程已经睡眠,需要调用 io_uring_enter() 使内核发现一个新的  IO
if ((*sqring->flags) & IORING_SQ_NEED_wAKEUP)
    io_uring_enter(ring_fd, to_submit, to_wait, IORING_ENTER_SQ_WAKEUP);
上一篇 下一篇

猜你喜欢

热点阅读