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 之所以能達到超高性能的原因主要在以下幾個方面:
- Mmap 機制減少了 內存複製
- 內核輪詢模式下,沒有用户態和內核態的切換降低了損耗
- 基於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 讀取文件
- 調用 io\_uring\_queue_init 初始化
- 獲取一個空 SQE用於提交任務
- io\_uring\_prep_readv 方法填充SQE 任務內容
- io\_uring\_submit 提交SQE
- io\_uring\_wait_cqe 獲取已完成的CQE
- io\_uring\_cqe_seen 更新CQ 隊列的head ,避免CQE被重複處理
- 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_CONNECTIONS
void 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 need
int portno = strtol(argv[1], NULL, 10);
struct sockaddr_in serv_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
// setup socket
int 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 listen
if (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_uring
struct io_uring_params params;
struct io_uring ring;
memset(¶ms, 0, sizeof(params));
if (io_uring_queue_init_params(2048, &ring, ¶ms) < 0) {
perror("io_uring_init_failed...\n");
exit(1);
}
// check if IORING_FEAT_FAST_POLL is supported
if (!(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 supported
struct 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 selection
struct 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 connections
add_accept(&ring, sock_listen_fd, (struct sockaddr *)&client_addr, &client_len, 0);
// start event loop
while (1) {
io_uring_submit_and_wait(&ring, 1);
struct io_uring_cqe *cqe;
unsigned head;
unsigned count = 0;
// go through all CQEs
io_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, >= 0
if (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 connections
add_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 buffer
add_provide_buf(&ring, bid, group_id);
// connection closed or error
close(conn_i.fd);
} else {
// bytes have been read into bufs, now add write to socket sqe
add_socket_write(&ring, conn_i.fd, bid, bytes_read, 0);
}
} else if (type == WRITE) {
// write has been completed, first re-add the buffer
add_provide_buf(&ring, conn_i.bid, group_id);
// add a new read for the existing connection
add_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未來的核心發展方向。