1.Linux IO 模型分类

相比于kernel bypass 模式须要联合具体的硬件撑持来讲,native IO是日常工作中接触到比拟多的一种,其中同步IO在较长一段时间内被宽泛应用,通常咱们接触到的IO操作次要分为网络IO和存储IO。在大流量高并发的明天,提到网络IO,很容易想到赫赫有名的epoll  以及reactor架构。然而epoll并不属于异步IO的领域。实质上是一个同步非阻塞的架构。对于同步异步,阻塞与非阻塞的概念区别这里做简要概述:

  • 什么是同步

指过程调用接口时须要期待接口解决完数据并相应过程能力继续执行。这里重点是数据处理活逻辑执行实现并返回,如果是异步则不用期待数据实现,亦能够继续执行。同步强调的是逻辑上的秩序性;

  • 什么是阻塞

当过程调用一个阻塞的零碎函数时,该过程被 置于睡眠(Sleep)状态,这时内核调度其它过程运行,直到该过程期待的事件产生了(比 如网络上接管到数据包,或者调用sleep指定的睡眠工夫到了)它才有可能持续运行。与睡眠状态绝对的是运行(Running)状态,在Linux内核中,处于运行状态的过程分为两种状况,一种是过程正在被CPU调度,另一种是处于就绪状态随时可能被调度的过程;阻塞强调的是函数调用下过程的状态。

2.Linux常见文件操作形式

2.1   open/close/read/write

基本操作API 如下:

#include <unistd.h>                                                  #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> //  返回值:胜利返回新调配的文件描述符,出错返回-1并设置errno                                  int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);// 返回值:胜利返回0,出错返回-1并设置errno                                                  int close(int fd); // 返回值:胜利返回读取的字节数,出错返回-1并设置errno,如果在调read之前已达到文件开端,则这次read返回0                                                  ssize_t read(int fd, void *buf, size_t count);   // 返回值:胜利返回写入的字节数,出错返回-1并设置errno                                               ssize_t write(int fd, const void *buf, size_t count);

在关上文件时能够指定为,只读,只写,读写等权限,以及阻塞或者非阻塞操作等;具体通过open函数的flags 参数指定 。这里以关上一个读写文件为例,同时定义了写文件的形式为追加写,以及应用间接IO模式操作文件,具体什么是间接IO下文会细述。open("/path/to/file", O\_RDWR|O\_APPEND|O_DIRECT);flags 可选参数如下:

Flag 参数含意
O_CREATE创立文件时,如果文件存在则出错返回
O_EXCL如果同时指定了O_CREAT,并且文件已存在,则出错返回。
O_TRUC把文件截断成0
O_RDONLY只读
O_WRONLY只写
O_RDWR读写
O_APPEND追加
O_NONBLOCK非阻塞标记
O_SYNC每次读写都期待物理IO操作实现
O_DIRECT提供最间接IO反对

通常读写操作的数据首先从用户缓冲区进入内核缓冲区,而后由内核缓冲区实现与IO设施的同步:

2.2   Mmap

// 胜利执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],// error被设为以下的某个值:// 1 EACCES:拜访出错// 2 EAGAIN:文件已被锁定,或者太多的内存已被锁定// 3 EBADF:fd不是无效的文件形容词// 4 EINVAL:一个或者多个参数有效// 5 ENFILE:已达到系统对关上文件的限度// 6 ENODEV:指定文件所在的文件系统不反对内存映射// 7 ENOMEM:内存不足,或者过程已超出最大内存映射数量// 8 EPERM:权能有余,操作不容许// 9 ETXTBSY:已写的形式关上文件,同时指定MAP_DENYWRITE标记//10 SIGSEGV:试着向只读区写入//11 SIGBUS:试着拜访不属于过程的内存区void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);// 胜利执行时,munmap()返回0。失败时,munmap返回-1,error返回标记和mmap统一;// 该调用在过程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小;int munmap( void * addr, size_t len )// 过程在映射空间的对共享内容的扭转并不间接写回到磁盘文件中,往往在调用munmap()后才执行该操作。// 如果冀望内存的数据变动可能立即反馈到磁盘上,能够通过调用msync()实现。int msync( void *addr, size_t len, int flags )

