在正文開始之前,我們先來看一段對話(演繹版本)

Jens Axboe :Linus,我有個好東西,你瞅瞅?

Linus:啥玩意兒,不是已經有 aio 了么,為啥又來一套,你咋不去好好修 aio 的問題。aio 還有 balabala 問題沒有修呢。Jens Axboe :我這個 DIAO 啊,balabalaLinus :看你誠意這麼足,那行吧,我先收到我的 tree 下,不 push out 出去,讓我測測先。

【不一會兒】

Linus :你這 IO 引用計數寫的辣雞,一看就有問題,去改吧。……

背景

Linus 和 Jens 在討論的,就是 Linux Kernel 即將在 5.1 版本加入一個重大 feature:io_uring。

對做存儲的來說,這是一個大事情,值得普大喜奔,廣而告之。libaio 即將埋入黃土,io_uring 拔地而起。

一句話總結 io_uring 就是:一套全新的 syscall,一套全新的 async API,更高的性能,更好的兼容性,來迎接高 IOPS,高吞吐量的未來。

先看一下性能數據(數據來自 Jens Axboe)。

4k randread,3D Xpoint 盤:

Interface QD Polled Latency IOPS
--------------------------------------------------------------------------
io_uring 1 0 9.5usec 77K
io_uring 2 0 8.2usec 183K
io_uring 4 0 8.4usec 383K
io_uring 8 0 13.3usec 449K

libaio 1 0 9.7usec 74K
libaio 2 0 8.5usec 181K
libaio 4 0 8.5usec 373K
libaio 8 0 15.4usec 402K

io_uring 1 1 6.1usec 139K
io_uring 2 1 6.1usec 272K
io_uring 4 1 6.3usec 519K
io_uring 8 1 11.5usec 592K

spdk 1 1 6.1usec 151K
spdk 2 1 6.2usec 293K
spdk 4 1 6.7usec 536K
spdk 8 1 12.6usec 586K

io_uring vs libaio,在非 polling 模式下,io_uring 性能提升不到 10%,好像並沒有什麼了不起的地方。

然而 io_uring 提供了 polling 模式。在 polling 模式下,io_uring 和 SPDK 的性能非常接近,特別是高 QueueDepth 下,io_uring 有趕超的架勢,同時完爆 libaio。

測試 per-core,4k randread 多設備下的最高 IOPS 能力:

Interface QD Polled IOPS
--------------------------------------------------------------------------
io_uring 128 1 1620K
libaio 128 0 608K
spdk 128 1 1739K

最近幾年一直流行 kernel bypass,從網路到存儲,各個領域開花,內核在性能方面被各種詬病。io_uring 出現以後,算是扳回一局。

io_uring 有如此出眾的性能,主要來源於以下幾個方面:

  • 用戶態和內核態共享提交隊列(submission queue)和完成隊列(completion queue)
  • IO 提交和收割可以 offload 給 Kernel,且提交和完成不需要經過系統調用(system call)
  • 支持 Block 層的 Polling 模式
  • 通過提前註冊用戶態內存地址,減少地址映射的開銷

不僅如此,io_uring 還可以完美支持 buffered IO,而 libaio 對於 buffered IO 的支持則一直是被詬病的地方。

io_uring

io_uring 提供了一套新的系統調用,應用程序可以使用兩個隊列,Submission Queue(SQ) 和 Completion Queue(CQ) 來和 Kernel 進行通信。這種方式類似 RDMA 或者 NVMe 的方式,可以高效處理 IO。

syscall
425 io_uring_setup
426 io_uring_enter
427 io_uring_register

io_uring 準備階段

io_uring_setup 需要兩個參數,entries 和 io_uring_params。

其中 entries,代表 queue depth。

io_uring_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;
};

struct io_sqring_offsets {
__u32 head;
__u32 tail;
__u32 ring_mask;
__u32 ring_entries;
__u32 flags;
__u32 dropped;
__u32 array;
__u32 resv1;
__u64 resv2;
};

