前言
傳統(tǒng)的服務(wù)器框架都是阻塞I/O類型的垦沉,為了提高并發(fā)性將迭代型(while)改為多進程亚情,但是多進程開銷較大歉闰,然后設(shè)計多線程的模型昙沦,節(jié)約系統(tǒng)開銷琢唾,但是考慮到傳統(tǒng)的accept函數(shù)是阻塞函數(shù),然后設(shè)計多個進程或是線程同時accept的方式提高并發(fā)處理能力盾饮。本質(zhì)沒有偏離阻塞I/O的基本很特性采桃,其原因就是我們的API接口均是阻塞型的,沒有請求就死等丐谋,沒有連接就耗著芍碧。思考有沒有一種非阻塞的方式可以實現(xiàn)同時監(jiān)聽多個socket?
默認方式下号俐,accept處于阻塞狀態(tài)泌豆,將套接字文件屬性設(shè)置為非阻塞時,accept處于非阻塞狀態(tài)(其實與該套接字相關(guān)的系統(tǒng)調(diào)用都是非阻塞的了)吏饿。
阻塞與非阻塞
- 阻塞與非阻塞是程序(線程)等待消息通知時的狀態(tài)角度來說的踪危。
- 阻塞模式:程序(線程)在執(zhí)行I/O操作完成前會一直等待蔬浙,不會把程序的控制權(quán)交給CPU。如(connect贞远、accept畴博、recv、recvfrom函數(shù))默認都是阻塞的蓝仲。
阻塞模式下俱病,進程或是線程執(zhí)行到這些函數(shù)時必須等待某個事件的發(fā)生,如果事件沒有發(fā)生袱结,進程或線程就被阻塞(死等在被阻塞的地方)亮隙,函數(shù)不會立即返回。
- 非阻塞模式:程序(線程)在執(zhí)行I/O操作時垢夹,進行系統(tǒng)調(diào)用時在不能立刻得到結(jié)果之前溢吻,該函數(shù)不會阻塞當(dāng)前線程,而會立刻返回果元。
- 非阻塞的方式可以明顯的提高CPU的利用率促王,但是增加系統(tǒng)的線程切換增加。所以增加的CPU執(zhí)行時間能不能補償系統(tǒng)的切換成本需要好好評估
- 非阻塞non-block模式下而晒,進程或線程執(zhí)行此函數(shù)時不必非要等待事件的發(fā)生蝇狼,一旦執(zhí)行肯定返回,以返回值的不同來反映函數(shù)的執(zhí)行情況倡怎,如果事件發(fā)生則與阻塞方式相同题翰,若事件沒有發(fā)生則返回一個代碼來告知事件未發(fā)生,而進程或線程繼續(xù)執(zhí)行诈胜,所以非阻塞模式效率較高。
select描述(同步IO復(fù)用)
- select系統(tǒng)調(diào)用可以實現(xiàn)一個進程同時監(jiān)聽多個文件描述符(socket描述符)的狀態(tài)變化冯事,默認的當(dāng)調(diào)用select()函數(shù)焦匈,程序會阻塞到這里(除非設(shè)置timeout),直到被監(jiān)視的文件描述符有某一個或多個發(fā)生了狀態(tài)改變昵仅,則select函數(shù)返回缓熟。
select機制中提供了一個 fd_set數(shù)據(jù)結(jié)構(gòu),僅僅包含一個整形數(shù)組摔笤,數(shù)組的每一個元素的每一位(bit)標(biāo)記一個文件描述符够滑,某個文件描述的狀態(tài)改變時,設(shè)置相應(yīng)位為1吕世,表示就緒彰触。fd_set能容納的文件描述符的數(shù)量是由FD_SETSIZE決定,默認為1024命辖,除非修改宏定義况毅,并編譯內(nèi)核分蓖。
select函數(shù)原型
NAME
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous
I/O multiplexing
SYNOPSIS
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#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);
void FD_CLR(int fd, fd_set *set); //用來清除描述詞組set中相關(guān)fd 的位
int FD_ISSET(int fd, fd_set *set);//用來測試描述詞組set中相關(guān)fd 的位是否為真
void FD_SET(int fd, fd_set *set);//用來設(shè)置描述詞組set中相關(guān)fd的位
void FD_ZERO(fd_set *set); // 用來清除描述詞組set的全部位。
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
參數(shù)詳解
- ndfs:select監(jiān)視的文件描述符數(shù)尔许,視進程中打開的文件數(shù)而定么鹤,一般設(shè)置為監(jiān)視各文件中的最大文件描述符值加1。
- readfds:這個文件描述符集合監(jiān)視文件集中的任何文件是否有數(shù)據(jù)可讀味廊,當(dāng)select函數(shù)返回的時候蒸甜,readfds將清除其中不可讀的文件描述符,只留下可讀的文件描述符余佛。
- writefds:這個文件描述符集合監(jiān)視文件集中的任何文件是否有數(shù)據(jù)可寫柠新,當(dāng)select函數(shù)返回的時候,writefds將清除其中不可寫的文件描述符衙熔,只留下可寫的文件描述符登颓。
- exceptfds:這個文件集將監(jiān)視文件集中的任何文件是否發(fā)生錯誤。
- timeout:本次select()的超時結(jié)束時間红氯,使得select處于三種不同的狀態(tài):
- 若將NULL以形參傳入框咙,即不傳入時間結(jié)構(gòu),就是將select置于阻塞狀態(tài)痢甘,一定等到監(jiān)視文件描述符集合中某個文件描述符發(fā)生變化為止喇嘱;
- 若將時間值設(shè)為0秒0毫秒,就變成一個純粹的非阻塞函數(shù)塞栅,不管文件描述符是否有變化者铜,都立刻返回繼續(xù)執(zhí)行,文件無變化返回0放椰,有變化返回一個正值作烟;
- timeout的值大于0,這就是等待的超時時間砾医,即select在timeout時間內(nèi)阻塞拿撩,超時時間之內(nèi)有事件到來就返回了,否則在超時后不管怎樣一定返回如蚜,返回值同上述压恒。
Linux Socket編程中select的常見用處
-
accept函數(shù)的非阻塞實現(xiàn){服務(wù)器端}
如果將正在listen的socket設(shè)置到readfds中,調(diào)用select错邦,如果有客戶端connect探赫,select將返回正值,通過宏FD_ISSET可檢測到該socket可讀撬呢,此時再用accept接受新的socket伦吠,并通過FD_SET將accept返回的new socket描述符添加到readfds中,若是active connection 就調(diào)用recv()讀取數(shù)據(jù)。
- connect函數(shù)的非阻塞實現(xiàn)(TCP){客戶端}
EINPROGRESS(man connect的描述)
The socket is nonblocking and the connection cannot be completed
immediately. It is possible to select(2) or poll(2) for comple‐
tion by selecting the socket for writing. After select(2) indi‐
cates writability, use getsockopt(2) to read the SO_ERROR option
at level SOL_SOCKET to determine whether connect() completed
successfully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is
one of the usual error codes listed here, explaining the reason
for the failure).
描述了connect出錯時的一種值errno值:EINPROGRESS讨勤,這種錯誤發(fā)生在非阻塞的socket調(diào)用從connect箭跳,而連接又沒有立即建立時,在這種情況下潭千,可以調(diào)用select谱姓、poll等函數(shù)來監(jiān)聽這個連接的失敗的socket上的可寫事件,當(dāng)select刨晴、poll等函數(shù)返回時屉来,在利用getsockopt來讀取錯誤碼并清除該socket上的錯誤,如果錯誤碼是0狈癞,表示連接建立成功茄靠。
主動寫socket時對方突然關(guān)閉連接的處理,則可以簡單地捕捉信號SIGPIPE并作出相應(yīng)關(guān)閉本地socket等等的處理蝶桶。SIGPIPE的解釋是:寫入無讀者方的管道慨绳。
缺點
- 單個進程可以監(jiān)聽的描述符的個數(shù)限制。
- 開銷大(內(nèi)存拷貝):包含大量文件描述符的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核的地址空間之間真竖,每次在select調(diào)用之前脐雪,需要將監(jiān)聽的描述符從用戶態(tài)空間拷貝到內(nèi)核的地址空間,在select調(diào)用都返回整個用戶注冊的事件集合(包括據(jù)就緒的和為就緒的)恢共,它的開銷隨著文件描述符數(shù)量的增加而線性增大战秋。
- 效率問題:內(nèi)核在幫助應(yīng)用程序監(jiān)聽多個描述符的時候,是一種輪循檢測就緒事件的方式讨韭,掃描判斷哪個socket描述符的位是就緒的脂信,這是耗時的,時間復(fù)雜度為O(n)透硝。
poll的改進:只是描述fd集合的方式不同狰闪,
epoll的改進
- 沒有最大并發(fā)連接的限制,上限是最大可以打開文件的數(shù)目濒生。cat /proc/sys/fs/file-max察看尝哆。
- 共享內(nèi)存:在epoll_ctl函數(shù)中,每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD)甜攀,會把所有的fd拷貝進內(nèi)核,而不是在epoll_wait的時候重復(fù)拷貝琐馆。epoll保證了每個fd在整個過程中只會拷貝一次规阀。利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞,即epoll使用mmap減少復(fù)制開銷瘦麸。
- 效率提升:只有活躍可用的FD才會調(diào)用callback函數(shù)谁撼;即Epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數(shù)無關(guān)。
- select厉碟,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次喊巍,并且要把current往設(shè)備等待隊列中掛一次,而epoll只要一次拷貝箍鼓,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始崭参,注意這里的等待隊列并不是設(shè)備等待隊列,只是一個epoll內(nèi)部定義的等待隊列)款咖。這也能節(jié)省不少的開銷何暮。
- select,poll實現(xiàn)需要自己不斷輪詢所有fd集合铐殃,直到設(shè)備就緒海洼,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調(diào)用epoll_wait不斷輪詢就緒鏈表富腊,期間也可能多次睡眠和喚醒交替坏逢,但是它是設(shè)備就緒時,調(diào)用回調(diào)函數(shù)赘被,把就緒fd放入就緒鏈表中是整,并喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替帘腹,但是select和poll在“醒著”的時候要遍歷整個fd集合贰盗,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時間阳欲。這就是回調(diào)機制帶來的性能提升舵盈。