作者: rock (遊手好閒的石頭成) E-mail: shirock@mail.educities.edu.tw --------------------------- select() - 輸出入多重發訊器 讓我們回想一下,當我們需要從一個設備中讀取或寫入資料時,我們通常很直覺地 使用輸出入函數,如系統呼叫 read() 及 write() 等,而沒有想到先確認目標設備 中是否已經存有資料可供處理。 例如: read(fd, buf, len); . . . write(fd, buf, len); 這是因為系統也很清楚,不能預期每次要對設備進行資料的輸出入時,設備裡都剛 好可以處理,有太多的理由,使得系統必須等待設備,因此每當目標設備無法立即 處理待輸出入的資料時,系統便會自動擱置目前的工作,亦即將目前工作的工作進 程停在輸出入函數的地方,如 read(), write() 處,等到目標設備可以處理資料 時,才完成停頓的動作,繼續下一個步驟。 這樣的做法很合理,也是大多數程式常碰見的情形,程式一次只需要處理一個設備 的輸出入即可,例如從鍵盤取得使用者輸入,從檔案讀取記錄,再寫入另一個檔案 ,都是一個步驟只需處理一個設備,當這個設備暫時無法處理資料時,自然希望它 能自動停在那邊,等資料來到。 例如: puts("Do you sure? (Y/N):"); if( getchar() != 'Y' ) return; read(fin, buf, len); write(fout, buf, len); 上面的程式碼是很典型的,每個人都寫過,每個人都希望,能先從 getchar() 取 得使用者的輸入後,才開始做 read() ,再做 write() ,系統的動作也很符合設 計者的預期,先停在 getchar() 處,等使用者從鍵盤輸入一個字元後,才決定要 不要繼續下一個步驟。 不過也有上面的情形無法處理的時候,例如,你正在寫一個使用者對談程式,此時 你將有至少兩個資料輸入來源,一個來自目前使用者的鍵盤輸入,及一個來自交談 對方的鍵盤輸入,更糟的是,你無法預期哪個設備何時會有資料進入。 如果你寫下下列的程式碼: fgets(mystr, sizeof(mystr), stdin); fputs(mystr, strlen(mystr), hisout); printf("I say: %s", mystr); fgets(hisstr, sizeof(hisstr), hisin); printf("He says: %s", hisstr); 將會碰到下列的典型狀態: 1.當目前使用者尚未輸入一行文字時,系統將會停在 fgets(mystr ...) 處, 等目前使用者輸入。 2.然而當目前的程式正在等目前使用者輸入時,對方的程式可不知道這種情形, 對方比目前使用者早輸入完一行文字。 亦即對方比目前使用者早進行到 fgets(hisstr ...) 處,等目前使用者送來 的資料。 3.雖然對方已經輸入完一行文字了,但是目前使用者還沒輸入,程式仍然等在 fgets(mystr ...) 的地方,因此目前使用者還看不到對方輸入的內容。 4.結果是,當目前使用者還沒輸入一行文字前,目前使用者看不到對方輸入的內 容,同時,對方也在等取得目前使用者輸入的文字,而無法繼續輸入。 不過在 unix 系統中, SVR4 及 BSD43 ,都提供了一個函數 select() ,處理需 要同時面對多個輸出入設備時的情形。 簡單地說, select() 是一個多重發訊器,可以同時面對多個輸出入設備,並且選 出最快能處理資料的設備,讓程式可以順利的進行下個工作,減少等待的時間。 再以剛說的對談程式為例,利用 select() 改寫後,如下: (1) FD_SET(fileno(stdin), &readmask); (2) FD_SET(fileno(hisin), &readmask); (3) select(2, &readmask, NULL, NULL, NULL); (4) if( FD_ISSET(fileno(stdin), &readmask) ) { fgets(mystr, sizeof(mystr), stdin); fputs(mystr, strlen(mystr), hisout); printf("I say: %s", mystr); } (5) else if( FD_ISSET(fileno(hisin), &readmask) ) { fgets(hisstr, sizeof(hisstr), hisin); printf("He says: %s", hisstr); } 此程式碼的意義為: (1) 將 stdin 加入 readmask 變數中,此 readmask 變數將傳給 select() , 告訴 select() 有 stdin 這個設備等著要讀取資料。 (2) 將 hisin 加入 readmask 變數中,告訴 select() 有 hisin 這個設備等 著要讀取資料。 (3) 執行 select() ,此時 select() 將會等待 stdin 及 hisin 兩個設備, 並將最快有資料可處理的設備代號,儲在 readmask 中傳回。 (4) 如果目前有資料可處理的設備是 stdin ,則讀取目前使用者的輸入。 (5) 如果目前有資料可處理的設備是 hisin ,則讀取對方的輸入。 透過 select() ,程式將可以馬上處理已經有資料到來的設備,而不必枯等尚 無資料到來的設備。 select() 雖然是一個普遍性的函數,在 SVR4 及 BSD43 中都有提供,但卻不是 一個標準的函數,在不同系統間,存在不同的行為表現。 select() 的原型是: #include #include #include int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *execptfds, struct timeval *tvptr); 而它的共同行為是: 1. maxfd 表示共有幾個設備要 select() 處理。 2. readfds 儲存要處理的輸入設備的檔案描述詞的集合。 3. writefds 儲存要處理的輸出設備的檔案描述詞的集合。 4. execptfds 儲存有突發狀態發生的設備的檔案描述詞的集合。 5. tvptr 表示要求 select() 等待的時間。 6. 在回傳值上,當有錯誤發生時,回傳 -1 並設定 errno 的值。 當超過 select() 的等待時間時,回傳 0 ,表示處理逾時。 當有設備可以處理時,則回傳大於 0 的值。 而差異有: 1. 在 BSD 上,如果同一個檔案描述詞在兩個檔案描述詞的集合都可以處理 時(例如 readfds 及 writefds) ,則 BSD 將回傳 2 ,表示有兩個設備 可以處理了。 而在 SVR4 上,則只回傳 1 ,視為一個。 2. 在 BSD 系統中, tvptr 的內容,被視為唯讀的,當 select() 回傳結果 後, tvptr 的內容不會被改變。 即使 select() 是被 signal 所中斷,系統也不會改變 tvptr 的內容。 在 Linux 中 (不是指 SVR4 ,我不知道 SVR4 是如何處理 tvptr 的) , 則會改變 tvptr 的值,將可等待的時間減掉實際等待的時間,所得到的剩 餘時間存在 tvptr 中回傳。 這個做法在碰到 select() 被 signal 中斷時,是相當有用的。 3. 在 winsock 中, maxfd 沒有意義,純粹為了相容 unix 系統而存在。 綜合以上數點,一個可以通用的 select() 用法將如: struct timeval timeout; #if !defined( LINUX ) time_t lasttime; #endif /* 在 LINUX 系統中, lasttime 不需要用到,下面會說明 */ #if !defined( LINUX ) lasttime = time(NULL); /* 保存目前時間(秒數) */ #endif timeout.tv_sec = nsec; /* 設定要 select() 等待的時間 */ timeout.tv_usec = 0; while( some_condition ) { rc = select( nfds, &readfds, &writefds, &execptfds, &timeout ); if( rc < 0 ) { /* 有錯誤發生 */ if( errno == EINTR ) { /* 被 signal 中斷了 */ /* 扣掉已經等待的時間後,再繼續呼叫 select() 等待剩餘的時間。 由於 LINUX 系統會自動扣掉已等待的時間,所以我們不需要自已動手。*/ #if !defined( LINUX ) timeout.tv_sec -= (time(NULL) - lasttime); lasttime = time(NULL); #endif continue; } else abort(); } else if( rc == 0 ) { /* TIMEOUT */ . . . } else { /* 有設備可以處理 */ if( FD_ISSET( . . . ) ) /* 判斷是哪個設備可以處理 */ . . . } #if !defined( LINUX ) lasttime = time(NULL); #endif timeout.tv_sec = nsec; timeout.tv_usec = 0; /* 雖然已知目前的 BSD 系統不會變更 timeout 的內容,但是不保證未來的 BSD 系統及其他的系統如 SVR 等也不會變更 timeout 的內容,因此還是 再設定一次後,才繼續使用 select() 等待下一個可處理的設備,是比較 可靠的做法。 */ }