struct io_cqring_offsets {
__u32 head;
__u32 tail;
__u32 ring_mask;
__u32 ring_entries;
__u32 overflow;
__u32 cqes;
__u64 resv[2];
};

其中,flags、sq_thread_cpu、sq_thread_idle 屬於輸入參數,用於定義 io_uring 在內核中的行為。其他參數屬於輸出參數,由內核負責設置。

在 io_setup 返回的時候,內核已經初始化好了 SQ 和 CQ,此外,還有內核還提供了一個 Submission Queue Entries(SQEs)數組。

之所以額外採用了一個數組保存 SQEs,是為了方便通過 RingBuffer 提交內存上不連續的請求。SQ 和 CQ 中每個節點保存的都是 SQEs 數組的偏移量,而不是實際的請求,實際的請求只保存在 SQEs 數組中。這樣在提交請求時,就可以批量提交一組 SQEs 上不連續的請求。

但由於 SQ,CQ,SQEs 是在內核中分配的,所以用戶態程序並不能直接訪問。io_setup 的返回值是一個 fd,應用程序使用這個 fd 進行 mmap,和 kernel 共享一塊內存。

這塊內存共分為三個區域,分別是 SQ,CQ,SQEs。kernel 返回的 io_sqring_offset 和 io_cqring_offset 分別描述了 SQ 和 CQ 的指針在 mmap 中的 offset。而 SQEs 則直接對應了 mmap 中的 SQEs 區域。

mmap 的時候需要傳入 MAP_POPULATE 參數,以防止內存被 page fault。

IO 提交

IO 提交的做法是找到一個空閑的 SQE,根據請求設置 SQE,並將這個 SQE 的索引放到 SQ 中。SQ 是一個典型的 RingBuffer,有 head,tail 兩個成員,如果 head == tail,意味著隊列為空。SQE 設置完成後,需要修改 SQ 的 tail,以表示向 RingBuffer 中插入一個請求。

當所有請求都加入 SQ 後,就可以使用 :

int io_uring_enter(unsigned int fd, u32 to_submit, u32 min_complete, u32 flags);

來提交 IO 請求。

io_uring_enter 被調用後會陷入到內核,內核將 SQ 中的請求提交給 Block 層。to_submit 表示一次提交多少個 IO。

如果 flags 設置了 IORING_ENTER_GETEVENTS,並且 min_complete > 0,那麼這個系統調用會同時處理 IO 收割。這個系統調用會一直 block,直到 min_complete 個 IO 已經完成。

這個流程貌似和 libaio 沒有什麼區別,IO 提交的過程中依然會產生系統調用。

但 io_uring 的精髓在於,提供了 submission offload 模式,使得提交過程完全不需要進行系統調用。

如果在調用 io_uring_setup 時設置了 IORING_SETUP_SQPOLL 的 flag,內核會額外啟動一個內核線程,我們稱作 SQ 線程。這個內核線程可以運行在某個指定的 core 上(通過 sq_thread_cpu 配置)。這個內核線程會不停的 Poll SQ,除非在一段時間內沒有 Poll 到任何請求(通過 sq_thread_idle 配置),才會被掛起。

當程序在用戶態設置完 SQE,並通過修改 SQ 的 tail 完成一次插入時,如果此時 SQ 線程處於喚醒狀態,那麼可以立刻捕獲到這次提交,這樣就避免了用戶程序調用 io_uring_enter 這個系統調用。如果 SQ 線程處於休眠狀態,則需要通過調用 io_uring_enter,並使用 IORING_SQ_NEED_WAKEUP 參數,來喚醒 SQ 線程。用戶態可以通過 sqring 的 flags 變數獲取 SQ 線程的狀態。

IO 收割

當 IO 完成時,內核負責將完成 IO 在 SQEs 中的 index 放到 CQ 中。由於 IO 在提交的時候可以順便返回完成的 IO,所以收割 IO 不需要額外系統調用。這是跟 libaio 比較大的不同,省去了一次系統調用。