Mmap 是一种内存映射办法,通过将文件映射到内存的某个地址空间上,在对该地址空间的读写操作时,会触发相应的缺页异样以及脏页回写操作,从而实现文件数据的读写操作;

2.3   间接IO

间接IO的形式比较简单,间接上文提及的open函数入参中指定 O_DIRECT 即可,相比一般IO操作,略过了内核的缓冲区间接操作下一层的文件文件。该操作比拟底层,相比一般的文件读写少了一次数据复制,个别须要联合用户态缓存来应用;下图所示为 DIO 透过 buffer层间接操作磁盘文件系统:

2.4   sendFile

严格来讲,sendfile 并不提供残缺的读写能力,仅用于减速读取数据到网络的能力,因为数据不通过用户空间,因而无奈对数据进行二次解决,也就是说从磁盘中读出来一成不变的发给网卡,下图展现了sendFile 的工作流程,

  • 数据首先以DMA的形式从磁盘上读取到内核的文件缓冲区,
  • 而后再从文件缓冲区读取到了socket的缓冲区,该过程由CPU负责实现。
  • 接着网卡再以DMA的形式从socket缓冲区 拷贝到本人网卡缓冲区,而后进行发送

Linux 内核2.4 版本当前对 sendFile 进行了进一步优化,提供了带有 scatter/gather的 sendfile 操作,将仅有一次的CPU参加copy 环节去掉,该操作须要网卡硬件的反对。其原理就是在内核空间 Read Buffer 和 Socket Buffer 不做数据复制,而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer 中。其本质和虚拟内存的解决办法思路统一,就是内存地址的记录。

2.5   splice

splice 调用和 sendfile  很类似,应用程序必须领有两个曾经关上的文件描述符,一个示意输出设施,一个示意输出设备。splice容许任意两个文件相互连贯,而并不只是文件与 socket 进行数据传输。对于从一个文件描述符发送数据到 socket 这种特例来说,简化为应用 sendfile 零碎调用,splice 适用范围更广且不须要硬件反对, sendfile  是 splice  的一个子集。

  • 用户过程调用 pipe()陷入内核态;创立匿名单向管道 pipe() 返回,从内核态切换回用户态;
  • 用户过程调用 splice()从用户态陷入内核态,DMA 控制器将数据从硬盘拷贝到内核缓冲区,从管道的写入端"拷贝"进管道,splice() 返回,从内核态切换为用户态;
  • 用户过程再次调用 splice(),从用户态陷入内核态,内核把数据从管道的读取端拷贝到socket缓冲区,DMA 控制器将数据从 socket 缓冲区拷贝到网卡,splice() 返回,上下文从内核态切换回用户态。

3.IO_URING是什么

io_uring 是 Linux 提供的一个异步非阻塞 I/O 接口,他既能反对磁盘IO也能反对网络IO,只是存储IO反对的比拟早较为成熟。IO\_URING的应用须要较高的linux 内核版本,个别倡议5.12 版本当前。上面会别离从存储和网络两个角度来介绍IO\_URING 。

3.1   IO_URING 架构

  • 应用程序提交的IO 申请会间接进入submission queue 队列的尾部,内核过程会一直的从SQ 队列的头部生产申请
  • 内核解决完的SQ后会更新CQ  tail 局部 ,应用程序读取到CQ 的head时,会更新CQ的head
  • SQ 中的工作称之为 SQE(entry), CQ中的工作称之为CQE

3.2   零碎调用API

