1. 程式人生 > >Linux網路程式設計---I/O多路複用之select

Linux網路程式設計---I/O多路複用之select

1.I/O多路複用(IO multiplexing)

我們之前講了I/O多路複用和其他I/O的區別,在這裡,我們再具體討論下I/O多路複用是怎麼工作?
I/O 多路複用技術就是為了解決程序或執行緒阻塞到某個 I/O 系統呼叫而出現的技術,使程序不阻塞於某個特定的 I/O 系統呼叫。

select(),poll(),epoll()都是I/O多路複用的機制。I/O多路複用通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒,就是這個檔案描述符進行讀寫操作之前),能夠通知程式進行相應的讀寫操作。 但select(),poll(),epoll()本質上都是同步I/O,因為他們都需要使用者程序在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現核心會負責把資料從核心拷貝到使用者空間。

與多執行緒和多程序相比,I/O 多路複用的最大優勢是系統開銷小,系統不需要建立新的程序或者執行緒,從而也就不必維護這些執行緒和程序。

具體來說就是單個程序(process)就可以同時處理多個網路連線的IO。它的基本原理就是select/epoll這個函式(function)會不斷的輪詢所負責的所有socket,當某個socket有資料到達了,就通知使用者程序。它的流程如圖:
在這裡插入圖片描述

如果還不清楚,我們接下來一個一個來理解。

I/O多路複用—select

select系統調⽤是⽤來讓我們的程式監視多個⽂件描述符的狀態變化的;
程式會停在select這⾥等待,直到被監視的⽂件描述符有⼀個或多個發⽣了狀態改變

1. 所需標頭檔案

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

2. 引數:

nfds : 要監視的檔案描述符的範圍,取當前監視的描述符中最大描述符的值+1,在 Linux 上最大值一般為1024。
readfds:

監視的可讀描述符集合,只要有檔案描述符即將進行讀操作,這個檔案描述符就儲存到這。
writefds: 監視的可寫描述符集合。
exceptfds: 監視的錯誤異常描述符集合

注:中間的”三個引數 readfds、writefds 和 exceptfds 指定我們要讓核心監測讀、寫和異常條件的描述字。如果不需要使用某一個的條件,就可以把它設為空指標( NULL )。集合fd_set 中存放的是檔案描述符,可通過以下四個巨集進行設定:

a.清空集合:用來清除描述片語set的全部位

void FD_ZERO(fd_set *fdset); 

b.設定:⽤來設定描述片語set中相關fd的位,將一個給定的檔案描述符加入集合之中

void FD_SET(int fd, fd_set *fdset);

c.⽤來清除描述片語set中相關fd 的位,將一個給定的檔案描述符從集合中刪除

void FD_CLR(int fd, fd_set *fdset);

d.⽤來測試描述片語set中相關fd 的位是否為真 ,檢查集合中指定的檔案描述符是否可以讀寫

int FD_ISSET(int fd, fd_set *fdset); 

timeout: 超時時間,它告知核心等待所指定描述字中的任何一個就緒可花多少時間。其 timeval 結構用於指定這段時間的秒數和微秒數。

  • NULL: 則表示select()沒有timeout,select將⼀直被阻塞,直到某個⽂件描述符上發⽣了事件;
  • 0: 不阻塞 僅檢測描述符集合的狀態,然後立即返回,並不等待外部事件的發⽣。檢查描述字後立即返回,這稱為輪詢,
  • 特定的時間值: 如果在指定的時間段⾥沒有事件發生,select將超時返回
struct timeval
{
	time_t tv_sec;       /* 秒 */
	suseconds_t tv_usec; /* 微秒 */
};

3. 返回值

成功:就緒描述符的數目,超時返回 0,
失敗:-1,錯誤原因存於errno,錯誤值可能為:

  • EBADF :檔案描述詞為⽆效的或該⽂件已關閉
  • EINTR :此調⽤被訊號所中斷
  • EINVAL: 引數n為負值。
  • ENOMEM: 核⼼記憶體不⾜

4. 功能

監視並等待多個檔案描述符的屬性變化(可讀、可寫或錯誤異常)。select()函式監視的檔案描述符分 3 類,分別是writefds、readfds、和 exceptfds。呼叫後 select() 函式會阻塞,直到有描述符就緒(有資料可讀、可寫、或者有錯誤異常),或者超時( timeout 指定等待時間),函式才返回。當 select()函式返回後,可以通過遍歷 fdset,來找到就緒的描述符。

接下來我們來理解select的執行過程

5.select原理