如果使用了 IORING_SETUP_SQPOLL 參數,IO 收割也不需要系統調用的參與。由於內核和用戶態共享內存,所以收割的時候,用戶態遍歷 [cring->head, cring->tail) 區間,這是已經完成的 IO 隊列,然後找到相應的 CQE 並進行處理,最後移動 head 指針到 tail,IO 收割就到此結束了。

由於提交和收割的時候需要訪問共享內存的 head,tail 指針,所以需要使用 rmb/wmb 內存屏障操作確保時序。

所以在最理想的情況下,IO 提交和收割都不需要使用系統調用。

其它高級特性

io_uring 支持還支持以下特性。

IORING_REGISTER_FILES

這個的用途是避免每次 IO 對文件做 fget/fput 操作,當批量 IO 的時候,這組原子操作可以避免掉。

IORING_SETUP_IOPOLL

這個功能讓內核採用 Polling 的模式收割 Block 層的請求。當沒有使用 SQ 線程時,io_uring_enter 函數會主動的 Poll,以檢查提交給 Block 層的請求是否已經完成,而不是掛起,並等待 Block 層完成後再被喚醒。使用 SQ 線程時也是同理。

通過 perf 可以看到,當使用 IOPOLL 時,88% 的 CPU 時間花費在調用 blkdev_iopoll 和 blk_poll 上。

IORING_REGISTER_BUFFERS

如果應用提交到內核的虛擬內存地址是固定的,那麼可以提前完成虛擬地址到物理 pages 的映射,避免在 IO 路徑上進行轉換,從而優化性能。用法是,在 setup io_uring 之後,調用 io_uring_register,傳遞 IORING_REGISTER_BUFFERS 作為 opcode,參數是一個指向 iovec 的數組,表示這些地址需要 map 到內核。在做 IO 的時候,使用帶 FIXED 版本的opcode(IORING_OP_READ_FIXED /IORING_OP_WRITE_FIXED)來操作 IO 即可。

內核在處理 IORING_REGISTER_BUFFERS 時,提前使用 get_user_pages 來獲得 userspace 虛擬地址對應的物理 pages。在做 IO 的時候,如果提交的虛擬地址曾經被註冊過,那麼就免去了虛擬地址到 pages 的轉換。

下面是兩個版本的 perf 數據。

帶 fixed buffer:

不帶 fixed buffer:

可以明顯看到,提前 map pages,可以減少 iov_iter_get_pages 7% 的 CPU 時間消耗。

關於名字

取名一直是一個老大難的問題,io_uring 這個名字,有點意思,社區有人吐槽,看起來像 io urine(*不是一個很好的詞*),太歡樂了。

有人說可以叫做 aio_ring,io_ring,ring_io。

總結

io_uring 的介面雖然簡單,但操作起來有些複雜,需要手動 mmap 來映射內存。可以看到,io_uring 是完全為性能而生的新一代 native async IO 模型,比 libaio 高級不少。通過全新的設計,共享內存,IO 過程不需要系統調用,由內核完成 IO 的提交, 以及 IO completion polling 機制,實現了高IOPS,高 Bandwidth。相比 kernel bypass,這種 native 的方式顯得友好一些。

當然,不可否認,aio 也在與時俱進。自從 kernel 2.5 進入 upstream 以來,aio 一直都沒有實現完整。 aio 對 Direct IO 支持的很好,但是其他的 IO 類型支持的不完善。嘗試使用其他類型的 IO,例如 buffered IO,可能導致同步的行為。polling 也是一個方向,最近 aio 的 polling 機制已經實現,感興趣的可以嘗試一下。

參考

  1. lore.kernel.org/linux-b
  2. lwn.net/ml/linux-fsdeve
  3. git.kernel.dk/cgit/fio/
  4. lore.kernel.org/linux-b
  5. lwn.net/Articles/743714
  6. io_uring_setup.2man - liburing - io_uring library

作者介紹

@panic,SmartX 存儲研發工程師。SmartX 擁有國內最頂尖的分散式存儲和超融合架構研發團隊,是國內超融合領域的技術領導者。

推薦閱讀:

相关文章