就緒模型與完成模型(readiness model / completion model)
要讓電腦做 I/O 而不凍結,根本上有兩種不同的方式。在就緒模型裡,你問的是「當我能在不阻塞的情況下讀或寫時,告訴我」,一旦被告知,由「你自己」執行讀或寫。在完成模型裡,你說的是「替我把這個讀取做完、放進這個緩衝區,做完了再告訴我」。前者遞給你一盞綠燈;後者遞回一件做完的工作。光是這一個區別,就組織了地球上幾乎每一套高效能 I/O 系統。
用下載來走一遍兩者。就緒模型(經典的 Unix 做法,select、poll、epoll、kqueue 所用):核心通知「這個通訊端現在有你可讀的位元組了」;接著你的程式碼自己呼叫 read() 把那些位元組複製進你自己的緩衝區。複製是你做的;核心只告訴你它不會阻塞。完成模型(Windows IOCP、POSIX AIO、Linux io_uring 所用):你把一個緩衝區交給核心並說「從這個通訊端把它填滿」;核心在背景完成整個讀取,稍後張貼一則完成通知說「好了,1400 個位元組現在在你的緩衝區裡」。你從不自己呼叫 read()——作業系統已經做了。
為何重要:就緒模型較簡單、與非阻塞通訊端對得很乾淨,但每個就緒事件仍要花你一個系統呼叫才能真正搬資料,而且對磁碟檔案運作不良(它們總回報「就緒」)。完成模型省掉了第二個系統呼叫、並把磁碟與網路一視同仁地處理,但你必須讓緩衝區一直釘住、存活到作業系統發出完成通知為止,這較難做對。這也正是你會在設計模式文獻裡遇到的「就緒等於反應器、完成等於前攝器」之分。
/* 就緒 */ epoll_wait(...); n = read(fd, buf, len); /* 由你執行讀取 */ /* 完成 */ io_uring 提交讀取; cqe = wait(); /* 核心已把 buf 填好 */
就緒告訴你「讀取不會阻塞」;完成告訴你「讀取已經做完」。
不要把非阻塞和非同步混為一談。非阻塞的就緒呼叫在你被告知就緒後,工作仍在你的執行緒裡做——那是「拒絕等待」的同步 I/O。真正的非同步(完成式)I/O 是核心在背景替你完成傳輸。