理解select模型的關鍵在於理解fd_set,其實這個結構就是⼀個整數陣列, 更嚴格的說, 是⼀個 “點陣圖”. 使⽤點陣圖中對應的位來表⽰要監視的⽂件描述符。如圖:fd_set結構體裡面包含了一個長整型陣列。
在這裡插入圖片描述
在這裡插入圖片描述

舉個例子:
為說明⽅便,取fd_set⻓度為1位元組,fd_set中的每⼀bit可以對應⼀個⽂件描述符fd。則1位元組⻓的fd_set最⼤可以對應8個fd。
(1)執行fdset set; FD_ZERO(&set);則set用位表示是0000,0000。
(2)若fd=5,執⾏FD_SET(fd,&set);後set變為0001,0000(第5位置為1)
(3)若再加⼊fd=2,fd=1,則set變為0001,0011
(4)執⾏select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都發⽣可讀事件,則select返回,此時set變為0000,0011。
注意:沒有事件發⽣的fd=5被清空

6.select執行過程(重點)

重點內容:
首先,在呼叫select函式之前,我們事要設定select引數,即將我們要將要監控的fd(檔案描述符)放入fd集合(readfds,writefds,exceptfds)中,在呼叫select之後,這些fd集合便會從使用者態拷貝到核心態這時程式會停在select阻塞等待,直到被監視的檔案描述符有某一個或多個發生了狀態改變。而此時核心做的工作就是將這些fd集合遍歷一遍,看有沒有事件就緒的fd。此時分為兩種情況:
1.若有IO事件就緒,則核心就會將該事件所對應的fd標誌位置1,由此來通知執行了select()的程序哪些Socket或檔案可讀可寫, 而沒有就緒的檔案描述符fd 則會從集合中被剔除掉,也就是說最後返回都是就緒的檔案描述符,然後,當select返回時,再將這些修改過的fd集合又從核心態拷貝到使用者態selec返回值為就緒fd的個數,最後使用者程序就可以處理這些讀寫操作。
2.若此時核心遍歷一遍沒有就緒的檔案描述符fd,那麼核心將不做操作,而select這時也會返回(因為select是同步),只不過select這時還是阻塞的,它會一直輪詢,直到有就緒的檔案操作符fd,並且等待核心將就緒的檔案操作符修改標誌位,最後select返回就緒的檔案描述符的個數,這樣才會執行完畢。

以上就是select函式完整的執行過程,我們從中也發現他不少特點:

先來說缺點吧,
7.select優缺點(重點)

1.最大併發數限制
因為一個程序所開啟的FD(檔案描述符)是有限制的,由FD_SETSIZE設定(上圖中有FD_SETSIZE),預設值是1024/2048,因此select模型的最大併發數就被相應限制了。
雖然使用者可以自己修改FD_SETSIZE,然後重新編譯核心,但是其實,並不推薦這麼做,因為有可能導致程式崩潰,效能方面不言而喻肯定下降。

include/linux/posix_types.h:
#define __FD_SETSIZE         1024

因為Linux下fd_set是一個 1024 位的點陣圖,因此當程序內的 fd 值 >= 1024 時,就會越界,可能會造成崩潰。對於伺服器程式,fd >= 1024 很容易達到,只要連線數 + 開啟的檔案數足夠大即可發生。效能方面不言而喻肯定下降。而效能方面核心每次都要遍歷一遍全部的fd集合,fd越多,效能越慢。

2.效率問題:
就是我們上面說的,select每次呼叫都會核心都會線性掃描全部的fd集合,這樣效率就會呈現線性下降.

3.核心/使用者空間 記憶體拷貝問題
因為fd集合要從核心和使用者之間拷貝多次,而當於服務端要面對成百上千的連線時,這時fd就非常多,效能不言而喻就非常慢了。
4.編碼麻煩
每次有就緒事件發生時,都會修改描述符集合 ,把沒有就緒的剔除出去,因此我們要監控的描述符每次迴圈都需要新增,並且select返回值並不會直接告訴使用者哪個描述符就緒,因此需要遍歷所有的描述符看哪一個就緒,導致編碼麻煩。

說完了缺點,當然還有優點:
1.跨平臺: select目前幾乎在所有的平臺上支援,其良好跨平臺支援也是它的一個優點。epoll是僅只有Linux下。
2.適用於UDP,適用於單個描述符方便。並且監控的超時時間比較精細(精確到微秒,epoll只能到毫秒)

適用場景:
select適用於在單個描述符的監控上用的頻率非常高
多路複用技術(select/epoll/poll)都是適用於有大量客戶端連線,但是同一時間只有少量活躍的情況

還有poll 和 epoll ,下個部落格介紹

資料參考:https://blog.csdn.net/gatieme/article/details/50979090
https://blog.csdn.net/tennysonsky/article/details/45745887