同步/非同步與阻塞/非阻塞是兩組正交概念,常被混淆。本文以「銀行辦事」類比,配合 read/write、select/poll、aio 實例,釐清四種組合的行為差異與效能影響。
| 維度 | 關注點 | 銀行類比 |
|---|---|---|
| 同步/非同步 | 「誰來通知完成」 | 同步=自己盯著排隊;非同步=領號碼牌,櫃台廣播通知 |
| 阻塞/非阻塞 | 「等待時能不能做別事」 | 阻塞=站著不能動;非阻塞=邊刷手機邊等 |
read(fd, buf, n) 預設行為。資料未就緒 → 執行緒睡眠,資料到達 → 複製到用戶緩衝 → 返回。fcntl(fd, F_SETFL, O_NONBLOCK)。資料未就緒 → 立即返回 -EAGAIN,應用需輪詢或配合 select。同步非阻塞需應用自行「輪詢狀態 → 發起 I/O」,造成用戶態/核心態頻繁切換,效率低。
select/poll/epoll 配合 timeout=NULL。等待事件 → 阻塞於系統呼叫;事件就緒 → 返回就緒 fd 列表 → 應用發起 read/write(通常不阻塞)。aio_read/aio_write (POSIX AIO) 或 io_uring (Linux 5.1+)。提交請求 → 立即返回;完成時透過訊號/回調/完成佇列通知。| 阻塞 | 非阻塞 | |
|---|---|---|
| 同步 | read/write (預設) | O_NONBLOCK + read/write 輪詢 |
| 非同步 | select/poll/epoll (timeout=-1) | aio_* / io_uring / signalfd + epoll |
O_NONBLOCK,同步變非阻塞。select(NULL) 會阻塞等待事件;aio 才是真正全非阻塞。read/write 仍由應用發起,屬於同步操作。epoll (非同步阻塞) 或 io_uring (非同步非阻塞) 最佳。read/write + 多執行緒/多進程 模型開發效率最高。epoll 邊緣觸發。// 同步阻塞
n = read(fd, buf, sizeof(buf));
// 同步非阻塞
fcntl(fd, F_SETFL, O_NONBLOCK);
while ((n = read(fd, buf, sizeof(buf))) == -1 && errno == EAGAIN)
; // 忙等或配合 select
// 非同步阻塞 (epoll)
epoll_wait(epfd, events, MAX_EVENTS, -1); // 等待事件
for (e : events) read(e.data.fd, buf, sizeof(buf));
// 非同步非阻塞 (io_uring)
io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_read(sqe, fd, buf, sizeof(buf), 0);
io_uring_submit(ring);
// 稍後從 cqe 取得完成結果
理解兩組概念正交性,才能根據延遲、吞吐、複雜度需求選擇最適 I/O 模型。