阻塞與非阻塞檔案描述符(blocking / non-blocking,O_NONBLOCK)
想像你向服務生要一杯水。在阻塞模式下,你呆站在桌邊、什麼都不做,直到水送來為止。在非阻塞模式下,服務生說「還沒好」,你立刻回頭去忙別的事,稍後再來問一次。檔案描述符——你的程式用來指稱一個已開啟的檔案、通訊端或管線的那個小整數——可以被設成這兩種行為之一,而這個選擇徹底決定了一個伺服器能否擴展。
預設情況下,描述符處於阻塞模式。當你呼叫 read(fd, ...) 而尚無資料抵達時,核心會把你的執行緒停泊(暫停)起來,把 CPU 讓給別人,只有當位元組就緒或連線關閉時才喚醒你。一個執行緒服務一條連線時,這樣很好。但只要設上 O_NONBLOCK 旗標(用 open() 的旗標,或用 fcntl(fd, F_SETFL, ...)),呼叫就會立刻返回:若資料就緒你便拿到它,若沒有,read() 回傳 -1 並把 errno 設為 EAGAIN(在 Linux 上與 EWOULDBLOCK 同值),意思是「現在沒有,等會再試」。一個非阻塞的 write() 若無法立刻接受全部位元組,便寫入它能寫的那麼多並回傳那個較短的數字,或在一個都寫不進去時回傳 EAGAIN。
為何阻塞式讀取無法擴展:如果每條連線都得有自己的執行緒停泊在 read() 裡等待,那一萬條閒置連線就要付出一萬個停泊的執行緒,每個都有自己的堆疊(常常是一 MiB 以上)以及排程器的負擔。非阻塞描述符是讓「一個」執行緒同時兼顧數千條連線的基礎:你逐一問每條「有資料嗎?」,跳過那些回答 EAGAIN 的,永遠不會為了某個慢客戶端而凍結。代價是你現在必須去問一個就緒機制(select、poll、epoll 或 kqueue)哪些描述符真的就緒,否則就會在一個毫無意義的 EAGAIN 迴圈裡空轉、燒掉 CPU。
int fl = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, fl | O_NONBLOCK); ssize_t n = read(fd, buf, sizeof buf); if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { /* 現在沒資料,去做別的事 */ }
把描述符標為非阻塞,於是一個沒有資料的讀取會回傳 EAGAIN,而不是凍結執行緒。
O_NONBLOCK 影響的是描述符,而非底下的檔案本身。在 Linux 上 EAGAIN 與 EWOULDBLOCK 是同一個數字,但 POSIX 允許它們不同,所以可攜的程式碼應同時檢查兩者。此外,一般磁碟檔案基本上忽略 O_NONBLOCK——它們被當成永遠就緒——這正是為何需要 io_uring 來做真正的非同步磁碟 I/O。