新一代异步IO框架 io_uring | 得物技术

news/2024/5/5 4:32:59/文章来源:https://blog.csdn.net/SmartCodeTech/article/details/130123739

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的data
void *io_uring_cqe_get_data(const struct io_uring_cqe *cqe)// 清理io_uring
void 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        4
int 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未来的核心发展方向。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_286097.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

光伏电池片技术N型迭代,机器视觉检测赋能完成产量“弯道超车”

电池片是光伏发电的核心部件&#xff0c;其技术路线和工艺水平直接影响光伏组件的发电效率和使用寿命。随着硅料、硅片技术逐渐接近其升级迭代空间的瓶颈&#xff0c;电池片环节正处于技术变革期&#xff0c;是光伏产业链中迭代最快的部分。P型中PERC电池片是现阶段市场的主流产…

C/C++每日一练(20230413)

目录 1. 与浮点数A最接近的分数B/C &#x1f31f; 2. 比较版本号 &#x1f31f;&#x1f31f; 3. 无重复字符的最长子串 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每…

Multi-modal Alignment using Representation Codebook

Multi-modal Alignment using Representation Codebook 题目Multi-modal Alignment using Representation Codebook译题使用表示代码集的多模态对齐期刊/会议CVPR 摘要&#xff1a;对齐来自不同模态的信号是视觉语言表征学习&#xff08;representation learning&#xff09;的…

Vue2_02_指令

模板语法 — Vue.js (vuejs.org) 指令 (Directives) 是带有 v- 前缀的特殊 attribute 参数 一些指令能够接收一个“参数”&#xff0c;在指令名称之后以冒号表示 <a v-bind:href"url">...</a> 动态参数 可以用方括号括起来的 JavaScript 表达式作为一…

JWT与Token详解

前言&#xff1a;JWT全称“JSON Web Token”&#xff0c;是实现Token的机制。官网&#xff1a;https://jwt.io/ JWT的应用 JWT用于登录身份验证。用户登录成功后&#xff0c;后端通过JWT机制生成一个token&#xff0c;返回给客户端。客户端后续的每次请求都需要携带token&…

常用加密算法

目录 常见的加密算法可以分成三种&#xff1a; 对称加密算法 DES 3DES AES 非对称加密 RSA ECC Hash算法 MD5 SHA1 算法对比 算法选择 常见的加密算法可以分成三种&#xff1a; 对称加密算法&#xff1b;非对称加密算法&#xff1b;Hash算法&#xff1b;接下来我们…

面试手撕堆排序

堆排序代码如下&#xff08;注释见下&#xff09;&#xff1a; 首先将待排序的数组构造成一个大根堆&#xff0c;此时&#xff0c;整个数组的最大值就是堆结构的堆顶 将堆顶的数与末尾的数交换&#xff0c;此时&#xff0c;末尾的数为最大值&#xff0c;剩余待排序数组个数为n…

Linux设备驱动开发 - 块设备驱动ramdisk实例

By: fulinux E-mail: fulinuxsina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅&#xff01; 你的喜欢就是我写作的动力&#xff01; 目录理论来源源码编译测试理论来源 ramdisk驱动&#xff0c;区别在与使用最新的内核版本&#xff0c;比如linux-4.15。…

MySQL数据库:聚合函数、分组查询、约束、默认值设置、自增属性

一、聚合函数 1.聚合函数 在MySQL数据库中预定义好的一些数据统计函数。 2.count(*) 功能&#xff1a;统计结果条数。 3.sum(字段名) 功能&#xff1a;对指定字段的数据求和。 4.avg(字段名) 功能&#xff1a;对指定字段的数据求平均值。 5.max(字段名) 和 min(字段名) …

答疑——20年国赛题(JAVA解法)

题目链接&#xff1a;用户登录https://www.lanqiao.cn/problems/1025/learning/?page3&first_category_id1&sortstudents_count 题目描述 有 n 位同学同时找老师答疑。每位同学都预先估计了自己答疑的时间。 老师可以安排答疑的顺序&#xff0c;同学们要依次进入老…

sqoop数据导出、脚本使用

