異步IO一直是 Linux 系統的痛。Linux 很早就有 POSIX AIO 這套異步IO實現,但它是在用戶空間本身開用戶線程模擬的,效率極其低下。後來在 Linux 2.6 引入了真正的內核級別支持的異步IO實現(Linux aio),可是它只支持 Direct IO,只支持磁盤文件讀寫,並且對文件大小還有限制,總之各類麻煩。到目前爲止(2019年5月),libuv 仍是在用pthread+preadv的形式實現異步IO。node
隨着 Linux 5.1 的發佈,Linux 終於有了本身好用的異步IO實現,而且支持大多數文件類型(磁盤文件、socket,管道等),這個就是本文的主角:io_uringlinux
於IO多路複用模型 epoll 不一樣,io_uring 的思想更相似於 Windows 上的 IOCP。用快遞來舉例:同步模型就是你從在電商平臺下單前,就在你家樓下一直等,直到快遞公司把貨送到樓下,你再把東西帶上樓。epoll 相似於你下單,快遞公司送到樓下,通知你能夠去樓下取貨了,這時你下樓把東西帶上來。雖然仍是須要用戶下樓取貨(有一段同步讀寫的時間),可是因爲不須要等快遞在路上的時間,效率已經有很是大的提高。可是,epoll不適用於磁盤IO,由於磁盤文件老是可讀的。git
而 IOCP 就是一步到位,直接送貨上門,連下樓取的動做都不須要。整個過程徹底是非阻塞的。github
io_uring 是一套系統調用接口,雖然總共就3個系統調用,但實際使用卻很是複雜。這裏直接介紹封裝過便於用戶使用的 liburing。數組
在嘗試前請首先確認本身的 Linux 內核版本在 5.1 以上(uname -r)。liburing 須要本身編譯(以後可能會被各大Linux發行版以軟件包的形式收錄),git clone
後直接 ./configure && sudo make install
就行了。瀏覽器
liburing 提供了本身的核心結構 io_uring,它內部封裝了 io_uring 本身的文件描述符(fd)以及其餘與內核通訊所需變量。服務器
struct io_uring { struct io_uring_sq sq; struct io_uring_cq cq; int ring_fd; };
使用以前須要先初始化,使用 io_uring_queue_init 初始化此結構。異步
extern int io_uring_queue_init(unsigned entries, struct io_uring *ring, unsigned flags);
如函數名稱所示, io_uring 是一個循環隊列(ring_buffer)。第一個參數 entries
表示隊列大小(實際空間可能比用戶指定的大);第二個參數 ring 就是須要初始化的 io_uring 結構指針;第三個參數 flags
是標誌參數,無特殊須要傳 0 便可。例如socket
#include <liburing.h> struct io_uring ring; io_uring_queue_init(32, &ring, 0);
首先使用 io_uring_get_sqe 獲取 sqe 結構。async
extern struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);
一個 sqe(submission queue entry)表明一次 IO 請求,佔用循環隊列一個空位。io_uring 隊列滿時 io_uring_get_sqe 會返回 NULL,注意錯誤處理。注意這裏的隊列是指未提交的請求,已提交的(但未完成)請求不佔位置。
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
而後使用 io_uring_prep_readv 或 io_uring_prep_writev 初始化 sqe 結構。
static inline void io_uring_prep_readv(struct io_uring_sqe *sqe, int fd, const struct iovec *iovecs, unsigned nr_vecs, off_t offset); static inline void io_uring_prep_writev(struct io_uring_sqe *sqe, int fd, const struct iovec *iovecs, unsigned nr_vecs, off_t offset);
第一個參數 sqe
即前面獲取的 sqe 結構指針;fd 爲須要讀寫的文件描述符,能夠是磁盤文件也能夠是socket;iovecs
爲 iovec 數組,具體使用請參照 readv 和 writev,nr_vecs
爲 iovecs 數組元素個數,offset 爲文件操做的偏移量。
能夠看到這兩個函數徹底按照 preadv
和 pwritev
設計,語義也相同,因此很好上手。須要注意的是,若是須要順序讀寫文件,偏移量 offset 須要程序本身維護。
struct iovec iov = { .iov_base = "Hello world", .iov_len = strlen("Hello world"), }; io_uring_prep_writev(sqe, fd, &iov, 1, 0);
初始化 sqe 後,能夠用 io_uring_sqe_set_data,傳入你本身的數據,通常是一個 malloc 獲得的指針,C++ 裏面能夠直接傳 this。
static inline void io_uring_sqe_set_data(struct io_uring_sqe *sqe, void *data);
注意 prep_*
中會 memset(0),因此必定要先 prep_*
再 set_data
。筆者這裏糾結了兩個小時。
準備好 sqe 後便可使用 io_uring_submit 提交請求。
extern int io_uring_submit(struct io_uring *ring);
你能夠初始化多個 sqe
而後一次性 submit
。
io_uring_submit(&ring);
io_uring_submit 都是異步操做,不會阻塞當前線程。那麼如何得知提交的操做什麼時候完成呢?liburing 提供了函數 io_uring_peek_cqe 和 io_uring_wait_cqe 兩個函數獲取當前已完成的 IO 操做。
extern int io_uring_peek_cqe(struct io_uring *ring, struct io_uring_cqe **cqe_ptr); extern int io_uring_wait_cqe(struct io_uring *ring, struct io_uring_cqe **cqe_ptr);
第一個參數是 io_uring 結構指針;第二個參數 cqe_ptr
是輸出參數,是 cqe 指針變量的地址。
cqe(completion queue entry)標記一個已完成的 IO 操做,同時也記錄的以前傳入的用戶數據。每一個 cqe 都與前面的 sqe 對應。
這兩個函數,io_uring_peek_cqe 若是沒有已完成的 IO 操做時,也會當即返回,cqe_ptr 被置空;而
io_uring_wait_cqe 會阻塞線程,等待 IO 操做完成。
for (;;) { io_uring_peek_cqe(&ring, &cqe); if (!cqe) { puts("Waiting..."); // accept 新鏈接,作其餘事 } else { puts("Finished."); break; } }
上文簡單起見用忙等待作示例,在實際應用場景中應該是一個事件循環,瀏覽器、nodejs 給咱們內部隱藏了事件循環的實現,而寫 C/C++ 語言只能咱們本身作。
可經過 io_uring_cqe_get_data 獲取前面給 sqe 設置的用戶數據。
static inline void *io_uring_cqe_get_data(struct io_uring_cqe *cqe);
默認狀況下 IO 完成事件不會從隊列中清除,致使 io_uring_peek_cqe
會獲取到相同事件,使用 io_uring_cqe_seen
標記該事件已被處理
static inline void io_uring_cqe_seen(struct io_uring *ring, struct io_uring_cqe *cqe);
io_uring_cqe_seen(&ring, cqe);
清除 io_uring 結構使用 io_uring_queue_exit
extern void io_uring_queue_exit(struct io_uring *ring);
io_uring_queue_exit(&ring);
完整代碼列舉以下:這段代碼做用就是建立文件 /home/carter/test.txt
並寫入字符串 Hello world
#include <liburing.h> #include <unistd.h> #include <fcntl.h> #include <stdio.h> int main() { struct io_uring ring; io_uring_queue_init(32, &ring, 0); struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); int fd = open("/home/carter/test.txt", O_WRONLY | O_CREAT); struct iovec iov = { .iov_base = "Hello world", .iov_len = strlen("Hello world"), }; io_uring_prep_writev(sqe, fd, &iov, 1, 0); io_uring_submit(&ring); struct io_uring_cqe *cqe; for (;;) { io_uring_peek_cqe(&ring, &cqe); if (!cqe) { puts("Waiting..."); // accept 新鏈接,作其餘事 } else { puts("Finished."); break; } } io_uring_cqe_seen(&ring, cqe); io_uring_queue_exit(&ring); }
能夠看到,C語言的異步操做仍是比同步操做複雜很多,libuv(nodejs 的底層 IO 庫)已經 表示會引入 io_uring。若是要本身用,必定要使用一個協程庫簡化異步操做。
這裏 是我使用本身編寫的協程庫 Cxx-yield 實現的一個簡單的文件服務器 demo。能夠看到,通過簡單封裝後,異步文件讀寫能夠簡化到一行:https://github.com/CarterLi/C...。就是那種在 JavaScript 裏寫 async、await 的快感