2019 - io_uring 與 5.x 核心(非同步 I/O 的終極解法)#
案件摘要#
2019 年 5 月,io_uring 進入 Linux 核心 5.1(Jens Axboe)——
\[ \text{io_uring} = \text{提交佇列(SQ)} + \text{完成佇列(CQ)} + \text{共享記憶體環}. \]
非同步 I/O 的終極解法——用兩個共享記憶體的環形佇列,把系統呼叫開銷降到接近零。
前因 -- 為什麼會有這個案子#
- 歷代非同步 I/O 的缺陷:
- select/poll(1990s):O(n) 掃描、fd 上限 1024——彌補的缺陷:規模天花板。
- epoll(2003,2.5.44/2.6):O(1) 事件通知——但仍每次要系統呼叫(epoll_wait)。
- libaio(2006):只支援 O_DIRECT 檔案 I/O,網路/普通檔案都不行——彌補的缺陷:支援範圍窄。
- NVMe SSD 的硬體契機:2015 年代 NVMe SSD 的 IOPS 達百萬級——每次 I/O 一次系統呼叫的開銷成為瓶頸(系統呼叫比 I/O 本身還慢)。
- 理論基礎:生產者-消費者環形緩衝——應用程式(生產者)與核心(消費者)透過共享記憶體通訊,不需系統呼叫——把「系統呼叫邊界」這個最後的效能牆拆掉。
線索與推理 -- 技術面貌(2019+)#
| 技術 | 狀態 | 版本年份脈絡 | 彌補的缺陷 |
|---|---|---|---|
| io_uring | 核心 5.1(2019.5) | Axboe | libaio 支援窄、epoll 系統呼叫開銷 |
| 批次提交 | SQ 一次交多筆 I/O | 5.1 | 一次一筆的呼叫開銷 |
| IORING_OP_POLL | 非同步 poll | 5.1+ | epoll_wait 的阻塞 |
| I/O 零拷貝 | send/recv with rings | 5.6(2020.3) | 資料在核心/使用者間拷貝 |
| NVMe | 原生支援 | 4.x 世代 | SATA 的佇列上限 |
C 範例:io_uring 的環形佇列#
/* io_uring (2019):共享記憶體的提交/完成環 */
#include <liburing.h>
int main(void) {
struct io_uring ring;
io_uring_queue_init(8, &ring, 0); /* 深度 8 的環 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, 4096, 0); /* 提交讀取(不阻塞) */
io_uring_submit(&ring); /* 批次提交(可多筆) */
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe); /* 等完成佇列 */
printf("讀取 %d bytes\n", cqe->res);
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
}
C 範例:歷代 I/O 模型的對照#
/* 1990s: select——O(n) 掃描、fd 上限 1024 */
fd_set readfds;
select(maxfd + 1, &readfds, NULL, NULL, NULL); /* 每次要重建集合 */
/* 2003: epoll——O(1) 事件,但仍要系統呼叫 */
int ep = epoll_create1(0);
epoll_ctl(ep, EPOLL_CTL_ADD, fd, &ev);
epoll_wait(ep, events, MAX, -1); /* 每次一個系統呼叫 */
/* 2019: io_uring——共享記憶體,可零系統呼叫(SQPOLL 模式) */
io_uring_prep_read(sqe, fd, buf, n, off); /* 只寫共享記憶體 */
Python 範例:非同步 I/O 的演化#
import selectors, socket
# 2003 模型:selectors(epoll 的封裝)
sel = selectors.DefaultSelector()
s = socket.socket()
sel.register(s, selectors.EVENT_READ)
for key, _ in sel.select(): # 每次一個系統呼叫
data = key.fileobj.recv(1024)
# 2019 模型(io_uring 的精神):批次、非同步、零拷貝
# Python 3.10+ 可用 liburing 綁定(example with uvloop-like speed)
Shell 範例:效能的實證#
# fio 效能測試:io_uring vs libaio(NVMe SSD 上)
$ fio --name=t --ioengine=io_uring --rw=read --bs=4k \
--iodepth=32 --numjobs=1 --filename=/dev/nvme0n1
# io_uring:可達 100 萬+ IOPS,CPU 使用率更低
$ fio --name=t --ioengine=libaio --rw=read --bs=4k \
--iodepth=32 --filename=/dev/nvme0n1
# libaio:IOPS 相近但 CPU 開銷高、不支援網路
結案 -- 後果與影響#
- 高效能伺服器的新基礎:Redis、PostgreSQL(2021 起實驗支援)、NGINX 陸續採用 io_uring——非同步 I/O 的統一介面。
- 網路與檔案的統一:io_uring 同時支援檔案與 socket——一切皆檔案的 I/O 層完成(1969 年理想的 2019 年實現)。
- 零系統呼叫(SQPOLL):核心執行緒代為提交——系統呼叫邊界這道 50 年的牆被拆掉。
- 演化:io_uring 5.1(2019)→ 5.6 零拷貝(2020)→ 6.0 世代支援 network zero-copy(2022+)。
- 歷史教訓:當硬體快 100 倍,軟體的每次邊界開銷都成為瓶頸——io_uring 是「共享記憶體通訊」對系統呼叫的勝利。
關鍵人物與文獻#
- J. Axboe:io_uring (2019, 5.1);〈Efficient IO with io_uring〉。
- epoll 作者:D. Probst / SGI 團隊 (2003)。
- 相關案件:
2003-2.6核心革命.md、2014-eBPF與systemd普及.md。