How to use the select(), an I/O Multiplexer
根據《Boost application performance using asynchronous I/O》一文所做的區分,在 POSIX 上的 I/O 處理模式可分為四種。該文所舉的 POSIX AIO API 是晚近較新的處理模式,是 POSIX.1b 基於 realtime system (即時系統、實時系統) 之需求而定的規範內容,其概念是事件驅動模式, AJAX 中的 XMLHttpRequest 調用方式就是這種模式。在 POSIX 傳統上的非同步 I/O 模式 (即 Asynchronous blocking I/O) ,則是使用 select() 來達成。本文將說明這種傳統模式的使用方式。
作者: rock (遊手好閒的石頭成)
E-mail: shirock@educities.edu.tw
First edition: 1998-10-01 [How to use select()]
Last modify: 2006-10-19
Synchronous I/O
讓我們回想一下,當我們需要從一個設備中讀取或寫入資料時,我們通常很直覺地使用輸出入函數,如系統呼叫 read() 及 write() 等,而沒有想到先確認目標設備中是否已經存有資料可供處理。例如:
這是因為系統也很清楚不能預期每次要對設備進行資料的輸出入時,設備裡都剛好有資料可以處理。有太多理由使得系統必須等待設備,因此每當目標設備無法立即處理待輸出入的資料時,系統便會自動擱置 (blocking) 目前的工作,亦即將目前工作的工作進程停在輸出入函數的地方,如 read(), write() 處,等到目標設備可以處理資料時,才結束等待的動作,繼續下一個步驟。這樣的做法很合理,也是大多數程式常碰見的情形,程式一次只需要處理一個設備的輸出入即可,例如從鍵盤取得使用者輸入,從檔案讀取記錄,再寫入另一個檔案,都是一個步驟只需處理一個設備,當這個設備暫時無法處理資料時,自然希望它能自動停在那邊,等資料來到。例如:
上面的程式碼是很典型的設計模式。 Programmer 預期能先從 getchar() 取得使用者的輸入內容後,才開始做 read() ,再做 write() ,系統的動作也很符合設計者的預期,先停在 getchar() 處,等使用者從鍵盤輸入一個字元後,才決定要不要繼續下一個步驟。由於程式碼中的每一個動作,都必須等待上一行的動作結束才會跟著執行,前面的進一步,後面的才能跟進一步,因此這種處理方式被稱為「同步處理 (Synchronous process)」。
不過也有上面的情形無法處理的時候,例如,你正在寫一個使用者對談程式,此時你將有至少兩個資料輸入來源,一個來自本地使用者的鍵盤輸入,及一個來自交談對方的鍵盤輸入,更糟的是,你無法預期哪個設備何時會有資料進入。以下列程式碼為例,說明程式將會碰到哪些典型狀態。
-
當本地使用者尚未輸入一行文字時,本地行程將會擱置在第 1 行 fgets(mystr ...) ,等本地使用者輸入。
-
然而當本地行程正在等待本地使用者輸入時,對方的行程可不知道這種情形,且對方比本地使用者早輸入完一行文字。亦即對方比本地使用者更早進行到第 5 行 fgets(hisstr ...) 處,等待本地使用者送來的資料。
-
雖然對方已經輸入完一行文字了,但是本地使用者還沒輸入,本地行程仍然擱置在第一行 fgets(mystr ...) ,因此本地使用者還看不到對方輸入的內容。
-
結果當本地使用者還沒輸入一行文字前,本地使用者看不到對方輸入的內容。同時,對方也在等取得本地使用者輸入的文字,而無法繼續輸入。
Asynchronous blocking I/O
對於這種情形。在 Unix 系統中的 SVR4 及 BSD 家族,都提供了一個 API: select() ,處理需要同時面對多個輸出入設備時的情形。簡單地說, select() 是一個多重發訊器 (multiplexer) ,可以同時監視多個輸出入設備,並且選出最快能處理資料的設備,讓行程可以順利的進行下一個工作,減少等待的時間。再以剛說的對談程式為例,利用 select() 改寫後如下:
-
第 1 行: 將 stdin 加入 readmask 變數中,此 readmask 變數將傳給 select() ,告訴 select() 有 stdin 這個設備等著要讀取資料。
-
第 2 行: 將 hisin 加入 readmask 變數中,告訴 select() 有 hisin 這個設備等著要讀取資料。
-
第 3 行: 執行 select() ,此時 select() 將會等待 stdin 及 hisin 兩個設備,並將最快有資料可處理的設備代號,儲在 readmask 中傳回。
-
第 4-8 行: 如果目前有資料可處理的設備是 stdin ,則讀取目前使用者的輸入。
-
第 9-12 行: 如果目前有資料可處理的設備是 hisin ,則讀取對方的輸入。
透過 select() ,程式將可以馬上處理已經有資料到來的設備,而不必枯等尚無資料到來的設備。上述運用 select() 的模式是 Unix 系統傳統的非同步 I/O 處理模型 (Asynchronous blocking I/O) ,然而 select() 雖然是一個具普遍性的 API ,在 SVR4 及 BSD 中都有提供,但卻是一個行為多變的 API ,在不同系統間存在不同的行為表現。 select() 的原型及其行為異同如下列。
共同行為
-
maxfd 表示共有幾個設備要 select() 處理。
-
readfds 儲存要處理的輸入設備的檔案描述詞的集合。
-
writefds 儲存要處理的輸出設備的檔案描述詞的集合。
-
execptfds 儲存有突發狀態發生的設備的檔案描述詞的集合。
-
tvptr 表示要求 select() 等待的時間。
-
在回傳值上,當有錯誤發生時,回傳 -1 並設定 errno 的值。當超過 select() 的等待時間時,回傳 0 ,表示處理逾時。當有設備可以處理時,則回傳大於 0 的值。
各平台間的差異
-
在 BSD 上,如果同一個檔案描述詞在兩個檔案描述詞的集合都可以處理時(例如 readfds 及 writefds) ,則 BSD 將回傳 2 ,表示有兩個設備可以處理了。而在 SVR4 上,永遠只回傳 1 ,視為一個。
-
在 BSD 系統中, tvptr 的內容,被視為唯讀的,當 select() 回傳結果後, tvptr 的內容不會被改變。即使 select() 是被 signal 所中斷,系統也不會改變 tvptr 的內容。在 Linux 中 (不是指 SVR4 ,我不知道 SVR4 是如何處理 tvptr 的) ,則會改變 tvptr 的值,將可等待的時間減掉實際等待的時間,所得到的剩餘時間存在 tvptr 中回傳。這個做法在碰到 select() 被 signal 中斷時,是相當有用的。
-
在 winsock 中, maxfd 沒有意義,純粹為了相容 unix 系統而存在。
通用性 select() 使用模式