# io_uring介绍

最近看到liburing的[文档](https://kernel.dk/io_uring.pdf)，感觉对iouring的很多的原理和使用方式有一个比较大概的了解，对于想知道这套接口到底做了什么能比较好的入门，而且[liburing](https://github.com/axboe/liburing)这套lib的代码也非常容易懂，下面是自己看文档的笔记.

## 1\. io_uring的起源

Linux之前其实已经有一套异步io的接口，但是这套接口有着几个比较明显的限制:

- 最最大的限制在于: 只有使用`O_DIRECT`模式下面才能支持异步访问，因为只能支持`O_DIRECT`的话，那么就不能使用缓存和需要做内存对齐的限制，所以导致这个接口并不是很通用;

- 依然会blocking; 即使你满足了所有关于这个接口的限制，依然存在多种方式会导致blocking; 比如meta-data需要被执行io的话，那么提交这个过程将会blocking；又或者当底层存储设备的slot都在被使用的过程中，也会导致blocking，直到最终一个slot变得可用；看到描述的感觉就是不可控，而且对于异步编程来说，blocking是一个很重大的问题，因为会导致你的整个异步框架问题；

- API 不是很好；这边主要说的是性能问题，有太多的内核到userspace的copy；



#### 为何选择重新设计一套接口而不是优化之前的aio的接口呢？

当时内核组的想法是提升aio性能和复用原先的接口，原因是

- 改善和优化现存接口会由于提供新的接口；用户采用一个新的接口是需要很长很长的时间

- 兼容原先接口可以兼容原先使用老接口的应用，从这个成眠来说减少了接入的工作量

但是最终发现要修复和改进目前的状态，一方面会导致整个接口变得无比复杂，也不能到达所谓的预期，于是就推到重来;

#### 新设计的目标

- 更加容易被使用，更加难被误用；

- 有更加广泛的使用场景；主要是说这套接口能使用场景更加广泛，除了文件io还能支持类似的block设备或者网络接口等等；这套接口需要对未来有足够的开放性；

- 更丰富的扩展性；这边主要是的是需要对兼容目前已有的使用方式，能让用户一套接口实现目前已有的所有；防止用户需要为了不同的功能用两套不同的接口;

- 更丰富的功能；这边主要是的是需要对兼容目前已有的使用方式，能让用户一套接口实现目前已有的所有；防止用户需要为了不同的功能用两套不同的接口;

- 更加好的性能；

- 可伸缩性；可能是能随着底层设备的伸缩，接口本身能支持到线性扩展的能力；

## 2\. 正式进入IO_URING

io_uring虽然有那么多的目标，但是第一优先级在于性能，而性能的最重要的点在于`kernel-user`之间的内存copy，要去除这个copy的话，就需要共享内存，但是同时需要避免所谓的并发问题的；最终找到了一个数据结构能得到要求：`ring-buffer`, 应该就是环形队列的；

1. **SPSC**: 单生产者单消费者；

2. 通过`memory-barrier`的方式来保证并发安全；在这边极力的避免所谓的lock的操作，lock对性能影响太大；

这边一定要注意的是单消费单生产者的这个前提，不然会觉得后面的实现貌似不thread-safe; 假设我们用`writeIndex`和`readIndex`来表示写和读的索引，可能会产生线程安全的问题主要在于判断队列full或者empty，这个通过比较writeIndex和readIndex的方式来进行，而对这两个变量的访问可以通过`memory-barrier`的方式来保护；

对于目前的异步接口，有两个最为重要的操作；分别为: 提交请求和与消费这个请求关联的完成事件；对于提交事件来说,application是生产者，而kernel是消费者；对于消费完成事件的话，kernel是生产者，application是消费者；因此这个新的接口会有两个`ring-buffer`

- SQ: 提交队列

- CQ: 完成队列



### 2\.1 数据结构

```cpp
// complete event struct
struct io_uring_cqe {
__u64 user_data;
__s32 res;
__u32 flags;
};
```

- `user_data`: 一个指针，从提交请求那边带过来的，可以带各种信心，内核是不会去碰这个数据的；

- `res`: 本次request的最终结果，就是正常system call的结果，和之前read/write的一样；

- `flags`: 用来带与这个操作的元信息，但是目前还没用；

相比于完成event的数据结构，request的数据结构会更加复杂很多；

```cpp
struct io_uring_sqe {
__u8 opcode;
__u8 flags;
__u16 ioprio;
__s32 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`: 本次request的操作类型，比如`IOURING_OP_READV`表示向量读

- `flags`: 一些特殊的配置，通过flags来进行设置

- `ioprio`:操作优先级，可以通过`ioprio_set`的方式来进行设置

- `fd`: 本次请求关联的文件句柄

- `off`:offset

- `addr`: 读写最终的地址，比如buffer的地址，又或者如果`opcode`设置成`IOURING_OP_READV`的话，那么addr是就是数组首地址；

- `len`:配合addr一起事情，比如读取或者写入的长度;

- `union`:需要配合特殊的opcode来使用；

- `user_data`:和之前完成event的field对应的；

- `buf_index`:也是高端使用；

### 2\.2 内核和用户空间的通信 - ring-buffer with no lock

- cqr: completion queue ringbuffer

    完成队列kernel负责生产，而producer负责消费，整体的size是一个32bits的整数，这个方法的好处是：可以充分利用所有的空间，不需要去设置所谓的full flag来表示空间满的问题；劣势是size的长度必须是2的指数次的大小；

    ```plain
    环形队列在没有特殊的flag的情况下是如何判断队列满或者空呢？
    * tailer:表示之前写入的位置
    * head: 表示第一个可读的位置;
    
    在push的过程中，判断tailer+1 == head: 如果相同，说明队列满；
    在pop的过程中，判断head==tailer: 如果相同，说明队列空；
    
    这种适合SPSC这种场景
    ```

    下面是一段伪码，用来描述消费这个ring的过程:

    ```cpp
    unsigned head;
    head = cqring→head;
    read_barrier();
    if (head != cqring→tail) {
      struct io_uring_cqe *cqe;
      unsigned index;
      index = head & (cqring→mask);
      cqe = &cqring→cqes[index];
      /* process completed cqe here */
      ...
      /* we've now consumed this entry */
      head++;
    }
    cqring→head = head;
    write_barrier();
    ```

    memory-barrier主要是**用来保证在多线程情况下，各个线程看到指令可排序**；它并不用来解决并发问题，它主要是来解决内存load和store的顺序问题；

    1. 获得head

    2. 判断队列是否为空

    3. 消费entry

    4. 修改head

    `ring-cqe[]`是一个共享内存数组，连续的; 这个与sqe ring 有点不一样；

- sqr: subimssion queue ringbuffer

    sqr与cqr的最大区别点在于：cqr是一个连续的数据，并且cqe就是这个数组的entry；sqr本质上有两个数组，也就是两个ring；主要的目的在于获得sqe,并且填充sqe，最后提交sqe之间的过程是分开的的；一个索引ring主要是为了对真正提交的sqe，这个ring也是给kernel来看的，在不主动提交io请求之前，也就是获得sqe并且在填充sqe过程中，索引的ring的tail是不会变化的，内核也是不会知道有数据需要被消费的；这样从用户层面来说可以按照业务的要求提交多个io请求；

    这个设计我一开始很奇怪，但是文档上给出的理由是：这种方式比较灵活;

    > Some applications may embed request units inside internal data structures, and this allows\
    > them the flexibility to do so while retaining the ability to submit multiple sqes in one operation. That in turns allows for\
    > easier conversion of said applications to the io_uring interface.
    >
    > 一个op中包含了多个sqe操作；使用这种方式的话会更加灵活，没有顺序的限制；

    ```cpp
    struct io_uring_sqe *sqe;
    unsigned tail, index;
    tail = sqring→tail;
    index = tail & (*sqring→ring_mask);
    
    sqe = &sqring→sqes[index];
    /* this call fills in the sqe entries for this IO */
    init_io(sqe);
    /* fill the sqe index into the SQ ring array */
    sqring→array[index] = index;
    tail++;
    write_barrier();
    sqring→tail = tail;
    write_barrier();
    ```

完成事件到达的速度是无序的，和提交到内核的是没有关系的，如果需要支持提交事件的依赖的话，有一些高级功能iouring也支持；

### 2\.3 io_uring提供的接口

#### 1\. 初始化iouring的实例

```cpp
int io_uring_setup(unsigned entries, struct io_uring_params *params);

struct io_uring_params {
  __u32 sq_entries;
  __u32 cq_entries;
  __u32 flags;
  __u32 sq_thread_cpu;
  __u32 sq_thread_idle;
  __u32 resv[5];
  struct io_sqring_offsets sq_off;
  struct io_cqring_offsets cq_off;
};
```

- `entries`: 用来指定sqe的个数，可以认为是提交队列的长度，cqe的长度是sqe的两倍; 要求为2的指数倍

- `params`: 一些传给内核的参数；这个参数比较特殊，是有用户和kernel两边来读写的；说明kernel会通过这个参数返回一些数据

    - `sq_entries`: 个数

    - `cq_entries`: 同上

    - `sq_off`和`cq_off`：这两个参数很重要，会返回一些内核中ring的内存信息，而user可以通过`mmap`系统调用的方式来进行无缝对接，这样跟后面的所有的访问可以做到无内核态和用户态的内存copy，这个一段公共内存；

```cpp
struct io_sqring_offsets {
  __u32 head; /* offset of ring head */
  __u32 tail; /* offset of ring tail */
  __u32 ring_mask; /* ring mask value */
  __u32 ring_entries; /* entries in ring */
  __u32 flags; /* ring flags */
  __u32 dropped; /* number of sqes not submitted */
  __u32 array; /* sqe index array */
  __u32 resv1;
  __u64 resv2;
};
```

通过这些参数我们就可以对iouring中的sqe的ring和cqe的ring来进行操作的；下面这段是libiouring对这个参数的使用:

```cpp
static int io_uring_mmap(int fd, struct io_uring_params *p,
			 struct io_uring_sq *sq, struct io_uring_cq *cq)
{
	size_t size;
	int ret;

	size = sizeof(struct io_uring_cqe);
	if (p->flags & IORING_SETUP_CQE32)
		size += sizeof(struct io_uring_cqe);

	sq->ring_sz = p->sq_off.array + p->sq_entries * sizeof(unsigned);
	cq->ring_sz = p->cq_off.cqes + p->cq_entries * size;

	if (p->features & IORING_FEAT_SINGLE_MMAP) {
		if (cq->ring_sz > sq->ring_sz)
			sq->ring_sz = cq->ring_sz;
		cq->ring_sz = sq->ring_sz;
	}
// array index
	sq->ring_ptr = __sys_mmap(0, sq->ring_sz, PROT_READ | PROT_WRITE,
				  MAP_SHARED | MAP_POPULATE, fd,
				  IORING_OFF_SQ_RING);
	if (IS_ERR(sq->ring_ptr))
		return PTR_ERR(sq->ring_ptr);

	if (p->features & IORING_FEAT_SINGLE_MMAP) {
		cq->ring_ptr = sq->ring_ptr;
	} else {
		cq->ring_ptr = __sys_mmap(0, cq->ring_sz, PROT_READ | PROT_WRITE,
					  MAP_SHARED | MAP_POPULATE, fd,
					  IORING_OFF_CQ_RING);
		if (IS_ERR(cq->ring_ptr)) {
			ret = PTR_ERR(cq->ring_ptr);
			cq->ring_ptr = NULL;
			goto err;
		}
	}

	size = sizeof(struct io_uring_sqe);
	if (p->flags & IORING_SETUP_SQE128)
		size += 64;
//真正存储sqe的
	sq->sqes = __sys_mmap(0, size * p->sq_entries, PROT_READ | PROT_WRITE,
			      MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQES);
	if (IS_ERR(sq->sqes)) {
		ret = PTR_ERR(sq->sqes);
err:
		io_uring_unmap_rings(sq, cq);
		return ret;
	}

	io_uring_setup_ring_pointers(p, sq, cq);
	return 0;
}
```

`array`:这个表示array在整个区域的偏移量；上面的mmap映射是用来访问sqe的内存结构；通过这种方式，其实我们就可以实现之前的ring-buffer；可以看到两次mmap的最后一个参数是不一样的，用来表示从ring_fd的不同位置；

```cpp
#define IORING_OFF_SQ_RING		0ULL
#define IORING_OFF_CQ_RING		0x8000000ULL
#define IORING_OFF_SQES			0x10000000ULL
#define IORING_OFF_PBUF_RING		0x80000000ULL
#define IORING_OFF_PBUF_SHIFT		16
#define IORING_OFF_MMAP_MASK		0xf8000000ULL
```

看到上面的真实实现之后，最终在用户层构建两个sq ring和cq ring，那么后面用户态的访问就可以继续这两个数据结构来进行；

#### 2\. 提交和消费事件

```!cpp
int io_uring_enter(unsigned int fd, unsigned int to_submit,unsigned int min_complete, unsigned int flags,sigset_t sig);

// to_submit: 表示有几个请求已经提交给你了，内核你可以消费了
// min_complete: 表示我需要等待几个完成事件
// flags: 可以设置一些行为
// IORING_ENTER_GETEVENTS: 表示内核会主动等待有min_complete event完成； 如果想要等待完成事件，这个flag需要设置

```

这个系统调用有两种作用，分别为：

- 提交io请求

- 消费已经完成的event

用户消费cqe的方式有两种，一种是主动等待，也就是io_uring_enter中主动等待，另外一种是去检查cqring即可， 没必要主动等待；



#### 3\. 其他

- 初始化ioring实例的时候flag:

    - IOSQE_IO_DRAIN: 表示在它之前的所有io请求都操作完，才操作它；主要是顺序性；但是这对性能影响很大的，因为有一定的依赖关系，请求与请求之间不能并行进行操作，即使io本身请求无关联；DRAIN表示排空，意思就是排空之前所有的io请求；

    - `IOSQE_IO_LINK`: `DRAIN`这个关系太强了点，尤其是不相关的io都被关联起来的，有的时候可能只需要几个io之前有依赖关系，这个时候就可以用LINK这个标识，来关联几个IO操作；对于这类的IO如果返回的结果是`-ECANCELED`就表示在中间可能出现了一些错误，从而中断了整个chain；从第一个

    - `IORING_OP_TIMEOUT`: 并非是一个真实的数据操作的，而是一个超时，做了做定时器使用的；如果设置了flag的话，那么addr是timeout的参数，offset是cq 完成的个数，两种触发机制；

### 2\.4 memory ordering

1. `memory_order_relaxed`: 最轻松的内存序，不提供任何同步语义。操作可以以任意顺序执行，没有对其他线程的可见性保证。

2. `memory_order_acquire`: 用于获取操作（读取操作），确保对其他线程的读取和写入操作的可见性。该操作前的读取操作不能被重排序到该操作之后，确保所需的数据的可见性。

3. `memory_order_release`: 用于释放操作（写入操作），确保对其他线程的写入操作的可见性。该操作后的写入操作不能被重排序到该操作之前，确保对其他线程的修改的可见性。

4. `memory_order_acq_rel`: 同时具有获取和释放操作的特性，即确保前面的读取操作不会与后面的写入操作重排序，并确保后面的写入操作不会与前面的读取操作重排序。

5. `memory_order_seq_cst`: 顺序一致性内存序，提供最强的内存顺序保证。所有操作都按照全局顺序执行，并且提供了对其他线程操作的最严格的可见性保证

目前我的理解是，memory ordering主要是为了解决单线程情况下的指令排序的问题，虽然说编译器或者cpu在保障单线程结果不影响的情况下进行排序来加速整个效率，但是它保证不了多线程情况下的这样的排序依然是对的；所以需要程序员自己去衡量；而memory ordering本质上不是为了解决多线程的线程安全问题，保证的在单线程的读写操作的顺序是可被保证的；比如当我读到a=1的时候，一定能保证b=2，因为在单线程中，我通过memory ordering保证了这个顺序，那么其他的线程看到a=1的时候就可以保证b一定等于2；当然前提是其他线程也要用memory ordering的方式去读；\* 

```cpp
1: sqe→opcode = IORING_OP_READV;
2: sqe→fd = fd;
3: sqe→off = 0;
4: sqe→addr = &iovec;
5: sqe→len = 1;
6: sqe→user_data = some_value;
write_barrier();/* ensure previous writes are seen before tail write */
7: sqring→tail = sqring→tail + 1;
write_barrier();/* ensure tail write is seen */
```

- `write_barrier`:保证这之后的写操作不会被调整到write_barrier之前，这样保证有什么用呢，这样保证当tail在变化的时候，sqe已经初始化好了；后面的write_barrier，保证这之后的指令都能看到这次修改；

- `read_barrier`: 保证之前的读操作不是被调整到read_barrier这个之后；这样的好处是，当我读到数据是某一个特定值的时候，那么就表示关于这个特定值的假设是成立的；



## 3\. liburing

提供这个库的主要目的：

1. 减少setup iouring的代码

2. 提供更加简单的api

### 3\.1 主要api

- 设置iouring实例

    ```cpp
    struct io_uring ring;
    io_uring_queue_init(ENTRIES, &ring, 0);
    io_uring_queue_exit(&ring);
    ```

    包含了iouring的实例 + mmap的映射之类的，都帮我们完成

- 提交和消费

    ```cpp
    struct io_uring_sqe sqe;
    struct io_uring_cqe cqe;
    /* get an sqe and fill in a READV operation */
    
    sqe = io_uring_get_sqe(&ring);
    io_uring_prep_readv(sqe, fd, &iovec, 1, offset);
    /* tell the kernel we have an sqe ready for consumption */
    io_uring_submit(&ring);
    /* wait for the sqe to complete */
    io_uring_wait_cqe(&ring, &cqe);
    /* read and process cqe event */
    app_handle_cqe(cqe);
    io_uring_cqe_seen(&ring, cqe);
    ```

    `io_uring_cqe_seen`:主要是说某一个sqe已经完成的，可以减少飞行模式的sqe；也会更改cq的head的位置，所以必须在application操作完成之后才能提交完成；有点类似于kafka

    `io_uring_peek_cqe`: 不需要等待，非阻塞的

### 3\.2 高端功能

- 注册文件句柄

    正常情况下，每次向内核提交io的request的时候，内核都会去检索与之相关的文件，当io结束的之后，这个drop掉这个引用；这样每次都进行这样的操作，在一些高性能的场景中是很难被接受的，所以io_uring提供了另外的system call的接口，可以预先注册一些文件句柄到io_uring中，这样就省的每次都关联和关闭的操作；

    ```cpp
    int io_uring_register(unsigned int fd, unsigned int opcode, void *arg,
    unsigned int nr_args);
    /**
    fd: iouring实例的文件句柄
    opcode: 操作类型IORING_REGISTER_FILES
    arg: 一个文件句柄的数组
    nr_args:个数；
    */
    ```

    如何使用呢？在后期的使用的过程中，通过array index的方式来替换真的fd提交给iouring(`sqe→fd`), 然后通过`IOSQE_FIXED_FILE`设置在flag删除，这样iouring会自动在数组中找自己要的fd，并且也不会都drop；有两种会关闭这个资源:

    - iouring实例被free掉

    - 主动调用`io_uring_register`，并且op_code设置`IORING_UNREGISTER_FILES`，可以把已经注册的fd给取消掉；

    还有可以将buffer固定下来，这样的好处是不需要每次都通过mmap把application的buffer到内核空间，可以通过`IORING_REGISTER_BUFFERS`的方式来进行设置，每次提交sqe的时候用`IORING_OP_READ_FIXED`和`IORING_OP_WRITE_FIXED`的方式来告诉内核，可以用固定的buffer，当然前提是所有的读写不能操作固定buffer的长度；

- POLLED IO: 这个很重要的模式；

    正常的io，包括网络或者文件之类io请求，操作系统cpu不会主动去询问当前的io 请求是否完成，都是通过`soft/hard interrupt`来实现的，而cpu会有相关的中断程序来处理这些中断，并且通知对应的应用程序请求已经完成了，可以继续处理；其实这个方式挺好的，可以让出cpu，让cpu去做其他的事情；整体的缺点就是可能会有一定的延迟，需要通过整个中断处理过程来触发的，有的时候cpu进入不能被中断的时候就需要等待，有一定的延迟的；而且当中断太多的时候，也会降低cpu的整体的效率，需要不断的去处理中断处理程序；所以如果为了降低延迟，提高整体的io性能的话，iouring提供了`IORING_SETUP_IOPOLL`的flag，这个flag的作用是: 应用程序不会再去检查cq ring的tail，因为之前正常的`no-polled io`的使用方式是通过去检查cq ring tail的方式来得到是否有完成的event，但是当你开启了polled io之后，将不会同步硬件完成event的通知，也就可能说cq ring 可能是不会再被改变了；代替的就是应用程序必须不断的的调用`io_uring_enter`来询问是否有完成的event；

    关于这个标识必须在初始化iouring的时候就进行设置，后期是修改不了的；

- `KERNEL SIDE POLLING`: 这也是一个很重要的模式

    之前提到，提交io请求需要最终调用`io_uring_enter`的系统调用的；也就是告诉内核目前有数据了，可以消费了；为了追求高性能的，iouring还提供了不需要主动提交事件的方式，因为当你修改tail之后其实已经表示了你提交了一个io事件，提交这个过程中application主动push的过程，虽然最终还是需要内核去操作，但是application会做一些事情，而不是仅仅提交这些io请求；

    开启了`IORING_SETUP_SQPOLL`这个之后，内核会启动消费内核线程，主动去轮训 sq ring，application不需要去submit这个请求，这样整个性能的提升是会有的，起码对`io_uring_enter`的系统调用可以省去，这个消耗也是可观的；

    而且可以通过`IORING_SETUP_SQ_AFF` 去设定这个内核线程在哪些cpu核心上进行；为了避免浪费太多的无效的cpu，因为轮训本身是比较消耗cpu的；所以如果在一定周期内内核线程空闲的话，会被自动sleep的，用户这段需要通过调用`IORING_SQ_NEED_WAKEUP`的flag去唤醒这个线程的；

    ```!cpp
    if ((*sqring→flags) & IORING_SQ_NEED_WAKEUP)
      io_uring_enter(ring_fd, to_submit, to_wait, IORING_ENTER_SQ_WAKEUP);
    ```

    应用段的代码可能为了防止内核线程sleep，不处理请求，可以设定一个周期或者关心上一次提交请求的距离，如果很久了，那就提交一个唤醒操作，这样保证能正常处理请求；
