非同步與高效能輸入輸出

select 與 poll

/ select -> see-LEKT; poll -> pohl /

假設你盯著十個水壺,想在任何一個一開始沸騰的瞬間就行動,又不想一個個輪流盯。你需要一道單一命令,意思是「同時看著這全部,至少有一個就緒時就叫醒我」。對檔案描述符而言,最古老的兩道這種命令就是 select() 與 poll()。它們讓一個執行緒一次等待許多描述符,只要任一個變得可讀、可寫或出錯就返回——這是「不用一條連線一個執行緒就服務眾多連線」的最初答案。

select() 如何運作:你填三張位元圖(描述符號碼的集合)——一張管可讀就緒、一張管可寫、一張管錯誤——呼叫 select(),它會阻塞到至少一個描述符就緒,然後改寫你的位元圖,精確標出是哪些就緒。接著你巡走自己的描述符、處理就緒的那些。poll() 做同樣的工作但介面更乾淨:你傳入一個 struct pollfd 陣列,每一項指名一個描述符與你在意的事件,poll() 在每一項上填好 revents 欄位說明實際發生了什麼。兩者都只有在你每次呼叫重新備妥輸入時才是非破壞性的。

為何它們在大規模下式微:兩者在被監看描述符的數量上都是 O(n)。每一次呼叫你都得把「整份」清單交給核心,核心得掃過「整份」清單,返回時你又得再掃一遍去找出就緒的那些。十條連線時這不算什麼;十萬條時,即使只有一個通訊端進了一個位元組,你每次喚醒都要複製並走過一份十萬項的清單。select() 還有個歷史瑕疵:它通常被卡在 FD_SETSIZE(常為 1024)個描述符的上限。正是這個「每次呼叫 O(n)」的代價,催生了 epoll 與 kqueue 來把它移除。

struct pollfd fds[2] = { { sock1, POLLIN, 0 }, { sock2, POLLIN, 0 } }; int r = poll(fds, 2, 1000 /* 毫秒逾時 */); if (fds[0].revents & POLLIN) { /* sock1 可讀 */ }

poll() 同時等待兩個通訊端,並在 revents 中回報哪一個變得可讀。

一個常見錯誤:select() 會就地改寫它的位元圖,所以你每次呼叫前都必須重建 fd_set——重用被改過的集合會無聲地監看到錯誤的描述符。poll() 避開了這點,因為它讀 fd 與 events、卻只寫 revents。

又稱
I/O multiplexing輸入輸出多工(I/O multiplexing)