// 创立一个 SQ 和一个 CQ,queue size 至多 entries 个元素// 返回一个文件描述符,随后用于在这个 io_uring 实例上执行操作。// 参数p 有两个作用:// 1.作为入参:利用用来配置 io_uring 的一些行为// 2.作为出参:内核返回的 SQ/CQ 地址信息等也通过它带回来。int io_uring_setup(u32 entries, struct io_uring_params *p);// 注册用于异步 I/O 的文件或用户缓冲区(files or user buffers):int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);// 用于初始化和实现I/O,应用共享的 SQ 和 CQ。单次调用同时提交新的 I/O 申请和期待 I/O 实现操作// fd 是 io_uring_setup() 返回的文件描述符;// to_submit 指定了 SQ 中提交的 I/O 数量;// 默认模式下如果指定了 min_complete,会期待这个数量的 I/O 事件实现再返回;// 轮询模式(2种)://   0:要求内核返回以后以及实现的所有 events,无阻塞;//   非0:如果有事件实现,内核依然立刻返回;如果没有实现事件,内核会 poll,期待指定的次数实现,或者这个过程的工夫片用完。int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);

3.3   三种工作模式

3.3.1  中断驱动模式:**

默认模式。可通过 io\_uring\_enter() 提交 I/O 申请,而后间接查看 CQ 状态判断是否实现。也能够通过 min_complete 来睡在 enter 办法上,期待实现事件达到  ;

3.3.2  轮询模式

相比中断驱动形式,这种形式提早更低, 然而会耗费更多的CPU,利用线程须要一直的调用 enter 函数,而后陷入内核态后继续地 polling,等到一个 min_complete 达到。然而留神的是此时 polling 关注的是实现事件3.3.3  内核轮询模式这种模式中,会创立一个内核线程(kernel thread)来执行 SQ 的轮询工作( 是否有新的SQE提交 )。应用这种模式利用无需切到到内核态 就能触发(issue)I/O 操作。利用线程通过mmap 机制更新SQ 来提交 SQE,以及监控 CQ 的实现状态,利用无需任何零碎调用,就能提交和收割 I/O(submit and reap I/Os)。如果内核线程的闲暇工夫超过了用户的配置值,它会告诉利用,而后进入 idle 状态。这种状况下,利用必须调用 io_uring_enter() 来唤醒内核线程。如果 I/O 始终很忙碌,内核线程是不会 sleep 的。在日常的应用中个别倡议抉择后两种轮训模式,用户线程轮存在用户态到内核态的切换,相比内核轮询存在肯定的性能损耗;io_uring 之所以能达到超高性能的起因次要在以下几个方面:

  1. Mmap 机制缩小了 内存复制
  2. 内核轮询模式下,没有用户态和内核态的切换升高了损耗
  3. 基于SQ和CQ 机制下的数据竞争打消,即没有并发竞争损耗

3.4   liburing

io\_uring的外围零碎调用只有三个,但应用起来较为简单,开发者在io\_uring 之上封装了新的liburing 库,简化应用。

// io_uring 构造体中蕴含须要应用到的 SQ和CQ ,以及须要关联的文件FD, 和相干的配置参数falgs; struct io_uring {            struct io_uring_sq sq;            struct io_uring_cq cq;            unsigned flags;            int ring_fd;};struct io_uring_sq {            unsigned *khead;            unsigned *ktail;            unsigned *kring_mask;            unsigned *kring_entries;            unsigned *kflags;            unsigned *kdropped;            unsigned *array;            struct io_uring_sqe *sqes;    unsigned sqe_head;            unsigned sqe_tail;    size_t ring_sz;            void *ring_ptr;};struct io_uring_cq {            unsigned *khead;            unsigned *ktail;            unsigned *kring_mask;            unsigned *kring_entries;            unsigned *koverflow;            struct io_uring_cqe *cqes;    size_t ring_sz;    void *ring_ptr;};// 用户初始化 io_uring。该办法中蕴含了内存空间的初始化以及mmap 调用,entries:队列深度 int io_uring_queue_init(unsigned entries, struct io_uring *ring, unsigned flags);// 为了提交IO申请,须要获取外面queue的一个闲暇项struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);// 非零碎调用,筹备阶段,和libaio封装的io_prep_writev一样void io_uring_prep_writev(struct io_uring_sqe *sqe, int fd,const struct iovec *iovecs, unsigned nr_vecs, off_t offset)// 非零碎调用,筹备阶段,和libaio封装的io_prep_readv一样void io_uring_prep_readv(struct io_uring_sqe *sqe, int fd, const struct iovec *iovecs, unsigned nr_vecs, off_t offset)// 提交sq的entry,不会阻塞等到其实现,内核在其实现后会主动将sqe的偏移信息退出到cq,在提交时须要加锁int io_uring_submit(struct io_uring *ring);// 提交sq的entry,阻塞等到其实现,在提交时须要加锁。int io_uring_submit_and_wait(struct io_uring *ring, unsigned wait_nr);// 非零碎调用 遍历时,能够获取cqe的datavoid *io_uring_cqe_get_data(const struct io_uring_cqe *cqe)// 清理io_uringvoid io_uring_queue_exit(struct io_uring *ring);