目录 准备表与数据 数据导出 脚本调用 准备表与数据 mysql表 CREATE TABLE user (id int(20),name varchar(20) )ENGINEINNODB DEFAULT CHARSETutf8; hive表 create table users( id bigint, name string ) row format delimited fields terminated by "\t";…

软考初级程序员--学习

1、十进制 转 二进制 十进制87 转换为 二进制为 1010111 2、二进制 转 十进制 二进制1010111 转换为 十进制 3、循环队列 计算长度通用公式&#xff1a; front&#xff1a;表示队首 rear&#xff1a;表示队尾 M&#xff1a;表示队列容量 队列长度 &#xff08;rear - fr…

Verilog | 二进制与格雷码

一、格雷码简介 格雷码是一个叫弗兰克格雷的人在 1953 年发明的&#xff0c;最初用于通信。格雷码是一种循环二进制码或者叫作反射二进制码。格雷码的特点是从一个数变为相邻的一个数时&#xff0c;只有一个数据位发生跳变&#xff0c;由于这种特点&#xff0c;就可以避免二进…

进销存管理系统能为企业带来哪些实际效益?

随着互联网的不断发展&#xff0c;如今的商业世界已经越来越向数字化转型。拥有一套完整的数字化的进销存管理能够极大地提升公司货物进出库存情况的效率和准确性&#xff0c;避免过程中出现不必要的错误和漏洞&#xff0c;从而帮助企业更加稳健地自我发展。那么&#xff0c;一…

3.7.2数据库系统-数据库控制技术:数据库的安全性、数据库备份与恢复技术、数据备份、数据库故障与恢复

3.7.2数据库系统-数据库控制技术&#xff1a;数据库的安全性、数据库备份与恢复技术、数据备份、数据库故障与恢复数据库的安全性数据库备份与恢复技术数据备份数据库故障与恢复数据库的安全性 在做信息系统开发的过程当中&#xff0c;数据库是其中很大的占比&#xff0c;信息…

【MySQL入门指南】数据库基本操作

文章目录MySQL库操作一、SQL语句二、创建数据库1.语法2.案例3.极其不推荐的方式三、查看数据库1.语法四、修改数据库五、删除数据库六、字符集与校验规则1.是什么2.相关指令3.校验规则的影响七、备份数据库1.基本语法2.注意事项MySQL库操作 一、SQL语句 DDL(data definition l…

Visual Studio Code跳转到CSS定义

Visual Studio Code 快速跳转到 VUE文件 或 CSS文件的定义位置&#xff08;跳转到class定义&#xff0c;跳转到css定义&#xff09;&#xff0c;插件Css Peek、Vue Peek 对提升开发效率上&#xff0c;事半功倍。 目录 1、跳转到CSS定义 1.1、CSS Peek 1.2、Vue Peek 2、其他…

全国青少年软件编程(Scratch)等级考试一级考试真题2023年3月——持续更新.....

一、单选题(共25题&#xff0c;共50分) 1. 下列说法不正确的是&#xff1f;&#xff08; &#xff09; A.可以从声音库中随机导入声音 B.可以录制自己的声音上传 C.可以修改声音的大小 D.不能修改声音的速度 试题解析&#xff1a;针对声音可以进行导入&#xff0c;上传&…

【C++】哈希的应用 -- 布隆过滤器

文章目录一、布隆过滤器的引入二、哈希函数个数的选择三、布隆过滤器的实现四、布隆过滤器的应用五、布隆过滤器总结一、布隆过滤器的引入 我们在上一节中学习了 位图&#xff0c;知道了位图可以用来快速判断某个数据是否在一个集合中&#xff0c;但是位图有如下的缺点&#x…

计网第五章.运输层—TCP的三次握手与四次挥手

以下来自湖科大计算机网络公开课笔记及个人所搜集资料 目录一、TCP三次握手建立连接为什么TCP客户进程最后还要发送一个普通的TCP确认报文段呢&#xff1f;能不能两次握手&#xff1f;总结&#xff1a;二、TCP四次挥手释放连接四次挥手过程问题1&#xff1a;TCP客户进程在发送完…