~洋大~ 金牌會員
劃線工人- 越線受罰 - UID
- 8007
- 帖子
- 3288
- 精華
- 350
- 積分
- 55619
- 金幣
- 175160
- 威望
- 1265
- 推廣
- 0
- 閱讀權限
- 88
- 來自
- 神魔分界線
- 註冊時間
- 2006-6-1
- 最後登錄
- 2015-4-13
|
2樓
大 中
小 發表於 2007-4-13 21:27
我們下面分別討論多路I/O的兩種實現方法:
1. 非阻塞通信方法
對一個文件描述符指定的文件或設備, 有兩種工作方式: 阻塞與非阻塞。所謂阻塞方式的意思是指, 當試圖對該文件描述符進行讀寫時, 如果當時沒有東西可讀,或者暫時不可寫, 程序就進入等待狀態, 直到有東西可讀或者可寫為止。而對於非阻塞狀態, 如果沒有東西可讀, 或者不可寫, 讀寫函數馬上返回, 而不會等待。缺省情況下, 文件描述符處於阻塞狀態。在實現聊天室時, server 需要輪流查詢與各client 建立的 socket, 一旦可讀就將該 socket 中的字符讀出來並向所有其他client 發送。並且, server 還要隨時查看是否有新的 client 試圖建立連接,這樣, 如果 server 在任何一個地方阻塞了, 其他 client 發送的內容就會受到影響,得不到服務器的及時響應。新 client 試圖建立連接也會受到影響。所以我們在這裡不能使用缺省的阻塞的文件工作方式,而需要將文件的工作方式變成非阻塞方式。在UNIX下,函數fcntl()可以用來改變文件I/O操作的工作方式,函數描述如下:
fcntl( sockfd, F_SETFL, O_NONBLOCK);
// sockfd 是要改變狀態的文件描述符.
// F_SETFL 表明要改變文件描述符的狀態
// O_NONBLOCK 表示將文件描述符變為非阻塞的.
為了節省篇幅我們使用自然語言描述聊天室 server :
while ( 1) {
if 有新連接 then 建立並記錄該新連接;
for ( 所有的有效連接)
begin
if 該連接中有字符可讀 then
begin
讀入字符串;
for ( 所有其他的有效連接)
begin
將該字符串發送給該連接;
end;
end;
end;
end.
由於判斷是否有新連接, 是否可讀都是非阻塞的, 因此每次判斷,不管有還是沒有, 都會馬上返回. 這樣,任何一個 client 向 server 發送字符或者試圖建立新連接, 都不會對其他 client 的活動造成影響。
對 client 而言, 建立連接之後, 只需要處理兩個文件描述符, 一個是建立了連接的 socket 描述符, 另一個是標準輸入. 和 server 一樣, 如果使用阻塞方式的話, 很容易因為其中一個暫時沒有輸入而影響另外一個的讀入.. 因此將它們都變成非阻塞的, 然後client 進行如下動作:
while ( 不想退出)
begin
if ( 與 server 的連接有字符可讀)
begin
從該連接讀入, 並輸出到標準輸出上去.
End;
if ( 標準輸入可讀)
Begin
從標準輸入讀入, 並輸出到與 server 的連接中去.
End;
End.
上面的讀寫分別調用這樣兩個函數:
read( userfd, line, MAX_LINE);
// userfd 是指第 i 個 client 連接的文件描述符.
// line 是指讀出的字符存放的位置.
// MAX_LINE 是一次最多讀出的字符數.
// 返回值是實際讀出的字符數.
write( userfd[j], line, strlen( line));
// userfd[j] 是第 j 個 client 的文件描述符.
// line 是要發送的字符串.
// strlen( line) 是要發送的字符串長度.
分析上面的程序可以知道, 不管是 server 還是 client, 它們都不停的輪流查詢各個文件描述符, 一旦可讀就讀入並進行處理. 這樣的程序, 不停的在執行, 只要有CPU 資源, 就不會放過。因此對系統資源的消耗非常大。server 或者 client 單獨執行時, CPU 資源的 98% 左右都被其佔用。極大的消耗了系統資源。
select 方法
因此,雖然我們不希望在某一個用戶沒有反應時阻塞其他的用戶,但我們卻應該在沒有任何用戶有反應的情況之下停止程序的運行,讓出搶佔的系統資源,進入阻塞狀態。有沒有這種方法呢?現在的UNIX系統中都提供了select方法,具體實現方式如下:
select 方法中, 所有文件描述符都是阻塞的. 使用 select 判斷一組文件描述符中是否有一個可讀(寫), 如果沒有就阻塞, 直到有一個的時候就被喚醒. 我們先看比較簡單的 client 的實現:
由於 client 只需要處理兩個文件描述符, 因此, 需要判斷是否有可讀寫的文件描述符只需要加入兩項:
FD_ZERO( sockset);
// 將 sockset 清空
FD_SET( sockfd, sockset);
// 把 sockfd 加入到 sockset 集合中
FD_SET( 0, sockset);
// 把 0 (標準輸入) 加入到 sockset 集合中
然後 client 的處理如下:
while ( 不想退出) {
select( sockfd+1, &sockset, NULL, NULL, NULL);
// 此時該函數將阻塞直到標準輸入或者 sockfd 中有一個可讀為止
// 第一個參數是 0 和 sockfd 中的最大值加一
// 第二個參數是 讀集, 也就是 sockset
// 第三, 四個參數是寫集和異常集, 在本程序中都為空
// 第五個參數是超時時間, 即在指定時間內仍沒有可讀, 則出錯
// 並返回. 當這個參數為NULL 時, 超時時間被設置為無限長.
// 當 select 因為可讀返回時, sockset 中包含的只是可讀的
// 那些文件描述符.
if ( FD_ISSET( sockfd, &sockset)) {
// FD_ISSET 這個宏判斷 sockfd 是否屬於可讀的文件描述符
從 sockfd 中讀入, 輸出到標準輸出上去.
}
if ( FD_ISSET( 0, &sockset)) {
// FD_ISSET 這個宏判斷 sockfd 是否屬於可讀的文件描述符
從標準輸入讀入, 輸出到 sockfd 中去.
}
重新設置 sockset. (即將 sockset 清空, 並將 sockfd 和 0 加入)
}
下面看 server 的情況:
設置 sockset 如下:
FD_ZERO( sockset);
FD_SET( sockfd, sockset);
for ( 所有有效連接)
FD_SET( userfd, sockset);
}
maxfd = 最大的文件描述符號 + 1;
server 處理如下:
while ( 1) {
select( maxfd, &sockset, NULL, NULL, NULL);
if ( FD_ISSET( sockfd, &sockset)) {
// 有新連接
建立新連接, 並將該連接描述符加入到 sockset 中去了.
}
for ( 所有有效連接) {
if ( FD_ISSET ( userfd, &sockset)) {
// 該連接中有字符可讀
從該連接中讀入字符, 並發送到其他有效連接中去.
}
}
重新設置 sockset;
}
性能比較
由於採用 select 機制, 因此當沒有字符可讀時, 程序處於阻塞狀態,最小程度的佔用CPU 資源, 在同一台機器上執行一個 server 和若干個client 時, 系統負載只有 0.1 左右, 而採用原來的非阻塞通信方法, 只運行一個 server, 系統負載就可以達到 1.5 左右. 因此我們推薦使用 select.
參考文獻:
[1] UNIX Network Programming Volume 1 W.Richard Stevens 1998 Prentice Hall
[2] 計算機實用網絡編程 湯毅堅 1993 人民郵電出版社
[3] UNIX? SYSTEM V RELEASE 4 Programmer's Guide:STREAMS AT&T 1990 Prentice Hall
[4] UNIX? SYSTEM V RELEASE 4 Network Programmer's Guide AT&T 1990 Prentice Hall
所有源程序均登載在eDOC網站上,如有需要可以去訪客無法瀏覽此圖片或連結,請先 註冊 或 登入會員 。 下載
|