liburing  github地址 : https://github.com/axboe/liburing

3.5   应用形式

3.5.1  读取文件

  1. 调用 io\_uring\_queue_init  初始化
  2. 获取一个空 SQE用于提交工作
  3. io\_uring\_prep_readv  办法填充SQE 工作内容
  4. io\_uring\_submit 提交SQE
  5. io\_uring\_wait_cqe 获取已实现的CQE
  6. io\_uring\_cqe_seen   更新CQ 队列的head ,防止CQE被反复解决
  7. io\_uring\_queue\_exit 退出 io\_uring

上面是liburing github 上的example 代码适当精简后的代码

#include <stdio.h>#include <fcntl.h>#include <string.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>#include "liburing.h"#define QD 4int main(int argc, char *argv[]){struct io_uring ring;int i, fd, ret, pending, done;struct io_uring_sqe *sqe;struct io_uring_cqe *cqe;struct iovec *iovecs;struct stat sb;ssize_t fsize;off_t offset;void *buf;ret = io_uring_queue_init(QD, &ring, 0);if (ret < 0) {fprintf(stderr, "queue_init: %s\n", strerror(-ret)); return 1;}fd = open(argv[1], O_RDONLY | O_DIRECT);fsize = 0;iovecs = calloc(QD, sizeof(struct iovec));for (i = 0; i < QD; i++) {if (posix_memalign(&buf, 4096, 4096))return 1;iovecs[i].iov_base = buf;iovecs[i].iov_len = 4096;fsize += 4096;}offset = 0;i = 0;do {sqe = io_uring_get_sqe(&ring);if (!sqe) break;io_uring_prep_readv(sqe, fd, &iovecs[i], 1, offset);offset += iovecs[i].iov_len;i++;if (offset > sb.st_size) break;} while (1);ret = io_uring_submit(&ring);if (ret < 0) {fprintf(stderr, "io_uring_submit: %s\n", strerror(-ret)); return 1;} else if (ret != i) {fprintf(stderr, "io_uring_submit submitted less %d\n", ret); return 1;}done = 0;pending = ret;fsize = 0;for (i = 0; i < pending; i++) {ret = io_uring_wait_cqe(&ring, &cqe);if (ret < 0) {fprintf(stderr, "io_uring_wait_cqe: %s\n", strerror(-ret));return 1;}done++;ret = 0;if (cqe->res != 4096 && cqe->res + fsize != sb.st_size) {fprintf(stderr, "ret=%d, wanted 4096\n", cqe->res);ret = 1;}fsize += cqe->res;io_uring_cqe_seen(&ring, cqe);if (ret) break;}printf("Submitted=%d, completed=%d, bytes=%lu\n", pending, done, (unsigned long) fsize);close(fd);io_uring_queue_exit(&ring);return 0;}

3.5.2  网络服务

网络服务这里间接参考 Github 地址:GitHub - frevib/io\_uring-echo-server: io\_uring echo server

#include <errno.h>#include <fcntl.h>#include <netinet/in.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <strings.h>#include <sys/poll.h>#include <sys/socket.h>#include <unistd.h>#include "liburing.h"#define MAX_CONNECTIONS 4096#define BACKLOG 512#define MAX_MESSAGE_LEN 2048#define BUFFERS_COUNT MAX_CONNECTIONSvoid add_accept(struct io_uring *ring, int fd, struct sockaddr *client_addr, socklen_t *client_len, unsigned flags);void add_socket_read(struct io_uring *ring, int fd, unsigned gid, size_t size, unsigned flags);void add_socket_write(struct io_uring *ring, int fd, __u16 bid, size_t size, unsigned flags);void add_provide_buf(struct io_uring *ring, __u16 bid, unsigned gid);enum {ACCEPT,READ,WRITE,PROV_BUF,};typedef struct conn_info {__u32 fd;__u16 type;__u16 bid;} conn_info;char bufs[BUFFERS_COUNT][MAX_MESSAGE_LEN] = {0};int group_id = 1337;int main(int argc, char *argv[]) {if (argc < 2) {printf("Please give a port number: ./io_uring_echo_server [port]\n");exit(0);}// some variables we needint portno = strtol(argv[1], NULL, 10);struct sockaddr_in serv_addr, client_addr;socklen_t client_len = sizeof(client_addr);// setup socketint sock_listen_fd = socket(AF_INET, SOCK_STREAM, 0);const int val = 1;setsockopt(sock_listen_fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(portno);serv_addr.sin_addr.s_addr = INADDR_ANY;// bind and listenif (bind(sock_listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {perror("Error binding socket...\n");exit(1);}if (listen(sock_listen_fd, BACKLOG) < 0) {perror("Error listening on socket...\n");exit(1);}printf("io_uring echo server listening for connections on port: %d\n", portno);// initialize io_uringstruct io_uring_params params;struct io_uring ring;memset(&params, 0, sizeof(params));if (io_uring_queue_init_params(2048, &ring, &params) < 0) {perror("io_uring_init_failed...\n");exit(1);}// check if IORING_FEAT_FAST_POLL is supportedif (!(params.features & IORING_FEAT_FAST_POLL)) {printf("IORING_FEAT_FAST_POLL not available in the kernel, quiting...\n");exit(0);}// check if buffer selection is supportedstruct io_uring_probe *probe;probe = io_uring_get_probe_ring(&ring);if (!probe || !io_uring_opcode_supported(probe, IORING_OP_PROVIDE_BUFFERS)) {printf("Buffer select not supported, skipping...\n");exit(0);}free(probe);// register buffers for buffer selectionstruct io_uring_sqe *sqe;struct io_uring_cqe *cqe;sqe = io_uring_get_sqe(&ring);io_uring_prep_provide_buffers(sqe, bufs, MAX_MESSAGE_LEN, BUFFERS_COUNT, group_id, 0);io_uring_submit(&ring);io_uring_wait_cqe(&ring, &cqe);if (cqe->res < 0) {printf("cqe->res = %d\n", cqe->res);exit(1);}io_uring_cqe_seen(&ring, cqe);// add first accept SQE to monitor for new incoming connectionsadd_accept(&ring, sock_listen_fd, (struct sockaddr *)&client_addr, &client_len, 0);// start event loopwhile (1) {io_uring_submit_and_wait(&ring, 1);struct io_uring_cqe *cqe;unsigned head;unsigned count = 0;// go through all CQEsio_uring_for_each_cqe(&ring, head, cqe) {++count;struct conn_info conn_i;memcpy(&conn_i, &cqe->user_data, sizeof(conn_i));int type = conn_i.type;if (cqe->res == -ENOBUFS) {fprintf(stdout, "bufs in automatic buffer selection empty, this should not happen...\n");fflush(stdout);exit(1);} else if (type == PROV_BUF) {if (cqe->res < 0) {printf("cqe->res = %d\n", cqe->res);exit(1);}} else if (type == ACCEPT) {int sock_conn_fd = cqe->res;// only read when there is no error, >= 0if (sock_conn_fd >= 0) {add_socket_read(&ring, sock_conn_fd, group_id, MAX_MESSAGE_LEN, IOSQE_BUFFER_SELECT);}// new connected client; read data from socket and re-add accept to monitor for new connectionsadd_accept(&ring, sock_listen_fd, (struct sockaddr *)&client_addr, &client_len, 0);} else if (type == READ) {int bytes_read = cqe->res;int bid = cqe->flags >> 16;if (cqe->res <= 0) {// read failed, re-add the bufferadd_provide_buf(&ring, bid, group_id);// connection closed or errorclose(conn_i.fd);} else {// bytes have been read into bufs, now add write to socket sqeadd_socket_write(&ring, conn_i.fd, bid, bytes_read, 0);}} else if (type == WRITE) {// write has been completed, first re-add the bufferadd_provide_buf(&ring, conn_i.bid, group_id);// add a new read for the existing connectionadd_socket_read(&ring, conn_i.fd, group_id, MAX_MESSAGE_LEN, IOSQE_BUFFER_SELECT);}}io_uring_cq_advance(&ring, count);}}void add_accept(struct io_uring *ring, int fd, struct sockaddr *client_addr, socklen_t *client_len, unsigned flags) {struct io_uring_sqe *sqe = io_uring_get_sqe(ring);io_uring_prep_accept(sqe, fd, client_addr, client_len, 0);io_uring_sqe_set_flags(sqe, flags);conn_info conn_i = {.fd = fd,.type = ACCEPT,};memcpy(&sqe->user_data, &conn_i, sizeof(conn_i));}void add_socket_read(struct io_uring *ring, int fd, unsigned gid, size_t message_size, unsigned flags) {struct io_uring_sqe *sqe = io_uring_get_sqe(ring);io_uring_prep_recv(sqe, fd, NULL, message_size, 0);io_uring_sqe_set_flags(sqe, flags);sqe->buf_group = gid;conn_info conn_i = {.fd = fd,.type = READ,};memcpy(&sqe->user_data, &conn_i, sizeof(conn_i));}void add_socket_write(struct io_uring *ring, int fd, __u16 bid, size_t message_size, unsigned flags) {struct io_uring_sqe *sqe = io_uring_get_sqe(ring);io_uring_prep_send(sqe, fd, &bufs[bid], message_size, 0);io_uring_sqe_set_flags(sqe, flags);conn_info conn_i = {.fd = fd,.type = WRITE,.bid = bid,};memcpy(&sqe->user_data, &conn_i, sizeof(conn_i));}void add_provide_buf(struct io_uring *ring, __u16 bid, unsigned gid) {struct io_uring_sqe *sqe = io_uring_get_sqe(ring);io_uring_prep_provide_buffers(sqe, bufs[bid], MAX_MESSAGE_LEN, 1, gid, bid);conn_info conn_i = {.fd = 0,.type = PROV_BUF,};memcpy(&sqe->user_data, &conn_i, sizeof(conn_i));}

4.性能比照

4.1   存储IO

  • Synchronous I/O、 Libaio和IO_uring 个性比照

  • io_uring和spdk的个性比照

SPDK 全名 Storage Performance Development Kit,是一种存储性能开发套件 。针对于反对nvme协定的SSD设施。是一种高性能的解决方案。

  • io_uring和spdk的性能比照

非polling模式,io\_uring相比libaio晋升不是很显著;在polling模式下,io\_uring能与spdk靠近,甚至在queue depth较高时性能更好,性能超过libaio。在queue depth较低时有约7%的差距,但在queue depth较高时根本靠近。

比照论断:

io_uring在非polling模式下,相比libaio,性能晋升不是十分显著。 _io_uring在polling模式下,性能晋升显著,与spdk靠近,在队列深度较高时性能更好。_

4.2   网络IO

  • Epoll 性能比照

与epoll的性能比照差别还是很大的,参考这篇文章的数据  https://juejin.cn/post/7074212680071905311测试环境:wsl2,内核版本5.10.60.1,发行版为Debian硬件:I5-9400,16gDDR4应用webbench进行繁难测试,模仿10500、30500台客户端,持续时间为5s,别离在失常拜访和不期待返回两种模式下进行测试,两个客户端均敞开日志记录,epoll开启双ET模式,比拟每分钟发送页面数,后果如下:

比照论断:

毋庸置疑,碾压性的后果。

5.总结

得益于精妙的设计,io\_uring的性能根本超过linux 内核以往任何软件层面的IO解决方案,达到了与硬件级解决方案媲美的性能。io\_uring 须要较高版本的内核反对,目前还没有大面积遍及,但能够意料他是 linux 内核 IO将来的外围倒退方向。